Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-27 03:26:28 +00:00
parent b7ef36fd90
commit 00e2fbb908
63 changed files with 1155 additions and 333 deletions

View File

@ -1 +1 @@
v17.1.0 v17.1.1

View File

@ -95,7 +95,7 @@ export default {
<div v-if="testCase.file" class="gl-display-flex gl-flex-wrap -gl-mx-4 gl-my-3"> <div v-if="testCase.file" class="gl-display-flex gl-flex-wrap -gl-mx-4 gl-my-3">
<strong class="col-sm-3">{{ $options.text.file }}</strong> <strong class="col-sm-3">{{ $options.text.file }}</strong>
<div class="col-sm-9" data-testid="test-case-file"> <div class="col-sm-9" data-testid="test-case-file">
<gl-link v-if="testCase.filePath" :href="testCase.filePath"> <gl-link v-if="testCase.filePath" class="gl-break-words" :href="testCase.filePath">
{{ testCase.file }} {{ testCase.file }}
</gl-link> </gl-link>
<span v-else>{{ testCase.file }}</span> <span v-else>{{ testCase.file }}</span>

View File

@ -1,6 +1,6 @@
<script> <script>
import { GlCollapsibleListbox } from '@gitlab/ui'; import { GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash'; import { isEqual, inRange } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import GroupsView from '~/organizations/shared/components/groups_view.vue'; import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue'; import ProjectsView from '~/organizations/shared/components/projects_view.vue';
@ -32,6 +32,7 @@ export default {
i18n: { i18n: {
pageTitle: __('Groups and projects'), pageTitle: __('Groups and projects'),
displayListboxHeaderText: __('Display'), displayListboxHeaderText: __('Display'),
filteredSearchPlaceholder: __('Search (3 character minimum)'),
}, },
components: { components: {
FilteredSearchAndSort, FilteredSearchAndSort,
@ -148,6 +149,13 @@ export default {
}, },
onFilter(filters) { onFilter(filters) {
const { display, sort_name, sort_direction } = this.$route.query; const { display, sort_name, sort_direction } = this.$route.query;
const { [FILTERED_SEARCH_TERM_KEY]: search = '' } = filters;
// API requires search to be 3 characters
// Don't search if length is between 1 and 3 characters
if (inRange(search.length, 1, 3)) {
return;
}
this.pushQuery({ this.pushQuery({
display, display,
@ -183,6 +191,7 @@ export default {
:is-ascending="isAscending" :is-ascending="isAscending"
:sort-options="$options.sortItems" :sort-options="$options.sortItems"
:active-sort-option="activeSortItem" :active-sort-option="activeSortItem"
:search-input-placeholder="$options.i18n.filteredSearchPlaceholder"
@filter="onFilter" @filter="onFilter"
@sort-direction-change="onSortDirectionChange" @sort-direction-change="onSortDirectionChange"
@sort-by-change="onSortByChange" @sort-by-change="onSortByChange"

View File

@ -0,0 +1,62 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import emptySearchSvgPath from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
import { __ } from '~/locale';
export default {
i18n: {
title: __('No results found'),
description: __('Edit your criteria and try again.'),
},
components: { GlEmptyState },
props: {
svgPath: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
search: {
type: String,
required: true,
},
},
computed: {
glEmptyStateProps() {
const baseProps = {
svgHeight: 144,
};
if (this.search !== '') {
return {
...baseProps,
svgPath: emptySearchSvgPath,
title: this.$options.i18n.title,
description: this.$options.i18n.description,
};
}
return {
...baseProps,
svgPath: this.svgPath,
title: this.title,
description: this.description,
};
},
},
};
</script>
<template>
<gl-empty-state v-bind="glEmptyStateProps">
<template #actions>
<slot name="actions"></slot>
</template>
</gl-empty-state>
</template>

View File

@ -1,5 +1,5 @@
<script> <script>
import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { createAlert } from '~/alert'; import { createAlert } from '~/alert';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue'; import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
@ -15,6 +15,7 @@ import {
import groupsQuery from '../graphql/queries/groups.query.graphql'; import groupsQuery from '../graphql/queries/groups.query.graphql';
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants'; import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import NewGroupButton from './new_group_button.vue'; import NewGroupButton from './new_group_button.vue';
import GroupsAndProjectsEmptyState from './groups_and_projects_empty_state.vue';
export default { export default {
i18n: { i18n: {
@ -32,7 +33,13 @@ export default {
}, },
group: __('Group'), group: __('Group'),
}, },
components: { GlLoadingIcon, GlEmptyState, GlKeysetPagination, GroupsList, NewGroupButton }, components: {
GlLoadingIcon,
GlKeysetPagination,
GroupsList,
NewGroupButton,
GroupsAndProjectsEmptyState,
},
inject: { inject: {
organizationGid: {}, organizationGid: {},
groupsEmptyStateSvgPath: {}, groupsEmptyStateSvgPath: {},
@ -140,16 +147,6 @@ export default {
isLoading() { isLoading() {
return this.$apollo.queries.groups.loading; return this.$apollo.queries.groups.loading;
}, },
emptyStateProps() {
const baseProps = {
svgHeight: 144,
svgPath: this.groupsEmptyStateSvgPath,
title: this.$options.i18n.emptyState.title,
description: this.$options.i18n.emptyState.description,
};
return baseProps;
},
timestampType() { timestampType() {
return timestampType(this.sortName); return timestampType(this.sortName);
}, },
@ -203,9 +200,15 @@ export default {
<gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" /> <gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" />
</div> </div>
</div> </div>
<gl-empty-state v-else v-bind="emptyStateProps"> <groups-and-projects-empty-state
v-else
:svg-path="groupsEmptyStateSvgPath"
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:search="search"
>
<template v-if="shouldShowEmptyStateButtons" #actions> <template v-if="shouldShowEmptyStateButtons" #actions>
<new-group-button /> <new-group-button />
</template> </template>
</gl-empty-state> </groups-and-projects-empty-state>
</template> </template>

View File

@ -1,5 +1,5 @@
<script> <script>
import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants'; import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
@ -15,6 +15,7 @@ import {
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants'; import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import projectsQuery from '../graphql/queries/projects.query.graphql'; import projectsQuery from '../graphql/queries/projects.query.graphql';
import NewProjectButton from './new_project_button.vue'; import NewProjectButton from './new_project_button.vue';
import GroupsAndProjectsEmptyState from './groups_and_projects_empty_state.vue';
export default { export default {
i18n: { i18n: {
@ -35,9 +36,9 @@ export default {
components: { components: {
ProjectsList, ProjectsList,
GlLoadingIcon, GlLoadingIcon,
GlEmptyState,
GlKeysetPagination, GlKeysetPagination,
NewProjectButton, NewProjectButton,
GroupsAndProjectsEmptyState,
}, },
inject: { inject: {
organizationGid: {}, organizationGid: {},
@ -146,16 +147,6 @@ export default {
isLoading() { isLoading() {
return this.$apollo.queries.projects.loading; return this.$apollo.queries.projects.loading;
}, },
emptyStateProps() {
const baseProps = {
svgHeight: 144,
svgPath: this.projectsEmptyStateSvgPath,
title: this.$options.i18n.emptyState.title,
description: this.$options.i18n.emptyState.description,
};
return baseProps;
},
timestampType() { timestampType() {
return timestampType(this.sortName); return timestampType(this.sortName);
}, },
@ -208,9 +199,15 @@ export default {
<gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" /> <gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" />
</div> </div>
</div> </div>
<gl-empty-state v-else v-bind="emptyStateProps"> <groups-and-projects-empty-state
v-else
:svg-path="projectsEmptyStateSvgPath"
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:search="search"
>
<template v-if="shouldShowEmptyStateButtons" #actions> <template v-if="shouldShowEmptyStateButtons" #actions>
<new-project-button /> <new-project-button />
</template> </template>
</gl-empty-state> </groups-and-projects-empty-state>
</template> </template>

View File

@ -0,0 +1,3 @@
import { initYourWorkProjects } from '~/projects/your_work';
initYourWorkProjects();

View File

@ -0,0 +1,16 @@
<script>
import { __ } from '~/locale';
export default {
name: 'YourWorkProjectsApp',
i18n: {
listText: __('Projects list'),
},
};
</script>
<template>
<div>
<p>{{ $options.i18n.listText }}</p>
</div>
</template>

View File

@ -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);
},
});
};

View File

@ -1,7 +1,7 @@
- page_title s_('BackgroundMigrations|Background Migrations') - page_title s_('BackgroundMigrations|Background Migrations')
- @breadcrumb_link = admin_background_migrations_path(database: params[:database]) - @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 .gl-flex-grow-1
%h3= s_('BackgroundMigrations|Background Migrations') %h3= s_('BackgroundMigrations|Background Migrations')
%p.light.gl-mb-0 %p.light.gl-mb-0

View File

@ -4,19 +4,15 @@
- page_title @group.name, _("Groups") - page_title @group.name, _("Groups")
- current_user_is_group_owner = @group && @group.has_owner?(current_user) - current_user_is_group_owner = @group && @group.has_owner?(current_user)
%h1.page-title.gl-font-size-h-display = render ::Layouts::PageHeadingComponent.new(@group.full_name) do |c|
= _('Group: %{group_name}') % { group_name: @group.full_name } - 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), .gl-grid.md:gl-grid-cols-2.gl-gap-5
button_options: { class: 'gl-float-right', data: { testid: 'edit-group-link' }}, .gl-flex.gl-flex-col.gl-gap-5
icon: 'pencil') do = render ::Layouts::CrudComponent.new(_('Group information')) do |c|
= _('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:')
- c.with_body do - c.with_body do
%ul.content-list.content-list-items-padding %ul.content-list.content-list-items-padding
%li %li
@ -29,32 +25,25 @@
%span.light= _('Path:') %span.light= _('Path:')
%strong %strong
= @group.path = @group.path
%li %li
%span.light= _('Description:') %span.light= _('Description:')
%strong %strong
= @group.description = @group.description
%li %li
%span.light= _('Visibility level:') %span.light= _('Visibility level:')
%strong %strong
= visibility_level_label(@group.visibility_level) = visibility_level_label(@group.visibility_level)
%li %li
%span.light= _('Created on:') %span.light= _('Created on:')
%strong %strong
= @group.created_at.to_fs(:medium) = @group.created_at.to_fs(:medium)
%li %li
%span.light= _('ID:') %span.light= _('ID:')
%strong %strong
= @group.id = @group.id
= render_if_exists 'admin/namespace_plan_info', namespace: @group = render_if_exists 'admin/namespace_plan_info', namespace: @group
%li %li
= render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group = render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group
%li %li
%span.light= _('Group Git LFS status:') %span.light= _('Group Git LFS status:')
%strong %strong
@ -64,33 +53,30 @@
= render_if_exists 'namespaces/shared_runner_status', namespace: @group = render_if_exists 'namespaces/shared_runner_status', namespace: @group
= render_if_exists 'namespaces/additional_minutes_status', namespace: @group = render_if_exists 'namespaces/additional_minutes_status', namespace: @group
= render 'shared/custom_attributes', custom_attributes: @group.custom_attributes = render ::Layouts::CrudComponent.new(_('Projects'),
icon: 'project',
= render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group count: @group.projects.count) do |c|
= 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
- c.with_body do - c.with_body do
%ul.content-list.content-list-items-padding - if @projects.any?
- @projects.each do |project| %ul.content-list.content-list-items-padding
%li - @projects.each do |project|
%strong %li
= link_to project.full_name, [:admin, project] %strong
= gl_badge_tag storage_counter(project.statistics.storage_size) = link_to project.full_name, [:admin, project]
%span.gl-float-right.light = gl_badge_tag storage_counter(project.statistics.storage_size)
%span.monospace= project.full_path + '.git' %span.gl-float-right.light
- unless @projects.size < Kaminari.config.default_per_page %span.monospace= project.full_path + '.git'
- c.with_footer do - else
= paginate @projects, param_name: 'projects_page', theme: 'gitlab' %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) - shared_projects = @group.shared_projects.sort_by(&:name)
- unless shared_projects.empty? - unless shared_projects.empty?
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c| = render ::Layouts::CrudComponent.new(_('Projects shared with %{group_name}') % { group_name: @group.name },
- c.with_header do icon: 'project',
= _('Projects shared with %{group_name}') % { group_name: @group.name } count: shared_projects.size) do |c|
= gl_badge_tag shared_projects.size
- c.with_body do - c.with_body do
%ul.content-list.content-list-items-padding %ul.content-list.content-list-items-padding
- shared_projects.each do |project| - shared_projects.each do |project|
@ -101,16 +87,16 @@
%span.gl-float-right.light %span.gl-float-right.light
%span.monospace= project.full_path + '.git' %span.monospace= project.full_path + '.git'
.col-md-6
= render 'shared/admin/admin_note'
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
= render 'shared/members/requests', membership_source: @group, group: @group, requesters: @requesters, force_mobile_view: true = 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| .gl-flex.gl-flex-col.gl-gap-5
- c.with_header do = render 'shared/admin/admin_note'
= html_escape(_("%{group_name} group members")) % { group_name: "<strong>#{html_escape(@group.name)}</strong>".html_safe }
= gl_badge_tag @group.users_count = 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) = render 'shared/members/manage_access_button', path: group_group_members_path(@group)
- c.with_body do - c.with_body do
%ul.content-list.group-users-list.members-list %ul.content-list.group-users-list.members-list
@ -122,3 +108,7 @@
- unless @members.size < Kaminari.config.default_per_page - unless @members.size < Kaminari.config.default_per_page
- c.with_footer do - c.with_footer do
= paginate @members, param_name: 'members_page', theme: 'gitlab' = 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

View File

@ -11,6 +11,9 @@
- if show_projects?(@projects, params) - if show_projects?(@projects, params)
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
= render 'nav' = render 'nav'
= render 'projects' - if Feature.enabled?(:your_work_projects_vue, current_user)
#js-your-work-projects-app
- else
= render 'projects'
- else - else
= render "zero_authorized_projects" = render "zero_authorized_projects"

View File

@ -6,6 +6,9 @@
= render 'dashboard/projects_head', project_tab_filter: :starred = render 'dashboard/projects_head', project_tab_filter: :starred
- if params[:filter_projects] || any_projects?(@projects) - 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 - else
= render empty_page = render empty_page

View File

@ -1,8 +1,9 @@
- return unless custom_attributes.present? - return unless custom_attributes.present?
= render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0' }) do |c| = render ::Layouts::CrudComponent.new(_('Custom attributes')) do |c|
- c.with_header do - c.with_actions do
= link_to(_('Custom Attributes'), help_page_path('api/custom_attributes')) = render Pajamas::ButtonComponent.new(href: help_page_path('api/custom_attributes'), size: :small, category: :tertiary, variant: :confirm) do
= _('Learn more')
- c.with_body do - c.with_body do
%ul.content-list %ul.content-list
- custom_attributes.each do |custom_attribute| - custom_attributes.each do |custom_attribute|

View File

@ -1,7 +1,5 @@
- if @group.admin_note&.note? - if @group.admin_note&.note?
- text = @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| = render ::Layouts::CrudComponent.new(s_('Admin|Admin notes')) do |c|
- c.with_header do
= s_('Admin|Admin notes')
- c.with_body do - c.with_body do
%p= text %p= text

View File

@ -1,5 +1,5 @@
- path = local_assigns.fetch(:path, nil) - path = local_assigns.fetch(:path, nil)
.gl-float-right .gl-float-right
= link_button_to path, size: :small, icon: 'pencil' do = render Pajamas::ButtonComponent.new(href: path, size: :small) do
= _('Manage access') = _('Manage access')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -19,3 +19,4 @@ desired_sharding_key:
table: ml_candidates table: ml_candidates
sharding_key: project_id sharding_key: project_id
belongs_to: candidate belongs_to: candidate
desired_sharding_key_migration_job_name: BackfillMlCandidateMetadataProjectId

View File

@ -10,3 +10,7 @@ milestone: '8.16'
gitlab_schema: gitlab_main_cell gitlab_schema: gitlab_main_cell
sharding_key: sharding_key:
project_id: projects 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
4704d2511a36e5af924d1b92fcf0c425d6868aa848f29431d8bff6410c5c967e

View File

@ -0,0 +1 @@
6f396093e4cd6ec606540af198fe04853bebd74b51702e06ff171ed0b8c12921

View File

@ -0,0 +1 @@
1e15fef4845db49e85604b7e959fda58bcaa623a6408620f7d0a9d882c26438a

View File

@ -0,0 +1 @@
5f2cf06eb8a34e95b71704fa544e8e1eb0996435dea2c7c892e850590b42777b

View File

@ -0,0 +1 @@
42601d461c570aa82234d58db59784158f044f0bafdbe6b83855123d21bf61e9

View File

@ -0,0 +1 @@
06dda6411338c7bdd342da847d11ba8512279f09e85b4dfa26991fbb8184b3e0

View File

@ -934,6 +934,22 @@ RETURN NEW;
END 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 CREATE FUNCTION trigger_25fe4f7da510() RETURNS trigger
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
@ -12314,7 +12330,7 @@ CREATE TABLE merge_request_context_commits (
message text, message text,
merge_request_id bigint, merge_request_id bigint,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL, trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
project_id bigint project_id bigint,
CONSTRAINT check_1dc6b5f2ac CHECK ((merge_request_id IS NOT NULL)) CONSTRAINT check_1dc6b5f2ac CHECK ((merge_request_id IS NOT NULL))
); );
@ -12764,6 +12780,7 @@ CREATE TABLE ml_candidate_metadata (
candidate_id bigint NOT NULL, candidate_id bigint NOT NULL,
name text NOT NULL, name text NOT NULL,
value text NOT NULL, value text NOT NULL,
project_id bigint,
CONSTRAINT check_6b38a286a5 CHECK ((char_length(name) <= 255)), CONSTRAINT check_6b38a286a5 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_9453f4a8e9 CHECK ((char_length(value) <= 5000)) 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_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_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); 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_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_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(); 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 ALTER TABLE ONLY fork_network_members
ADD CONSTRAINT fk_b01280dae4 FOREIGN KEY (forked_from_project_id) REFERENCES projects(id) ON DELETE SET NULL; 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 ALTER TABLE ONLY sbom_occurrences
ADD CONSTRAINT fk_b1b65d8d17 FOREIGN KEY (source_package_id) REFERENCES sbom_source_packages(id) ON DELETE CASCADE; ADD CONSTRAINT fk_b1b65d8d17 FOREIGN KEY (source_package_id) REFERENCES sbom_source_packages(id) ON DELETE CASCADE;

View File

@ -18082,7 +18082,7 @@ CI/CD variables for a project.
| <a id="cirunnerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner. | | <a id="cirunnerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner. |
| <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. | | <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. |
| <a id="cirunnertokenexpiresat"></a>`tokenExpiresAt` | [`Time`](#time) | Runner token expiration time. | | <a id="cirunnertokenexpiresat"></a>`tokenExpiresAt` | [`Time`](#time) | Runner token expiration time. |
| <a id="cirunnerupgradestatus"></a>`upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | **Introduced** in GitLab 14.10. **Status**: Experiment. Availability of upgrades for the runner. | | <a id="cirunnerupgradestatus"></a>`upgradeStatus` | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | Availability of upgrades for the runner. |
| <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. | | <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. |
#### Fields with arguments #### Fields with arguments

View File

@ -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. > - [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. > - `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: FLAG:
The availability of this feature is controlled by feature flags. 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. - You must be on GitLab Runner 17.0 and later.
- An administrator must enable the `FF_TIMESTAMPS` - An administrator must enable the `FF_TIMESTAMPS`
[feature flag](../../administration/feature_flags.md) in `.gitlab-ci.yml`. [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 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. [ISO 8601 format](https://www.iso.org/iso-8601-date-and-time-format.html) for each line in the CI log.

View File

@ -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. Optional. To link a self-managed GitLab instance with Jira, select **Change GitLab version**.
1. Select all checkboxes, then select **Next**. 1. Select all checkboxes, then select **Next**.
1. Enter your **GitLab instance URL**, then select **Save**. 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 **Authorize**. A list of groups is now visible.
1. Select **Link groups**. 1. Select **Link groups**.
1. To link to a group, select **Link**. 1. To link to a group, select **Link**.

View File

@ -81,7 +81,7 @@ To switch organizations:
1. On the left sidebar, select **Organizations** and find the organization you want to manage. 1. On the left sidebar, select **Organizations** and find the organization you want to manage.
1. Select **Manage > Groups and projects**. 1. Select **Manage > Groups and projects**.
1. Optional. Filter the results: 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. - 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. 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.

View File

@ -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

View File

@ -90,6 +90,7 @@ namespace :tw do
CONTRIBUTOR_DOCS_PATH = '/doc/development/' CONTRIBUTOR_DOCS_PATH = '/doc/development/'
CONTRIBUTOR_DOCS_CODE_OWNER_RULES = [ CONTRIBUTOR_DOCS_CODE_OWNER_RULES = [
CodeOwnerRule.new('AI-powered', '@gitlab-org/ai-powered'),
CodeOwnerRule.new('Analytics Instrumentation', CodeOwnerRule.new('Analytics Instrumentation',
'@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \ '@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \
'@gitlab-org/analytics-section/analytics-instrumentation/engineers'), '@gitlab-org/analytics-section/analytics-instrumentation/engineers'),

View File

@ -832,9 +832,6 @@ msgstr ""
msgid "%{group_name} activity" msgid "%{group_name} activity"
msgstr "" msgstr ""
msgid "%{group_name} group members"
msgstr ""
msgid "%{group_name} is approaching the limit of available seats" msgid "%{group_name} is approaching the limit of available seats"
msgstr "" msgstr ""
@ -16079,6 +16076,9 @@ msgstr ""
msgid "Custom analyzers: language support" msgid "Custom analyzers: language support"
msgstr "" msgstr ""
msgid "Custom attributes"
msgstr ""
msgid "Custom confirmation message: %{message}" msgid "Custom confirmation message: %{message}"
msgstr "" msgstr ""
@ -19765,6 +19765,9 @@ msgstr ""
msgid "Edit wiki page" msgid "Edit wiki page"
msgstr "" msgstr ""
msgid "Edit your criteria and try again."
msgstr ""
msgid "Edit your most recent comment in a thread (from an empty textarea)" msgid "Edit your most recent comment in a thread (from an empty textarea)"
msgstr "" msgstr ""
@ -25077,9 +25080,6 @@ msgstr ""
msgid "Group import requests" msgid "Group import requests"
msgstr "" msgstr ""
msgid "Group info:"
msgstr ""
msgid "Group information" msgid "Group information"
msgstr "" msgstr ""
@ -34940,6 +34940,9 @@ msgstr ""
msgid "No project subscribes to the pipelines in this project." msgid "No project subscribes to the pipelines in this project."
msgstr "" msgstr ""
msgid "No projects"
msgstr ""
msgid "No projects available." msgid "No projects available."
msgstr "" msgstr ""
@ -42158,6 +42161,9 @@ msgstr ""
msgid "Projects in this group can use Git LFS" msgid "Projects in this group can use Git LFS"
msgstr "" msgstr ""
msgid "Projects list"
msgstr ""
msgid "Projects shared with %{group_name}" msgid "Projects shared with %{group_name}"
msgstr "" 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." msgid "SecurityOrchestration|Branch: %{boldStart}%{branchName}%{boldEnd} was not found in project: %{boldStart}%{projectName}%{boldEnd}. Edit or remove this entry."
msgstr "" 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" msgid "SecurityOrchestration|Cadence is invalid"
msgstr "" msgstr ""
@ -48083,6 +48095,9 @@ msgstr ""
msgid "SecurityOrchestration|Security Scan" msgid "SecurityOrchestration|Security Scan"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Security job template"
msgstr ""
msgid "SecurityOrchestration|Security policy overwrites this setting" msgid "SecurityOrchestration|Security policy overwrites this setting"
msgstr "" msgstr ""
@ -48368,6 +48383,9 @@ msgstr ""
msgid "SecurityOrchestration|compliance frameworks" msgid "SecurityOrchestration|compliance frameworks"
msgstr "" msgstr ""
msgid "SecurityOrchestration|default"
msgstr ""
msgid "SecurityOrchestration|except projects" msgid "SecurityOrchestration|except projects"
msgstr "" msgstr ""
@ -48383,6 +48401,9 @@ msgstr ""
msgid "SecurityOrchestration|have no fix available" msgid "SecurityOrchestration|have no fix available"
msgstr "" msgstr ""
msgid "SecurityOrchestration|latest"
msgstr ""
msgid "SecurityOrchestration|more than %{allowed}" msgid "SecurityOrchestration|more than %{allowed}"
msgstr "" msgstr ""

View File

@ -66,7 +66,7 @@
"@gitlab/favicon-overlay": "2.0.0", "@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0", "@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.103.0", "@gitlab/svgs": "3.103.0",
"@gitlab/ui": "85.3.0", "@gitlab/ui": "85.4.1",
"@gitlab/web-ide": "^0.0.1-dev-20240613133550", "@gitlab/web-ide": "^0.0.1-dev-20240613133550",
"@mattiasbuelens/web-streams-adapter": "^0.1.0", "@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-4", "@rails/actioncable": "7.0.8-4",

View File

@ -61,8 +61,15 @@ module QA
private private
def has_project_with_access_role?(project_name, access_role) def has_project_with_access_role?(project_name, access_role)
within_element('project-content', text: project_name) do # Since we are very early in the Vue migration, there isn't much value in testing
has_element?('user-access-role', text: access_role) # 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 end
end end

View File

@ -110,7 +110,7 @@ RSpec.describe 'Admin Groups', feature_category: :groups_and_projects do
visit admin_group_path(group) 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}") expect(page).to have_content("ID: #{group.id}")
end end

View File

@ -499,7 +499,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
within(:css, '.gl-mb-3 + .gl-card') do within(:css, '.gl-mb-3 + .gl-card') do
click_link group.name click_link group.name
end end
expect(page).to have_content "Group: #{group.name}" expect(page).to have_content group.name
expect(page).to have_content project.name expect(page).to have_content project.name
end end

View File

@ -12,244 +12,270 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
sign_in(user) sign_in(user)
end 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 before do
stub_feature_flags(your_work_projects_vue: true)
end
it 'mounts JS app' do
visit dashboard_projects_path visit dashboard_projects_path
expect(page).to have_content('Projects')
expect(page).to have_content('Projects list')
end end
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 it 'does not mount JS app' 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 visit dashboard_projects_path
within_testid('user-access-role') do expect(page).to have_content('Projects')
expect(page).to have_content('Developer') 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
end end
context 'when role changes', :use_clean_rails_memory_store_fragment_caching do it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_projects_path, :projects
it 'displays the right role' do
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 visit dashboard_projects_path
within_testid('user-access-role') do within_testid('user-access-role') do
expect(page).to have_content('Developer') expect(page).to have_content('Developer')
end 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 project.members.last.update!(access_level: 40)
expect(page).to have_content('Maintainer')
visit dashboard_projects_path
within_testid('user-access-role') do
expect(page).to have_content('Maintainer')
end
end end
end end
end end
end
context 'when last_activity_at and update_at are present' do context 'when last_activity_at and update_at are present' do
it 'shows the last_activity_at attribute as the update date' 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) 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}']") 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')
end end
end end
shared_examples 'hidden pipeline status' do context 'when last_activity_at is missing' do
it 'does not show the pipeline status' 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 visit dashboard_projects_path
within_testid('project_controls') do 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).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).to have_css("[data-testid='ci-icon']")
expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]') expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_link('Pipeline: passed') 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 end
end end
context 'guest user of project and project has private pipelines' do context 'last push widget', :use_clean_rails_memory_store_caching do
let(:guest_user) { create(:user) }
before do before do
project.update!(public_builds: false) event = create(:push_event, project: project, author: user)
project.add_guest(guest_user)
sign_in(guest_user)
end
it_behaves_like 'hidden pipeline status' create(:push_event_payload, event: event, ref: 'feature', action: :created)
end
context "when last_pipeline is missing" do Users::LastPushEventService.new(user).cache_last_push_event(event)
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 visit dashboard_projects_path
end
expect(page).to have_selector('[data-testid="project_topic_list"]') it 'shows "Create merge request" button' do
expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1')) 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
end end
context 'when project does not have topics' do it 'avoids an N+1 query in dashboard index' do
it 'does not show project topics' do create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha)
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)
visit dashboard_projects_path 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
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
end end

View File

@ -146,17 +146,47 @@ describe('GroupsAndProjectsApp', () => {
}); });
describe('when filtered search bar is submitted', () => { 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(() => { beforeEach(() => {
createComponent(); 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`, () => { describe('when search term is less than 3 characters', () => {
expect(routerMock.push).toHaveBeenCalledWith({ const searchTerm = 'fo';
query: { [FILTERED_SEARCH_TERM_KEY]: searchTerm },
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' },
});
}); });
}); });
}); });

View File

@ -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: '<div data-testid="actions-slot"></div>' } });
expect(wrapper.findByTestId('actions-slot').exists()).toBe(true);
});
});

View File

@ -1,10 +1,11 @@
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vue from 'vue'; 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 organizationGroupsGraphQlResponse from 'test_fixtures/graphql/organizations/groups.query.graphql.json';
import GroupsView from '~/organizations/shared/components/groups_view.vue'; import GroupsView from '~/organizations/shared/components/groups_view.vue';
import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants'; import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue'; import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
import GroupsAndProjectsEmptyState from '~/organizations/shared/components/groups_and_projects_empty_state.vue';
import { import {
renderDeleteSuccessToast, renderDeleteSuccessToast,
deleteParams, deleteParams,
@ -142,12 +143,12 @@ describe('GroupsView', () => {
await waitForPromises(); await waitForPromises();
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ expect(wrapper.findComponent(GroupsAndProjectsEmptyState).props()).toMatchObject({
title: "You don't have any groups yet.", title: "You don't have any groups yet.",
description: description:
'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.', '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, svgPath: defaultProvide.groupsEmptyStateSvgPath,
search: 'foo',
}); });
expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons); expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons);

View File

@ -1,10 +1,11 @@
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vue from 'vue'; 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 organizationProjectsGraphQlResponse from 'test_fixtures/graphql/organizations/projects.query.graphql.json';
import ProjectsView from '~/organizations/shared/components/projects_view.vue'; import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants'; import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue'; 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 projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
import { import {
renderDeleteSuccessToast, renderDeleteSuccessToast,
@ -95,7 +96,7 @@ describe('ProjectsView', () => {
const findPagination = () => wrapper.findComponent(GlKeysetPagination); const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GroupsAndProjectsEmptyState);
const findProjectsList = () => wrapper.findComponent(ProjectsList); const findProjectsList = () => wrapper.findComponent(ProjectsList);
const findProjectsListProjectById = (projectId) => const findProjectsListProjectById = (projectId) =>
findProjectsList() findProjectsList()
@ -149,8 +150,8 @@ describe('ProjectsView', () => {
title: "You don't have any projects yet.", title: "You don't have any projects yet.",
description: description:
'Projects are where you can store your code, access issues, wiki, and other features of GitLab.', 'Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
svgHeight: 144,
svgPath: defaultProvide.projectsEmptyStateSvgPath, svgPath: defaultProvide.projectsEmptyStateSvgPath,
search: 'foo',
}); });
expect(findNewProjectButton().exists()).toBe(shouldShowEmptyStateButtons); expect(findNewProjectButton().exists()).toBe(shouldShowEmptyStateButtons);

View File

@ -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');
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -342,6 +342,10 @@ RSpec.configure do |config|
# We want this this FF disabled by default # We want this this FF disabled by default
stub_feature_flags(synced_epic_work_item_editable: false) 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 else
unstub_all_feature_flags unstub_all_feature_flags
end end

View File

@ -2,25 +2,96 @@
require 'spec_helper' 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) } let_it_be(:user) { build(:user) }
before do before do
allow(view).to receive(:limited_counter_with_delimiter) allow(view).to receive(:limited_counter_with_delimiter)
allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:time_ago_with_tooltip) allow(view).to receive(:time_ago_with_tooltip)
assign(:projects, [build(:project, name: 'awesome stuff')])
end end
it 'shows the project the user is a member of in the list' do context 'when feature :your_work_projects_vue is enabled' do
render 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 end
it 'shows the "New project" button' do context 'when feature :your_work_projects_vue is disabled' do
render 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
end end

View File

@ -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

View File

@ -1331,10 +1331,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.103.0.tgz#af61387481100eadef2bea8fe8605250311ac582" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.103.0.tgz#af61387481100eadef2bea8fe8605250311ac582"
integrity sha512-jVWCrRVRF6nw2A+Aowc0quXV2bdRPl2v08ElCPSestfdKjQ92tSlCrIsLB8GvdW5aI0eFsD1vJ1w2qkzZdpA4A== integrity sha512-jVWCrRVRF6nw2A+Aowc0quXV2bdRPl2v08ElCPSestfdKjQ92tSlCrIsLB8GvdW5aI0eFsD1vJ1w2qkzZdpA4A==
"@gitlab/ui@85.3.0": "@gitlab/ui@85.4.1":
version "85.3.0" version "85.4.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-85.3.0.tgz#29abdfa121ca6720ca5558951319af20b55d0282" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-85.4.1.tgz#4cc7ed0bec0d022003e996a790d7ea9c37cce5ff"
integrity sha512-G8l8ZrdgOp6O6Azhypb/+HL5ZXU0/tIXjsHX7t6VkSBT7pSLBH06CnaH3Hrs8xEeCBSXfZDiMS4gq1FdEXGT2A== integrity sha512-Q2QsIILLlipv6StUEAjS11OaJFkoZ5jlIE3QpFpJeGkHnskD6viRdnFcFDCYXwGMtrm1JphEp7iZZs6uX/MIkw==
dependencies: dependencies:
"@floating-ui/dom" "1.4.3" "@floating-ui/dom" "1.4.3"
echarts "^5.3.2" echarts "^5.3.2"