Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-18 12:26:26 +00:00
parent f5aa3d1cc5
commit ef1f98e770
90 changed files with 1384 additions and 464 deletions

View File

@ -1130,7 +1130,6 @@ RSpec/FeatureCategory:
- 'ee/spec/views/admin/users/index.html.haml_spec.rb'
- 'ee/spec/views/clusters/clusters/show.html.haml_spec.rb'
- 'ee/spec/views/compliance_management/compliance_framework/_compliance_frameworks_info.html.haml_spec.rb'
- 'ee/spec/views/devise/sessions/new.html.haml_spec.rb'
- 'ee/spec/views/groups/hook_logs/show.html.haml_spec.rb'
- 'ee/spec/views/groups/hooks/edit.html.haml_spec.rb'
- 'ee/spec/views/groups/security/discover/show.html.haml_spec.rb'

View File

@ -119,7 +119,7 @@ export default {
<crud-component
ref="crudComponent"
:title="$options.i18n.title"
icon="messages"
icon="bullhorn"
:count="messagesCount"
:toggle-text="$options.i18n.addButton"
>

View File

@ -0,0 +1,38 @@
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import EMPTY_VARIABLES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-variables-md.svg';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
components: {
GlEmptyState,
GlSprintf,
GlLink,
},
EMPTY_VARIABLES_SVG,
i18n: {
title: s__('ManualVariables|There are no manually-specified variables for this pipeline'),
description: s__(
'ManualVariables|When you %{helpPageUrlStart}run a pipeline manually%{helpPageUrlEnd}, you can specify additional CI/CD variables to use in that pipeline run.',
),
},
runPipelineManuallyDocUrl: helpPagePath('ci/pipelines/index', {
anchor: 'run-a-pipeline-manually',
}),
};
</script>
<template>
<gl-empty-state :svg-path="$options.EMPTY_VARIABLES_SVG" :title="$options.i18n.title">
<template #description>
<gl-sprintf :message="$options.i18n.description">
<template #helpPageUrl="{ content }">
<gl-link :href="$options.runPipelineManuallyDocUrl" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</template>
</gl-empty-state>
</template>

View File

@ -0,0 +1,17 @@
query getManualVariables($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
__typename
id
pipeline(iid: $iid) {
id
manualVariables {
__typename
nodes {
id
key
value
}
}
}
}
}

View File

@ -0,0 +1,51 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import EmptyState from './empty_state.vue';
import VariableTable from './variable_table.vue';
import getManualVariablesQuery from './graphql/queries/get_manual_variables.query.graphql';
export default {
name: 'ManualVariablesApp',
components: {
EmptyState,
GlLoadingIcon,
VariableTable,
},
inject: ['manualVariablesCount', 'projectPath', 'pipelineIid'],
apollo: {
variables: {
query: getManualVariablesQuery,
skip() {
return !this.hasManualVariables;
},
variables() {
return {
projectPath: this.projectPath,
iid: this.pipelineIid,
};
},
update({ project }) {
return project?.pipeline?.manualVariables?.nodes || [];
},
},
},
computed: {
loading() {
return this.$apollo.queries.variables.loading;
},
hasManualVariables() {
return Boolean(this.manualVariablesCount > 0);
},
},
};
</script>
<template>
<div>
<div v-if="hasManualVariables" class="manual-variables-table">
<gl-loading-icon v-if="loading" />
<variable-table v-else :variables="variables" />
</div>
<empty-state v-else />
</div>
</template>

View File

@ -0,0 +1,90 @@
<script>
import { GlButton, GlPagination, GlTableLite } from '@gitlab/ui';
import { __ } from '~/locale';
// The number of items per page is based on the design mockup.
// Please refer to https://gitlab.com/gitlab-org/gitlab/-/issues/323097/designs/TabVariables.png
const VARIABLES_PER_PAGE = 15;
export default {
components: {
GlButton,
GlPagination,
GlTableLite,
},
inject: ['manualVariablesCount', 'canReadVariables'],
props: {
variables: {
type: Array,
required: true,
},
},
data() {
return {
revealed: false,
currentPage: 1,
hasPermission: true,
};
},
computed: {
buttonText() {
return this.revealed ? __('Hide values') : __('Reveal values');
},
showPager() {
return this.manualVariablesCount > VARIABLES_PER_PAGE;
},
items() {
const start = (this.currentPage - 1) * VARIABLES_PER_PAGE;
const end = start + VARIABLES_PER_PAGE;
return this.variables.slice(start, end);
},
},
methods: {
toggleRevealed() {
this.revealed = !this.revealed;
},
},
TABLE_FIELDS: [
{
key: 'key',
label: __('Key'),
},
{
key: 'value',
label: __('Value'),
},
],
VARIABLES_PER_PAGE,
};
</script>
<template>
<!-- This negative margin top is a hack for the purpose to eliminate default padding of tab container -->
<!-- For context refer to: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159206#note_1999122459 -->
<div class="-gl-mt-3">
<div v-if="canReadVariables" class="gl-p-3 gl-bg-gray-10">
<gl-button :aria-label="buttonText" @click="toggleRevealed">{{ buttonText }}</gl-button>
</div>
<gl-table-lite :fields="$options.TABLE_FIELDS" :items="items">
<template #cell(key)="{ value }">
<span class="gl-text-secondary">
{{ value }}
</span>
</template>
<template #cell(value)="{ value }">
<div class="gl-text-secondary" data-testid="manual-variable-value">
<span v-if="revealed">{{ value }}</span>
<span v-else>****</span>
</div>
</template>
</gl-table-lite>
<gl-pagination
v-if="showPager"
v-model="currentPage"
class="gl-mt-6"
:per-page="$options.VARIABLES_PER_PAGE"
:total-items="manualVariablesCount"
align="center"
/>
</div>
</template>

View File

