Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b7ef36fd90
commit
00e2fbb908
|
|
@ -1 +1 @@
|
|||
v17.1.0
|
||||
v17.1.1
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { initYourWorkProjects } from '~/projects/your_work';
|
||||
|
||||
initYourWorkProjects();
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -19,3 +19,4 @@ desired_sharding_key:
|
|||
table: ml_candidates
|
||||
sharding_key: project_id
|
||||
belongs_to: candidate
|
||||
desired_sharding_key_migration_job_name: BackfillMlCandidateMetadataProjectId
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
4704d2511a36e5af924d1b92fcf0c425d6868aa848f29431d8bff6410c5c967e
|
||||
|
|
@ -0,0 +1 @@
|
|||
6f396093e4cd6ec606540af198fe04853bebd74b51702e06ff171ed0b8c12921
|
||||
|
|
@ -0,0 +1 @@
|
|||
1e15fef4845db49e85604b7e959fda58bcaa623a6408620f7d0a9d882c26438a
|
||||
|
|
@ -0,0 +1 @@
|
|||
5f2cf06eb8a34e95b71704fa544e8e1eb0996435dea2c7c892e850590b42777b
|
||||
|
|
@ -0,0 +1 @@
|
|||
42601d461c570aa82234d58db59784158f044f0bafdbe6b83855123d21bf61e9
|
||||
|
|
@ -0,0 +1 @@
|
|||
06dda6411338c7bdd342da847d11ba8512279f09e85b4dfa26991fbb8184b3e0
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue