Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d861e5b4ca
commit
accfc89b9a
|
|
@ -1 +1 @@
|
|||
dd6ef2a257606d4499cab51c80966a474f1a840f
|
||||
f8f71797e1e8a1ad0e451becfe5294578e28894d
|
||||
|
|
|
|||
|
|
@ -381,6 +381,8 @@ export default {
|
|||
},
|
||||
resetForm() {
|
||||
this.variable = { ...defaultVariableState };
|
||||
|
||||
this.visibility = VISIBILITY_VISIBLE;
|
||||
},
|
||||
setEnvironmentScope(scope) {
|
||||
this.variable = { ...this.variable, environmentScope: scope };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { getDiffMode } from '~/diffs/store/utils';
|
|||
import { diffViewerModes } from '~/ide/constants';
|
||||
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||
import { isCollapsed } from '~/diffs/utils/diff_file';
|
||||
import { FILE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
import { FILE_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
|
||||
const FIRST_CHAR_REGEX = /^(\+|-| )/;
|
||||
|
||||
|
|
@ -69,11 +69,21 @@ export default {
|
|||
return this.discussion.position?.position_type;
|
||||
},
|
||||
isFileDiscussion() {
|
||||
if (!this.discussion.diff_file) {
|
||||
return (
|
||||
this.discussion.original_position.position_type === IMAGE_DIFF_POSITION_TYPE ||
|
||||
this.discussion.original_position.position_type === FILE_DIFF_POSITION_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
return this.positionType === FILE_DIFF_POSITION_TYPE;
|
||||
},
|
||||
showHeader() {
|
||||
if (this.discussion.diff_file) return true;
|
||||
|
||||
return (
|
||||
this.discussion.diff_file || this.discussion.original_position.position_type === 'file'
|
||||
this.discussion.original_position.position_type === FILE_DIFF_POSITION_TYPE ||
|
||||
this.discussion.original_position.position_type === IMAGE_DIFF_POSITION_TYPE
|
||||
);
|
||||
},
|
||||
backfilledDiffFile() {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
|
|||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import { detectAndConfirmSensitiveTokens } from '~/lib/utils/secret_detection';
|
||||
import { FILE_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
import eventHub from '../event_hub';
|
||||
import noteable from '../mixins/noteable';
|
||||
import resolvable from '../mixins/resolvable';
|
||||
|
|
@ -163,9 +164,22 @@ export default {
|
|||
},
|
||||
canShowReplyActions() {
|
||||
if (this.shouldRenderDiffs) {
|
||||
if (this.discussion.diff_file?.diff_refs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* https://gitlab.com/gitlab-com/gl-infra/production/-/issues/19118
|
||||
*
|
||||
* For most diff discussions we should have a `diff_file`.
|
||||
* However in some cases we might we might not have this object.
|
||||
* In these we need to check if the `original_position.position_type`
|
||||
* is either a file or an image, doing this allows us to still
|
||||
* render the reply actions.
|
||||
*/
|
||||
return (
|
||||
this.discussion.original_position.position_type === 'file' ||
|
||||
this.discussion.diff_file?.diff_refs
|
||||
this.discussion.original_position?.position_type === FILE_DIFF_POSITION_TYPE ||
|
||||
this.discussion.original_position?.position_type === IMAGE_DIFF_POSITION_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlForm, GlFormInput, GlFormSelect, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN = 'ADMIN';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
inject: ['projectPath'],
|
||||
data() {
|
||||
return {
|
||||
protectionRuleFormData: {
|
||||
tagNamePattern: '',
|
||||
minimumAccessLevelForPush: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
minimumAccessLevelForDelete: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
},
|
||||
};
|
||||
},
|
||||
minimumAccessLevelOptions: [
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: __('Admin') },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form>
|
||||
<gl-form-group
|
||||
:label="s__('ContainerRegistry|Protect container tags matching')"
|
||||
label-for="input-tag-name-pattern"
|
||||
>
|
||||
<gl-form-input
|
||||
id="input-tag-name-pattern"
|
||||
v-model.trim="protectionRuleFormData.tagNamePattern"
|
||||
type="text"
|
||||
/>
|
||||
<template #description>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'ContainerRegistry|Tags with names that match this regex pattern are protected. Must be less than 100 characters. %{linkStart}What regex patterns are supported?%{linkEnd}',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link href="https://github.com/google/re2/wiki/syntax" target="_blank">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
:label="s__('ContainerRegistry|Minimum role allowed to push')"
|
||||
label-for="input-minimum-access-level-for-push"
|
||||
>
|
||||
<gl-form-select
|
||||
id="input-minimum-access-level-for-push"
|
||||
v-model="protectionRuleFormData.minimumAccessLevelForPush"
|
||||
:options="$options.minimumAccessLevelOptions"
|
||||
/>
|
||||
<template #description>
|
||||
{{
|
||||
s__(
|
||||
'ContainerRegistry|Only users with at least this role can push tags with a name that matches the protection rule.',
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
:label="s__('ContainerRegistry|Minimum role allowed to delete')"
|
||||
label-for="input-minimum-access-level-for-delete"
|
||||
>
|
||||
<gl-form-select
|
||||
id="input-minimum-access-level-for-delete"
|
||||
v-model="protectionRuleFormData.minimumAccessLevelForDelete"
|
||||
:options="$options.minimumAccessLevelOptions"
|
||||
/>
|
||||
<template #description>
|
||||
{{
|
||||
s__(
|
||||
'ContainerRegistry|Only users with at least this role can delete tags with a name that matches the protection rule.',
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
</gl-form-group>
|
||||
</gl-form>
|
||||
</template>
|
||||
|
|
@ -3,6 +3,7 @@ import {
|
|||
GlAlert,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
|
|
@ -11,6 +12,7 @@ import {
|
|||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import ContainerProtectionTagRuleForm from '~/packages_and_registries/settings/project/components/container_protection_tag_rule_form.vue';
|
||||
import getContainerProtectionTagRulesQuery from '~/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql';
|
||||
import deleteContainerProtectionTagRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_container_protection_tag_rule.mutation.graphql';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
|
@ -22,10 +24,12 @@ const I18N_MINIMUM_ACCESS_LEVEL_TO_DELETE = s__('ContainerRegistry|Minimum acces
|
|||
|
||||
export default {
|
||||
components: {
|
||||
ContainerProtectionTagRuleForm,
|
||||
CrudComponent,
|
||||
GlAlert,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
|
|
@ -63,6 +67,7 @@ export default {
|
|||
protectionRuleMutationItem: null,
|
||||
protectionRulesQueryPayload: { nodes: [], pageInfo: {} },
|
||||
protectionRulesQueryPaginationParams: { first: MAX_LIMIT },
|
||||
showDrawer: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -98,6 +103,9 @@ export default {
|
|||
clearAlertMessage() {
|
||||
this.alertErrorMessage = '';
|
||||
},
|
||||
closeDrawer() {
|
||||
this.showDrawer = false;
|
||||
},
|
||||
async deleteProtectionRule(protectionRule) {
|
||||
this.clearAlertMessage();
|
||||
|
||||
|
|
@ -121,6 +129,9 @@ export default {
|
|||
this.resetProtectionRuleMutation();
|
||||
}
|
||||
},
|
||||
openDrawer() {
|
||||
this.showDrawer = true;
|
||||
},
|
||||
refetchProtectionRules() {
|
||||
this.$apollo.queries.protectionRulesQueryPayload.refetch();
|
||||
},
|
||||
|
|
@ -181,8 +192,11 @@ export default {
|
|||
|
||||
<template>
|
||||
<crud-component
|
||||
:collapsed="false"
|
||||
:title="$options.i18n.title"
|
||||
:toggle-text="s__('ContainerRegistry|Add protection rule')"
|
||||
data-testid="project-container-protection-tag-rules-settings"
|
||||
@showForm="openDrawer"
|
||||
>
|
||||
<template v-if="containsTableItems" #count>
|
||||
<gl-badge>
|
||||
|
|
@ -196,6 +210,7 @@ export default {
|
|||
</gl-sprintf>
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<p
|
||||
class="gl-pb-0 gl-text-subtle"
|
||||
|
|
@ -252,6 +267,18 @@ export default {
|
|||
<p v-else data-testid="empty-text" class="gl-text-subtle">
|
||||
{{ s__('ContainerRegistry|No container image tags are protected.') }}
|
||||
</p>
|
||||
|
||||
<gl-drawer :z-index="1400" :open="showDrawer" @close="closeDrawer">
|
||||
<template #title>
|
||||
<h2 class="gl-my-0 gl-text-size-h2 gl-leading-24">
|
||||
{{ s__('ContainerRegistry|Add protection rule') }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #default>
|
||||
<container-protection-tag-rule-form />
|
||||
</template>
|
||||
</gl-drawer>
|
||||
|
||||
<gl-modal
|
||||
v-if="protectionRuleMutationItem"
|
||||
:modal-id="$options.modal.id"
|
||||
|
|
|
|||
|
|
@ -200,21 +200,6 @@ export default {
|
|||
return !this.isUsingLfs || (this.isUsingLfs && this.lfsWarningDismissed);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
createNewBranch: {
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.form.fields.branch_name.value = '';
|
||||
} else {
|
||||
this.form.fields.branch_name = {
|
||||
...this.form.fields.branch_name,
|
||||
value: this.originalBranch,
|
||||
state: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs[this.modalId].show();
|
||||
|
|
@ -233,9 +218,7 @@ export default {
|
|||
e.preventDefault(); // Prevent modal from closing
|
||||
|
||||
if (this.showLfsWarning) {
|
||||
this.lfsWarningDismissed = true;
|
||||
await this.$nextTick();
|
||||
this.$refs.message?.$el.focus();
|
||||
await this.handleContinueLfsWarning();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export default {
|
|||
|
||||
formData.append('dir_name', this.dir);
|
||||
if (!formData.has('branch_name')) {
|
||||
formData.append('branch_name', this.targetBranch);
|
||||
formData.append('branch_name', this.originalBranch);
|
||||
}
|
||||
|
||||
return axios
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export default {
|
|||
:href="targetUrl"
|
||||
:data-track-label="trackingLabel"
|
||||
:data-track-action="$options.TRACK_ACTION"
|
||||
class="gl-flex gl-min-w-0 gl-flex-1 gl-flex-wrap gl-justify-end gl-gap-y-3 !gl-text-default !gl-no-underline !gl-outline-none sm:gl-flex-nowrap sm:gl-items-center"
|
||||
class="gl-flex gl-min-w-0 gl-flex-1 gl-flex-wrap gl-justify-end gl-gap-y-3 !gl-text-default !gl-no-underline sm:gl-flex-nowrap sm:gl-items-center"
|
||||
>
|
||||
<div
|
||||
class="gl-w-64 gl-flex-grow-2 gl-self-center gl-overflow-hidden gl-overflow-x-auto sm:gl-w-auto"
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
snooze: s__('Todos|Snooze'),
|
||||
snooze: s__('Todos|Snooze...'),
|
||||
snoozeError: s__('Todos|Failed to snooze todo. Try again later.'),
|
||||
unSnooze: s__('Todos|Remove snooze'),
|
||||
unSnoozeError: s__('Todos|Failed to un-snooze todo. Try again later.'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { GlBadge, GlIcon } from '@gitlab/ui';
|
||||
import MultipleChoiceSelector from './multiple_choice_selector.vue';
|
||||
import MultipleChoiceSelectorItem from './multiple_choice_selector_item.vue';
|
||||
|
||||
export default {
|
||||
component: MultipleChoiceSelector,
|
||||
title: 'vue_shared/multiple_choice_selector',
|
||||
};
|
||||
|
||||
const data = () => ({
|
||||
selected: ['option', 'option-two'],
|
||||
});
|
||||
|
||||
const Template = () => ({
|
||||
components: { MultipleChoiceSelector, MultipleChoiceSelectorItem, GlBadge, GlIcon },
|
||||
data,
|
||||
template: `<multiple-choice-selector :selected="selected">
|
||||
<multiple-choice-selector-item value="option" title="Option name" description="This is a description for this option. Descriptions are optional." :disabled="false"></multiple-choice-selector-item>
|
||||
<multiple-choice-selector-item value="option-two" title="Option name" description="This is a description for this option. Descriptions are optional." :disabled="false"></multiple-choice-selector-item>
|
||||
<multiple-choice-selector-item value="option-3" description="This is a description for this option. Descriptions are optional." :disabled="false">
|
||||
Option name
|
||||
<gl-badge variant="muted">Beta</gl-badge>
|
||||
<div class="gl-flex gl-gap-2">
|
||||
<gl-icon name="tanuki" />
|
||||
<gl-icon name="github" />
|
||||
<gl-icon name="bitbucket" />
|
||||
<gl-icon name="gitea" />
|
||||
</div>
|
||||
</multiple-choice-selector-item>
|
||||
<multiple-choice-selector-item value="option-4" title="Option name" description="This is a description for this option. Descriptions are optional." :disabled="true" disabledMessage="This option is only available in other cases"></multiple-choice-selector-item>
|
||||
</multiple-choice-selector>`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import { GlFormCheckboxGroup } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: { GlFormCheckboxGroup },
|
||||
props: {
|
||||
selected: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedOptions: this.selected,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-checkbox-group
|
||||
v-model="selectedOptions"
|
||||
class="multiple-choice-selector gl-border gl-block gl-rounded-base"
|
||||
>
|
||||
<slot></slot>
|
||||
</gl-form-checkbox-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<script>
|
||||
import { GlFormCheckbox } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: { GlFormCheckbox },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
disabledMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multiple-choice-selector-item gl-p-5">
|
||||
<gl-form-checkbox :value="value" :disabled="disabled">
|
||||
<div class="multiple-choice-selector-click-area"></div>
|
||||
<div
|
||||
class="multiple-choice-selector-item-title gl-flex gl-flex-wrap gl-items-center gl-gap-3 gl-font-bold"
|
||||
>
|
||||
<slot>
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
<p v-if="disabled && disabledMessage" class="help-text !gl-text-warning">
|
||||
{{ disabledMessage }}
|
||||
</p>
|
||||
<p v-if="description" class="help-text">
|
||||
{{ description }}
|
||||
</p>
|
||||
</gl-form-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { GlBadge, GlIcon } from '@gitlab/ui';
|
||||
import SingleChoiceSelector from './single_choice_selector.vue';
|
||||
import SingleChoiceSelectorItem from './single_choice_selector_item.vue';
|
||||
|
||||
export default {
|
||||
component: SingleChoiceSelector,
|
||||
title: 'vue_shared/single_choice_selector',
|
||||
};
|
||||
|
||||
const data = () => ({
|
||||
checked: 'option',
|
||||
});
|
||||
|
||||
const Template = () => ({
|
||||
components: { SingleChoiceSelector, SingleChoiceSelectorItem, GlBadge, GlIcon },
|
||||
data,
|
||||
template: `<single-choice-selector :checked="checked">
|
||||
<single-choice-selector-item value="option" title="Option name" description="This is a description for this option. Descriptions are optional." :disabled="false"></single-choice-selector-item>
|
||||
<single-choice-selector-item value="option-two" title="Option name" description="This is a description for this option. Descriptions are optional." :disabled="false"></single-choice-selector-item>
|
||||
<single-choice-selector-item value="option-3" description="This is a description for this option. Descriptions are optional." :disabled="false">
|
||||
Option name
|
||||
<gl-badge variant="muted">Beta</gl-badge>
|
||||
<div class="gl-flex gl-gap-2">
|
||||
<gl-icon name="tanuki" />
|
||||
<gl-icon name="github" />
|
||||
<gl-icon name="bitbucket" />
|
||||
<gl-icon name="gitea" />
|
||||
</div>
|
||||
</single-choice-selector-item>
|
||||
<single-choice-selector-item value="option-4" title="Option name" description="This is a description for this option. Descriptions are optional." :disabled="true" disabledMessage="This option is only available in other cases"></single-choice-selector-item>
|
||||
</single-choice-selector>`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import { GlFormRadioGroup } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: { GlFormRadioGroup },
|
||||
props: {
|
||||
checked: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
checkedOptions: this.checked,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-radio-group
|
||||
v-model="checkedOptions"
|
||||
class="multiple-choice-selector gl-border gl-block gl-rounded-base"
|
||||
>
|
||||
<slot></slot>
|
||||
</gl-form-radio-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import { GlFormRadio } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: { GlFormRadio },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
disabledMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multiple-choice-selector-item gl-p-5">
|
||||
<gl-form-radio :value="value" :disabled="disabled">
|
||||
<div class="multiple-choice-selector-click-area"></div>
|
||||
<div class="gl-flex gl-flex-wrap gl-items-center gl-gap-3 gl-font-bold">
|
||||
<slot>
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
<p v-if="disabled && disabledMessage" class="help-text !gl-text-warning">
|
||||
{{ disabledMessage }}
|
||||
</p>
|
||||
<p v-if="description" class="help-text">
|
||||
{{ description }}
|
||||
</p>
|
||||
</gl-form-radio>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
@import './content_editor';
|
||||
@import './deployment_instance';
|
||||
@import './detail_page';
|
||||
@import './multiple_choice_selector';
|
||||
@import './ref_selector';
|
||||
@import './related_items_list';
|
||||
@import './severity/icons';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
.multiple-choice-selector {
|
||||
&-item {
|
||||
@include gl-prefers-reduced-motion-transition;
|
||||
transition: background-color $gl-transition-duration-medium $gl-easing-out-cubic,
|
||||
border-color $gl-transition-duration-medium $gl-easing-out-cubic;
|
||||
|
||||
&:not(:last-child) {
|
||||
@apply gl-border-b;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@apply gl-rounded-t-base;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply gl-rounded-b-base;
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
&.multiple-choice-selector-item .gl-form-checkbox.gl-form-checkbox label,
|
||||
&.multiple-choice-selector-item .gl-form-radio.gl-form-radio label {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:has(input:checked) {
|
||||
border: 1px solid var(--gl-control-border-color-selected-default);
|
||||
@apply gl-bg-subtle gl-rounded-base;
|
||||
}
|
||||
|
||||
&:has(input:checked) + &:has(input:checked) {
|
||||
@apply gl-rounded-t-none;
|
||||
}
|
||||
|
||||
&:has(input:checked):has(+ & input:checked) {
|
||||
@apply gl-rounded-b-none;
|
||||
}
|
||||
|
||||
&:not(:last-child):has(input:checked) {
|
||||
margin: -1px -1px 0;
|
||||
}
|
||||
|
||||
&:last-child:has(input:checked) {
|
||||
margin: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&-click-area {
|
||||
@apply gl-absolute -gl-top-5 -gl-left-7 gl-w-full gl-h-full gl-p-5 gl-pl-7 gl-box-content -gl-z-1;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,19 @@ module IconsHelper
|
|||
|
||||
DEFAULT_ICON_SIZE = 16
|
||||
|
||||
VARIANT_CLASSES = {
|
||||
current: 'gl-fill-current',
|
||||
default: 'gl-fill-icon-default',
|
||||
subtle: 'gl-fill-icon-subtle',
|
||||
strong: 'gl-fill-icon-strong',
|
||||
disabled: 'gl-fill-icon-disabled',
|
||||
link: 'gl-fill-icon-link',
|
||||
info: 'gl-fill-icon-info',
|
||||
warning: 'gl-fill-icon-warning',
|
||||
danger: 'gl-fill-icon-danger',
|
||||
success: 'gl-fill-icon-success'
|
||||
}.freeze
|
||||
|
||||
def custom_icon(icon_name, size: DEFAULT_ICON_SIZE)
|
||||
memoized_icon("#{icon_name}_#{size}") do
|
||||
render partial: "shared/icons/#{icon_name}", formats: :svg, locals: { size: size }
|
||||
|
|
@ -29,8 +42,8 @@ module IconsHelper
|
|||
ActionController::Base.helpers.image_path('file_icons/file_icons.svg', host: sprite_base_url)
|
||||
end
|
||||
|
||||
def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil, file_icon: false, aria_label: nil)
|
||||
memoized_icon("#{icon_name}_#{size}_#{css_class}") do
|
||||
def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil, file_icon: false, aria_label: nil, variant: nil)
|
||||
memoized_icon("#{icon_name}_#{size}_#{css_class}_#{variant}") do
|
||||
unknown_icon = file_icon ? unknown_file_icon_sprite(icon_name) : unknown_icon_sprite(icon_name)
|
||||
if unknown_icon
|
||||
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
|
||||
|
|
@ -39,7 +52,10 @@ module IconsHelper
|
|||
|
||||
css_classes = []
|
||||
css_classes << "s#{size}" if size
|
||||
css_classes << VARIANT_CLASSES[variant&.to_sym]
|
||||
css_classes << css_class.to_s unless css_class.blank?
|
||||
css_classes.compact!
|
||||
|
||||
sprite_path = file_icon ? sprite_file_icons_path : sprite_icon_path
|
||||
|
||||
content_tag(
|
||||
|
|
|
|||
|
|
@ -34,11 +34,7 @@ module TreeHelper
|
|||
def tree_edit_branch(project = @project, ref = @ref)
|
||||
return unless can_edit_tree?(project, ref)
|
||||
|
||||
if user_access(project).can_push_to_branch?(ref)
|
||||
ref
|
||||
else
|
||||
patch_branch_name(ref)
|
||||
end
|
||||
patch_branch_name(ref)
|
||||
end
|
||||
|
||||
# Generate a patch branch name that should look like:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
note_id: note.id } }
|
||||
.timeline-entry-inner
|
||||
- if note.system
|
||||
.gl-float-left.gl-flex.gl-justify-center.gl-items-center.gl-rounded-full.-gl-mt-1.gl-ml-2.gl-w-6.gl-h-6.gl-bg-gray-50.gl-text-subtle
|
||||
.gl-float-left.gl-flex.gl-justify-center.gl-items-center.gl-rounded-full.-gl-mt-1.gl-ml-2.gl-w-6.gl-h-6.gl-bg-strong.gl-text-subtle
|
||||
= icon_for_system_note(note)
|
||||
- else
|
||||
.timeline-avatar.gl-float-left
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@ Premailer::Rails.config.merge!(
|
|||
remove_comments: true,
|
||||
remove_ids: false,
|
||||
remove_scripts: false,
|
||||
output_encoding: 'US-ASCII',
|
||||
strategies: ::Rails.env.production? ? [:asset_pipeline] : [:asset_pipeline, :network]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ feature_category: runner
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172422
|
||||
milestone: '17.8'
|
||||
queued_migration_version: 20241230163745
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: 20250113153424
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: >-
|
|||
Removes ci_runner_machines_687967fa8a records that don't have a matching ci_runners_e59bb2812d record.
|
||||
This can happen because there was a period in time where a FK didn't exist.
|
||||
feature_category: fleet_visibility
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172422
|
||||
milestone: '17.8'
|
||||
queued_migration_version: 20241231094025
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/176702
|
||||
milestone: '17.9'
|
||||
queued_migration_version: 20250113164152
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeDeleteOrphanedCiRunnerProjects < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.9'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'DeleteOrphanedCiRunnerProjects',
|
||||
table_name: :ci_runner_projects,
|
||||
column_name: :runner_id,
|
||||
job_arguments: [],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ValidateForeignKeyForRunnerIdInCiRunnerProjects < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
validate_foreign_key(:ci_runner_projects, :runner_id)
|
||||
end
|
||||
|
||||
def down
|
||||
# Can be safely a no-op if we don't roll back the inconsistent data.
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RequeueDeleteOrphanedPartitionedCiRunnerMachineRecords < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.9'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
MIGRATION = "DeleteOrphanedPartitionedCiRunnerMachineRecords"
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 1000
|
||||
SUB_BATCH_SIZE = 100
|
||||
|
||||
def up
|
||||
delete_batched_background_migration(MIGRATION, :ci_runner_machines_687967fa8a, :runner_id, [])
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:ci_runner_machines_687967fa8a,
|
||||
:runner_id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
batch_class_name: 'LooseIndexScanBatchingStrategy',
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :ci_runner_machines_687967fa8a, :runner_id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
1a31c384457bee054e5b2b0c3d9029282075c5a00699d8a1ba995b3fda461e72
|
||||
|
|
@ -0,0 +1 @@
|
|||
e1e13c73f5443941b257172c9fda0cc463928685d490f58baaf4a3f5e0ed9d24
|
||||
|
|
@ -0,0 +1 @@
|
|||
6cc3cc278a9b65bd2b1aa56e6ea9b70ec194083090d6b60d5f0b8b6c3641f4d2
|
||||
|
|
@ -37392,7 +37392,7 @@ ALTER TABLE ONLY approval_project_rules_users
|
|||
ADD CONSTRAINT fk_0dfcd9e339 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_runner_projects
|
||||
ADD CONSTRAINT fk_0e743433ff FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE NOT VALID;
|
||||
ADD CONSTRAINT fk_0e743433ff FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY security_policy_project_links
|
||||
ADD CONSTRAINT fk_0eba4d5d71 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
|
|
|||
|
|
@ -30,9 +30,13 @@ To connect to an external repository:
|
|||
1. Select **GitHub** or **Repository by URL**.
|
||||
1. Complete the fields.
|
||||
|
||||
If the **Run CI/CD for external repository** option is not available, the GitLab instance
|
||||
might not have any import sources configured. Ask an administrator for your instance to check
|
||||
the [import sources configuration](../../administration/settings/import_and_export_settings.md#configure-allowed-import-sources).
|
||||
If the **Run CI/CD for external repository** option is not available:
|
||||
|
||||
- The GitLab instance might not have any import sources configured.
|
||||
Ask an administrator to check the [import sources configuration](../../administration/settings/import_and_export_settings.md#configure-allowed-import-sources).
|
||||
- [Project mirroring](../../user/project/repository/mirror/index.md) might be disabled.
|
||||
If disabled, only administrators can use the **Run CI/CD for external repository** option.
|
||||
Ask an administrator to check the [project mirroring configuration](../../administration/settings/visibility_and_access_controls.md#enable-project-mirroring).
|
||||
|
||||
## Pipelines for external pull requests
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
|
|||
the upgrade. This bug has been fixed with GitLab 17.1.2 and upgrading from GitLab 16.x directly to 17.1.2 will not
|
||||
cause these issues.
|
||||
|
||||
- A [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/476542) in the Git versions shipped with
|
||||
GitLab 17.0.x and GitLab 17.1.x causes a noticeable increase in CPU usage when under load. The primary cause of
|
||||
this regression was resolved in the Git versions shipped with GitLab 17.2 so, for systems that see heavy peak loads,
|
||||
you should upgrade to GitLab 17.2.
|
||||
|
||||
### Linux package installations
|
||||
|
||||
Specific information applies to Linux package installations:
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ If you encounter issues:
|
|||
1. Ensure that the project you want to use it with meets the [prerequisites](#prerequisites).
|
||||
1. Ensure that the folder you opened in VS Code has a Git repository for your GitLab project.
|
||||
1. Ensure that you've checked out the branch for the code you'd like to change.
|
||||
1. Check your Docker and Docker socket configuration:
|
||||
1. Check your Docker configuration:
|
||||
1. [Install Docker and set the socket file path](#install-docker-and-set-the-socket-file-path).
|
||||
1. Restart your container manager. For example, if you use Colima, `colima restart`.
|
||||
1. Pull the base Docker image:
|
||||
|
|
@ -254,15 +254,8 @@ If you encounter issues:
|
|||
docker pull registry.gitlab.com/gitlab-org/duo-workflow/default-docker-image/workflow-generic-image:v0.0.4
|
||||
```
|
||||
|
||||
1. If this does not work the DNS configuration of Colima might be at fault. Edit the DNS setting in `~/.colima/default/colima.yaml` to `dns: [1.1.1.1]` and then restart Colima with `colima restart`.
|
||||
1. Check the Language Server logs:
|
||||
1. To open the logs in VS Code, select **View** > **Output**. In the output panel at the bottom, in the top-right corner, select **GitLab Workflow** or **GitLab Language Server** from the list.
|
||||
1. Review for errors, warnings, connection issues, or authentication problems.
|
||||
1. For more output in the logs, open the settings:
|
||||
- On macOS: <kbd>Cmd</kbd> + <kbd>,</kbd>
|
||||
1. For permission issues, ensure your operating system user has the necessary Docker permissions.
|
||||
1. Verify Docker's internet connectivity by executing the command `docker image pull redhat/ubi8`.
|
||||
|
||||
If this does not work, the DNS configuration of Colima might be at fault.
|
||||
Edit the DNS setting in `~/.colima/default/colima.yaml` to `dns: [1.1.1.1]` and then restart Colima with `colima restart`.
|
||||
1. Check the Language Server logs:
|
||||
|
|
|
|||
|
|
@ -1010,9 +1010,6 @@ msgstr ""
|
|||
msgid "%{labelStart}Project:%{labelEnd} %{project}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Report Type:%{labelEnd} %{reportType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Scanner:%{labelEnd} %{scanner}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1022,6 +1019,9 @@ msgstr ""
|
|||
msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Tool:%{labelEnd} %{reportType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}URL:%{labelEnd} %{url}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2413,7 +2413,7 @@ msgstr ""
|
|||
msgid "AI|Write a summary to fill out the selected issue template"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI|Your request does not seem to contain code to %{action}. To %{human_name} select the lines of code in your editor and then type the command %{command_name} in the chat. You may add additional instructions after this command. If you have no code to select, you can also simply add the code after the command."
|
||||
msgid "AI|Your request does not seem to contain code to %{action}. To %{human_name} select the lines of code in your %{platform} and then type the command %{command_name} in the chat. You may add additional instructions after this command. If you have no code to select, you can also simply add the code after the command."
|
||||
msgstr ""
|
||||
|
||||
msgid "API"
|
||||
|
|
@ -15603,6 +15603,12 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Minimum access level to push"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Minimum role allowed to delete"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Minimum role allowed to push"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15621,6 +15627,12 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Only users with at least this role can delete tags with a name that matches the protection rule."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Only users with at least this role can push tags with a name that matches the protection rule."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Partial cleanup complete"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15630,6 +15642,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Please try different search criteria"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Protect container tags matching"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Protected container image tags"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15749,6 +15764,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags with names that match this regex pattern are protected. Must be less than 100 characters. %{linkStart}What regex patterns are supported?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47464,9 +47482,6 @@ msgstr ""
|
|||
msgid "Reports|New"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Report Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|See test results while the pipeline is running"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47479,6 +47494,9 @@ msgstr ""
|
|||
msgid "Reports|Test summary results are being parsed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports|View partial report"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -59295,6 +59313,9 @@ msgstr ""
|
|||
msgid "Todos|Snooze"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Snooze..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Snoozed"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -243,6 +243,23 @@ describe('CI Variable Drawer', () => {
|
|||
expect(findVisibilityRadioGroup().attributes('checked')).toBe(expectedVisibility);
|
||||
},
|
||||
);
|
||||
|
||||
it('is updated on variable update', async () => {
|
||||
await createComponent({
|
||||
props: {
|
||||
selectedVariable: {
|
||||
...mockProjectVariableFileType,
|
||||
masked: true,
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findVisibilityRadioGroup().attributes('checked')).toBe(VISIBILITY_HIDDEN);
|
||||
await wrapper.setProps({ mutationResponse: { message: 'Success', hasError: false } });
|
||||
|
||||
expect(findVisibilityRadioGroup().attributes('checked')).toBe(VISIBILITY_VISIBLE);
|
||||
});
|
||||
});
|
||||
|
||||
it('is disabled when editing a hidden variable', () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import imageDiscussionFixture from 'test_fixtures/merge_requests/image_diff_disc
|
|||
import { createStore } from '~/mr_notes/stores';
|
||||
import DiffWithNote from '~/notes/components/diff_with_note.vue';
|
||||
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
|
||||
describe('diff_with_note', () => {
|
||||
let store;
|
||||
|
|
@ -22,6 +23,7 @@ describe('diff_with_note', () => {
|
|||
};
|
||||
|
||||
const findDiffViewer = () => wrapper.findComponent(DiffViewer);
|
||||
const findDiffFileHeader = () => wrapper.findComponent(DiffFileHeader);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
|
@ -76,16 +78,76 @@ describe('diff_with_note', () => {
|
|||
});
|
||||
|
||||
describe('image diff', () => {
|
||||
beforeEach(() => {
|
||||
const imageDiscussion = imageDiscussionFixture[0];
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: imageDiscussion, diffFile: {} },
|
||||
store,
|
||||
describe('when discussion has a diff_file', () => {
|
||||
beforeEach(() => {
|
||||
const imageDiscussion = imageDiscussionFixture[0];
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: imageDiscussion, diffFile: {} },
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows image diff', () => {
|
||||
expect(selectors.diffTable.exists()).toBe(false);
|
||||
expect(findDiffViewer().exists()).toBe(true);
|
||||
expect(findDiffFileHeader().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows image diff', () => {
|
||||
expect(selectors.diffTable.exists()).toBe(false);
|
||||
describe('when discussion does not have a diff_file', () => {
|
||||
beforeEach(() => {
|
||||
const imageDiscussion = JSON.parse(JSON.stringify(imageDiscussionFixture[0]));
|
||||
delete imageDiscussion.diff_file;
|
||||
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: imageDiscussion, diffFile: {} },
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show image diff', () => {
|
||||
expect(findDiffViewer().exists()).toBe(false);
|
||||
expect(selectors.diffTable.exists()).toBe(false);
|
||||
expect(findDiffFileHeader().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('file diff', () => {
|
||||
describe('when discussion has a diff_file', () => {
|
||||
beforeEach(() => {
|
||||
const fileDiscussion = JSON.parse(JSON.stringify(discussionFixture[0]));
|
||||
fileDiscussion.position.position_type = 'file';
|
||||
fileDiscussion.original_position.position_type = 'file';
|
||||
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: fileDiscussion, diffFile: {} },
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows file header', () => {
|
||||
expect(findDiffFileHeader().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when discussion does not have a diff_file', () => {
|
||||
beforeEach(() => {
|
||||
const fileDiscussion = JSON.parse(JSON.stringify(discussionFixture[0]));
|
||||
delete fileDiscussion.diff_file;
|
||||
|
||||
fileDiscussion.position.position_type = 'file';
|
||||
fileDiscussion.original_position.position_type = 'file';
|
||||
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: fileDiscussion, diffFile: {} },
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows file header', () => {
|
||||
expect(findDiffFileHeader().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,25 @@ describe('noteable_discussion component', () => {
|
|||
expect(wrapper.find('.discussion-header').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when diff discussion does not have a diff_file', () => {
|
||||
it.each`
|
||||
positionType
|
||||
${'file'}
|
||||
${'image'}
|
||||
`('should show reply actions when position_type is $positionType', async ({ positionType }) => {
|
||||
const discussion = { ...discussionMock, original_position: { position_type: positionType } };
|
||||
discussion.diff_file = { ...getDiffFileMock(), diff_refs: null };
|
||||
discussion.diff_discussion = true;
|
||||
|
||||
wrapper.setProps({ discussion });
|
||||
await nextTick();
|
||||
|
||||
const replyWrapper = wrapper.find('[data-testid="reply-wrapper"]');
|
||||
|
||||
expect(replyWrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drafts', () => {
|
||||
useLocalStorageSpy();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ContainerProtectionTagRuleForm from '~/packages_and_registries/settings/project/components/container_protection_tag_rule_form.vue';
|
||||
|
||||
describe('container Protection Rule Form', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProvidedValues = {
|
||||
projectPath: 'path',
|
||||
};
|
||||
|
||||
const findTagNamePatternInput = () =>
|
||||
wrapper.findByRole('textbox', { name: /protect container tags matching/i });
|
||||
const findMinimumAccessLevelForPushSelect = () =>
|
||||
wrapper.findByRole('combobox', { name: /minimum role allowed to push/i });
|
||||
const findMinimumAccessLevelForDeleteSelect = () =>
|
||||
wrapper.findByRole('combobox', { name: /minimum role allowed to delete/i });
|
||||
|
||||
const mountComponent = ({ config, provide = defaultProvidedValues } = {}) => {
|
||||
wrapper = mountExtended(ContainerProtectionTagRuleForm, {
|
||||
provide,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
describe('form fields', () => {
|
||||
describe('form field "tagNamePattern"', () => {
|
||||
it('exists', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findTagNamePatternInput().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form field "minimumAccessLevelForPush"', () => {
|
||||
const minimumAccessLevelForPushOptions = () =>
|
||||
findMinimumAccessLevelForPushSelect()
|
||||
.findAll('option')
|
||||
.wrappers.map((option) => option.element.value);
|
||||
|
||||
it.each(['MAINTAINER', 'OWNER', 'ADMIN'])(
|
||||
'includes the access level "%s" as an option',
|
||||
(accessLevel) => {
|
||||
mountComponent();
|
||||
|
||||
expect(findMinimumAccessLevelForPushSelect().exists()).toBe(true);
|
||||
expect(minimumAccessLevelForPushOptions()).toContain(accessLevel);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('form field "minimumAccessLevelForDelete"', () => {
|
||||
const minimumAccessLevelForDeleteOptions = () =>
|
||||
findMinimumAccessLevelForDeleteSelect()
|
||||
.findAll('option')
|
||||
.wrappers.map((option) => option.element.value);
|
||||
|
||||
it.each(['MAINTAINER', 'OWNER', 'ADMIN'])(
|
||||
'includes the access level "%s" as an option',
|
||||
(accessLevel) => {
|
||||
mountComponent();
|
||||
|
||||
expect(findMinimumAccessLevelForDeleteSelect().exists()).toBe(true);
|
||||
expect(minimumAccessLevelForDeleteOptions()).toContain(accessLevel);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlAlert, GlBadge, GlModal, GlSprintf, GlTable } from '@gitlab/ui';
|
||||
import { GlAlert, GlBadge, GlDrawer, GlModal, GlSprintf, GlTable } from '@gitlab/ui';
|
||||
|
||||
import containerProtectionTagRuleEmptyRulesQueryPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql.empty_rules.json';
|
||||
import containerProtectionTagRuleNullProjectQueryPayload from 'test_fixtures/graphql/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql.null_project.json';
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import ContainerProtectionTagRules from '~/packages_and_registries/settings/project/components/container_protection_tag_rules.vue';
|
||||
import ContainerProtectionTagRuleForm from '~/packages_and_registries/settings/project/components/container_protection_tag_rule_form.vue';
|
||||
import getContainerProtectionTagRulesQuery from '~/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql';
|
||||
import deleteContainerProtectionTagRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/delete_container_protection_tag_rule.mutation.graphql';
|
||||
import { MinimumAccessLevelOptions } from '~/packages_and_registries/settings/project/constants';
|
||||
|
|
@ -36,6 +37,9 @@ describe('ContainerProtectionTagRules', () => {
|
|||
const findTableComponent = () => wrapper.findComponent(GlTable);
|
||||
const findBadge = () => wrapper.findComponent(GlBadge);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findDrawer = () => wrapper.findComponent(GlDrawer);
|
||||
const findDrawerTitle = () => wrapper.findComponent(GlDrawer).find('h2');
|
||||
const findForm = () => wrapper.findComponent(ContainerProtectionTagRuleForm);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
const defaultProvidedValues = {
|
||||
|
|
@ -83,6 +87,7 @@ describe('ContainerProtectionTagRules', () => {
|
|||
|
||||
it('renders card component with title', () => {
|
||||
expect(findCrudComponent().props('title')).toBe('Protected container image tags');
|
||||
expect(findCrudComponent().props('toggleText')).toBe('Add protection rule');
|
||||
});
|
||||
|
||||
it('renders card component with description', () => {
|
||||
|
|
@ -95,6 +100,10 @@ describe('ContainerProtectionTagRules', () => {
|
|||
expect(findLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('drawer is hidden', () => {
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
});
|
||||
|
||||
it('hides the table', () => {
|
||||
expect(findTableComponent().exists()).toBe(false);
|
||||
});
|
||||
|
|
@ -113,6 +122,31 @@ describe('ContainerProtectionTagRules', () => {
|
|||
expect.objectContaining({ projectPath: defaultProvidedValues.projectPath, first: 5 }),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when `Add protection rule` button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
await findCrudComponent().vm.$emit('showForm');
|
||||
});
|
||||
|
||||
it('opens drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(true);
|
||||
expect(findDrawerTitle().text()).toBe('Add protection rule');
|
||||
});
|
||||
|
||||
it('renders form', () => {
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when drawer emits `close` event', () => {
|
||||
beforeEach(async () => {
|
||||
await findDrawer().vm.$emit('close');
|
||||
});
|
||||
|
||||
it('closes drawer', () => {
|
||||
expect(findDrawer().props('open')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded & contains tag protection rules', () => {
|
||||
|
|
|
|||
|
|
@ -235,26 +235,6 @@ describe('CommitChangesModal', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('clear branch name when new branch option is selected', async () => {
|
||||
createComponent();
|
||||
expect(wrapper.vm.$data.form.fields.branch_name).toEqual({
|
||||
feedback: null,
|
||||
required: true,
|
||||
state: true,
|
||||
value: 'some-target-branch',
|
||||
});
|
||||
|
||||
findFormRadioGroup().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.$data.form.fields.branch_name).toEqual({
|
||||
feedback: null,
|
||||
required: true,
|
||||
state: true,
|
||||
value: '',
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
input | value | emptyRepo | canPushCode | canPushToBranch | exist
|
||||
${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true} | ${true}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const initialProps = {
|
|||
const defaultFormValue = {
|
||||
dirName: 'foo',
|
||||
originalBranch: initialProps.originalBranch,
|
||||
branchName: initialProps.targetBranch,
|
||||
targetBranch: initialProps.targetBranch,
|
||||
commitMessage: initialProps.commitMessage,
|
||||
createNewMr: true,
|
||||
};
|
||||
|
|
@ -60,8 +60,12 @@ describe('NewDirectoryModal', () => {
|
|||
await nextTick();
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
findCommitChangesModal().vm.$emit('submit-form', new FormData());
|
||||
const submitForm = async ({ branchName } = {}) => {
|
||||
const formData = new FormData();
|
||||
if (branchName) {
|
||||
formData.append('branch_name', branchName);
|
||||
}
|
||||
findCommitChangesModal().vm.$emit('submit-form', formData);
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
|
|
@ -108,15 +112,28 @@ describe('NewDirectoryModal', () => {
|
|||
expect(findCommitChangesModal().props('valid')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes additional formData', async () => {
|
||||
const { dirName, branchName } = defaultFormValue;
|
||||
mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {});
|
||||
await fillForm();
|
||||
await submitForm();
|
||||
describe('passes additional formData', () => {
|
||||
it('passes original branch name as branch name if branch name does not exist on formData', async () => {
|
||||
const { dirName, originalBranch } = defaultFormValue;
|
||||
mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {});
|
||||
await fillForm();
|
||||
await submitForm();
|
||||
|
||||
const formData = mock.history.post[0].data;
|
||||
expect(formData.get('dir_name')).toBe(dirName);
|
||||
expect(formData.get('branch_name')).toBe(branchName);
|
||||
const formData = mock.history.post[0].data;
|
||||
expect(formData.get('dir_name')).toBe(dirName);
|
||||
expect(formData.get('branch_name')).toBe(originalBranch);
|
||||
});
|
||||
|
||||
it('passes target branch name as branch name if branch name does exist on formData', async () => {
|
||||
const { dirName, targetBranch } = defaultFormValue;
|
||||
mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {});
|
||||
await fillForm();
|
||||
await submitForm({ branchName: targetBranch });
|
||||
|
||||
const formData = mock.history.post[0].data;
|
||||
expect(formData.get('dir_name')).toBe(dirName);
|
||||
expect(formData.get('branch_name')).toBe(targetBranch);
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to the new directory', async () => {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ describe('ToggleSnoozedStatus', () => {
|
|||
createComponent({ props: { isSnoozed: false, isPending: true } });
|
||||
|
||||
expect(findSnoozeDropdown().props()).toMatchObject({
|
||||
toggleText: 'Snooze',
|
||||
toggleText: 'Snooze...',
|
||||
icon: 'clock',
|
||||
placement: 'bottom-end',
|
||||
textSrOnly: true,
|
||||
|
|
@ -202,7 +202,7 @@ describe('ToggleSnoozedStatus', () => {
|
|||
const tooltip = findGlTooltip();
|
||||
|
||||
expect(tooltip.exists()).toBe(true);
|
||||
expect(tooltip.text()).toBe('Snooze');
|
||||
expect(tooltip.text()).toBe('Snooze...');
|
||||
});
|
||||
|
||||
it('only shows the tooltip when the dropdown is closed', async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { GlFormCheckbox } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MultipleChoiceSelectorItem from '~/vue_shared/components/multiple_choice_selector_item.vue';
|
||||
|
||||
describe('MultipleChoiceSelectorItem', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent({ propsData = {} } = {}) {
|
||||
wrapper = shallowMount(MultipleChoiceSelectorItem, {
|
||||
propsData: {
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
|
||||
it('renders checkbox', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findCheckbox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
createComponent({ propsData: { title: 'Option title' } });
|
||||
|
||||
expect(findCheckbox().text()).toContain('Option title');
|
||||
});
|
||||
|
||||
it('renders description', () => {
|
||||
createComponent({ propsData: { description: 'Option description' } });
|
||||
|
||||
expect(wrapper.text()).toContain('Option description');
|
||||
});
|
||||
|
||||
it('renders disabled message', () => {
|
||||
createComponent({ propsData: { disabledMessage: 'Option disabled message', disabled: true } });
|
||||
|
||||
expect(wrapper.text()).toContain('Option disabled message');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { GlFormCheckboxGroup } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MultipleChoiceSelector from '~/vue_shared/components/multiple_choice_selector.vue';
|
||||
|
||||
describe('MultipleChoiceSelector', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultPropsData = {
|
||||
selected: ['option'],
|
||||
};
|
||||
|
||||
function createComponent({ propsData = {} } = {}) {
|
||||
wrapper = shallowMount(MultipleChoiceSelector, {
|
||||
propsData: {
|
||||
...defaultPropsData,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
|
||||
|
||||
it('renders checkbox group', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findCheckboxGroup().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { GlFormRadio } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SingleChoiceSelectorItem from '~/vue_shared/components/single_choice_selector_item.vue';
|
||||
|
||||
describe('SingleChoiceSelectorItem', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent({ propsData = {} } = {}) {
|
||||
wrapper = shallowMount(SingleChoiceSelectorItem, {
|
||||
propsData: {
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findRadio = () => wrapper.findComponent(GlFormRadio);
|
||||
|
||||
it('renders radio', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findRadio().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
createComponent({ propsData: { title: 'Option title' } });
|
||||
|
||||
expect(findRadio().text()).toContain('Option title');
|
||||
});
|
||||
|
||||
it('renders description', () => {
|
||||
createComponent({ propsData: { description: 'Option description' } });
|
||||
|
||||
expect(wrapper.text()).toContain('Option description');
|
||||
});
|
||||
|
||||
it('renders disabled message', () => {
|
||||
createComponent({ propsData: { disabledMessage: 'Option disabled message', disabled: true } });
|
||||
|
||||
expect(wrapper.text()).toContain('Option disabled message');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { GlFormRadioGroup } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SingleChoiceSelector from '~/vue_shared/components/single_choice_selector.vue';
|
||||
|
||||
describe('SingleChoice', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultPropsData = {
|
||||
checked: 'option',
|
||||
};
|
||||
|
||||
function createComponent({ propsData = {} } = {}) {
|
||||
wrapper = shallowMount(SingleChoiceSelector, {
|
||||
propsData: {
|
||||
...defaultPropsData,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
|
||||
|
||||
it('renders radio group', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findRadioGroup().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -48,6 +48,11 @@ RSpec.describe IconsHelper do
|
|||
.to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
|
||||
end
|
||||
|
||||
it 'returns svg icon html + size + variant classes' do
|
||||
expect(sprite_icon(icon_name, size: 72, variant: 'subtle').to_s)
|
||||
.to eq "<svg class=\"s72 gl-fill-icon-subtle\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
|
||||
end
|
||||
|
||||
it 'returns svg icon html + size classes + additional class' do
|
||||
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
|
||||
.to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,24 @@ RSpec.describe TreeHelper, feature_category: :source_code_management do
|
|||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe '#tree_edit_branch' do
|
||||
let(:ref) { 'main' }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:patch_branch_name).and_return('patch-1')
|
||||
end
|
||||
|
||||
it 'returns nil when cannot edit tree' do
|
||||
allow(helper).to receive(:can_edit_tree?).and_return(false)
|
||||
expect(helper.tree_edit_branch(project, ref)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns the patch branch name when can edit tree' do
|
||||
allow(helper).to receive(:can_edit_tree?).and_return(true)
|
||||
expect(helper.tree_edit_branch(project, ref)).to eq('patch-1')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#breadcrumb_data_attributes' do
|
||||
let(:ref) { 'main' }
|
||||
let(:base_attributes) do
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedCiRunnerProjects, feature_category: :runner, migration: :gitlab_ci do
|
||||
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedCiRunnerProjects, feature_category: :runner,
|
||||
migration: :gitlab_ci, schema: 20250113153424 do
|
||||
let(:connection) { Ci::ApplicationRecord.connection }
|
||||
let(:runners) { table(:ci_runners, database: :ci, primary_key: :id) }
|
||||
let(:runner_projects) { table(:ci_runner_projects, database: :ci, primary_key: :id) }
|
||||
|
|
|
|||
|
|
@ -89,16 +89,16 @@ RSpec.describe Notify, feature_category: :code_review_workflow do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'with HTML-encoded entities' do
|
||||
describe 'with non-ASCII characters' do
|
||||
before do
|
||||
described_class.test_email('test@test.com', 'Subject', 'Some body with —').deliver
|
||||
described_class.test_email('test@test.com', 'Subject', 'Some body with 中文 —').deliver
|
||||
end
|
||||
|
||||
subject { ActionMailer::Base.deliveries.last }
|
||||
|
||||
it 'retains 7bit encoding' do
|
||||
expect(subject.body.ascii_only?).to eq(true)
|
||||
expect(subject.body.encoding).to eq('7bit')
|
||||
it 'removes HTML encoding and uses UTF-8 charset' do
|
||||
expect(subject.charset).to eq('UTF-8')
|
||||
expect(subject.body).to include('中文 —')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RequeueDeleteOrphanedPartitionedCiRunnerMachineRecords, migration: :gitlab_ci,
|
||||
feature_category: :fleet_visibility 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: :ci_runner_machines_687967fa8a,
|
||||
column_name: :runner_id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE,
|
||||
gitlab_schema: :gitlab_ci
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue