diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index aef77f192b7..badb307dc9e 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -v17.1.0 +v17.1.1 diff --git a/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue index 85e051b8bf7..cab420be610 100644 --- a/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue +++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue @@ -95,7 +95,7 @@ export default {
{{ $options.text.file }}
- + {{ testCase.file }} {{ testCase.file }} diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue index 575ca28eb10..3a554b36c48 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue +++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue @@ -1,6 +1,6 @@ + + diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue index 8e81eca805b..80a16277490 100644 --- a/app/assets/javascripts/organizations/shared/components/groups_view.vue +++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue @@ -1,5 +1,5 @@ + + diff --git a/app/assets/javascripts/projects/your_work/index.js b/app/assets/javascripts/projects/your_work/index.js new file mode 100644 index 00000000000..2e20312cd76 --- /dev/null +++ b/app/assets/javascripts/projects/your_work/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import YourWorkProjectsApp from './components/app.vue'; + +export const initYourWorkProjects = () => { + const el = document.getElementById('js-your-work-projects-app'); + + if (!el) return false; + + return new Vue({ + el, + name: 'YourWorkProjectsRoot', + render(createElement) { + return createElement(YourWorkProjectsApp); + }, + }); +}; diff --git a/app/views/admin/background_migrations/index.html.haml b/app/views/admin/background_migrations/index.html.haml index e7212f00e5b..804fb3cb1db 100644 --- a/app/views/admin/background_migrations/index.html.haml +++ b/app/views/admin/background_migrations/index.html.haml @@ -1,7 +1,7 @@ - page_title s_('BackgroundMigrations|Background Migrations') - @breadcrumb_link = admin_background_migrations_path(database: params[:database]) -.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100 +.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_background_migrations_pageload' } } .gl-flex-grow-1 %h3= s_('BackgroundMigrations|Background Migrations') %p.light.gl-mb-0 diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 74549b8b688..191e6dda595 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -4,19 +4,15 @@ - page_title @group.name, _("Groups") - current_user_is_group_owner = @group && @group.has_owner?(current_user) -%h1.page-title.gl-font-size-h-display - = _('Group: %{group_name}') % { group_name: @group.full_name } += render ::Layouts::PageHeadingComponent.new(@group.full_name) do |c| + - c.with_actions do + = render Pajamas::ButtonComponent.new(href: admin_group_edit_path(@group), + button_options: { data: { testid: 'edit-group-link' }}) do + = _('Edit') - = render Pajamas::ButtonComponent.new(href: admin_group_edit_path(@group), - button_options: { class: 'gl-float-right', data: { testid: 'edit-group-link' }}, - icon: 'pencil') do - = _('Edit') -%hr -.row - .col-md-6 - = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c| - - c.with_header do - = _('Group info:') +.gl-grid.md:gl-grid-cols-2.gl-gap-5 + .gl-flex.gl-flex-col.gl-gap-5 + = render ::Layouts::CrudComponent.new(_('Group information')) do |c| - c.with_body do %ul.content-list.content-list-items-padding %li @@ -29,32 +25,25 @@ %span.light= _('Path:') %strong = @group.path - %li %span.light= _('Description:') %strong = @group.description - %li %span.light= _('Visibility level:') %strong = visibility_level_label(@group.visibility_level) - %li %span.light= _('Created on:') %strong = @group.created_at.to_fs(:medium) - %li %span.light= _('ID:') %strong = @group.id - = render_if_exists 'admin/namespace_plan_info', namespace: @group - %li = render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group - %li %span.light= _('Group Git LFS status:') %strong @@ -64,33 +53,30 @@ = render_if_exists 'namespaces/shared_runner_status', namespace: @group = render_if_exists 'namespaces/additional_minutes_status', namespace: @group - = render 'shared/custom_attributes', custom_attributes: @group.custom_attributes - - = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group - - = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c| - - c.with_header do - = _('Projects') - = gl_badge_tag @group.projects.count + = render ::Layouts::CrudComponent.new(_('Projects'), + icon: 'project', + count: @group.projects.count) do |c| - c.with_body do - %ul.content-list.content-list-items-padding - - @projects.each do |project| - %li - %strong - = link_to project.full_name, [:admin, project] - = gl_badge_tag storage_counter(project.statistics.storage_size) - %span.gl-float-right.light - %span.monospace= project.full_path + '.git' - - unless @projects.size < Kaminari.config.default_per_page - - c.with_footer do - = paginate @projects, param_name: 'projects_page', theme: 'gitlab' + - if @projects.any? + %ul.content-list.content-list-items-padding + - @projects.each do |project| + %li + %strong + = link_to project.full_name, [:admin, project] + = gl_badge_tag storage_counter(project.statistics.storage_size) + %span.gl-float-right.light + %span.monospace= project.full_path + '.git' + - else + %p.gl-new-card-empty.gl-text-center= _('No projects') + - unless @projects.size < Kaminari.config.default_per_page + - c.with_footer do + = paginate @projects, param_name: 'projects_page', theme: 'gitlab' - shared_projects = @group.shared_projects.sort_by(&:name) - unless shared_projects.empty? - = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c| - - c.with_header do - = _('Projects shared with %{group_name}') % { group_name: @group.name } - = gl_badge_tag shared_projects.size + = render ::Layouts::CrudComponent.new(_('Projects shared with %{group_name}') % { group_name: @group.name }, + icon: 'project', + count: shared_projects.size) do |c| - c.with_body do %ul.content-list.content-list-items-padding - shared_projects.each do |project| @@ -101,16 +87,16 @@ %span.gl-float-right.light %span.monospace= project.full_path + '.git' - .col-md-6 - = render 'shared/admin/admin_note' - - if can?(current_user, :admin_group_member, @group) = render 'shared/members/requests', membership_source: @group, group: @group, requesters: @requesters, force_mobile_view: true - = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c| - - c.with_header do - = html_escape(_("%{group_name} group members")) % { group_name: "#{html_escape(@group.name)}".html_safe } - = gl_badge_tag @group.users_count + .gl-flex.gl-flex-col.gl-gap-5 + = render 'shared/admin/admin_note' + + = render ::Layouts::CrudComponent.new(_('Group members'), + icon: 'user', + count: @group.users_count) do |c| + - c.with_actions do = render 'shared/members/manage_access_button', path: group_group_members_path(@group) - c.with_body do %ul.content-list.group-users-list.members-list @@ -122,3 +108,7 @@ - unless @members.size < Kaminari.config.default_per_page - c.with_footer do = paginate @members, param_name: 'members_page', theme: 'gitlab' + + = render 'shared/custom_attributes', custom_attributes: @group.custom_attributes + + = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index c74a9f4cbe6..3e303963f80 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -11,6 +11,9 @@ - if show_projects?(@projects, params) = render 'dashboard/projects_head' = render 'nav' - = render 'projects' + - if Feature.enabled?(:your_work_projects_vue, current_user) + #js-your-work-projects-app + - else + = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/dashboard/projects/shared/_common.html.haml b/app/views/dashboard/projects/shared/_common.html.haml index f6f67ad7712..d07d55ca7c4 100644 --- a/app/views/dashboard/projects/shared/_common.html.haml +++ b/app/views/dashboard/projects/shared/_common.html.haml @@ -6,6 +6,9 @@ = render 'dashboard/projects_head', project_tab_filter: :starred - if params[:filter_projects] || any_projects?(@projects) - = render 'projects' + - if Feature.enabled?(:your_work_projects_vue, current_user) + #js-your-work-projects-app + - else + = render 'projects' - else = render empty_page diff --git a/app/views/shared/_custom_attributes.html.haml b/app/views/shared/_custom_attributes.html.haml index 33f3ca93b9c..a18077628d9 100644 --- a/app/views/shared/_custom_attributes.html.haml +++ b/app/views/shared/_custom_attributes.html.haml @@ -1,8 +1,9 @@ - return unless custom_attributes.present? -= render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0' }) do |c| - - c.with_header do - = link_to(_('Custom Attributes'), help_page_path('api/custom_attributes')) += render ::Layouts::CrudComponent.new(_('Custom attributes')) do |c| + - c.with_actions do + = render Pajamas::ButtonComponent.new(href: help_page_path('api/custom_attributes'), size: :small, category: :tertiary, variant: :confirm) do + = _('Learn more') - c.with_body do %ul.content-list - custom_attributes.each do |custom_attribute| diff --git a/app/views/shared/admin/_admin_note.html.haml b/app/views/shared/admin/_admin_note.html.haml index 77854439bbb..12a2e43455e 100644 --- a/app/views/shared/admin/_admin_note.html.haml +++ b/app/views/shared/admin/_admin_note.html.haml @@ -1,7 +1,5 @@ - if @group.admin_note&.note? - text = @group.admin_note.note - = render Pajamas::CardComponent.new(card_options: { class: 'gl-border-blue-500 gl-mb-5' }, header_options: { class: 'gl-bg-blue-500 gl-text-white' }) do |c| - - c.with_header do - = s_('Admin|Admin notes') + = render ::Layouts::CrudComponent.new(s_('Admin|Admin notes')) do |c| - c.with_body do %p= text diff --git a/app/views/shared/members/_manage_access_button.html.haml b/app/views/shared/members/_manage_access_button.html.haml index 910d62d4dc4..82ee467f904 100644 --- a/app/views/shared/members/_manage_access_button.html.haml +++ b/app/views/shared/members/_manage_access_button.html.haml @@ -1,5 +1,5 @@ - path = local_assigns.fetch(:path, nil) .gl-float-right - = link_button_to path, size: :small, icon: 'pencil' do + = render Pajamas::ButtonComponent.new(href: path, size: :small) do = _('Manage access') diff --git a/config/events/view_admin_background_migrations_pageload.yml b/config/events/view_admin_background_migrations_pageload.yml new file mode 100644 index 00000000000..fd600dd3b13 --- /dev/null +++ b/config/events/view_admin_background_migrations_pageload.yml @@ -0,0 +1,16 @@ +--- +description: Tracks pageviews for the admin background migrations page +internal_events: true +action: view_admin_background_migrations_pageload +identifiers: +- user +product_group: personal_productivity +milestone: '17.2' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156826 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/feature_flags/wip/runners_dashboard_for_groups.yml b/config/feature_flags/wip/runners_dashboard_for_groups.yml deleted file mode 100644 index f6ad76d6790..00000000000 --- a/config/feature_flags/wip/runners_dashboard_for_groups.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: runners_dashboard_for_groups -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151640 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/459052 -milestone: '17.0' -type: wip -group: group::runner -default_enabled: false diff --git a/config/feature_flags/wip/your_work_projects_vue.yml b/config/feature_flags/wip/your_work_projects_vue.yml new file mode 100644 index 00000000000..08f25eb7e22 --- /dev/null +++ b/config/feature_flags/wip/your_work_projects_vue.yml @@ -0,0 +1,9 @@ +--- +name: your_work_projects_vue +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/13066 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155472 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/465889 +milestone: '17.1' +type: wip +group: group::tenant scale +default_enabled: false diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_view_admin_background_migrations_pageload_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_view_admin_background_migrations_pageload_monthly.yml new file mode 100644 index 00000000000..b8b41e0331e --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_view_admin_background_migrations_pageload_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_background_migrations_pageload_monthly +description: Monthly count of unique users who visited the admin background migrations page +product_group: personal_productivity +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.2' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156826 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: view_admin_background_migrations_pageload + unique: user.id diff --git a/config/metrics/counts_28d/count_total_view_admin_background_migrations_pageload_monthly.yml b/config/metrics/counts_28d/count_total_view_admin_background_migrations_pageload_monthly.yml new file mode 100644 index 00000000000..a8051809f1e --- /dev/null +++ b/config/metrics/counts_28d/count_total_view_admin_background_migrations_pageload_monthly.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.count_total_view_admin_background_migrations_pageload_monthly +description: Monthly count of total users who visited the admin background migrations page +product_group: personal_productivity +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.2' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156826 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: view_admin_background_migrations_pageload diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_view_admin_background_migrations_pageload_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_view_admin_background_migrations_pageload_weekly.yml new file mode 100644 index 00000000000..87a5f2efa46 --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_view_admin_background_migrations_pageload_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_background_migrations_pageload_weekly +description: Weekly count of unique users who visited the admin background migrations page +product_group: personal_productivity +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.2' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156826 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: view_admin_background_migrations_pageload + unique: user.id diff --git a/config/metrics/counts_7d/count_total_view_admin_background_migrations_pageload_weekly.yml b/config/metrics/counts_7d/count_total_view_admin_background_migrations_pageload_weekly.yml new file mode 100644 index 00000000000..835562ff058 --- /dev/null +++ b/config/metrics/counts_7d/count_total_view_admin_background_migrations_pageload_weekly.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.count_total_view_admin_background_migrations_pageload_weekly +description: Weekly count of total users who visited the admin background migrations page +product_group: personal_productivity +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.2' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156826 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: view_admin_background_migrations_pageload diff --git a/db/docs/batched_background_migrations/backfill_ml_candidate_metadata_project_id.yml b/db/docs/batched_background_migrations/backfill_ml_candidate_metadata_project_id.yml new file mode 100644 index 00000000000..1e8db4e7cea --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_ml_candidate_metadata_project_id.yml @@ -0,0 +1,9 @@ +--- +migration_job_name: BackfillMlCandidateMetadataProjectId +description: Backfills sharding key `ml_candidate_metadata.project_id` from `ml_candidates`. +feature_category: mlops +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157484 +milestone: '17.2' +queued_migration_version: 20240626142206 +finalize_after: '2024-07-22' +finalized_by: # version of the migration that finalized this BBM diff --git a/db/docs/ml_candidate_metadata.yml b/db/docs/ml_candidate_metadata.yml index 406c30bc2fd..2ee851a1ef1 100644 --- a/db/docs/ml_candidate_metadata.yml +++ b/db/docs/ml_candidate_metadata.yml @@ -19,3 +19,4 @@ desired_sharding_key: table: ml_candidates sharding_key: project_id belongs_to: candidate +desired_sharding_key_migration_job_name: BackfillMlCandidateMetadataProjectId diff --git a/db/docs/project_statistics.yml b/db/docs/project_statistics.yml index 914ff94e13d..6f01e41cb12 100644 --- a/db/docs/project_statistics.yml +++ b/db/docs/project_statistics.yml @@ -10,3 +10,7 @@ milestone: '8.16' gitlab_schema: gitlab_main_cell sharding_key: project_id: projects +schema_inconsistencies: +- type: missing_indexes + object_name: index_project_statistics_on_wiki_size_and_project_id + introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156010 \ No newline at end of file diff --git a/db/migrate/20240626142202_add_project_id_to_ml_candidate_metadata.rb b/db/migrate/20240626142202_add_project_id_to_ml_candidate_metadata.rb new file mode 100644 index 00000000000..d0dbb36216c --- /dev/null +++ b/db/migrate/20240626142202_add_project_id_to_ml_candidate_metadata.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddProjectIdToMlCandidateMetadata < Gitlab::Database::Migration[2.2] + milestone '17.2' + + def change + add_column :ml_candidate_metadata, :project_id, :bigint + end +end diff --git a/db/post_migrate/20240611122408_remove_project_statistics_wiki_size_and_project_id_index.rb b/db/post_migrate/20240611122408_remove_project_statistics_wiki_size_and_project_id_index.rb new file mode 100644 index 00000000000..762f916b2c7 --- /dev/null +++ b/db/post_migrate/20240611122408_remove_project_statistics_wiki_size_and_project_id_index.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class RemoveProjectStatisticsWikiSizeAndProjectIdIndex < Gitlab::Database::Migration[2.2] + milestone '17.2' + + INDEX_NAME = 'index_project_statistics_on_wiki_size_and_project_id' + COLUMNS = %i[wiki_size project_id] + + # TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/466691 + def up + return unless should_run? + + prepare_async_index_removal :project_statistics, COLUMNS, name: INDEX_NAME + end + + def down + return unless should_run? + + unprepare_async_index :project_statistics, COLUMNS, name: INDEX_NAME + end + + def should_run? + Gitlab.com_except_jh? + end +end diff --git a/db/post_migrate/20240626142203_index_ml_candidate_metadata_on_project_id.rb b/db/post_migrate/20240626142203_index_ml_candidate_metadata_on_project_id.rb new file mode 100644 index 00000000000..1822882cf7f --- /dev/null +++ b/db/post_migrate/20240626142203_index_ml_candidate_metadata_on_project_id.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class IndexMlCandidateMetadataOnProjectId < Gitlab::Database::Migration[2.2] + milestone '17.2' + disable_ddl_transaction! + + INDEX_NAME = 'index_ml_candidate_metadata_on_project_id' + + def up + add_concurrent_index :ml_candidate_metadata, :project_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :ml_candidate_metadata, INDEX_NAME + end +end diff --git a/db/post_migrate/20240626142204_add_ml_candidate_metadata_project_id_fk.rb b/db/post_migrate/20240626142204_add_ml_candidate_metadata_project_id_fk.rb new file mode 100644 index 00000000000..c0caeb917b1 --- /dev/null +++ b/db/post_migrate/20240626142204_add_ml_candidate_metadata_project_id_fk.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddMlCandidateMetadataProjectIdFk < Gitlab::Database::Migration[2.2] + milestone '17.2' + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :ml_candidate_metadata, :projects, column: :project_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :ml_candidate_metadata, column: :project_id + end + end +end diff --git a/db/post_migrate/20240626142205_add_ml_candidate_metadata_project_id_trigger.rb b/db/post_migrate/20240626142205_add_ml_candidate_metadata_project_id_trigger.rb new file mode 100644 index 00000000000..bd5d3a4e535 --- /dev/null +++ b/db/post_migrate/20240626142205_add_ml_candidate_metadata_project_id_trigger.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class AddMlCandidateMetadataProjectIdTrigger < Gitlab::Database::Migration[2.2] + milestone '17.2' + + def up + install_sharding_key_assignment_trigger( + table: :ml_candidate_metadata, + sharding_key: :project_id, + parent_table: :ml_candidates, + parent_sharding_key: :project_id, + foreign_key: :candidate_id + ) + end + + def down + remove_sharding_key_assignment_trigger( + table: :ml_candidate_metadata, + sharding_key: :project_id, + parent_table: :ml_candidates, + parent_sharding_key: :project_id, + foreign_key: :candidate_id + ) + end +end diff --git a/db/post_migrate/20240626142206_queue_backfill_ml_candidate_metadata_project_id.rb b/db/post_migrate/20240626142206_queue_backfill_ml_candidate_metadata_project_id.rb new file mode 100644 index 00000000000..f2ce8e56d7b --- /dev/null +++ b/db/post_migrate/20240626142206_queue_backfill_ml_candidate_metadata_project_id.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class QueueBackfillMlCandidateMetadataProjectId < Gitlab::Database::Migration[2.2] + milestone '17.2' + restrict_gitlab_migration gitlab_schema: :gitlab_main_cell + + MIGRATION = "BackfillMlCandidateMetadataProjectId" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + def up + queue_batched_background_migration( + MIGRATION, + :ml_candidate_metadata, + :id, + :project_id, + :ml_candidates, + :project_id, + :candidate_id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration( + MIGRATION, + :ml_candidate_metadata, + :id, + [ + :project_id, + :ml_candidates, + :project_id, + :candidate_id + ] + ) + end +end diff --git a/db/schema_migrations/20240611122408 b/db/schema_migrations/20240611122408 new file mode 100644 index 00000000000..b56b566794f --- /dev/null +++ b/db/schema_migrations/20240611122408 @@ -0,0 +1 @@ +4704d2511a36e5af924d1b92fcf0c425d6868aa848f29431d8bff6410c5c967e \ No newline at end of file diff --git a/db/schema_migrations/20240626142202 b/db/schema_migrations/20240626142202 new file mode 100644 index 00000000000..1e7fec706a2 --- /dev/null +++ b/db/schema_migrations/20240626142202 @@ -0,0 +1 @@ +6f396093e4cd6ec606540af198fe04853bebd74b51702e06ff171ed0b8c12921 \ No newline at end of file diff --git a/db/schema_migrations/20240626142203 b/db/schema_migrations/20240626142203 new file mode 100644 index 00000000000..b3fbba139c5 --- /dev/null +++ b/db/schema_migrations/20240626142203 @@ -0,0 +1 @@ +1e15fef4845db49e85604b7e959fda58bcaa623a6408620f7d0a9d882c26438a \ No newline at end of file diff --git a/db/schema_migrations/20240626142204 b/db/schema_migrations/20240626142204 new file mode 100644 index 00000000000..6cf92f600bf --- /dev/null +++ b/db/schema_migrations/20240626142204 @@ -0,0 +1 @@ +5f2cf06eb8a34e95b71704fa544e8e1eb0996435dea2c7c892e850590b42777b \ No newline at end of file diff --git a/db/schema_migrations/20240626142205 b/db/schema_migrations/20240626142205 new file mode 100644 index 00000000000..36fd719da2b --- /dev/null +++ b/db/schema_migrations/20240626142205 @@ -0,0 +1 @@ +42601d461c570aa82234d58db59784158f044f0bafdbe6b83855123d21bf61e9 \ No newline at end of file diff --git a/db/schema_migrations/20240626142206 b/db/schema_migrations/20240626142206 new file mode 100644 index 00000000000..923d820dd7b --- /dev/null +++ b/db/schema_migrations/20240626142206 @@ -0,0 +1 @@ +06dda6411338c7bdd342da847d11ba8512279f09e85b4dfa26991fbb8184b3e0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5bd4f85fffd..883cfcb44b9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -934,6 +934,22 @@ RETURN NEW; END $$; +CREATE FUNCTION trigger_25d35f02ab55() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN +IF NEW."project_id" IS NULL THEN + SELECT "project_id" + INTO NEW."project_id" + FROM "ml_candidates" + WHERE "ml_candidates"."id" = NEW."candidate_id"; +END IF; + +RETURN NEW; + +END +$$; + CREATE FUNCTION trigger_25fe4f7da510() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -12314,7 +12330,7 @@ CREATE TABLE merge_request_context_commits ( message text, merge_request_id bigint, trailers jsonb DEFAULT '{}'::jsonb NOT NULL, - project_id bigint + project_id bigint, CONSTRAINT check_1dc6b5f2ac CHECK ((merge_request_id IS NOT NULL)) ); @@ -12764,6 +12780,7 @@ CREATE TABLE ml_candidate_metadata ( candidate_id bigint NOT NULL, name text NOT NULL, value text NOT NULL, + project_id bigint, CONSTRAINT check_6b38a286a5 CHECK ((char_length(name) <= 255)), CONSTRAINT check_9453f4a8e9 CHECK ((char_length(value) <= 5000)) ); @@ -27701,6 +27718,8 @@ CREATE UNIQUE INDEX index_ml_candidate_metadata_on_candidate_id_and_name ON ml_c CREATE INDEX index_ml_candidate_metadata_on_name ON ml_candidate_metadata USING btree (name); +CREATE INDEX index_ml_candidate_metadata_on_project_id ON ml_candidate_metadata USING btree (project_id); + CREATE INDEX index_ml_candidate_metrics_on_candidate_id ON ml_candidate_metrics USING btree (candidate_id); CREATE INDEX index_ml_candidate_params_on_candidate_id ON ml_candidate_params USING btree (candidate_id); @@ -31389,6 +31408,8 @@ CREATE TRIGGER trigger_2514245c7fc5 BEFORE INSERT OR UPDATE ON dast_site_profile CREATE TRIGGER trigger_25c44c30884f BEFORE INSERT OR UPDATE ON work_item_parent_links FOR EACH ROW EXECUTE FUNCTION trigger_25c44c30884f(); +CREATE TRIGGER trigger_25d35f02ab55 BEFORE INSERT OR UPDATE ON ml_candidate_metadata FOR EACH ROW EXECUTE FUNCTION trigger_25d35f02ab55(); + CREATE TRIGGER trigger_25fe4f7da510 BEFORE INSERT OR UPDATE ON vulnerability_issue_links FOR EACH ROW EXECUTE FUNCTION trigger_25fe4f7da510(); CREATE TRIGGER trigger_2ac3d66ed1d3 BEFORE INSERT OR UPDATE ON vulnerability_occurrence_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_2ac3d66ed1d3(); @@ -32492,6 +32513,9 @@ ALTER TABLE ONLY approval_merge_request_rules ALTER TABLE ONLY fork_network_members ADD CONSTRAINT fk_b01280dae4 FOREIGN KEY (forked_from_project_id) REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE ONLY ml_candidate_metadata + ADD CONSTRAINT fk_b044692715 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY sbom_occurrences ADD CONSTRAINT fk_b1b65d8d17 FOREIGN KEY (source_package_id) REFERENCES sbom_source_packages(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 940b1102bed..424663be707 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -18082,7 +18082,7 @@ CI/CD variables for a project. | `status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner. | | `tagList` | [`[String!]`](#string) | Tags associated with the runner. | | `tokenExpiresAt` | [`Time`](#time) | Runner token expiration time. | -| `upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | **Introduced** in GitLab 14.10. **Status**: Experiment. Availability of upgrades for the runner. | +| `upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Availability of upgrades for the runner. | | `userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. | #### Fields with arguments diff --git a/doc/ci/yaml/ci_job_log_timestamps.md b/doc/ci/yaml/ci_job_log_timestamps.md index 8114b188ff4..183641f0fcd 100644 --- a/doc/ci/yaml/ci_job_log_timestamps.md +++ b/doc/ci/yaml/ci_job_log_timestamps.md @@ -12,6 +12,7 @@ DETAILS: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/455582) in GitLab 17.1 [with flags](../../administration/feature_flags.md) named `FF_TIMESTAMPS` and `parse_ci_job_timestamps`. Disabled by default. > - `parse_ci_job_timestamps` [enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/455581) in GitLab 17.1. +> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/464785) in GitLab 17.2. Feature flag `parse_ci_job_timestamps` removed. FLAG: The availability of this feature is controlled by feature flags. @@ -22,8 +23,6 @@ Prerequisites: - You must be on GitLab Runner 17.0 and later. - An administrator must enable the `FF_TIMESTAMPS` [feature flag](../../administration/feature_flags.md) in `.gitlab-ci.yml`. -- On self-managed GitLab, an administrator must enable - the `parse_ci_job_timestamps` feature flag. This feature generates a timestamp in the [ISO 8601 format](https://www.iso.org/iso-8601-date-and-time-format.html) for each line in the CI log. diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index ea0073f8783..03a354bfb86 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -92,7 +92,7 @@ To configure the GitLab for Jira Cloud app: 1. Optional. To link a self-managed GitLab instance with Jira, select **Change GitLab version**. 1. Select all checkboxes, then select **Next**. 1. Enter your **GitLab instance URL**, then select **Save**. -1. Select **Sign in to GitLab**, then enter your credentials. +1. Select **Sign in to GitLab**, then enter your username and password. The integration doesn't support single-sign on such as SAML during configuration. 1. Select **Authorize**. A list of groups is now visible. 1. Select **Link groups**. 1. To link to a group, select **Link**. diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md index 079bd0c333b..2aefe87f979 100644 --- a/doc/user/organization/index.md +++ b/doc/user/organization/index.md @@ -81,7 +81,7 @@ To switch organizations: 1. On the left sidebar, select **Organizations** and find the organization you want to manage. 1. Select **Manage > Groups and projects**. 1. Optional. Filter the results: - - To search for specific groups or projects, in the search box enter your search term. + - To search for specific groups or projects, in the search box enter your search term (minimum three characters). - To view only groups or projects, from the **Display** dropdown list select an option. 1. Optional. To sort the results by name, date created, or date updated, from the dropdown list select an option. Then select ascending (**{sort-lowest}**) or descending (**{sort-highest}**) order. diff --git a/lib/gitlab/background_migration/backfill_ml_candidate_metadata_project_id.rb b/lib/gitlab/background_migration/backfill_ml_candidate_metadata_project_id.rb new file mode 100644 index 00000000000..1d6fe2021c4 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_ml_candidate_metadata_project_id.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + class BackfillMlCandidateMetadataProjectId < BackfillDesiredShardingKeyJob + operation_name :backfill_ml_candidate_metadata_project_id + feature_category :mlops + end + end +end diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index 37d26a473dd..da2f34d73f3 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -90,6 +90,7 @@ namespace :tw do CONTRIBUTOR_DOCS_PATH = '/doc/development/' CONTRIBUTOR_DOCS_CODE_OWNER_RULES = [ + CodeOwnerRule.new('AI-powered', '@gitlab-org/ai-powered'), CodeOwnerRule.new('Analytics Instrumentation', '@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \ '@gitlab-org/analytics-section/analytics-instrumentation/engineers'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b9076fca07d..31f27cb5004 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -832,9 +832,6 @@ msgstr "" msgid "%{group_name} activity" msgstr "" -msgid "%{group_name} group members" -msgstr "" - msgid "%{group_name} is approaching the limit of available seats" msgstr "" @@ -16079,6 +16076,9 @@ msgstr "" msgid "Custom analyzers: language support" msgstr "" +msgid "Custom attributes" +msgstr "" + msgid "Custom confirmation message: %{message}" msgstr "" @@ -19765,6 +19765,9 @@ msgstr "" msgid "Edit wiki page" msgstr "" +msgid "Edit your criteria and try again." +msgstr "" + msgid "Edit your most recent comment in a thread (from an empty textarea)" msgstr "" @@ -25077,9 +25080,6 @@ msgstr "" msgid "Group import requests" msgstr "" -msgid "Group info:" -msgstr "" - msgid "Group information" msgstr "" @@ -34940,6 +34940,9 @@ msgstr "" msgid "No project subscribes to the pipelines in this project." msgstr "" +msgid "No projects" +msgstr "" + msgid "No projects available." msgstr "" @@ -42158,6 +42161,9 @@ msgstr "" msgid "Projects in this group can use Git LFS" msgstr "" +msgid "Projects list" +msgstr "" + msgid "Projects shared with %{group_name}" msgstr "" @@ -47703,6 +47709,12 @@ msgstr "" msgid "SecurityOrchestration|Branch: %{boldStart}%{branchName}%{boldEnd} was not found in project: %{boldStart}%{projectName}%{boldEnd}. Edit or remove this entry." msgstr "" +msgid "SecurityOrchestration|CI/CD template edition to be enforced. The default template is stable, but may not have all the features of the latest template." +msgstr "" + +msgid "SecurityOrchestration|CI/CD template edition to be enforced. TheĀ latestĀ edition may introduce breaking changes." +msgstr "" + msgid "SecurityOrchestration|Cadence is invalid" msgstr "" @@ -48083,6 +48095,9 @@ msgstr "" msgid "SecurityOrchestration|Security Scan" msgstr "" +msgid "SecurityOrchestration|Security job template" +msgstr "" + msgid "SecurityOrchestration|Security policy overwrites this setting" msgstr "" @@ -48368,6 +48383,9 @@ msgstr "" msgid "SecurityOrchestration|compliance frameworks" msgstr "" +msgid "SecurityOrchestration|default" +msgstr "" + msgid "SecurityOrchestration|except projects" msgstr "" @@ -48383,6 +48401,9 @@ msgstr "" msgid "SecurityOrchestration|have no fix available" msgstr "" +msgid "SecurityOrchestration|latest" +msgstr "" + msgid "SecurityOrchestration|more than %{allowed}" msgstr "" diff --git a/package.json b/package.json index b6f884c8729..6e670ff7541 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", "@gitlab/svgs": "3.103.0", - "@gitlab/ui": "85.3.0", + "@gitlab/ui": "85.4.1", "@gitlab/web-ide": "^0.0.1-dev-20240613133550", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@rails/actioncable": "7.0.8-4", diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 437877b5e63..3d532b037d0 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -61,8 +61,15 @@ module QA private def has_project_with_access_role?(project_name, access_role) - within_element('project-content', text: project_name) do - has_element?('user-access-role', text: access_role) + # Since we are very early in the Vue migration, there isn't much value in testing + # when the feature flag is enabled. + # Please see https://gitlab.com/gitlab-org/gitlab/-/issues/466081 for tracking revisiting this. + if Runtime::Feature.enabled?(:your_work_projects_vue) + has_text?('Projects list') + else + within_element('project-content', text: project_name) do + has_element?('user-access-role', text: access_role) + end end end end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 1d8f768b22a..b740382b2c7 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -110,7 +110,7 @@ RSpec.describe 'Admin Groups', feature_category: :groups_and_projects do visit admin_group_path(group) - expect(page).to have_content("Group: #{group.name}") + expect(page).to have_content group.name expect(page).to have_content("ID: #{group.id}") end diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index 27a9b8296d9..d6aaa05f61a 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -499,7 +499,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do within(:css, '.gl-mb-3 + .gl-card') do click_link group.name end - expect(page).to have_content "Group: #{group.name}" + expect(page).to have_content group.name expect(page).to have_content project.name end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index a1b9cc46fb3..d8364ec0944 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -12,244 +12,270 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects sign_in(user) end - it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do + context 'when feature :your_work_projects_vue is enabled' do before do + stub_feature_flags(your_work_projects_vue: true) + end + + it 'mounts JS app' do visit dashboard_projects_path + + expect(page).to have_content('Projects') + expect(page).to have_content('Projects list') end end - it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_projects_path, :projects + context 'when feature :your_work_projects_vue is disabled' do + before do + stub_feature_flags(your_work_projects_vue: false) + end - it 'links to the "Explore projects" page' do - visit dashboard_projects_path - - expect(page).to have_link("Explore projects", href: starred_explore_projects_path) - end - - context 'when user has access to the project' do - it 'shows role badge' do + it 'does not mount JS app' do visit dashboard_projects_path - within_testid('user-access-role') do - expect(page).to have_content('Developer') + expect(page).to have_content('Projects') + expect(page).not_to have_content('Projects list') + end + + it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do + before do + visit dashboard_projects_path end end - context 'when role changes', :use_clean_rails_memory_store_fragment_caching do - it 'displays the right role' do + it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_projects_path, :projects + + it 'links to the "Explore projects" page' do + visit dashboard_projects_path + + expect(page).to have_link("Explore projects", href: starred_explore_projects_path) + end + + context 'when user has access to the project' do + it 'shows role badge' do visit dashboard_projects_path within_testid('user-access-role') do expect(page).to have_content('Developer') end + end - project.members.last.update!(access_level: 40) + context 'when role changes', :use_clean_rails_memory_store_fragment_caching do + it 'displays the right role' do + visit dashboard_projects_path - visit dashboard_projects_path + within_testid('user-access-role') do + expect(page).to have_content('Developer') + end - within_testid('user-access-role') do - expect(page).to have_content('Maintainer') + project.members.last.update!(access_level: 40) + + visit dashboard_projects_path + + within_testid('user-access-role') do + expect(page).to have_content('Maintainer') + end end end end - end - context 'when last_activity_at and update_at are present' do - it 'shows the last_activity_at attribute as the update date' do - project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.zone.now) + context 'when last_activity_at and update_at are present' do + it 'shows the last_activity_at attribute as the update date' do + project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.zone.now) - visit dashboard_projects_path + visit dashboard_projects_path - expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']") - end - end - - context 'when last_activity_at is missing' do - it 'shows the updated_at attribute as the update date' do - project.update!(last_activity_at: nil) - project.touch - - visit dashboard_projects_path - - expect(page).to have_xpath("//time[@datetime='#{project.updated_at.getutc.iso8601}']") - end - end - - context 'when on Your projects tab' do - it 'shows all projects by default' do - visit dashboard_projects_path - - expect(page).to have_content(project.name) - expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1) - end - - it 'shows personal projects on personal projects tab' do - project3 = create(:project, namespace: user.namespace) - - visit dashboard_projects_path - - click_link 'Personal' - - expect(page).not_to have_content(project.name) - expect(page).to have_content(project3.name) - end - - it 'sorts projects by most stars when sorting by most stars' do - project_with_most_stars = create(:project, namespace: user.namespace, star_count: 10) - - visit dashboard_projects_path(sort: :stars_desc) - - expect(first('.project-row')).to have_content(project_with_most_stars.title) - end - end - - context 'when on Starred projects tab' do - it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :starred_dashboard_projects_path, :projects - - it 'shows the empty state when there are no starred projects' do - visit(starred_dashboard_projects_path) - - expect(page).to have_text(s_("StarredProjectsEmptyState|You don't have starred projects yet.")) - end - - it 'shows only starred projects' do - user.toggle_star(project2) - - visit(starred_dashboard_projects_path) - - expect(page).not_to have_content(project.name) - expect(page).to have_content(project2.name) - expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1) - expect(find('.gl-tabs-nav li:nth-child(2) .badge-pill')).to have_content(1) - end - - it 'does not show tabs to filter by all projects or personal' do - visit(starred_dashboard_projects_path) - - expect(page).not_to have_content '.filtered-search-nav' - end - end - - describe 'with a pipeline', :clean_gitlab_redis_shared_state do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) } - - before do - # Since the cache isn't updated when a new pipeline is created - # we need the pipeline to advance in the pipeline since the cache was created - # by visiting the login page. - pipeline.succeed - end - - it 'shows that the last pipeline passed' do - visit dashboard_projects_path - - within_testid('project_controls') do - expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']") - expect(page).to have_css("[data-testid='ci-icon']") - expect(page).to have_css('[data-testid="status_success_borderless-icon"]') - expect(page).to have_link('Pipeline: passed') + expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']") end end - shared_examples 'hidden pipeline status' do - it 'does not show the pipeline status' do + context 'when last_activity_at is missing' do + it 'shows the updated_at attribute as the update date' do + project.update!(last_activity_at: nil) + project.touch + + visit dashboard_projects_path + + expect(page).to have_xpath("//time[@datetime='#{project.updated_at.getutc.iso8601}']") + end + end + + context 'when on Your projects tab' do + it 'shows all projects by default' do + visit dashboard_projects_path + + expect(page).to have_content(project.name) + expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1) + end + + it 'shows personal projects on personal projects tab' do + project3 = create(:project, namespace: user.namespace) + + visit dashboard_projects_path + + click_link 'Personal' + + expect(page).not_to have_content(project.name) + expect(page).to have_content(project3.name) + end + + it 'sorts projects by most stars when sorting by most stars' do + project_with_most_stars = create(:project, namespace: user.namespace, star_count: 10) + + visit dashboard_projects_path(sort: :stars_desc) + + expect(first('.project-row')).to have_content(project_with_most_stars.title) + end + end + + context 'when on Starred projects tab' do + it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :starred_dashboard_projects_path, :projects + + it 'shows the empty state when there are no starred projects' do + visit(starred_dashboard_projects_path) + + expect(page).to have_text(s_("StarredProjectsEmptyState|You don't have starred projects yet.")) + end + + it 'shows only starred projects' do + user.toggle_star(project2) + + visit(starred_dashboard_projects_path) + + expect(page).not_to have_content(project.name) + expect(page).to have_content(project2.name) + expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1) + expect(find('.gl-tabs-nav li:nth-child(2) .badge-pill')).to have_content(1) + end + + it 'does not show tabs to filter by all projects or personal' do + visit(starred_dashboard_projects_path) + + expect(page).not_to have_content '.filtered-search-nav' + end + end + + describe 'with a pipeline', :clean_gitlab_redis_shared_state do + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) } + + before do + # Since the cache isn't updated when a new pipeline is created + # we need the pipeline to advance in the pipeline since the cache was created + # by visiting the login page. + pipeline.succeed + end + + it 'shows that the last pipeline passed' do visit dashboard_projects_path within_testid('project_controls') do - expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']") - expect(page).not_to have_css("[data-testid='ci-icon']") - expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]') - expect(page).not_to have_link('Pipeline: passed') + expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']") + expect(page).to have_css("[data-testid='ci-icon']") + expect(page).to have_css('[data-testid="status_success_borderless-icon"]') + expect(page).to have_link('Pipeline: passed') + end + end + + shared_examples 'hidden pipeline status' do + it 'does not show the pipeline status' do + visit dashboard_projects_path + + within_testid('project_controls') do + expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']") + expect(page).not_to have_css("[data-testid='ci-icon']") + expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]') + expect(page).not_to have_link('Pipeline: passed') + end + end + end + + context 'guest user of project and project has private pipelines' do + let(:guest_user) { create(:user) } + + before do + project.update!(public_builds: false) + project.add_guest(guest_user) + sign_in(guest_user) + end + + it_behaves_like 'hidden pipeline status' + end + + context "when last_pipeline is missing" do + before do + project.last_pipeline.delete + end + + it_behaves_like 'hidden pipeline status' + end + end + + describe 'with topics' do + context 'when project has topics' do + before do + project.update_attribute(:topic_list, 'topic1') + end + + it 'shows project topics if exist' do + visit dashboard_projects_path + + expect(page).to have_selector('[data-testid="project_topic_list"]') + expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1')) + end + end + + context 'when project does not have topics' do + it 'does not show project topics' do + visit dashboard_projects_path + + expect(page).not_to have_selector('[data-testid="project_topic_list"]') end end end - context 'guest user of project and project has private pipelines' do - let(:guest_user) { create(:user) } - + context 'last push widget', :use_clean_rails_memory_store_caching do before do - project.update!(public_builds: false) - project.add_guest(guest_user) - sign_in(guest_user) - end + event = create(:push_event, project: project, author: user) - it_behaves_like 'hidden pipeline status' - end + create(:push_event_payload, event: event, ref: 'feature', action: :created) - context "when last_pipeline is missing" do - before do - project.last_pipeline.delete - end + Users::LastPushEventService.new(user).cache_last_push_event(event) - it_behaves_like 'hidden pipeline status' - end - end - - describe 'with topics' do - context 'when project has topics' do - before do - project.update_attribute(:topic_list, 'topic1') - end - - it 'shows project topics if exist' do visit dashboard_projects_path + end - expect(page).to have_selector('[data-testid="project_topic_list"]') - expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1')) + it 'shows "Create merge request" button' do + expect(page).to have_content 'You pushed to feature' + + within('#content-body') do + find_link('Create merge request', visible: false).click + end + + expect(page).to have_selector('.merge-request-form') + expect(page).to have_current_path project_new_merge_request_path(project), ignore_query: true + expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s + expect(page).to have_content "From feature into master" end end - context 'when project does not have topics' do - it 'does not show project topics' do - visit dashboard_projects_path - - expect(page).not_to have_selector('[data-testid="project_topic_list"]') - end - end - end - - context 'last push widget', :use_clean_rails_memory_store_caching do - before do - event = create(:push_event, project: project, author: user) - - create(:push_event_payload, event: event, ref: 'feature', action: :created) - - Users::LastPushEventService.new(user).cache_last_push_event(event) - + it 'avoids an N+1 query in dashboard index' do + create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha) visit dashboard_projects_path + + control = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path } + + new_project = create(:project, :repository, name: 'new project') + create(:ci_pipeline, :with_job, status: :success, project: new_project, ref: new_project.commit.sha) + new_project.add_developer(user) + + ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count + + # There are a few known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037 + # - User#max_member_access_for_project_ids + # - ProjectsHelper#load_pipeline_status / Ci::CommitWithPipeline#last_pipeline + # - Ci::Pipeline#detailed_status + + expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4) end - - it 'shows "Create merge request" button' do - expect(page).to have_content 'You pushed to feature' - - within('#content-body') do - find_link('Create merge request', visible: false).click - end - - expect(page).to have_selector('.merge-request-form') - expect(page).to have_current_path project_new_merge_request_path(project), ignore_query: true - expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s - expect(page).to have_content "From feature into master" - end - end - - it 'avoids an N+1 query in dashboard index' do - create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha) - visit dashboard_projects_path - - control = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path } - - new_project = create(:project, :repository, name: 'new project') - create(:ci_pipeline, :with_job, status: :success, project: new_project, ref: new_project.commit.sha) - new_project.add_developer(user) - - ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count - - # There are a few known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037 - # - User#max_member_access_for_project_ids - # - ProjectsHelper#load_pipeline_status / Ci::CommitWithPipeline#last_pipeline - # - Ci::Pipeline#detailed_status - - expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4) end end diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js index 8d17acd8a10..91f39afb42d 100644 --- a/spec/frontend/organizations/groups_and_projects/components/app_spec.js +++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js @@ -146,17 +146,47 @@ describe('GroupsAndProjectsApp', () => { }); describe('when filtered search bar is submitted', () => { - const searchTerm = 'foo bar'; + describe('when search term is 3 characters or more', () => { + const searchTerm = 'foo bar'; - beforeEach(() => { - createComponent(); + beforeEach(() => { + createComponent(); - findFilteredSearchAndSort().vm.$emit('filter', { [FILTERED_SEARCH_TERM_KEY]: searchTerm }); + findFilteredSearchAndSort().vm.$emit('filter', { [FILTERED_SEARCH_TERM_KEY]: searchTerm }); + }); + + it(`updates \`${FILTERED_SEARCH_TERM_KEY}\` query string`, () => { + expect(routerMock.push).toHaveBeenCalledWith({ + query: { [FILTERED_SEARCH_TERM_KEY]: searchTerm }, + }); + }); }); - it(`updates \`${FILTERED_SEARCH_TERM_KEY}\` query string`, () => { - expect(routerMock.push).toHaveBeenCalledWith({ - query: { [FILTERED_SEARCH_TERM_KEY]: searchTerm }, + describe('when search term is less than 3 characters', () => { + const searchTerm = 'fo'; + + beforeEach(() => { + createComponent(); + + findFilteredSearchAndSort().vm.$emit('filter', { [FILTERED_SEARCH_TERM_KEY]: searchTerm }); + }); + + it('does not update query string', () => { + expect(routerMock.push).not.toHaveBeenCalled(); + }); + }); + + describe('when search term is empty but there are other filters', () => { + beforeEach(() => { + createComponent(); + + findFilteredSearchAndSort().vm.$emit('filter', { foo: 'bar' }); + }); + + it('updates query string', () => { + expect(routerMock.push).toHaveBeenCalledWith({ + query: { foo: 'bar' }, + }); }); }); }); diff --git a/spec/frontend/organizations/shared/components/groups_and_projects_empty_state_spec.js b/spec/frontend/organizations/shared/components/groups_and_projects_empty_state_spec.js new file mode 100644 index 00000000000..19cec0b46fb --- /dev/null +++ b/spec/frontend/organizations/shared/components/groups_and_projects_empty_state_spec.js @@ -0,0 +1,58 @@ +import { GlEmptyState } from '@gitlab/ui'; +import emptySearchSvgPath from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import GroupsAndProjectsEmptyState from '~/organizations/shared/components/groups_and_projects_empty_state.vue'; + +describe('GroupsAndProjectsEmptyState', () => { + let wrapper; + + const defaultPropsData = { + svgPath: 'path/to/svg', + title: 'No results', + description: 'Try again', + search: '', + }; + + const createComponent = ({ propsData = {}, scopedSlots = {} } = {}) => { + wrapper = shallowMountExtended(GroupsAndProjectsEmptyState, { + propsData: { ...defaultPropsData, ...propsData }, + scopedSlots, + }); + }; + + describe('when search is empty', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders GlEmptyState component with passed props', () => { + expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + title: defaultPropsData.title, + description: defaultPropsData.description, + svgPath: defaultPropsData.svgPath, + svgHeight: 144, + }); + }); + }); + + describe('when search is not empty', () => { + beforeEach(() => { + createComponent({ propsData: { search: 'foo' } }); + }); + + it('renders GlEmptyState component with no results found message', () => { + expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + title: 'No results found', + description: 'Edit your criteria and try again.', + svgPath: emptySearchSvgPath, + svgHeight: 144, + }); + }); + }); + + it('renders actions slot', () => { + createComponent({ scopedSlots: { actions: '
' } }); + + expect(wrapper.findByTestId('actions-slot').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/organizations/shared/components/groups_view_spec.js b/spec/frontend/organizations/shared/components/groups_view_spec.js index e51176ea79f..2cb3b160357 100644 --- a/spec/frontend/organizations/shared/components/groups_view_spec.js +++ b/spec/frontend/organizations/shared/components/groups_view_spec.js @@ -1,10 +1,11 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; -import { GlEmptyState, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui'; +import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui'; import organizationGroupsGraphQlResponse from 'test_fixtures/graphql/organizations/groups.query.graphql.json'; import GroupsView from '~/organizations/shared/components/groups_view.vue'; import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants'; import NewGroupButton from '~/organizations/shared/components/new_group_button.vue'; +import GroupsAndProjectsEmptyState from '~/organizations/shared/components/groups_and_projects_empty_state.vue'; import { renderDeleteSuccessToast, deleteParams, @@ -142,12 +143,12 @@ describe('GroupsView', () => { await waitForPromises(); - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + expect(wrapper.findComponent(GroupsAndProjectsEmptyState).props()).toMatchObject({ title: "You don't have any groups yet.", description: 'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.', - svgHeight: 144, svgPath: defaultProvide.groupsEmptyStateSvgPath, + search: 'foo', }); expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons); diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js index 4919ed93756..c60f5d6bcd4 100644 --- a/spec/frontend/organizations/shared/components/projects_view_spec.js +++ b/spec/frontend/organizations/shared/components/projects_view_spec.js @@ -1,10 +1,11 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; -import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; +import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui'; import organizationProjectsGraphQlResponse from 'test_fixtures/graphql/organizations/projects.query.graphql.json'; import ProjectsView from '~/organizations/shared/components/projects_view.vue'; import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants'; import NewProjectButton from '~/organizations/shared/components/new_project_button.vue'; +import GroupsAndProjectsEmptyState from '~/organizations/shared/components/groups_and_projects_empty_state.vue'; import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql'; import { renderDeleteSuccessToast, @@ -95,7 +96,7 @@ describe('ProjectsView', () => { const findPagination = () => wrapper.findComponent(GlKeysetPagination); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findEmptyState = () => wrapper.findComponent(GroupsAndProjectsEmptyState); const findProjectsList = () => wrapper.findComponent(ProjectsList); const findProjectsListProjectById = (projectId) => findProjectsList() @@ -149,8 +150,8 @@ describe('ProjectsView', () => { title: "You don't have any projects yet.", description: 'Projects are where you can store your code, access issues, wiki, and other features of GitLab.', - svgHeight: 144, svgPath: defaultProvide.projectsEmptyStateSvgPath, + search: 'foo', }); expect(findNewProjectButton().exists()).toBe(shouldShowEmptyStateButtons); diff --git a/spec/frontend/projects/your_work/components/app_spec.js b/spec/frontend/projects/your_work/components/app_spec.js new file mode 100644 index 00000000000..30b36938afe --- /dev/null +++ b/spec/frontend/projects/your_work/components/app_spec.js @@ -0,0 +1,24 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import YourWorkProjectsApp from '~/projects/your_work/components/app.vue'; + +jest.mock('~/alert'); + +describe('YourWorkProjectsApp', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(YourWorkProjectsApp); + }; + + const findPageText = () => wrapper.find('p'); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders Vue app with Projects list p tag', () => { + expect(findPageText().text()).toBe('Projects list'); + }); + }); +}); diff --git a/spec/lib/gitlab/background_migration/backfill_ml_candidate_metadata_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_ml_candidate_metadata_project_id_spec.rb new file mode 100644 index 00000000000..55755037db5 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_ml_candidate_metadata_project_id_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillMlCandidateMetadataProjectId, + feature_category: :mlops, + schema: 20240626142202 do + include_examples 'desired sharding key backfill job' do + let(:batch_table) { :ml_candidate_metadata } + let(:backfill_column) { :project_id } + let(:backfill_via_table) { :ml_candidates } + let(:backfill_via_column) { :project_id } + let(:backfill_via_foreign_key) { :candidate_id } + end +end diff --git a/spec/migrations/20240611122408_remove_project_statistics_wiki_size_and_project_id_index_spec.rb b/spec/migrations/20240611122408_remove_project_statistics_wiki_size_and_project_id_index_spec.rb new file mode 100644 index 00000000000..bd357e09404 --- /dev/null +++ b/spec/migrations/20240611122408_remove_project_statistics_wiki_size_and_project_id_index_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe RemoveProjectStatisticsWikiSizeAndProjectIdIndex, feature_category: :consumables_cost_management do + let(:migration) { described_class.new } + let(:postgres_async_indexes) { table(:postgres_async_indexes) } + + describe '#up' do + subject(:up) { migration.up } + + it 'does nothing when not on gitlab.com' do + expect { up }.not_to change { postgres_async_indexes.count } + end + + it 'prepares async index removal when on gitlab.com', :saas do + expect { up }.to change { postgres_async_indexes.count }.from(0).to(1) + end + end + + describe '#down' do + subject(:down) { migration.down } + + before do + postgres_async_indexes.create!( + name: 'index_project_statistics_on_wiki_size_and_project_id', + table_name: 'project_statistics', + definition: 'test index' + ) + end + + it 'does nothing when not on gitlab.com' do + expect { down }.not_to change { postgres_async_indexes.count } + end + + it 'unprepares async index removal when on gitlab.com', :saas do + expect { down }.to change { postgres_async_indexes.count }.from(1).to(0) + end + end +end diff --git a/spec/migrations/20240626142206_queue_backfill_ml_candidate_metadata_project_id_spec.rb b/spec/migrations/20240626142206_queue_backfill_ml_candidate_metadata_project_id_spec.rb new file mode 100644 index 00000000000..8f9ecaf544d --- /dev/null +++ b/spec/migrations/20240626142206_queue_backfill_ml_candidate_metadata_project_id_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillMlCandidateMetadataProjectId, feature_category: :mlops do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :ml_candidate_metadata, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE, + gitlab_schema: :gitlab_main_cell, + job_arguments: [ + :project_id, + :ml_candidates, + :project_id, + :candidate_id + ] + ) + } + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce4a9d941fd..feede09549a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -342,6 +342,10 @@ RSpec.configure do |config| # We want this this FF disabled by default stub_feature_flags(synced_epic_work_item_editable: false) + + # Since we are very early in the Vue migration, there isn't much value in testing when the feature flag is enabled + # Please see https://gitlab.com/gitlab-org/gitlab/-/issues/466081 for tracking revisiting this. + stub_feature_flags(your_work_projects_vue: false) else unstub_all_feature_flags end diff --git a/spec/views/dashboard/projects/index.html.haml_spec.rb b/spec/views/dashboard/projects/index.html.haml_spec.rb index 8e051b821c6..3d9f988c4d2 100644 --- a/spec/views/dashboard/projects/index.html.haml_spec.rb +++ b/spec/views/dashboard/projects/index.html.haml_spec.rb @@ -2,25 +2,96 @@ require 'spec_helper' -RSpec.describe 'dashboard/projects/index.html.haml' do +RSpec.describe 'dashboard/projects/index.html.haml', feature_category: :groups_and_projects do let_it_be(:user) { build(:user) } before do allow(view).to receive(:limited_counter_with_delimiter) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:time_ago_with_tooltip) - assign(:projects, [build(:project, name: 'awesome stuff')]) end - it 'shows the project the user is a member of in the list' do - render + context 'when feature :your_work_projects_vue is enabled' do + before do + stub_feature_flags(your_work_projects_vue: true) + end - expect(rendered).to have_content('awesome stuff') + context 'when projects exist' do + before do + assign(:projects, [build(:project, name: 'awesome stuff')]) + allow(view).to receive(:any_projects?).and_return(true) + render + end + + it 'renders #js-your-work-projects-app and not legacy project list' do + render + + expect(rendered).to have_selector('#js-your-work-projects-app') + expect(rendered).not_to render_template('dashboard/projects/_projects') + end + end + + context 'when projects do not exist' do + before do + allow(view).to receive(:any_projects?).and_return(false) + render + end + + it 'does not render #js-your-work-projects-app and renders empty state' do + render + + expect(rendered).not_to have_selector('#js-your-work-projects-app') + expect(rendered).to render_template('dashboard/projects/_zero_authorized_projects') + end + end end - it 'shows the "New project" button' do - render + context 'when feature :your_work_projects_vue is disabled' do + before do + stub_feature_flags(your_work_projects_vue: false) + end - expect(rendered).to have_link('New project') + context 'when projects exist' do + before do + assign(:projects, [build(:project, name: 'awesome stuff')]) + allow(view).to receive(:show_projects?).and_return(true) + render + end + + it 'shows the project the user is a member of in the list' do + expect(rendered).to have_content('awesome stuff') + end + + it 'shows the "New project" button' do + expect(rendered).to have_link('New project') + end + + it 'does not render zero_authorized_projects partial' do + expect(rendered).not_to render_template('dashboard/projects/_zero_authorized_projects') + end + + it 'does not render #js-your-work-projects-app' do + expect(rendered).not_to have_selector('#js-your-work-projects-app') + end + end + + context 'when projects do not exist' do + before do + allow(view).to receive(:show_projects?).and_return(false) + render + end + + it 'does not show the "New project" button' do + expect(rendered).not_to have_link('New project') + end + + it 'does render zero_authorized_projects partial' do + expect(rendered).to render_template('dashboard/projects/_zero_authorized_projects') + end + + it 'does not render #js-your-work-projects-app' do + expect(rendered).not_to have_selector('#js-your-work-projects-app') + end + end end end diff --git a/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb b/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb new file mode 100644 index 00000000000..9885f4aa802 --- /dev/null +++ b/spec/views/dashboard/projects/shared/_common.html.haml_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'dashboard/projects/shared/_common.html.haml', feature_category: :groups_and_projects do + let_it_be(:user) { build(:user) } + + before do + view.lookup_context.prefixes = ['dashboard/projects'] + + allow(view).to receive(:limited_counter_with_delimiter) + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:time_ago_with_tooltip) + allow(view).to receive(:empty_page).and_return('starred_empty_state') + end + + context 'when feature :your_work_projects_vue is enabled' do + before do + stub_feature_flags(your_work_projects_vue: true) + end + + context 'when projects exist' do + before do + assign(:projects, [build(:project, name: 'awesome stuff')]) + allow(view).to receive(:any_projects?).and_return(true) + render + end + + it 'renders #js-your-work-projects-app and not legacy project list' do + render + + expect(rendered).to have_selector('#js-your-work-projects-app') + expect(rendered).not_to render_template('dashboard/projects/_projects') + end + end + + context 'when projects do not exist' do + before do + allow(view).to receive(:any_projects?).and_return(false) + render + end + + it 'does not render #js-your-work-projects-app and renders empty state' do + render + + expect(rendered).not_to have_selector('#js-your-work-projects-app') + expect(rendered).to render_template('dashboard/projects/_starred_empty_state') + end + end + end + + context 'when feature :your_work_projects_vue is disabled' do + before do + stub_feature_flags(your_work_projects_vue: false) + end + + context 'when projects exist' do + before do + assign(:projects, [build(:project, name: 'awesome stuff')]) + allow(view).to receive(:any_projects?).and_return(true) + render + end + + it 'shows the project the user is a member of in the list' do + expect(rendered).to have_content('awesome stuff') + end + + it 'shows the "New project" button' do + expect(rendered).to have_link('New project') + end + + it 'does not render starred_empty_state partial' do + expect(rendered).not_to render_template('dashboard/projects/_starred_empty_state') + end + + it 'does not render #js-your-work-projects-app' do + expect(rendered).not_to have_selector('#js-your-work-projects-app') + end + end + + context 'when projects do not exist' do + before do + allow(view).to receive(:any_projects?).and_return(false) + render + end + + it 'does show the "New project" button' do + expect(rendered).to have_link('New project') + end + + it 'does render starred_empty_state partial' do + expect(rendered).to render_template('dashboard/projects/_starred_empty_state') + end + + it 'does not render #js-your-work-projects-app' do + expect(rendered).not_to have_selector('#js-your-work-projects-app') + end + end + end +end diff --git a/yarn.lock b/yarn.lock index e56857f0917..90bdb2bf36c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1331,10 +1331,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.103.0.tgz#af61387481100eadef2bea8fe8605250311ac582" integrity sha512-jVWCrRVRF6nw2A+Aowc0quXV2bdRPl2v08ElCPSestfdKjQ92tSlCrIsLB8GvdW5aI0eFsD1vJ1w2qkzZdpA4A== -"@gitlab/ui@85.3.0": - version "85.3.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-85.3.0.tgz#29abdfa121ca6720ca5558951319af20b55d0282" - integrity sha512-G8l8ZrdgOp6O6Azhypb/+HL5ZXU0/tIXjsHX7t6VkSBT7pSLBH06CnaH3Hrs8xEeCBSXfZDiMS4gq1FdEXGT2A== +"@gitlab/ui@85.4.1": + version "85.4.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-85.4.1.tgz#4cc7ed0bec0d022003e996a790d7ea9c37cce5ff" + integrity sha512-Q2QsIILLlipv6StUEAjS11OaJFkoZ5jlIE3QpFpJeGkHnskD6viRdnFcFDCYXwGMtrm1JphEp7iZZs6uX/MIkw== dependencies: "@floating-ui/dom" "1.4.3" echarts "^5.3.2"