@ -7,7 +7,6 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
@ -15,6 +14,7 @@ import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { confirmJobConfirmationMessage } from '~/ci/pipeline_details/graph/utils';
import { TRACKING_CATEGORIES } from '../../constants';
import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
import jobPlayMutation from '../../jobs_page/graphql/mutations/job_play.mutation.graphql';
export default {
name: 'PipelinesManualActions',
@ -96,17 +96,13 @@ export default {
}
}
this.isLoading = true;
/**
* Ideally, the component would not make an api call directly.
* However, in order to use the eventhub and know when to
* toggle back the `isLoading` property we'd need an ID
* to track the request with a watcher - since this component
* is rendered at least 20 times in the same page, moving the
* api call directly here is the most performant solution
*/
axios
.post(`${action.playPath}.json`)
this.$apollo
.mutate({
mutation: jobPlayMutation,
variables: {
id: action.id,
},
})
.then(() => {
this.isLoading = false;
this.$emit('refresh-pipeline-table');

View File

@ -15,7 +15,7 @@ import TaskList from '~/task_list';
import { addHierarchyChild, removeHierarchyChild } from '~/work_items/graphql/cache_utils';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_CREATING,
@ -110,7 +110,7 @@ export default {
},
},
workItemTypes: {
query: projectWorkItemTypesQuery,
query: namespaceWorkItemTypesQuery,
variables() {
return {
fullPath: this.fullPath,

View File

@ -32,8 +32,7 @@ import {
WIDGET_TYPE_ROLLEDUP_DATES,
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import groupWorkItemTypesQuery from '../graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
@ -110,7 +109,7 @@ export default {
},
workItemTypes: {
query() {
return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery;
return namespaceWorkItemTypesQuery;
},
fetchPolicy() {
return this.workItemTypeName ? fetchPolicies.CACHE_ONLY : fetchPolicies.CACHE_FIRST;

View File

@ -10,8 +10,7 @@ import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
} from '../constants';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import groupWorkItemTypesQuery from '../graphql/group_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
import CreateWorkItem from './create_work_item.vue';
export default {
@ -43,7 +42,7 @@ export default {
apollo: {
workItemTypes: {
query() {
return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery;
return namespaceWorkItemTypesQuery;
},
variables() {
return {

View File

@ -41,7 +41,7 @@ import {
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
import WorkItemStateToggle from './work_item_state_toggle.vue';
export default {
@ -173,7 +173,7 @@ export default {
},
apollo: {
workItemTypes: {
query: projectWorkItemTypesQuery,
query: namespaceWorkItemTypesQuery,
variables() {
return {
fullPath: this.fullPath,

View File

@ -87,11 +87,10 @@ export default {
return this.isWidgetPresent(WIDGET_TYPE_ROLLEDUP_DATES);
},
workItemWeight() {
/** TODO remove this check after https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158021 is merged */
if (this.workItemType !== WORK_ITEM_TYPE_VALUE_EPIC) {
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
}
return false;
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
isWorkItemWeightEditable() {
return this.workItemWeight?.widgetDefinition?.editable;
},
workItemParticipants() {
return this.isWidgetPresent(WIDGET_TYPE_PARTICIPANTS);
@ -178,7 +177,7 @@ export default {
@labelsUpdated="$emit('attributesUpdated', { type: $options.ListType.label, ids: $event })"
/>
</template>
<template v-if="workItemWeight">
<template v-if="isWorkItemWeightEditable">
<work-item-weight
class="gl-mb-5"
:can-update="canUpdate"

View File

@ -26,6 +26,7 @@ import {
WORK_ITEM_REFERENCE_CHAR,
WORK_ITEM_TYPE_VALUE_TASK,
WORK_ITEM_TYPE_VALUE_EPIC,
WIDGET_TYPE_WEIGHT,
} from '../constants';
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
@ -284,6 +285,15 @@ export default {
workItemNotes() {
return this.isWidgetPresent(WIDGET_TYPE_NOTES);
},
workItemWeight() {
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
showRolledUpWeight() {
return this.workItemWeight?.widgetDefinition?.rollUp;
},
rolledUpWeight() {
return this.workItemWeight?.rolledUpWeight;
},
workItemBodyClass() {
return {
'gl-pt-5': !this.updateError && !this.isModal,
@ -673,6 +683,8 @@ export default {
:work-item-iid="workItemIid"
:can-update="canUpdate"
:can-update-children="canUpdateChildren"
:rolled-up-weight="rolledUpWeight"
:show-rolled-up-weight="showRolledUpWeight"
:confidential="workItem.confidential"
:allowed-child-types="allowedChildTypes"
@show-modal="openInModal"

View File

@ -4,8 +4,7 @@ import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
import groupWorkItemTypesQuery from '../../graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '../../graphql/namespace_work_item_types.query.graphql';
import updateWorkItemHierarchyMutation from '../../graphql/update_work_item_hierarchy.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import {
@ -88,7 +87,7 @@ export default {
apollo: {
workItemTypes: {
query() {
return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery;
return namespaceWorkItemTypesQuery;
},
variables() {
return {

View File

@ -6,8 +6,7 @@ import { __, s__ } from '~/locale';
import { STORAGE_KEY } from '~/super_sidebar/constants';
import AccessorUtilities from '~/lib/utils/accessor';
import { getTopFrequentItems } from '~/super_sidebar/utils';
import groupProjectsForLinksWidgetQuery from '../../graphql/group_projects_for_links_widget.query.graphql';
import relatedProjectsForLinksWidgetQuery from '../../graphql/related_projects_for_links_widget.query.graphql';
import namespaceProjectsForLinksWidgetQuery from '../../graphql/namespace_projects_for_links_widget.query.graphql';
import { SEARCH_DEBOUNCE, MAX_FREQUENT_PROJECTS } from '../../constants';
export default {
@ -46,7 +45,7 @@ export default {
apollo: {
projects: {
query() {
return this.isGroup ? groupProjectsForLinksWidgetQuery : relatedProjectsForLinksWidgetQuery;
return namespaceProjectsForLinksWidgetQuery;
},
variables() {
return {
@ -55,7 +54,7 @@ export default {
};
},
update(data) {
return this.isGroup ? data.group?.projects?.nodes : data.project?.group?.projects?.nodes;
return data.namespace?.projects?.nodes;
},
result() {
if (this.selectedProject === null) {

View File

@ -1,5 +1,5 @@
<script>
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
import { GlToggle, GlIcon, GlTooltip, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import {
FORM_TYPES,
@ -35,6 +35,8 @@ export default {
WorkItemTreeActions,
GlToggle,
GlLoadingIcon,
GlIcon,
GlTooltip,
},
inject: ['hasSubepicsFeature'],
props: {
@ -80,6 +82,16 @@ export default {
required: false,
default: () => [],
},
showRolledUpWeight: {
type: Boolean,
required: false,
default: false,
},
rolledUpWeight: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
@ -213,6 +225,20 @@ export default {
>
<template #header>
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
<span
v-if="showRolledUpWeight"
ref="weightData"
data-testid="rollup-weight"
class="gl-font-normal gl-ml-3 gl-display-flex gl-align-items-center gl-cursor-help gl-gap-2 gl-text-secondary"
>
<gl-icon name="weight" class="gl-text-secondary" />
<span data-testid="weight-value" class="gl-font-sm">{{ rolledUpWeight }}</span>
<gl-tooltip :target="() => $refs.weightData">
<span class="gl-font-bold">
{{ __('Weight') }}
</span>
</gl-tooltip>
</span>
</template>
<template #header-right>
<gl-toggle

View File

@ -318,9 +318,18 @@ export const setNewWorkItemCache = async (
}
if (widgetName === WIDGET_TYPE_WEIGHT) {
const weightWidgetData = widgetDefinitions.find(
(definition) => definition.type === WIDGET_TYPE_WEIGHT,
);
widgets.push({
type: 'WEIGHT',
weight: null,
rolledUpWeight: 0,
widgetDefinition: {
editable: weightWidgetData?.editable,
rollUp: weightWidgetData?.rollUp,
},
__typename: 'WorkItemWidgetWeight',
});
}

View File

@ -1,18 +0,0 @@
query groupProjectsForLinksWidget($fullPath: ID!, $projectSearch: String) {
group(fullPath: $fullPath) {
id
projects(search: $projectSearch, includeSubgroups: true) {
nodes {
id
name
avatarUrl
nameWithNamespace
fullPath
namespace {
id
name
}
}
}
}
}

View File

@ -0,0 +1,18 @@
query namespaceProjectsForLinksWidget($fullPath: ID!, $projectSearch: String) {
namespace(fullPath: $fullPath) {
id
projects(search: $projectSearch, includeSubgroups: true, includeSiblingProjects: true) {
nodes {
id
name
avatarUrl
nameWithNamespace
fullPath
namespace {
id
name
}
}
}
}
}

View File

@ -1,7 +1,7 @@
#import "ee_else_ce/work_items/graphql/work_item_type.fragment.graphql"
query groupWorkItemTypes($fullPath: ID!, $name: IssueType) {
workspace: group(fullPath: $fullPath) {
query namespaceWorkItemTypes($fullPath: ID!, $name: IssueType) {
workspace: namespace(fullPath: $fullPath) {
id
workItemTypes(name: $name) {
nodes {

View File

@ -1,12 +0,0 @@
#import "ee_else_ce/work_items/graphql/work_item_type.fragment.graphql"
query projectWorkItemTypes($fullPath: ID!, $name: IssueType) {
workspace: project(fullPath: $fullPath) {
id
workItemTypes(name: $name) {
nodes {
...WorkItemTypeFragment
}
}
}
}

View File

@ -1,21 +0,0 @@
query relatedProjectsForLinksWidget($fullPath: ID!, $projectSearch: String) {
project(fullPath: $fullPath) {
id
group {
id
projects(search: $projectSearch, includeSubgroups: true) {
nodes {
id
name
avatarUrl
nameWithNamespace
fullPath
namespace {
id
name
}
}
}
}
}
}

View File

@ -1,10 +0,0 @@
- expanded = local_assigns.fetch(:expanded)
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Variables')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= render "ci/variables/content", entity: @entity, variable_limit: @variable_limit

View File

@ -5,55 +5,49 @@
- expanded = expanded_by_default?
- general_expanded = @group.errors.empty? ? expanded : true
%h1.gl-sr-only= @breadcrumb_title
-# Given we only have one field in this form which is also admin-only,
-# we don't want to show an empty section to non-admin users,
- if can?(current_user, :update_max_artifacts_size, @group)
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("General pipelines")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Customize your pipeline configuration.")
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_("General pipelines"),
id: 'js-general-pipeline-settings',
expanded: general_expanded) do |c|
- c.with_description do
= _("Customize your pipeline configuration.")
- c.with_body do
= render 'groups/settings/ci_cd/form', group: @group
- if can?(current_user, :admin_cicd_variables, @group)
%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
= render 'ci/variables/header', expanded: expanded
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Variables'),
id: 'ci-variables',
expanded: expanded) do |c|
- c.with_description do
= render "ci/variables/content", entity: @entity, variable_limit: @variable_limit
- c.with_body do
= render 'ci/variables/index', save_endpoint: group_variables_path
- if can?(current_user, :admin_runner, @group)
%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Runners')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Runners'),
id: 'runners-settings',
expanded: expanded) do |c|
- c.with_description do
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
- c.with_body do
= render 'groups/runners/settings'
- if can?(current_user, :admin_group, @group)
%section.settings#auto-devops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Auto DevOps')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- auto_devops_url = help_page_path('topics/autodevops/index')
- quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
= html_escape(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}')) % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Auto DevOps'),
id: 'auto-devops-settings',
expanded: expanded) do |c|
- c.with_description do
- auto_devops_url = help_page_path('topics/autodevops/index')
- quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
= html_escape(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}')) % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
- c.with_body do
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
= render_if_exists 'groups/settings/ci_cd/protected_environments', expanded: expanded

View File

@ -24,7 +24,7 @@
- @pipeline.yaml_errors.split("\n").each do |error|
%li= error
- if can_view_pipeline_editor?(@project)
= render Pajamas::ButtonComponent.new(href: project_ci_pipeline_editor_path(@project), variant: :confirm) do
= render Pajamas::ButtonComponent.new(href: project_ci_pipeline_editor_path(@project, branch_name: @pipeline.source_ref), variant: :confirm) do
= s_("Pipelines|Go to the pipeline editor")
- else

View File

@ -5,123 +5,105 @@
- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
%h1.gl-sr-only= @breadcrumb_title
- if can?(current_user, :admin_pipeline, @project)
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("General pipelines")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Customize your pipeline configuration.")
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_("General pipelines"),
id: 'js-general-pipeline-settings',
expanded: general_expanded) do |c|
- c.with_description do
= _("Customize your pipeline configuration.")
- c.with_body do
= render 'form'
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'autodevops-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('CICD|Auto DevOps')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- auto_devops_url = help_page_path('topics/autodevops/index')
- quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer')
- quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end))
.settings-content
= render ::Layouts::SettingsBlockComponent.new(s_('CICD|Auto DevOps'),
id: 'autodevops-settings',
testid: 'autodevops-settings-content',
expanded: expanded) do |c|
- c.with_description do
- auto_devops_url = help_page_path('topics/autodevops/index')
- quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer')
- quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer')
= safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end))
- c.with_body do
= render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
= render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded
- if can?(current_user, :admin_runner, @project)
- expand_runners = expanded || params[:expand_runners]
%section.settings.no-animate#js-runners-settings{ class: ('expanded' if expand_runners), data: { testid: 'runners-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Runners")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expand_runners ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Runners'),
id: 'js-runners-settings',
testid: 'runners-settings-content',
expanded: expand_runners) do |c|
- c.with_description do
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
- c.with_body do
= render 'projects/runners/settings'
- if can?(current_user, :admin_pipeline, @project)
- if Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact?
%section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Artifacts")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("A job artifact is an archive of files and directories saved by a job when it finishes.")
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_("Artifacts"),
id: 'js-artifacts-settings',
expanded: expanded) do |c|
- c.with_description do
= _("A job artifact is an archive of files and directories saved by a job when it finishes.")
- c.with_body do
#js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/jobs/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } }
- if can?(current_user, :admin_cicd_variables, @project)
%section.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { testid: 'variables-settings-content' } }
.settings-header
= render 'ci/variables/header', expanded: expanded
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Variables'),
id: 'js-cicd-variables-settings',
testid: 'variables-settings-content',
expanded: expanded) do |c|
- c.with_description do
= render "ci/variables/content", entity: @entity, variable_limit: @variable_limit
- c.with_body do
= render 'ci/variables/index', save_endpoint: project_variables_path(@project)
- if can?(current_user, :admin_pipeline, @project)
%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Pipeline trigger tokens")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.")
= link_to _('Learn more.'), help_page_path('ci/triggers/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Pipeline trigger tokens'),
id: 'js-pipeline-triggers',
expanded: expanded) do |c|
- c.with_description do
= _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.")
= link_to _('Learn more.'), help_page_path('ci/triggers/index'), target: '_blank', rel: 'noopener noreferrer'
- c.with_body do
= render 'projects/triggers/index'
= render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded
- if can?(current_user, :create_freeze_period, @project)
%section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Deploy freezes")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze')
- freeze_period_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: freeze_period_docs }
= html_escape(s_('DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: '</a>'.html_safe, filename: tag.code('.gitlab-ci.yml') }
= render ::Layouts::SettingsBlockComponent.new(_('Deploy freezes'),
id: 'js-deploy-freeze-settings',
expanded: expanded) do |c|
- c.with_description do
- freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze')
- freeze_period_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: freeze_period_docs }
= html_escape(s_('DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: '</a>'.html_safe, filename: tag.code('.gitlab-ci.yml') }
- cron_syntax_url = 'https://crontab.guru/'
- cron_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: cron_syntax_url }
= s_('DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "</a>".html_safe }
.settings-content
- cron_syntax_url = 'https://crontab.guru/'
- cron_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: cron_syntax_url }
= s_('DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "</a>".html_safe }
- c.with_body do
= render 'ci/deploy_freeze/index'
%section.settings.no-animate#js-token-access{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Job token permissions")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Control which CI/CD job tokens can be used to authenticate with this project.")
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Job token permissions'),
id: 'js-token-access',
expanded: expanded) do |c|
- c.with_description do
= _("Control which CI/CD job tokens can be used to authenticate with this project.")
- c.with_body do
= render 'ci/token_access/index'
- if show_secure_files_setting(@project, current_user)
%section.settings.no-animate#js-secure-files{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Secure Files")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
= _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.")
= link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render ::Layouts::SettingsBlockComponent.new(_('Secure Files'),
id: 'js-secure-files',
expanded: expanded) do |c|
- c.with_description do
= _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.")
= link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer'
- c.with_body do
#js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } }

View File

@ -382,6 +382,10 @@ sbom_occurrences:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
sbom_sources:
- table: organizations
column: organization_id
on_delete: async_delete
security_scans:
- table: ci_builds
column: build_id

View File

@ -19,3 +19,4 @@ desired_sharding_key:
table: approval_group_rules
sharding_key: group_id
belongs_to: approval_group_rule
desired_sharding_key_migration_job_name: BackfillApprovalGroupRulesProtectedBranchesGroupId

View File

@ -0,0 +1,9 @@
---
migration_job_name: BackfillApprovalGroupRulesProtectedBranchesGroupId
description: Backfills sharding key `approval_group_rules_protected_branches.group_id` from `approval_group_rules`.
feature_category: source_code_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159584
milestone: '17.2'
queued_migration_version: 20240716135032
finalize_after: '2024-08-22'
finalized_by: # version of the migration that finalized this BBM

View File

@ -8,4 +8,5 @@ description: Stores information about where an SBoM component originated from
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90812
milestone: '15.2'
gitlab_schema: gitlab_sec
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/457096
sharding_key:
organization_id: organizations

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class AddOrganizationToSbomSources < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.3'
INDEX_NAME = 'index_sbom_sources_on_organization_id'
def up
with_lock_retries do
add_column :sbom_sources, :organization_id, :bigint, null: false,
default: Organizations::Organization::DEFAULT_ORGANIZATION_ID,
if_not_exists: true
end
add_concurrent_foreign_key :sbom_sources, :organizations, column: :organization_id, on_delete: :cascade
add_concurrent_index :sbom_sources, :organization_id, name: INDEX_NAME
end
def down
with_lock_retries do
remove_foreign_key :sbom_sources, column: :organization_id
end
remove_concurrent_index_by_name :sbom_sources, INDEX_NAME
with_lock_retries do
remove_column :sbom_sources, :organization_id, if_exists: true
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class ReplaceSbomSourcesUniqueIndex < Gitlab::Database::Migration[2.2]
REMOVED_INDEX_NAME = "index_sbom_sources_on_source_type_and_source"
ADDED_INDEX_NAME = "index_sbom_sources_on_source_type_and_source_and_org_id"
disable_ddl_transaction!
milestone '17.3'
def up
add_concurrent_index :sbom_sources, %i[source_type source organization_id], unique: true, name: ADDED_INDEX_NAME
remove_concurrent_index_by_name :sbom_sources, name: REMOVED_INDEX_NAME
end
def down
add_concurrent_index :sbom_sources, %i[source_type source], unique: true, name: REMOVED_INDEX_NAME
remove_concurrent_index_by_name :sbom_sources, name: ADDED_INDEX_NAME
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class UpdateSbomSources < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_sec
milestone '17.3'
class SbomSource < MigrationRecord
self.table_name = "sbom_sources"
end
def up
# no-op
end
def down
SbomSource.where.not(organization_id: 1).delete_all
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddGroupIdToApprovalGroupRulesProtectedBranches < Gitlab::Database::Migration[2.2]
milestone '17.2'
def change
add_column :approval_group_rules_protected_branches, :group_id, :bigint
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class IndexApprovalGroupRulesProtectedBranchesOnGroupId < Gitlab::Database::Migration[2.2]
milestone '17.2'
disable_ddl_transaction!
INDEX_NAME = 'index_approval_group_rules_protected_branches_on_group_id'
def up
add_concurrent_index :approval_group_rules_protected_branches, :group_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :approval_group_rules_protected_branches, INDEX_NAME
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddApprovalGroupRulesProtectedBranchesGroupIdFk < Gitlab::Database::Migration[2.2]
milestone '17.2'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :approval_group_rules_protected_branches, :namespaces, column: :group_id,
on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :approval_group_rules_protected_branches, column: :group_id
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AddApprovalGroupRulesProtectedBranchesGroupIdTrigger < Gitlab::Database::Migration[2.2]
milestone '17.2'
def up
install_sharding_key_assignment_trigger(
table: :approval_group_rules_protected_branches,
sharding_key: :group_id,
parent_table: :approval_group_rules,
parent_sharding_key: :group_id,
foreign_key: :approval_group_rule_id
)
end
def down
remove_sharding_key_assignment_trigger(
table: :approval_group_rules_protected_branches,
sharding_key: :group_id,
parent_table: :approval_group_rules,
parent_sharding_key: :group_id,
foreign_key: :approval_group_rule_id
)
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class QueueBackfillApprovalGroupRulesProtectedBranchesGroupId < Gitlab::Database::Migration[2.2]
milestone '17.2'
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
MIGRATION = "BackfillApprovalGroupRulesProtectedBranchesGroupId"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 1000
SUB_BATCH_SIZE = 100
def up
queue_batched_background_migration(
MIGRATION,
:approval_group_rules_protected_branches,
:id,
:group_id,
:approval_group_rules,
:group_id,
:approval_group_rule_id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(
MIGRATION,
:approval_group_rules_protected_branches,
:id,
[
:group_id,
:approval_group_rules,
:group_id,
:approval_group_rule_id
]
)
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveOrganizationsSbomSourcesOrganizationIdFk < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
FOREIGN_KEY_NAME = "fk_8d0c60c7e9"
def up
with_lock_retries do
remove_foreign_key_if_exists(:sbom_sources, :organizations,
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
end
end
def down
add_concurrent_foreign_key(:sbom_sources, :organizations,
name: FOREIGN_KEY_NAME, column: :organization_id,
target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
class PartitionedFkToCiPipelinesFromPCiStagesOnPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2]
include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :p_ci_stages
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_fb57e6cc56_p
PARTITION_COLUMN = :partition_id
def up
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: [PARTITION_COLUMN, COLUMN],
target_column: [PARTITION_COLUMN, TARGET_COLUMN],
validate: true,
reverse_lock_order: true,
on_update: :cascade,
on_delete: :cascade,
name: FK_NAME
)
end
def down
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: [PARTITION_COLUMN, COLUMN],
target_column: [PARTITION_COLUMN, TARGET_COLUMN],
validate: false,
reverse_lock_order: true,
on_update: :cascade,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class RemoveFkToCiPipelinesPCiStagesOnPipelineId < Gitlab::Database::Migration[2.2]
include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :p_ci_stages
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_fb57e6cc56
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: COLUMN,
target_column: TARGET_COLUMN,
validate: true,
reverse_lock_order: true,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
class PartitionedFkToCiPipelinesFromPCiPipelineVariables < Gitlab::Database::Migration[2.2]
include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :p_ci_pipeline_variables
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_f29c5f4380_p
PARTITION_COLUMN = :partition_id
def up
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: [PARTITION_COLUMN, COLUMN],
target_column: [PARTITION_COLUMN, TARGET_COLUMN],
validate: true,
reverse_lock_order: true,
on_update: :cascade,
on_delete: :cascade,
name: FK_NAME
)
end
def down
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: [PARTITION_COLUMN, COLUMN],
target_column: [PARTITION_COLUMN, TARGET_COLUMN],
validate: false,
reverse_lock_order: true,
on_update: :cascade,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class RemoveFkToCiPipelinesPCiPipelineVariablesOnPipelineId < Gitlab::Database::Migration[2.2]
include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :p_ci_pipeline_variables
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_f29c5f4380
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_partitioned_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: COLUMN,
target_column: TARGET_COLUMN,
validate: true,
reverse_lock_order: true,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1 @@
9985f33c8b5ab8018a3f743fb300306f77eb58466fbe132d8e6dbdaebfcb3e8a

View File

@ -0,0 +1 @@
cf9f90769fac3e309d5e416eec8b8d23a52cbaa0b72bb5e2d9de6efd2292c537

View File

@ -0,0 +1 @@
75b640c4e33089a8779a561f55ee50b9ee3d8929558016cbdb34d05ccd44e05c

View File

@ -0,0 +1 @@
40aaa66701b7c516703b682986213292015f01b5c8229f63623fb2686bb4850d

View File

@ -0,0 +1 @@
93787f4a9865498a2ccb09f7930ba695beac3a4acea8c696425cd206d95a592e

View File

@ -0,0 +1 @@
3e5b221dd034fc2374687b2869331a64bc464eb142848cef2184e21befde924f

View File

@ -0,0 +1 @@
59e03fafc35387d5e8a3f947d595215bb8db7d6c438e6391f65077128f0c9277

View File

@ -0,0 +1 @@
8892e07003e3aba2517253068209e578b2693df8ed84e4c966a49345a0c8eabb

View File

@ -0,0 +1 @@
97298cb5ba78afd0b6a8cc85f140f43f0d7893f28a9afccc6b8f48dfaca80fbc

View File

@ -0,0 +1 @@
b8fc1daefd2576064659f0715925470cba0eb408c42b7d213f1ee2f8f76d6036

View File

@ -0,0 +1 @@
98bf6b969e0aa5564b6482885b508aca297b507407d59793196e976030fc73a2

View File

@ -0,0 +1 @@
c32eb523d9a7abff2ee3252ddda9b7b7fb557a6aef62f61d2d8337482db03d5f

View File

@ -0,0 +1 @@
455aec6cffd85fbc004624767589c41c9db2c9c1d9fa2e54a2e75fe9a32cbb41

View File

@ -1149,6 +1149,22 @@ RETURN NEW;
END
$$;
CREATE FUNCTION trigger_49862b4b3035() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW."group_id" IS NULL THEN
SELECT "group_id"
INTO NEW."group_id"
FROM "approval_group_rules"
WHERE "approval_group_rules"."id" = NEW."approval_group_rule_id";
END IF;
RETURN NEW;
END
$$;
CREATE FUNCTION trigger_49e070da6320() RETURNS trigger
LANGUAGE plpgsql
AS $$
@ -5924,7 +5940,8 @@ ALTER SEQUENCE approval_group_rules_id_seq OWNED BY approval_group_rules.id;
CREATE TABLE approval_group_rules_protected_branches (
id bigint NOT NULL,
approval_group_rule_id bigint NOT NULL,
protected_branch_id bigint NOT NULL
protected_branch_id bigint NOT NULL,
group_id bigint
);
CREATE SEQUENCE approval_group_rules_protected_branches_id_seq
@ -17138,7 +17155,8 @@ CREATE TABLE sbom_sources (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
source_type smallint NOT NULL,
source jsonb DEFAULT '{}'::jsonb NOT NULL
source jsonb DEFAULT '{}'::jsonb NOT NULL,
organization_id bigint DEFAULT 1 NOT NULL
);
CREATE SEQUENCE sbom_sources_id_seq
@ -26171,6 +26189,8 @@ CREATE INDEX index_approval_group_rules_on_approval_policy_rule_id ON approval_g
CREATE INDEX index_approval_group_rules_on_scan_result_policy_id ON approval_group_rules USING btree (scan_result_policy_id);
CREATE INDEX index_approval_group_rules_protected_branches_on_group_id ON approval_group_rules_protected_branches USING btree (group_id);
CREATE INDEX index_approval_group_rules_users_on_group_id ON approval_group_rules_users USING btree (group_id);
CREATE INDEX index_approval_group_rules_users_on_user_id ON approval_group_rules_users USING btree (user_id);
@ -29099,7 +29119,9 @@ CREATE INDEX index_sbom_occurrences_vulnerabilities_on_vulnerability_id ON sbom_
CREATE INDEX index_sbom_source_packages_on_source_package_id_and_id ON sbom_occurrences USING btree (source_package_id, id);
CREATE UNIQUE INDEX index_sbom_sources_on_source_type_and_source ON sbom_sources USING btree (source_type, source);
CREATE INDEX index_sbom_sources_on_organization_id ON sbom_sources USING btree (organization_id);
CREATE UNIQUE INDEX index_sbom_sources_on_source_type_and_source_and_org_id ON sbom_sources USING btree (source_type, source, organization_id);
CREATE INDEX index_scan_execution_policy_rules_on_policy_mgmt_project_id ON scan_execution_policy_rules USING btree (security_policy_management_project_id);
@ -31813,6 +31835,8 @@ CREATE TRIGGER trigger_43484cb41aca BEFORE INSERT OR UPDATE ON wiki_repository_s
CREATE TRIGGER trigger_44558add1625 BEFORE INSERT OR UPDATE ON merge_request_assignees FOR EACH ROW EXECUTE FUNCTION trigger_44558add1625();
CREATE TRIGGER trigger_49862b4b3035 BEFORE INSERT OR UPDATE ON approval_group_rules_protected_branches FOR EACH ROW EXECUTE FUNCTION trigger_49862b4b3035();
CREATE TRIGGER trigger_49e070da6320 BEFORE INSERT OR UPDATE ON packages_dependency_links FOR EACH ROW EXECUTE FUNCTION trigger_49e070da6320();
CREATE TRIGGER trigger_4ad9a52a6614 BEFORE INSERT OR UPDATE ON sbom_occurrences_vulnerabilities FOR EACH ROW EXECUTE FUNCTION trigger_4ad9a52a6614();
@ -32409,6 +32433,9 @@ ALTER TABLE ONLY vulnerability_reads
ALTER TABLE ONLY approval_group_rules_groups
ADD CONSTRAINT fk_50edc8134e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY approval_group_rules_protected_branches
ADD CONSTRAINT fk_514003db08 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_51ab4b6089 FOREIGN KEY (prometheus_alert_id) REFERENCES prometheus_alerts(id) ON DELETE CASCADE;
@ -33361,10 +33388,7 @@ ALTER TABLE ONLY epic_user_mentions
ADD CONSTRAINT fk_f1ab52883e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE p_ci_pipeline_variables
ADD CONSTRAINT fk_f29c5f4380 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_variables
ADD CONSTRAINT fk_f29c5f4380_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_f29c5f4380_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY zoekt_indices
ADD CONSTRAINT fk_f34800a202 FOREIGN KEY (zoekt_node_id) REFERENCES zoekt_nodes(id) ON DELETE CASCADE;
@ -33409,10 +33433,7 @@ ALTER TABLE ONLY vulnerability_finding_evidences
ADD CONSTRAINT fk_fa3efd4e94 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE p_ci_stages
ADD CONSTRAINT fk_fb57e6cc56 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_stages
ADD CONSTRAINT fk_fb57e6cc56_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_fb57e6cc56_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY agent_group_authorizations
ADD CONSTRAINT fk_fb70782616 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;

View File

@ -275,9 +275,9 @@ Microsoft has documented how its platform works with [the OIDC protocol](https:/
You can migrate to the Generic OpenID Connect configuration from both `azure_activedirectory_v2` and `azure_oauth2`.
First, set the `uid_field`, which differs between providers:
First, set the `uid_field`. Both the `uid_field` and the `sub` claim that you can select as a `uid_field` vary depending on the provider. Signing in without setting the `uid_field` results in additional identities being created within GitLab that have to be manually modified:
| Provider | `uid` | Supporting information |
| Provider | `uid_field` | Supporting information |
|-----------------------------------------------------------------------------------------------------------------|-------|-----------------------------------------------------------------------|
| [`omniauth-azure-oauth2`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/omniauth-azure-oauth2) | `sub` | Additional attributes `oid` and `tid` are offered within the `info` object. |
| [`omniauth-azure-activedirectory-v2`](https://github.com/RIPAGlobal/omniauth-azure-activedirectory-v2/) | `oid` | You must configure `oid` as `uid_field` when migrating. |
@ -295,7 +295,7 @@ gitlab_rails['omniauth_providers'] = [
name: "azure_oauth2",
label: "Azure OIDC", # optional label for login button, defaults to "Openid Connect"
args: {
name: "azure_oauth2",
name: "azure_oauth2", # this matches the existing azure_oauth2 provider name, and only the strategy_class immediately below configures OpenID Connect
strategy_class: "OmniAuth::Strategies::OpenIDConnect",
scope: ["openid", "profile", "email"],
response_type: "code",
@ -343,6 +343,20 @@ gitlab_rails['omniauth_providers'] = [
::EndTabs
As you migrate from `azure_oauth2` to `omniauth_openid_connect` as part of upgrading to GitLab 17.0 or above, the `sub` claim value set for your organization can vary. `azure_oauth2` uses Microsoft V1 endpoint while `azure_activedirectory_v2` and `omniauth_openid_connect` both use Microsoft V2 endpoint with a common `sub` value.
- For users with an email address in Entra ID, configure [`omniauth_auto_link_user`](../../integration/omniauth.md#link-existing-users-to-omniauth-users) to allow falling back to email address and updating the user's identity.
- For users with no email address, administrators must take one of the following actions:
- Set up another authentication method or enable sign-in using GitLab username and password. The user can then sign in and link their Azure identity manually using their profile.
- Implement OpenID Connect as a new provider alongside the existing `azure_oauth2` so the user can sign in through OAuth2, and link their OpenID Connect identity (similar to the previous method). This method would also work for users with email addresses, as long as `auto_link_user` is enabled.
- Update `extern_uid` manually. To do this, use the [API or Rails console](../../integration/omniauth.md#change-apps-or-configuration) to update the `extern_uid` for each user.
This method may be required if the instance has already been upgraded to 17.0 or later, and users have attempted to sign in.
NOTE:
`azure_oauth2` might have used Entra ID's `upn` claim as the email address, if the `email` claim was missing or blank when provisioning GitLab accounts.
### Configure Microsoft Azure Active Directory B2C
GitLab requires special

View File

@ -2607,6 +2607,8 @@ job_with_id_tokens:
**Related topics**:
- [ID token authentication](../secrets/id_token_authentication.md).
- [Connect to cloud services](../cloud_services/index.md).
- [Keyless signing with Sigstore](signing_examples.md).
### `image`

View File

@ -216,8 +216,8 @@ Use sentence case for topic titles. For example:
#### UI text
When referring to specific user interface text, like a button label or menu
item, use the same capitalization that's displayed in the user interface.
When referring to specific user interface text, like a button label, page, tab,
or menu item, use the same capitalization that's displayed in the user interface.
If you think the user interface text contains style mistakes,
create an issue or an MR to propose a change to the user interface text.
@ -453,11 +453,15 @@ When the docs are generated, the output is:
To stop the command, press <kbd>Control</kbd>+<kbd>C</kbd>.
### Buttons in the UI
### Buttons, tabs, and pages in the UI
For elements with a visible label, use the label in bold with matching case.
For example: `Select **Cancel**.`
For example:
- `Select **Cancel**.`
- `On the **Issues** page...`
- `On the **Pipelines** tab...`
### Text entered in the UI

View File

@ -1173,6 +1173,10 @@ Instead of:
Do not use **list** when referring to a [**dropdown list**](#dropdown-list).
Use the full phrase **dropdown list** instead.
Also, do not use **list** when referring to a page. For example, the **Issues** page
is populated with a list of issues. However, you should call it the **Issues** page,
and not the **Issues** list.
## license
Licenses are different than subscriptions.
@ -1966,6 +1970,13 @@ Examples:
- Suggested Reviewers can recommend a person to review your merge request. (This phrase describes the feature.)
- As you type, Suggested Reviewers are displayed. (This phrase is generic but still uses capital letters.)
## tab
Use bold for tab names. For example:
- The **Pipelines** tab
- The **Overview** tab
## that
Do not use **that** when describing a noun. For example:

View File

@ -13,6 +13,15 @@ DETAILS:
You can enable the Microsoft Azure OAuth 2.0 OmniAuth provider and sign in to
GitLab with your Microsoft Azure credentials.
NOTE:
If you're integrating GitLab with Azure/Entra ID for the first time,
configure the [OpenID Connect protocol](../administration/auth/oidc.md#configure-microsoft-azure),
which uses the Microsoft identity platform (v2.0) endpoint.
## Migrate to Generic OpenID Connect configuration
In GitLab 17.0 and later, instances using `azure_oauth2` must migrate to the Generic OpenID Connect configuration. For more information, see [Migrating to the OpenID Connect protocol](../administration/auth/oidc.md#migrate-to-generic-openid-connect-configuration).
## Register an Azure application
To enable the Microsoft Azure OAuth 2.0 OmniAuth provider, you must register

View File

@ -48,3 +48,16 @@ For example, to change to the `main` branch:
```shell
git checkout main
```
## Keep a branch up-to-date
Your branch does not automatically include changes merged to the default branch from other branches.
To include changes merged after you created your branch, you must update your branch manually.
To update your branch with the latest changes in the default branch, either:
- Run `git rebase` to [rebase](git_rebase.md) your branch against the default branch. Use this command when you want
your changes to be listed in Git logs after the changes from the default branch.
- Run `git pull <remote-name> <default-branch-name>`. Use this command when you want your changes to appear in Git logs
in chronological order with the changes from the default branch, or if you're sharing your branch with others. If
you're unsure of the correct value for `<remote-name>`, run: `git remote`.

View File

@ -15,7 +15,6 @@ DETAILS:
FLAG:
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
Use Pipeline execution policies to enforce CI/CD jobs for all applicable projects.
@ -60,6 +59,7 @@ Note the following:
- The `override_project_ci` strategy will not override other security policy configurations.
- The `override_project_ci` strategy takes precedence over other policies using the `inject` strategy. If any policy with `override_project_ci` applies, the project CI configuration will be ignored.
- You should choose unique job names for pipeline execution policies. Some CI/CD configurations are based on job names and it can lead to unwanted results if a job exists multiple times in the same pipeline. The `needs` keyword, for example makes one job dependent on another. In case of multiple jobs with the same name, it will randomly depend on one of them.
- The ability to enforce a scan execution policy and pipeline execution policy concurrently against the same project is not currently supported. You can use pipeline execution policies in isolation, or you can create scan execution policies and pipeline execution policies that target a different set of projects within the scope. Support for enforcing both a scan execution policy and pipeline execution policy on the same project is proposed in [issue 473112](https://gitlab.com/gitlab-org/gitlab/-/issues/473112).
### Job naming best practice

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class BackfillApprovalGroupRulesProtectedBranchesGroupId < BackfillDesiredShardingKeyJob
operation_name :backfill_approval_group_rules_protected_branches_group_id
feature_category :source_code_management
end
end
end

View File

@ -16,7 +16,7 @@ module Sidebars
override :sprite_icon
def sprite_icon
'messages'
'bullhorn'
end
override :active_routes

View File

@ -13665,6 +13665,9 @@ msgstr ""
msgid "ComplianceReport|Create a new framework"
msgstr ""
msgid "ComplianceReport|Create policy"
msgstr ""
msgid "ComplianceReport|Dismiss"
msgstr ""
@ -31672,6 +31675,12 @@ msgstr ""
msgid "ManualOrdering|Couldn't save the order of the issues"
msgstr ""
msgid "ManualVariables|There are no manually-specified variables for this pipeline"
msgstr ""
msgid "ManualVariables|When you %{helpPageUrlStart}run a pipeline manually%{helpPageUrlEnd}, you can specify additional CI/CD variables to use in that pipeline run."
msgstr ""
msgid "Manually link this issue by adding it to the linked issue section of the %{linkStart}originating vulnerability%{linkEnd}."
msgstr ""

View File

@ -67,7 +67,7 @@
"@gitlab/cluster-client": "^2.2.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.106.0",
"@gitlab/svgs": "3.107.0",
"@gitlab/ui": "86.13.0",
"@gitlab/web-ide": "^0.0.1-dev-20240613133550",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",

View File

@ -23,11 +23,13 @@ RSpec.describe 'Database schema', feature_category: :database do
ci_daily_build_group_report_results: [%w[partition_id last_pipeline_id]], # index on last_pipeline_id is sufficient
ci_builds: [%w[partition_id stage_id], %w[partition_id execution_config_id], %w[partition_id upstream_pipeline_id], %w[auto_canceled_by_partition_id auto_canceled_by_id], %w[partition_id commit_id]], # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142804#note_1745483081
ci_pipeline_variables: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient
p_ci_pipeline_variables: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient
ci_pipelines_config: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient
ci_pipeline_metadata: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient
ci_pipeline_messages: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient
p_ci_builds: [%w[partition_id stage_id], %w[partition_id execution_config_id]], # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142804#note_1745483081
ci_stages: [%w[partition_id pipeline_id]], # the index on pipeline_id is sufficient
p_ci_stages: [%w[partition_id pipeline_id]], # the index on pipeline_id is sufficient
ai_testing_terms_acceptances: %w[user_id], # testing terms only have 1 entry, and if the user is deleted the record should remain
p_ci_builds_execution_configs: [%w[partition_id pipeline_id]], # the index on pipeline_id is enough
ci_sources_pipelines: [%w[source_partition_id source_pipeline_id], %w[partition_id pipeline_id]],

View File

@ -47,7 +47,7 @@ RSpec.describe 'Runners', feature_category: :fleet_visibility do
context 'when a project has enabled shared_runners' do
let_it_be(:project) { create(:project) }
before do
before_all do
project.add_maintainer(user)
end
@ -142,6 +142,48 @@ RSpec.describe 'Runners', feature_category: :fleet_visibility do
end
end
context 'when the project_runner_edit_form_vue feature is enabled', :js do
before do
stub_feature_flags(project_runner_edit_form_vue: true)
end
it 'user edits runner to set it as protected' do
visit project_runners_path(project)
within_testid 'assigned_project_runners' do
first('[data-testid="edit-runner-link"]').click
end
expect(page.find_field('protected')).not_to be_checked
check 'protected'
click_button 'Save changes'
expect(page).to have_content 'Protected Yes'
end
context 'when a runner has a tag' do
before do
project_runner.update!(tag_list: ['tag'])
end
it 'user edits runner to not run untagged jobs' do
visit project_runners_path(project)
within_testid 'assigned_project_runners' do
first('[data-testid="edit-runner-link"]').click
end
expect(page.find_field('run-untagged')).to be_checked
uncheck 'run-untagged'
click_button 'Save changes'
expect(page).to have_content 'Can run untagged jobs No'
end
end
end
context 'when a shared runner is activated on the project' do
let!(:shared_runner) { create(:ci_runner, :instance) }

View File

@ -0,0 +1,23 @@
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import EmptyState from '~/ci/pipeline_details/manual_variables/empty_state.vue';
describe('ManualVariablesEmptyState', () => {
describe('when component is created', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(EmptyState);
};
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
it('should render empty state with message', () => {
createComponent();
expect(findEmptyState().props()).toMatchObject({
svgPath: EmptyState.EMPTY_VARIABLES_SVG,
title: 'There are no manually-specified variables for this pipeline',
});
});
});
});

View File

@ -0,0 +1,67 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ManualVariablesApp from '~/ci/pipeline_details/manual_variables/manual_variables.vue';
import EmptyState from '~/ci/pipeline_details/manual_variables/empty_state.vue';
import VariableTable from '~/ci/pipeline_details/manual_variables/variable_table.vue';
import GetManualVariablesQuery from '~/ci/pipeline_details/manual_variables/graphql/queries/get_manual_variables.query.graphql';
import { generateVariablePairs, mockManualVariableConnection } from './mock_data';
Vue.use(VueApollo);
describe('ManualVariableApp', () => {
let wrapper;
const mockResolver = jest.fn();
const createMockApolloProvider = (resolver) => {
const requestHandlers = [[GetManualVariablesQuery, resolver]];
return createMockApollo(requestHandlers);
};
const createComponent = (variables = []) => {
mockResolver.mockResolvedValue(mockManualVariableConnection(variables));
wrapper = shallowMount(ManualVariablesApp, {
provide: {
manualVariablesCount: variables.length,
projectPath: 'root/ci-project',
pipelineIid: '1',
},
apolloProvider: createMockApolloProvider(mockResolver),
});
};
const findEmptyState = () => wrapper.findComponent(EmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findVariableTable = () => wrapper.findComponent(VariableTable);
afterEach(() => {
mockResolver.mockClear();
});
describe('when component is created', () => {
it('renders empty state when no variables were found', () => {
createComponent();
expect(findEmptyState().exists()).toBe(true);
});
it('renders loading state when variables were found', () => {
createComponent(generateVariablePairs(1));
expect(findEmptyState().exists()).toBe(false);
expect(findLoadingIcon().exists()).toBe(true);
expect(findVariableTable().exists()).toBe(false);
});
it('renders variable table when variables were retrieved', async () => {
createComponent(generateVariablePairs(1));
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
expect(findVariableTable().exists()).toBe(true);
});
});
});

View File

@ -0,0 +1,26 @@
export const generateVariablePairs = (count) => {
return Array.from({ length: count }).map((_, index) => ({
key: `key_${index}`,
value: `value_${index}`,
}));
};
export const mockManualVariableConnection = (variables = []) => ({
data: {
project: {
__typename: 'Project',
id: 'root/ci-project/1',
pipeline: {
id: '1',
manualVariables: {
__typename: 'CiManualVariableConnection',
nodes: variables.map((variable) => ({
...variable,
id: variable.key,
})),
},
__typename: 'Pipeline',
},
},
},
});

View File

@ -0,0 +1,113 @@
import { nextTick } from 'vue';
import { GlPagination, GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import VariableTable from '~/ci/pipeline_details/manual_variables/variable_table.vue';
import { generateVariablePairs } from './mock_data';
const defaultCanReadVariables = true;
const defaultManualVariablesCount = 0;
describe('ManualVariableTable', () => {
let wrapper;
const createComponent = (provides = {}, variables = []) => {
wrapper = mountExtended(VariableTable, {
provide: {
manualVariablesCount: defaultManualVariablesCount,
canReadVariables: defaultCanReadVariables,
...provides,
},
propsData: {
variables,
},
});
};
const findButton = () => wrapper.findComponent(GlButton);
const findPaginator = () => wrapper.findComponent(GlPagination);
const findValues = () => wrapper.findAllByTestId('manual-variable-value');
describe('when component is created', () => {
describe('reveal/hide button', () => {
it('should render the button when has permissions', () => {
createComponent();
expect(findButton().exists()).toBe(true);
});
it('should not render the button when does not have permissions', () => {
createComponent({
canReadVariables: false,
});
expect(findButton().exists()).toBe(false);
});
});
describe('paginator', () => {
it('should not render paginator without any data', () => {
createComponent();
expect(findPaginator().exists()).toBe(false);
});
it('should not render paginator with data less or equal to 15', () => {
const mockData = generateVariablePairs(15);
createComponent(
{
manualVariablesCount: mockData.length,
},
mockData,
);
expect(findPaginator().exists()).toBe(false);
});
it('should render paginator when data is greater than 15', () => {
const mockData = generateVariablePairs(16);
createComponent(
{
manualVariablesCount: mockData.length,
},
mockData,
);
expect(findPaginator().exists()).toBe(true);
});
});
});
describe('when click on the reveal/hide button', () => {
it('should toggle button text', async () => {
createComponent();
const button = findButton();
expect(button.text()).toBe('Reveal values');
button.vm.$emit('click');
await nextTick();
expect(button.text()).toBe('Hide values');
});
it('should reveal the values when click on the button', async () => {
const mockData = generateVariablePairs(15);
createComponent(
{
manualVariablesCount: mockData.length,
},
mockData,
);
const values = findValues();
expect(values).toHaveLength(mockData.length);
expect(values.wrappers.every((w) => w.text() === '****')).toBe(true);
await findButton().trigger('click');
expect(values.wrappers.map((w) => w.text())).toStrictEqual(mockData.map((d) => d.value));
});
});
});

View File

@ -1,5 +1,4 @@
import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import mockPipelineActionsQueryResponse from 'test_fixtures/graphql/pipelines/get_pipeline_actions.query.graphql.json';
@ -8,11 +7,10 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
import getPipelineActionsQuery from '~/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql';
import jobPlayMutation from '~/ci/jobs_page/graphql/mutations/job_play.mutation.graphql';
import { TRACKING_CATEGORIES } from '~/ci/constants';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
@ -23,9 +21,10 @@ jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Pipeline manual actions', () => {
let wrapper;
let mock;
const queryHandler = jest.fn().mockResolvedValue(mockPipelineActionsQueryResponse);
const jobPlayMutationHandler = jest.fn();
const {
data: {
project: {
@ -36,9 +35,12 @@ describe('Pipeline manual actions', () => {
},
} = mockPipelineActionsQueryResponse;
const mockPath = nodes[2].playPath;
const createComponent = (limit = 50) => {
const apolloProvider = createMockApollo([
[getPipelineActionsQuery, queryHandler],
[jobPlayMutation, jobPlayMutationHandler],
]);
wrapper = shallowMountExtended(PipelinesManualActions, {
provide: {
fullPath: 'root/ci-project',
@ -50,7 +52,7 @@ describe('Pipeline manual actions', () => {
stubs: {
GlDisclosureDropdown,
},
apolloProvider: createMockApollo([[getPipelineActionsQuery, queryHandler]]),
apolloProvider,
});
};
@ -82,8 +84,6 @@ describe('Pipeline manual actions', () => {
describe('loaded', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
createComponent();
findDropdown().vm.$emit('shown');
@ -92,7 +92,6 @@ describe('Pipeline manual actions', () => {
});
afterEach(() => {
mock.restore();
confirmAction.mockReset();
});
@ -108,8 +107,6 @@ describe('Pipeline manual actions', () => {
describe('on action click', () => {
it('makes a request and toggles the loading state', async () => {
mock.onPost(mockPath).reply(HTTP_STATUS_OK);
findAllDropdownItems().at(1).vm.$emit('action');
await nextTick();
@ -119,10 +116,11 @@ describe('Pipeline manual actions', () => {
await waitForPromises();
expect(findDropdown().props('loading')).toBe(false);
expect(jobPlayMutationHandler).toHaveBeenCalledTimes(1);
});
it('makes a failed request and toggles the loading state', async () => {
mock.onPost(mockPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
jobPlayMutationHandler.mockRejectedValueOnce(new Error('GraphQL error'));
findAllDropdownItems().at(1).vm.$emit('action');
@ -160,9 +158,7 @@ describe('Pipeline manual actions', () => {
.mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
});
it('makes post request after confirming', async () => {
mock.onPost(mockPath).reply(HTTP_STATUS_OK);
it('makes calls GraphQl mutation after confirming', async () => {
confirmAction.mockResolvedValueOnce(true);
findAllDropdownItems().at(2).vm.$emit('action');
@ -171,12 +167,10 @@ describe('Pipeline manual actions', () => {
await waitForPromises();
expect(mock.history.post).toHaveLength(1);
expect(jobPlayMutationHandler).toHaveBeenCalledTimes(1);
});
it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(mockPath).reply(HTTP_STATUS_OK);
it('does not call GraphQl mutation if confirmation is cancelled', async () => {
confirmAction.mockResolvedValueOnce(false);
findAllDropdownItems().at(2).vm.$emit('action');
@ -185,7 +179,7 @@ describe('Pipeline manual actions', () => {
await waitForPromises();
expect(mock.history.post).toHaveLength(0);
expect(jobPlayMutationHandler).not.toHaveBeenCalled();
});
it('displays the remaining time in the dropdown', () => {

View File

@ -8,25 +8,15 @@ RSpec.describe "Work items", '(JavaScript fixtures)', type: :request, feature_ca
include JavaScriptFixturesHelpers
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let(:group_work_item_types_query_path) { 'work_items/graphql/group_work_item_types.query.graphql' }
let(:project_work_item_types_query_path) { 'work_items/graphql/project_work_item_types.query.graphql' }
let(:namespace_work_item_types_query_path) { 'work_items/graphql/namespace_work_item_types.query.graphql' }
it 'graphql/work_items/group_work_item_types.query.graphql.json' do
query = get_graphql_query_as_string(group_work_item_types_query_path)
it 'graphql/work_items/namespace_work_item_types.query.graphql.json' do
query = get_graphql_query_as_string(namespace_work_item_types_query_path)
post_graphql(query, current_user: user, variables: { fullPath: group.full_path })
expect_graphql_errors_to_be_empty
end
it 'graphql/work_items/project_work_item_types.query.graphql.json' do
query = get_graphql_query_as_string(project_work_item_types_query_path)
post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
expect_graphql_errors_to_be_empty
end
end

View File

@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { TEST_HOST } from 'helpers/test_constants';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -10,7 +10,7 @@ import { createAlert } from '~/alert';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import workItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import TaskList from '~/task_list';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
@ -36,7 +36,7 @@ const $toast = {
};
const issueDetailsResponse = getIssueDetailsResponse();
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(namespaceWorkItemTypesQueryResponse);
describe('Description component', () => {
let wrapper;
@ -285,7 +285,7 @@ describe('Description component', () => {
it('calls a mutation to create a task', () => {
const workItemTypeIdForTask =
projectWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.find(
namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.find(
(node) => node.name === 'Task',
).id;
const { confidential, iteration, milestone } = issueDetailsResponse.data.issue;

View File

@ -2,15 +2,13 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import groupWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/group_work_item_types.query.graphql.json';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
import { setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
const showToast = jest.fn();
jest.mock('~/work_items/graphql/cache_utils', () => ({
@ -28,34 +26,19 @@ describe('CreateWorkItemModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.findComponent(CreateWorkItem);
const projectSingleWorkItemTypeQueryResponse = {
const namespaceSingleWorkItemTypeQueryResponse = {
data: {
workspace: {
...projectWorkItemTypesQueryResponse.data.workspace,
...namespaceWorkItemTypesQueryResponse.data.workspace,
workItemTypes: {
nodes: [projectWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes[0]],
},
},
},
};
const groupSingleWorkItemQueryResponse = {
data: {
workspace: {
...groupWorkItemTypesQueryResponse.data.workspace,
workItemTypes: {
nodes: [groupWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes[0]],
nodes: [namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes[0]],
},
},
},
};
const workItemTypesQueryHandler = jest.fn().mockResolvedValue({
data: projectSingleWorkItemTypeQueryResponse.data,
});
const groupWorkItemTypesQueryHandler = jest.fn().mockResolvedValue({
data: groupSingleWorkItemQueryResponse.data,
data: namespaceSingleWorkItemTypeQueryResponse.data,
});
const workItemTypesEmptyQueryHandler = jest.fn().mockResolvedValue({
@ -71,12 +54,11 @@ describe('CreateWorkItemModal', () => {
const createComponent = ({
workItemTypeName = 'EPIC',
projectWorkItemTypesQueryHandler = workItemTypesQueryHandler,
namespaceWorkItemTypesQueryHandler = workItemTypesQueryHandler,
asDropdownItem = false,
} = {}) => {
apolloProvider = createMockApollo([
[projectWorkItemTypesQuery, projectWorkItemTypesQueryHandler],
[groupWorkItemTypesQuery, groupWorkItemTypesQueryHandler],
[namespaceWorkItemTypesQuery, namespaceWorkItemTypesQueryHandler],
]);
wrapper = shallowMount(CreateWorkItemModal, {
@ -151,7 +133,7 @@ describe('CreateWorkItemModal', () => {
});
it('when there are no work item types it does not set the cache', async () => {
createComponent({ projectWorkItemTypesQueryHandler: workItemTypesEmptyQueryHandler });
createComponent({ namespaceWorkItemTypesQueryHandler: workItemTypesEmptyQueryHandler });
await waitForPromises();

View File

@ -2,8 +2,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import groupWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/group_work_item_types.query.graphql.json';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
@ -12,20 +11,19 @@ import WorkItemDescription from '~/work_items/components/work_item_description.v
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { WORK_ITEM_TYPE_ENUM_EPIC } from '~/work_items/constants';
import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { resolvers } from '~/graphql_shared/issuable_client';
import { createWorkItemMutationResponse, createWorkItemQueryResponse } from '../mock_data';
const projectSingleWorkItemTypeQueryResponse = {
const namespaceSingleWorkItemTypeQueryResponse = {
data: {
workspace: {
...projectWorkItemTypesQueryResponse.data.workspace,
...namespaceWorkItemTypesQueryResponse.data.workspace,
workItemTypes: {
nodes: [projectWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes[0]],
nodes: [namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes[0]],
},
},
},
@ -45,7 +43,9 @@ describe('Create work item component', () => {
.fn()
.mockResolvedValue(createWorkItemQueryResponse);
const groupWorkItemQuerySuccessHandler = jest.fn().mockResolvedValue(createWorkItemQueryResponse);
const namespaceWorkItemTypesHandler = jest
.fn()
.mockResolvedValue(namespaceWorkItemTypesQueryResponse);
const findFormTitle = () => wrapper.find('h1');
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(WorkItemTitle);
@ -71,24 +71,19 @@ describe('Create work item component', () => {
[groupWorkItemByIidQuery, groupWorkItemQuerySuccessHandler],
[workItemByIidQuery, projectWorkItemQuerySuccessHandler],
[createWorkItemMutation, mutationHandler],
[namespaceWorkItemTypesQuery, namespaceWorkItemTypesHandler],
],
resolvers,
{ typePolicies: { Project: { merge: true } } },
);
const projectWorkItemTypeResponse = singleWorkItemType
? projectSingleWorkItemTypeQueryResponse
: projectWorkItemTypesQueryResponse;
const namespaceWorkItemTypeResponse = singleWorkItemType
? namespaceSingleWorkItemTypeQueryResponse
: namespaceWorkItemTypesQueryResponse;
mockApollo.clients.defaultClient.cache.writeQuery({
query: isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery,
query: namespaceWorkItemTypesQuery,
variables: { fullPath: 'full-path', name: workItemTypeName },
data: isGroup
? {
...groupWorkItemTypesQueryResponse.data,
}
: {
...projectWorkItemTypeResponse.data,
},
data: namespaceWorkItemTypeResponse.data,
});
wrapper = shallowMount(CreateWorkItem, {
@ -173,27 +168,13 @@ describe('Create work item component', () => {
});
describe('Work item types dropdown', () => {
it('displays a list of project work item types', async () => {
it('displays a list of namespace work item types', async () => {
createComponent();
await waitForPromises();
// +1 for the "None" option
const expectedOptions =
projectWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.length + 1;
expect(findSelect().attributes('options').split(',')).toHaveLength(expectedOptions);
});
it('fetches group work item types when isGroup is true', async () => {
createComponent({
isGroup: true,
});
await waitForPromises();
const expectedOptions =
groupWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.length + 1;
namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.length + 1;
expect(findSelect().attributes('options').split(',')).toHaveLength(expectedOptions);
});

View File

@ -8,7 +8,7 @@ import {
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
@ -33,7 +33,7 @@ import {
} from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql';
import {
@ -86,7 +86,7 @@ describe('WorkItemActions component', () => {
hide: jest.fn(),
};
const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const typesQuerySuccessHandler = jest.fn().mockResolvedValue(namespaceWorkItemTypesQueryResponse);
const convertWorkItemMutationSuccessHandler = jest
.fn()
.mockResolvedValue(convertWorkItemMutationResponse);
@ -125,7 +125,7 @@ describe('WorkItemActions component', () => {
wrapper = shallowMountExtended(WorkItemActions, {
isLoggedIn: isLoggedIn(),
apolloProvider: createMockApollo([
[projectWorkItemTypesQuery, typesQuerySuccessHandler],
[namespaceWorkItemTypesQuery, typesQuerySuccessHandler],
[convertWorkItemMutation, convertWorkItemMutationHandler],
[updateWorkItemNotificationsMutation, notificationsMutationHandler],
[updateWorkItemMutation, lockDiscussionMutationHandler],

View File

@ -1,8 +1,7 @@
import Vue, { nextTick } from 'vue';
import { GlForm, GlFormGroup, GlFormInput, GlFormCheckbox, GlTooltip } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import groupWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/group_work_item_types.query.graphql.json';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
import { sprintf, s__ } from '~/locale';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -23,27 +22,24 @@ import {
WORK_ITEM_TYPE_ENUM_EPIC,
} from '~/work_items/constants';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import updateWorkItemHierarchyMutation from '~/work_items/graphql/update_work_item_hierarchy.mutation.graphql';
import groupProjectsForLinksWidgetQuery from '~/work_items/graphql/group_projects_for_links_widget.query.graphql';
import relatedProjectsForLinksWidgetQuery from '~/work_items/graphql/related_projects_for_links_widget.query.graphql';
import namespaceProjectsForLinksWidgetQuery from '~/work_items/graphql/namespace_projects_for_links_widget.query.graphql';
import {
availableWorkItemsResponse,
createWorkItemMutationResponse,
updateWorkItemMutationResponse,
mockIterationWidgetResponse,
groupProjectsList,
relatedProjectsList,
namespaceProjectsList,
} from '../../mock_data';
Vue.use(VueApollo);
const projectData = groupProjectsList.data.group.projects.nodes;
const projectData = namespaceProjectsList.data.namespace.projects.nodes;
const findWorkItemTypeId = (typeName) => {
return projectWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.find(
return namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.find(
(node) => node.name === typeName,
).id;
};
@ -63,12 +59,12 @@ describe('WorkItemLinksForm', () => {
const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
const createMutationRejection = jest.fn().mockRejectedValue(new Error('error'));
const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
const projectWorkItemTypesResolver = jest
const namespaceWorkItemTypesResolver = jest
.fn()
.mockResolvedValue(projectWorkItemTypesQueryResponse);
const groupWorkItemTypesResolver = jest.fn().mockResolvedValue(groupWorkItemTypesQueryResponse);
const groupProjectsFormLinksWidgetResolver = jest.fn().mockResolvedValue(groupProjectsList);
const relatedProjectsForLinksWidgetResolver = jest.fn().mockResolvedValue(relatedProjectsList);
.mockResolvedValue(namespaceWorkItemTypesQueryResponse);
const namespaceProjectsFormLinksWidgetResolver = jest
.fn()
.mockResolvedValue(namespaceProjectsList);
const mockParentIteration = mockIterationWidgetResponse;
@ -87,10 +83,8 @@ describe('WorkItemLinksForm', () => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, availableWorkItemsResolver],
[projectWorkItemTypesQuery, projectWorkItemTypesResolver],
[groupWorkItemTypesQuery, groupWorkItemTypesResolver],
[groupProjectsForLinksWidgetQuery, groupProjectsFormLinksWidgetResolver],
[relatedProjectsForLinksWidgetQuery, relatedProjectsForLinksWidgetResolver],
[namespaceWorkItemTypesQuery, namespaceWorkItemTypesResolver],
[namespaceProjectsForLinksWidgetQuery, namespaceProjectsFormLinksWidgetResolver],
[updateWorkItemHierarchyMutation, updateMutation],
[createWorkItemMutation, createMutation],
]),
@ -142,8 +136,8 @@ describe('WorkItemLinksForm', () => {
it.each`
workspace | isGroup | queryResolver
${'project'} | ${false} | ${projectWorkItemTypesResolver}
${'group'} | ${true} | ${groupWorkItemTypesResolver}
${'project'} | ${false} | ${namespaceWorkItemTypesResolver}
${'group'} | ${true} | ${namespaceWorkItemTypesResolver}
`(
'fetches $workspace work item types when isGroup is $isGroup',
async ({ isGroup, queryResolver }) => {

View File

@ -5,19 +5,13 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemProjectsListbox from '~/work_items/components/work_item_links/work_item_projects_listbox.vue';
import groupProjectsForLinksWidgetQuery from '~/work_items/graphql/group_projects_for_links_widget.query.graphql';
import relatedProjectsForLinksWidgetQuery from '~/work_items/graphql/related_projects_for_links_widget.query.graphql';
import namespaceProjectsForLinksWidgetQuery from '~/work_items/graphql/namespace_projects_for_links_widget.query.graphql';
import { SEARCH_DEBOUNCE } from '~/work_items/constants';
import {
groupProjectsList,
relatedProjectsList,
mockFrequentlyUsedProjects,
} from '../../mock_data';
import { namespaceProjectsList, mockFrequentlyUsedProjects } from '../../mock_data';
Vue.use(VueApollo);
const groupProjectsData = groupProjectsList.data.group.projects.nodes;
const relatedProjectsData = relatedProjectsList.data.project.group.projects.nodes;
const namespaceProjectsData = namespaceProjectsList.data.namespace.projects.nodes;
describe('WorkItemProjectsListbox', () => {
/**
@ -37,8 +31,9 @@ describe('WorkItemProjectsListbox', () => {
localStorage.removeItem(getLocalstorageKey());
};
const groupProjectsFormLinksWidgetResolver = jest.fn().mockResolvedValue(groupProjectsList);
const relatedProjectsFormLinksWidgetResolver = jest.fn().mockResolvedValue(relatedProjectsList);
const namespaceProjectsFormLinksWidgetResolver = jest
.fn()
.mockResolvedValue(namespaceProjectsList);
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findDropdownItemFor = (fullPath) => wrapper.findByTestId(`listbox-item-${fullPath}`);
@ -48,8 +43,7 @@ describe('WorkItemProjectsListbox', () => {
const createComponent = async (isGroup = true, fullPath = 'group-a') => {
wrapper = mountExtended(WorkItemProjectsListbox, {
apolloProvider: createMockApollo([
[groupProjectsForLinksWidgetQuery, groupProjectsFormLinksWidgetResolver],
[relatedProjectsForLinksWidgetQuery, relatedProjectsFormLinksWidgetResolver],
[namespaceProjectsForLinksWidgetQuery, namespaceProjectsFormLinksWidgetResolver],
]),
propsData: {
fullPath,
@ -72,10 +66,10 @@ describe('WorkItemProjectsListbox', () => {
expect(findDropdown().text()).not.toContain('Recently used');
const dropdownItem = findDropdownItemFor(groupProjectsData[0].fullPath);
const dropdownItem = findDropdownItemFor(namespaceProjectsData[0].fullPath);
expect(dropdownItem.text()).toContain(groupProjectsData[0].name);
expect(dropdownItem.text()).toContain(groupProjectsData[0].namespace.name);
expect(dropdownItem.text()).toContain(namespaceProjectsData[0].name);
expect(dropdownItem.text()).toContain(namespaceProjectsData[0].namespace.name);
});
it('supports selecting a project', async () => {
@ -85,13 +79,13 @@ describe('WorkItemProjectsListbox', () => {
await nextTick();
await findDropdownItemFor(groupProjectsData[0].fullPath).trigger('click');
await findDropdownItemFor(namespaceProjectsData[0].fullPath).trigger('click');
await nextTick();
const emitted = wrapper.emitted('selectProject');
expect(emitted[1][0]).toEqual(groupProjectsData[0]);
expect(emitted[1][0]).toEqual(namespaceProjectsData[0]);
});
it('renders recent projects if present', async () => {
@ -122,7 +116,7 @@ describe('WorkItemProjectsListbox', () => {
const emitted = wrapper.emitted('selectProject');
expect(emitted[1][0]).toEqual(groupProjectsData[1]);
expect(emitted[1][0]).toEqual(namespaceProjectsData[1]);
});
it('supports filtering recent projects via search input', async () => {
@ -143,7 +137,7 @@ describe('WorkItemProjectsListbox', () => {
content = findRecentDropdownItems();
expect(content).toHaveLength(1);
expect(content.at(0).text()).toContain(groupProjectsData[0].name);
expect(content.at(0).text()).toContain(namespaceProjectsData[0].name);
});
});
@ -158,10 +152,10 @@ describe('WorkItemProjectsListbox', () => {
expect(findDropdown().text()).not.toContain('Recently used');
const dropdownItem = findDropdownItemFor(relatedProjectsData[0].fullPath);
const dropdownItem = findDropdownItemFor(namespaceProjectsData[0].fullPath);
expect(dropdownItem.text()).toContain(relatedProjectsData[0].name);
expect(dropdownItem.text()).toContain(relatedProjectsData[0].namespace.name);
expect(dropdownItem.text()).toContain(namespaceProjectsData[0].name);
expect(dropdownItem.text()).toContain(namespaceProjectsData[0].namespace.name);
});
it('auto-selects the current project', async () => {
@ -169,7 +163,7 @@ describe('WorkItemProjectsListbox', () => {
const emitted = wrapper.emitted('selectProject');
expect(emitted[0][0]).toEqual(relatedProjectsData[0]);
expect(emitted[0][0]).toEqual(namespaceProjectsData[0]);
});
it('supports selecting a project', async () => {
@ -179,13 +173,13 @@ describe('WorkItemProjectsListbox', () => {
await nextTick();
await findDropdownItemFor(relatedProjectsData[1].fullPath).trigger('click');
await findDropdownItemFor(namespaceProjectsData[1].fullPath).trigger('click');
await nextTick();
const emitted = wrapper.emitted('selectProject');
expect(emitted[1][0]).toEqual(relatedProjectsData[1]);
expect(emitted[1][0]).toEqual(namespaceProjectsData[1]);
});
it('renders recent projects if present', async () => {
@ -216,7 +210,7 @@ describe('WorkItemProjectsListbox', () => {
const emitted = wrapper.emitted('selectProject');
expect(emitted[1][0]).toEqual(relatedProjectsData[1]);
expect(emitted[1][0]).toEqual(namespaceProjectsData[1]);
});
it('supports filtering recent projects via search input', async () => {
@ -237,7 +231,7 @@ describe('WorkItemProjectsListbox', () => {
content = findRecentDropdownItems();
expect(content).toHaveLength(1);
expect(content.at(0).text()).toContain(relatedProjectsData[0].name);
expect(content.at(0).text()).toContain(namespaceProjectsData[0].name);
});
});
});

View File

@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { GlLoadingIcon, GlToggle, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -43,6 +43,8 @@ describe('WorkItemTree', () => {
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
const findTreeActions = () => wrapper.findComponent(WorkItemTreeActions);
const findRolledUpWeight = () => wrapper.findByTestId('rollup-weight');
const findRolledUpWeightValue = () => wrapper.findByTestId('weight-value');
const createComponent = async ({
workItemType = 'Objective',
@ -53,6 +55,8 @@ describe('WorkItemTree', () => {
canUpdateChildren = true,
hasSubepicsFeature = true,
workItemHierarchyTreeHandler = workItemHierarchyTreeResponseHandler,
showRolledUpWeight = false,
rolledUpWeight = 0,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTree, {
propsData: {
@ -64,6 +68,8 @@ describe('WorkItemTree', () => {
confidential,
canUpdate,
canUpdateChildren,
showRolledUpWeight,
rolledUpWeight,
},
apolloProvider: createMockApollo([[getWorkItemTreeQuery, workItemHierarchyTreeHandler]]),
provide: {
@ -248,4 +254,31 @@ describe('WorkItemTree', () => {
},
);
});
describe('rollup data', () => {
describe('rolledUp weight', () => {
it.each`
showRolledUpWeight | rolledUpWeight | expected
${false} | ${0} | ${'rollup weight is not displayed'}
${false} | ${10} | ${'rollup weight is not displayed'}
${true} | ${0} | ${'rollup weight is displayed'}
${true} | ${10} | ${'rollup weight is displayed'}
`(
'When showRolledUpWeight is $showRolledUpWeight and rolledUpWeight is $rolledUpWeight, $expected',
({ showRolledUpWeight, rolledUpWeight }) => {
createComponent({ showRolledUpWeight, rolledUpWeight });
expect(findRolledUpWeight().exists()).toBe(showRolledUpWeight);
},
);
it('should show the correct value when rolledUpWeight is visible', () => {
createComponent({ showRolledUpWeight: true, rolledUpWeight: 10 });
expect(findRolledUpWeight().exists()).toBe(true);
expect(findRolledUpWeight().findComponent(GlIcon).props('name')).toBe('weight');
expect(findRolledUpWeightValue().text()).toBe('10');
});
});
});
});

View File

@ -2,7 +2,7 @@ import { GlButton, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -12,7 +12,7 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import toast from '~/vue_shared/plugins/global_toast';
import WorkItemNotificationsWidget from '~/work_items/components/work_item_notifications_widget.vue';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
import { updateWorkItemNotificationsMutationResponse } from '../mock_data';
@ -32,7 +32,7 @@ describe('WorkItemActions component', () => {
hide: jest.fn(),
};
const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const typesQuerySuccessHandler = jest.fn().mockResolvedValue(namespaceWorkItemTypesQueryResponse);
const toggleNotificationsOffHandler = jest
.fn()
.mockResolvedValue(updateWorkItemNotificationsMutationResponse(false));
@ -53,7 +53,7 @@ describe('WorkItemActions component', () => {
wrapper = shallowMountExtended(WorkItemNotificationsWidget, {
isLoggedIn: isLoggedIn(),
apolloProvider: createMockApollo([
[projectWorkItemTypesQuery, typesQuerySuccessHandler],
[namespaceWorkItemTypesQuery, typesQuerySuccessHandler],
[updateWorkItemNotificationsMutation, notificationsMutationHandler],
]),
propsData: {

View File

@ -930,6 +930,7 @@ export const workItemResponseFactory = ({
linkedItems = mockEmptyLinkedItems,
developmentItems = workItemDevelopmentFragmentResponse(),
color = '#1068bf',
editableWeightWidget = true,
} = {}) => ({
data: {
workItem: {
@ -1020,9 +1021,15 @@ export const workItemResponseFactory = ({
: { type: 'MOCK TYPE' },
weightWidgetPresent
? {
__typename: 'WorkItemWidgetWeight',
type: 'WEIGHT',
weight: 0,
weight: null,
rolledUpWeight: 0,
widgetDefinition: {
editable: editableWeightWidget,
rollUp: !editableWeightWidget,
__typename: 'WorkItemWidgetDefinitionWeight',
},
__typename: 'WorkItemWidgetWeight',
}
: { type: 'MOCK TYPE' },
iterationWidgetPresent
@ -4352,9 +4359,9 @@ export const allowedChildrenTypesResponse = {
export const generateWorkItemsListWithId = (count) =>
Array.from({ length: count }, (_, i) => ({ id: `gid://gitlab/WorkItem/${i + 1}` }));
export const groupProjectsList = {
export const namespaceProjectsList = {
data: {
group: {
namespace: {
id: 'gid://gitlab/Group/1',
projects: {
nodes: [
@ -4392,50 +4399,6 @@ export const groupProjectsList = {
},
};
export const relatedProjectsList = {
data: {
project: {
id: 'gid://gitlab/Project/1',
group: {
id: 'gid://gitlab/Group/33',
projects: {
nodes: [
{
id: 'gid://gitlab/Project/1',
name: 'Example project A',
avatarUrl: null,
nameWithNamespace: 'Group A / Example project A',
fullPath: 'group-a/example-project-a',
namespace: {
id: 'gid://gitlab/Group/1',
name: 'Group A',
__typename: 'Namespace',
},
__typename: 'Project',
},
{
id: 'gid://gitlab/Project/2',
name: 'Example project B',
avatarUrl: null,
nameWithNamespace: 'Group A / Example project B',
fullPath: 'group-a/example-project-b',
namespace: {
id: 'gid://gitlab/Group/1',
name: 'Group A',
__typename: 'Namespace',
},
__typename: 'Project',
},
],
__typename: 'ProjectConnection',
},
__typename: 'Group',
},
__typename: 'Project',
},
},
};
export const mockFrequentlyUsedProjects = [
{
id: 1,

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillApprovalGroupRulesProtectedBranchesGroupId,
feature_category: :source_code_management,
schema: 20240716135028 do
include_examples 'desired sharding key backfill job' do
let(:batch_table) { :approval_group_rules_protected_branches }
let(:backfill_column) { :group_id }
let(:backfill_via_table) { :approval_group_rules }
let(:backfill_via_column) { :group_id }
let(:backfill_via_foreign_key) { :approval_group_rule_id }
end
end

View File

@ -6,7 +6,7 @@ RSpec.describe Sidebars::Admin::Menus::MessagesMenu, feature_category: :navigati
it_behaves_like 'Admin menu',
link: '/admin/broadcast_messages',
title: s_('Admin|Messages'),
icon: 'messages'
icon: 'bullhorn'
it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :broadcast_messages }
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillApprovalGroupRulesProtectedBranchesGroupId, feature_category: :source_code_management 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: :approval_group_rules_protected_branches,
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: [
:group_id,
:approval_group_rules,
:group_id,
:approval_group_rule_id
]
)
}
end
end
end

View File

@ -40,14 +40,14 @@ RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_compositio
render
expect(rendered).to have_link s_('Go to the pipeline editor'),
href: project_ci_pipeline_editor_path(project)
href: project_ci_pipeline_editor_path(project, branch_name: pipeline.source_ref)
end
it 'renders the pipeline editor button with correct link for users who can not view' do
render
expect(rendered).not_to have_link s_('Go to the pipeline editor'),
href: project_ci_pipeline_editor_path(project)
href: project_ci_pipeline_editor_path(project, branch_name: pipeline.source_ref)
end
end

View File

@ -1354,10 +1354,10 @@
stylelint-declaration-strict-value "1.10.4"
stylelint-scss "6.0.0"
"@gitlab/svgs@3.106.0":
version "3.106.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.106.0.tgz#eb5a2331f59c5c258c54dd6f905dda57b4c68c0e"
integrity sha512-NOU2eW7aQaOtll2DxYeLR/IIphMP8l7WKHUqB1tbIAVt/QOPio9pJym17FzpBc1Deibxtf7dS0wEl4DoNLTxoA==
"@gitlab/svgs@3.107.0":
version "3.107.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.107.0.tgz#dfc55de9ae0af42255fe9acca00f3b0d55fe11fd"
integrity sha512-kKlCJsxdt3GGus60tkfb31fzdmH9P8eVeCqzaopyA05ojzcLjmD5LzU7AVQGrkELsGLRi9A0+5NLVR7v0gjL6A==
"@gitlab/ui@86.13.0":
version "86.13.0"