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), = render Pajamas::ButtonComponent.new(href: admin_group_edit_path(@group),
button_options: { class: 'gl-float-right', data: { testid: 'edit-group-link' }}, button_options: { data: { testid: 'edit-group-link' }}) do
icon: 'pencil') do
= _('Edit') = _('Edit')
%hr
.row .gl-grid.md:gl-grid-cols-2.gl-gap-5
.col-md-6 .gl-flex.gl-flex-col.gl-gap-5
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c| = render ::Layouts::CrudComponent.new(_('Group information')) 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,15 +53,11 @@
= 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
- if @projects.any?
%ul.content-list.content-list-items-padding %ul.content-list.content-list-items-padding
- @projects.each do |project| - @projects.each do |project|
%li %li
@ -81,16 +66,17 @@
= gl_badge_tag storage_counter(project.statistics.storage_size) = gl_badge_tag storage_counter(project.statistics.storage_size)
%span.gl-float-right.light %span.gl-float-right.light
%span.monospace= project.full_path + '.git' %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 - unless @projects.size < Kaminari.config.default_per_page
- c.with_footer do - c.with_footer do
= paginate @projects, param_name: 'projects_page', theme: 'gitlab' = 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'
- if Feature.enabled?(:your_work_projects_vue, current_user)
#js-your-work-projects-app
- else
= render 'projects' = 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)
- if Feature.enabled?(:your_work_projects_vue, current_user)
#js-your-work-projects-app
- else
= render 'projects' = 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,6 +61,12 @@ module QA
private private
def has_project_with_access_role?(project_name, access_role) def has_project_with_access_role?(project_name, 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 within_element('project-content', text: project_name) do
has_element?('user-access-role', text: access_role) has_element?('user-access-role', text: access_role)
end end
@ -69,5 +75,6 @@ module QA
end end
end end
end end
end
QA::Page::Dashboard::Projects.prepend_mod_with('Page::Dashboard::Projects', namespace: QA) QA::Page::Dashboard::Projects.prepend_mod_with('Page::Dashboard::Projects', namespace: QA)

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,6 +12,31 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
sign_in(user) sign_in(user)
end end
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
context 'when feature :your_work_projects_vue is disabled' do
before do
stub_feature_flags(your_work_projects_vue: false)
end
it 'does not mount JS app' do
visit dashboard_projects_path
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 it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
before do before do
visit dashboard_projects_path visit dashboard_projects_path
@ -253,3 +278,4 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4) expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4)
end end
end end
end

View File

@ -146,6 +146,7 @@ describe('GroupsAndProjectsApp', () => {
}); });
describe('when filtered search bar is submitted', () => { describe('when filtered search bar is submitted', () => {
describe('when search term is 3 characters or more', () => {
const searchTerm = 'foo bar'; const searchTerm = 'foo bar';
beforeEach(() => { beforeEach(() => {
@ -161,6 +162,35 @@ describe('GroupsAndProjectsApp', () => {
}); });
}); });
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' },
});
});
});
});
describe('when display listbox is changed', () => { describe('when display listbox is changed', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();

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)
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')]) 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
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(:show_projects?).and_return(true)
render
end end
it 'shows the project the user is a member of in the list' do it 'shows the project the user is a member of in the list' do
render
expect(rendered).to have_content('awesome stuff') expect(rendered).to have_content('awesome stuff')
end end
it 'shows the "New project" button' do it 'shows the "New project" button' do
render
expect(rendered).to have_link('New project') expect(rendered).to have_link('New project')
end 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

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"