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">
<strong class="col-sm-3">{{ $options.text.file }}</strong>
<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 }}
</gl-link>
<span v-else>{{ testCase.file }}</span>

View File

@ -1,6 +1,6 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { isEqual, inRange } from 'lodash';
import { __ } from '~/locale';
import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
@ -32,6 +32,7 @@ export default {
i18n: {
pageTitle: __('Groups and projects'),
displayListboxHeaderText: __('Display'),
filteredSearchPlaceholder: __('Search (3 character minimum)'),
},
components: {
FilteredSearchAndSort,
@ -148,6 +149,13 @@ export default {
},
onFilter(filters) {
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({
display,
@ -183,6 +191,7 @@ export default {
:is-ascending="isAscending"
:sort-options="$options.sortItems"
:active-sort-option="activeSortItem"
:search-input-placeholder="$options.i18n.filteredSearchPlaceholder"
@filter="onFilter"
@sort-direction-change="onSortDirectionChange"
@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>
import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { s__, __ } from '~/locale';
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 { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import NewGroupButton from './new_group_button.vue';
import GroupsAndProjectsEmptyState from './groups_and_projects_empty_state.vue';
export default {
i18n: {
@ -32,7 +33,13 @@ export default {
},
group: __('Group'),
},
components: { GlLoadingIcon, GlEmptyState, GlKeysetPagination, GroupsList, NewGroupButton },
components: {
GlLoadingIcon,
GlKeysetPagination,
GroupsList,
NewGroupButton,
GroupsAndProjectsEmptyState,
},
inject: {
organizationGid: {},
groupsEmptyStateSvgPath: {},
@ -140,16 +147,6 @@ export default {
isLoading() {
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() {
return timestampType(this.sortName);
},
@ -203,9 +200,15 @@ export default {
<gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" />
</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>
<new-group-button />
</template>
</gl-empty-state>
</groups-and-projects-empty-state>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
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 projectsQuery from '../graphql/queries/projects.query.graphql';
import NewProjectButton from './new_project_button.vue';
import GroupsAndProjectsEmptyState from './groups_and_projects_empty_state.vue';
export default {
i18n: {
@ -35,9 +36,9 @@ export default {
components: {
ProjectsList,
GlLoadingIcon,
GlEmptyState,
GlKeysetPagination,
NewProjectButton,
GroupsAndProjectsEmptyState,
},
inject: {
organizationGid: {},
@ -146,16 +147,6 @@ export default {
isLoading() {
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() {
return timestampType(this.sortName);
},
@ -208,9 +199,15 @@ export default {
<gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" />
</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>
<new-project-button />
</template>
</gl-empty-state>
</groups-and-projects-empty-state>
</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')
- @breadcrumb_link = admin_background_migrations_path(database: params[:database])
.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_background_migrations_pageload' } }
.gl-flex-grow-1
%h3= s_('BackgroundMigrations|Background Migrations')
%p.light.gl-mb-0

View File

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

View File

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

View File

@ -6,6 +6,9 @@
= render 'dashboard/projects_head', project_tab_filter: :starred
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- if Feature.enabled?(:your_work_projects_vue, current_user)
#js-your-work-projects-app
- else
= render 'projects'
- else
= render empty_page

View File

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

View File

@ -1,7 +1,5 @@
- if @group.admin_note&.note?
- text = @group.admin_note.note
= render Pajamas::CardComponent.new(card_options: { class: 'gl-border-blue-500 gl-mb-5' }, header_options: { class: 'gl-bg-blue-500 gl-text-white' }) do |c|
- c.with_header do
= s_('Admin|Admin notes')
= render ::Layouts::CrudComponent.new(s_('Admin|Admin notes')) do |c|
- c.with_body do
%p= text

View File

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

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
sharding_key: project_id
belongs_to: candidate
desired_sharding_key_migration_job_name: BackfillMlCandidateMetadataProjectId

View File

@ -10,3 +10,7 @@ milestone: '8.16'
gitlab_schema: gitlab_main_cell
sharding_key:
project_id: projects
schema_inconsistencies:
- type: missing_indexes
object_name: index_project_statistics_on_wiki_size_and_project_id
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156010

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
$$;
CREATE FUNCTION trigger_25d35f02ab55() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW."project_id" IS NULL THEN
SELECT "project_id"
INTO NEW."project_id"
FROM "ml_candidates"
WHERE "ml_candidates"."id" = NEW."candidate_id";
END IF;
RETURN NEW;
END
$$;
CREATE FUNCTION trigger_25fe4f7da510() RETURNS trigger
LANGUAGE plpgsql
AS $$
@ -12314,7 +12330,7 @@ CREATE TABLE merge_request_context_commits (
message text,
merge_request_id bigint,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_1dc6b5f2ac CHECK ((merge_request_id IS NOT NULL))
);
@ -12764,6 +12780,7 @@ CREATE TABLE ml_candidate_metadata (
candidate_id bigint NOT NULL,
name text NOT NULL,
value text NOT NULL,
project_id bigint,
CONSTRAINT check_6b38a286a5 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_9453f4a8e9 CHECK ((char_length(value) <= 5000))
);
@ -27701,6 +27718,8 @@ CREATE UNIQUE INDEX index_ml_candidate_metadata_on_candidate_id_and_name ON ml_c
CREATE INDEX index_ml_candidate_metadata_on_name ON ml_candidate_metadata USING btree (name);
CREATE INDEX index_ml_candidate_metadata_on_project_id ON ml_candidate_metadata USING btree (project_id);
CREATE INDEX index_ml_candidate_metrics_on_candidate_id ON ml_candidate_metrics USING btree (candidate_id);
CREATE INDEX index_ml_candidate_params_on_candidate_id ON ml_candidate_params USING btree (candidate_id);
@ -31389,6 +31408,8 @@ CREATE TRIGGER trigger_2514245c7fc5 BEFORE INSERT OR UPDATE ON dast_site_profile
CREATE TRIGGER trigger_25c44c30884f BEFORE INSERT OR UPDATE ON work_item_parent_links FOR EACH ROW EXECUTE FUNCTION trigger_25c44c30884f();
CREATE TRIGGER trigger_25d35f02ab55 BEFORE INSERT OR UPDATE ON ml_candidate_metadata FOR EACH ROW EXECUTE FUNCTION trigger_25d35f02ab55();
CREATE TRIGGER trigger_25fe4f7da510 BEFORE INSERT OR UPDATE ON vulnerability_issue_links FOR EACH ROW EXECUTE FUNCTION trigger_25fe4f7da510();
CREATE TRIGGER trigger_2ac3d66ed1d3 BEFORE INSERT OR UPDATE ON vulnerability_occurrence_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_2ac3d66ed1d3();
@ -32492,6 +32513,9 @@ ALTER TABLE ONLY approval_merge_request_rules
ALTER TABLE ONLY fork_network_members
ADD CONSTRAINT fk_b01280dae4 FOREIGN KEY (forked_from_project_id) REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE ONLY ml_candidate_metadata
ADD CONSTRAINT fk_b044692715 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY sbom_occurrences
ADD CONSTRAINT fk_b1b65d8d17 FOREIGN KEY (source_package_id) REFERENCES sbom_source_packages(id) ON DELETE CASCADE;

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="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. |
| <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. |
#### 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.
> - `parse_ci_job_timestamps` [enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/455581) in GitLab 17.1.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/464785) in GitLab 17.2. Feature flag `parse_ci_job_timestamps` removed.
FLAG:
The availability of this feature is controlled by feature flags.
@ -22,8 +23,6 @@ Prerequisites:
- You must be on GitLab Runner 17.0 and later.
- An administrator must enable the `FF_TIMESTAMPS`
[feature flag](../../administration/feature_flags.md) in `.gitlab-ci.yml`.
- On self-managed GitLab, an administrator must enable
the `parse_ci_job_timestamps` feature flag.
This feature generates a timestamp in the
[ISO 8601 format](https://www.iso.org/iso-8601-date-and-time-format.html) for each line in the CI log.

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. Select all checkboxes, then select **Next**.
1. Enter your **GitLab instance URL**, then select **Save**.
1. Select **Sign in to GitLab**, then enter your credentials.
1. Select **Sign in to GitLab**, then enter your username and password. The integration doesn't support single-sign on such as SAML during configuration.
1. Select **Authorize**. A list of groups is now visible.
1. Select **Link groups**.
1. To link to a group, select **Link**.

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. Select **Manage > Groups and projects**.
1. Optional. Filter the results:
- To search for specific groups or projects, in the search box enter your search term.
- To search for specific groups or projects, in the search box enter your search term (minimum three characters).
- To view only groups or projects, from the **Display** dropdown list select an option.
1. Optional. To sort the results by name, date created, or date updated, from the dropdown list select an option. Then select ascending (**{sort-lowest}**) or descending (**{sort-highest}**) order.

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_CODE_OWNER_RULES = [
CodeOwnerRule.new('AI-powered', '@gitlab-org/ai-powered'),
CodeOwnerRule.new('Analytics Instrumentation',
'@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \
'@gitlab-org/analytics-section/analytics-instrumentation/engineers'),

View File

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

View File

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

View File

@ -61,8 +61,15 @@ module QA
private
def has_project_with_access_role?(project_name, access_role)
within_element('project-content', text: project_name) do
has_element?('user-access-role', text: access_role)
# Since we are very early in the Vue migration, there isn't much value in testing
# when the feature flag is enabled.
# Please see https://gitlab.com/gitlab-org/gitlab/-/issues/466081 for tracking revisiting this.
if Runtime::Feature.enabled?(:your_work_projects_vue)
has_text?('Projects list')
else
within_element('project-content', text: project_name) do
has_element?('user-access-role', text: access_role)
end
end
end
end

View File

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

View File

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

View File

@ -12,244 +12,270 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
sign_in(user)
end
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
context 'when feature :your_work_projects_vue is enabled' do
before do
stub_feature_flags(your_work_projects_vue: true)
end
it 'mounts JS app' do
visit dashboard_projects_path
expect(page).to have_content('Projects')
expect(page).to have_content('Projects list')
end
end
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_projects_path, :projects
context 'when feature :your_work_projects_vue is disabled' do
before do
stub_feature_flags(your_work_projects_vue: false)
end
it 'links to the "Explore projects" page' do
visit dashboard_projects_path
expect(page).to have_link("Explore projects", href: starred_explore_projects_path)
end
context 'when user has access to the project' do
it 'shows role badge' do
it 'does not mount JS app' do
visit dashboard_projects_path
within_testid('user-access-role') do
expect(page).to have_content('Developer')
expect(page).to have_content('Projects')
expect(page).not_to have_content('Projects list')
end
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
before do
visit dashboard_projects_path
end
end
context 'when role changes', :use_clean_rails_memory_store_fragment_caching do
it 'displays the right role' do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :dashboard_projects_path, :projects
it 'links to the "Explore projects" page' do
visit dashboard_projects_path
expect(page).to have_link("Explore projects", href: starred_explore_projects_path)
end
context 'when user has access to the project' do
it 'shows role badge' do
visit dashboard_projects_path
within_testid('user-access-role') do
expect(page).to have_content('Developer')
end
end
project.members.last.update!(access_level: 40)
context 'when role changes', :use_clean_rails_memory_store_fragment_caching do
it 'displays the right role' do
visit dashboard_projects_path
visit dashboard_projects_path
within_testid('user-access-role') do
expect(page).to have_content('Developer')
end
within_testid('user-access-role') do
expect(page).to have_content('Maintainer')
project.members.last.update!(access_level: 40)
visit dashboard_projects_path
within_testid('user-access-role') do
expect(page).to have_content('Maintainer')
end
end
end
end
end
context 'when last_activity_at and update_at are present' do
it 'shows the last_activity_at attribute as the update date' do
project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.zone.now)
context 'when last_activity_at and update_at are present' do
it 'shows the last_activity_at attribute as the update date' do
project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.zone.now)
visit dashboard_projects_path
visit dashboard_projects_path
expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']")
end
end
context 'when last_activity_at is missing' do
it 'shows the updated_at attribute as the update date' do
project.update!(last_activity_at: nil)
project.touch
visit dashboard_projects_path
expect(page).to have_xpath("//time[@datetime='#{project.updated_at.getutc.iso8601}']")
end
end
context 'when on Your projects tab' do
it 'shows all projects by default' do
visit dashboard_projects_path
expect(page).to have_content(project.name)
expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
end
it 'shows personal projects on personal projects tab' do
project3 = create(:project, namespace: user.namespace)
visit dashboard_projects_path
click_link 'Personal'
expect(page).not_to have_content(project.name)
expect(page).to have_content(project3.name)
end
it 'sorts projects by most stars when sorting by most stars' do
project_with_most_stars = create(:project, namespace: user.namespace, star_count: 10)
visit dashboard_projects_path(sort: :stars_desc)
expect(first('.project-row')).to have_content(project_with_most_stars.title)
end
end
context 'when on Starred projects tab' do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :starred_dashboard_projects_path, :projects
it 'shows the empty state when there are no starred projects' do
visit(starred_dashboard_projects_path)
expect(page).to have_text(s_("StarredProjectsEmptyState|You don't have starred projects yet."))
end
it 'shows only starred projects' do
user.toggle_star(project2)
visit(starred_dashboard_projects_path)
expect(page).not_to have_content(project.name)
expect(page).to have_content(project2.name)
expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
expect(find('.gl-tabs-nav li:nth-child(2) .badge-pill')).to have_content(1)
end
it 'does not show tabs to filter by all projects or personal' do
visit(starred_dashboard_projects_path)
expect(page).not_to have_content '.filtered-search-nav'
end
end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
before do
# Since the cache isn't updated when a new pipeline is created
# we need the pipeline to advance in the pipeline since the cache was created
# by visiting the login page.
pipeline.succeed
end
it 'shows that the last pipeline passed' do
visit dashboard_projects_path
within_testid('project_controls') do
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).to have_css("[data-testid='ci-icon']")
expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).to have_link('Pipeline: passed')
expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']")
end
end
shared_examples 'hidden pipeline status' do
it 'does not show the pipeline status' do
context 'when last_activity_at is missing' do
it 'shows the updated_at attribute as the update date' do
project.update!(last_activity_at: nil)
project.touch
visit dashboard_projects_path
expect(page).to have_xpath("//time[@datetime='#{project.updated_at.getutc.iso8601}']")
end
end
context 'when on Your projects tab' do
it 'shows all projects by default' do
visit dashboard_projects_path
expect(page).to have_content(project.name)
expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
end
it 'shows personal projects on personal projects tab' do
project3 = create(:project, namespace: user.namespace)
visit dashboard_projects_path
click_link 'Personal'
expect(page).not_to have_content(project.name)
expect(page).to have_content(project3.name)
end
it 'sorts projects by most stars when sorting by most stars' do
project_with_most_stars = create(:project, namespace: user.namespace, star_count: 10)
visit dashboard_projects_path(sort: :stars_desc)
expect(first('.project-row')).to have_content(project_with_most_stars.title)
end
end
context 'when on Starred projects tab' do
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :starred_dashboard_projects_path, :projects
it 'shows the empty state when there are no starred projects' do
visit(starred_dashboard_projects_path)
expect(page).to have_text(s_("StarredProjectsEmptyState|You don't have starred projects yet."))
end
it 'shows only starred projects' do
user.toggle_star(project2)
visit(starred_dashboard_projects_path)
expect(page).not_to have_content(project.name)
expect(page).to have_content(project2.name)
expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
expect(find('.gl-tabs-nav li:nth-child(2) .badge-pill')).to have_content(1)
end
it 'does not show tabs to filter by all projects or personal' do
visit(starred_dashboard_projects_path)
expect(page).not_to have_content '.filtered-search-nav'
end
end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
before do
# Since the cache isn't updated when a new pipeline is created
# we need the pipeline to advance in the pipeline since the cache was created
# by visiting the login page.
pipeline.succeed
end
it 'shows that the last pipeline passed' do
visit dashboard_projects_path
within_testid('project_controls') do
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).not_to have_css("[data-testid='ci-icon']")
expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_link('Pipeline: passed')
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).to have_css("[data-testid='ci-icon']")
expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).to have_link('Pipeline: passed')
end
end
shared_examples 'hidden pipeline status' do
it 'does not show the pipeline status' do
visit dashboard_projects_path
within_testid('project_controls') do
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).not_to have_css("[data-testid='ci-icon']")
expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]')
expect(page).not_to have_link('Pipeline: passed')
end
end
end
context 'guest user of project and project has private pipelines' do
let(:guest_user) { create(:user) }
before do
project.update!(public_builds: false)
project.add_guest(guest_user)
sign_in(guest_user)
end
it_behaves_like 'hidden pipeline status'
end
context "when last_pipeline is missing" do
before do
project.last_pipeline.delete
end
it_behaves_like 'hidden pipeline status'
end
end
describe 'with topics' do
context 'when project has topics' do
before do
project.update_attribute(:topic_list, 'topic1')
end
it 'shows project topics if exist' do
visit dashboard_projects_path
expect(page).to have_selector('[data-testid="project_topic_list"]')
expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
end
end
context 'when project does not have topics' do
it 'does not show project topics' do
visit dashboard_projects_path
expect(page).not_to have_selector('[data-testid="project_topic_list"]')
end
end
end
context 'guest user of project and project has private pipelines' do
let(:guest_user) { create(:user) }
context 'last push widget', :use_clean_rails_memory_store_caching do
before do
project.update!(public_builds: false)
project.add_guest(guest_user)
sign_in(guest_user)
end
event = create(:push_event, project: project, author: user)
it_behaves_like 'hidden pipeline status'
end
create(:push_event_payload, event: event, ref: 'feature', action: :created)
context "when last_pipeline is missing" do
before do
project.last_pipeline.delete
end
Users::LastPushEventService.new(user).cache_last_push_event(event)
it_behaves_like 'hidden pipeline status'
end
end
describe 'with topics' do
context 'when project has topics' do
before do
project.update_attribute(:topic_list, 'topic1')
end
it 'shows project topics if exist' do
visit dashboard_projects_path
end
expect(page).to have_selector('[data-testid="project_topic_list"]')
expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
it 'shows "Create merge request" button' do
expect(page).to have_content 'You pushed to feature'
within('#content-body') do
find_link('Create merge request', visible: false).click
end
expect(page).to have_selector('.merge-request-form')
expect(page).to have_current_path project_new_merge_request_path(project), ignore_query: true
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
expect(page).to have_content "From feature into master"
end
end
context 'when project does not have topics' do
it 'does not show project topics' do
visit dashboard_projects_path
expect(page).not_to have_selector('[data-testid="project_topic_list"]')
end
end
end
context 'last push widget', :use_clean_rails_memory_store_caching do
before do
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event, ref: 'feature', action: :created)
Users::LastPushEventService.new(user).cache_last_push_event(event)
it 'avoids an N+1 query in dashboard index' do
create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha)
visit dashboard_projects_path
control = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }
new_project = create(:project, :repository, name: 'new project')
create(:ci_pipeline, :with_job, status: :success, project: new_project, ref: new_project.commit.sha)
new_project.add_developer(user)
ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count
# There are a few known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037
# - User#max_member_access_for_project_ids
# - ProjectsHelper#load_pipeline_status / Ci::CommitWithPipeline#last_pipeline
# - Ci::Pipeline#detailed_status
expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4)
end
it 'shows "Create merge request" button' do
expect(page).to have_content 'You pushed to feature'
within('#content-body') do
find_link('Create merge request', visible: false).click
end
expect(page).to have_selector('.merge-request-form')
expect(page).to have_current_path project_new_merge_request_path(project), ignore_query: true
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
expect(page).to have_content "From feature into master"
end
end
it 'avoids an N+1 query in dashboard index' do
create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha)
visit dashboard_projects_path
control = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }
new_project = create(:project, :repository, name: 'new project')
create(:ci_pipeline, :with_job, status: :success, project: new_project, ref: new_project.commit.sha)
new_project.add_developer(user)
ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count
# There are a few known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037
# - User#max_member_access_for_project_ids
# - ProjectsHelper#load_pipeline_status / Ci::CommitWithPipeline#last_pipeline
# - Ci::Pipeline#detailed_status
expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4)
end
end

View File

@ -146,17 +146,47 @@ describe('GroupsAndProjectsApp', () => {
});
describe('when filtered search bar is submitted', () => {
const searchTerm = 'foo bar';
describe('when search term is 3 characters or more', () => {
const searchTerm = 'foo bar';
beforeEach(() => {
createComponent();
beforeEach(() => {
createComponent();
findFilteredSearchAndSort().vm.$emit('filter', { [FILTERED_SEARCH_TERM_KEY]: searchTerm });
findFilteredSearchAndSort().vm.$emit('filter', { [FILTERED_SEARCH_TERM_KEY]: searchTerm });
});
it(`updates \`${FILTERED_SEARCH_TERM_KEY}\` query string`, () => {
expect(routerMock.push).toHaveBeenCalledWith({
query: { [FILTERED_SEARCH_TERM_KEY]: searchTerm },
});
});
});
it(`updates \`${FILTERED_SEARCH_TERM_KEY}\` query string`, () => {
expect(routerMock.push).toHaveBeenCalledWith({
query: { [FILTERED_SEARCH_TERM_KEY]: searchTerm },
describe('when search term is less than 3 characters', () => {
const searchTerm = 'fo';
beforeEach(() => {
createComponent();
findFilteredSearchAndSort().vm.$emit('filter', { [FILTERED_SEARCH_TERM_KEY]: searchTerm });
});
it('does not update query string', () => {
expect(routerMock.push).not.toHaveBeenCalled();
});
});
describe('when search term is empty but there are other filters', () => {
beforeEach(() => {
createComponent();
findFilteredSearchAndSort().vm.$emit('filter', { foo: 'bar' });
});
it('updates query string', () => {
expect(routerMock.push).toHaveBeenCalledWith({
query: { foo: 'bar' },
});
});
});
});

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

View File

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

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
stub_feature_flags(synced_epic_work_item_editable: false)
# Since we are very early in the Vue migration, there isn't much value in testing when the feature flag is enabled
# Please see https://gitlab.com/gitlab-org/gitlab/-/issues/466081 for tracking revisiting this.
stub_feature_flags(your_work_projects_vue: false)
else
unstub_all_feature_flags
end

View File

@ -2,25 +2,96 @@
require 'spec_helper'
RSpec.describe 'dashboard/projects/index.html.haml' do
RSpec.describe 'dashboard/projects/index.html.haml', feature_category: :groups_and_projects do
let_it_be(:user) { build(:user) }
before do
allow(view).to receive(:limited_counter_with_delimiter)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:time_ago_with_tooltip)
assign(:projects, [build(:project, name: 'awesome stuff')])
end
it 'shows the project the user is a member of in the list' do
render
context 'when feature :your_work_projects_vue is enabled' do
before do
stub_feature_flags(your_work_projects_vue: true)
end
expect(rendered).to have_content('awesome stuff')
context 'when projects exist' do
before do
assign(:projects, [build(:project, name: 'awesome stuff')])
allow(view).to receive(:any_projects?).and_return(true)
render
end
it 'renders #js-your-work-projects-app and not legacy project list' do
render
expect(rendered).to have_selector('#js-your-work-projects-app')
expect(rendered).not_to render_template('dashboard/projects/_projects')
end
end
context 'when projects do not exist' do
before do
allow(view).to receive(:any_projects?).and_return(false)
render
end
it 'does not render #js-your-work-projects-app and renders empty state' do
render
expect(rendered).not_to have_selector('#js-your-work-projects-app')
expect(rendered).to render_template('dashboard/projects/_zero_authorized_projects')
end
end
end
it 'shows the "New project" button' do
render
context 'when feature :your_work_projects_vue is disabled' do
before do
stub_feature_flags(your_work_projects_vue: false)
end
expect(rendered).to have_link('New project')
context 'when projects exist' do
before do
assign(:projects, [build(:project, name: 'awesome stuff')])
allow(view).to receive(:show_projects?).and_return(true)
render
end
it 'shows the project the user is a member of in the list' do
expect(rendered).to have_content('awesome stuff')
end
it 'shows the "New project" button' do
expect(rendered).to have_link('New project')
end
it 'does not render zero_authorized_projects partial' do
expect(rendered).not_to render_template('dashboard/projects/_zero_authorized_projects')
end
it 'does not render #js-your-work-projects-app' do
expect(rendered).not_to have_selector('#js-your-work-projects-app')
end
end
context 'when projects do not exist' do
before do
allow(view).to receive(:show_projects?).and_return(false)
render
end
it 'does not show the "New project" button' do
expect(rendered).not_to have_link('New project')
end
it 'does render zero_authorized_projects partial' do
expect(rendered).to render_template('dashboard/projects/_zero_authorized_projects')
end
it 'does not render #js-your-work-projects-app' do
expect(rendered).not_to have_selector('#js-your-work-projects-app')
end
end
end
end

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"
integrity sha512-jVWCrRVRF6nw2A+Aowc0quXV2bdRPl2v08ElCPSestfdKjQ92tSlCrIsLB8GvdW5aI0eFsD1vJ1w2qkzZdpA4A==
"@gitlab/ui@85.3.0":
version "85.3.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-85.3.0.tgz#29abdfa121ca6720ca5558951319af20b55d0282"
integrity sha512-G8l8ZrdgOp6O6Azhypb/+HL5ZXU0/tIXjsHX7t6VkSBT7pSLBH06CnaH3Hrs8xEeCBSXfZDiMS4gq1FdEXGT2A==
"@gitlab/ui@85.4.1":
version "85.4.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-85.4.1.tgz#4cc7ed0bec0d022003e996a790d7ea9c37cce5ff"
integrity sha512-Q2QsIILLlipv6StUEAjS11OaJFkoZ5jlIE3QpFpJeGkHnskD6viRdnFcFDCYXwGMtrm1JphEp7iZZs6uX/MIkw==
dependencies:
"@floating-ui/dom" "1.4.3"
echarts "^5.3.2"