Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cdda3d117c
commit
e18e22ce4c
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
|
||||
|
||||
export default {
|
||||
name: 'FootnoteDefinitionWrapper',
|
||||
components: {
|
||||
NodeViewWrapper,
|
||||
NodeViewContent,
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<node-view-wrapper class="gl-display-flex gl-font-sm" as="div">
|
||||
<span
|
||||
data-testid="footnote-label"
|
||||
contenteditable="false"
|
||||
class="gl-display-inline-flex gl-mr-2"
|
||||
>{{ node.attrs.label }}:</span
|
||||
>
|
||||
<node-view-content />
|
||||
</node-view-wrapper>
|
||||
</template>
|
|
@ -1,12 +1,26 @@
|
|||
import { mergeAttributes, Node } from '@tiptap/core';
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue-2';
|
||||
import FootnoteDefinitionWrapper from '../components/wrappers/footnote_definition.vue';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||
|
||||
const extractFootnoteIdentifier = (idAttribute) => /^fn-(\w+)-\d+$/.exec(idAttribute)?.[1];
|
||||
|
||||
export default Node.create({
|
||||
name: 'footnoteDefinition',
|
||||
|
||||
content: 'paragraph',
|
||||
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
addAttributes() {
|
||||
return {
|
||||
identifier: {
|
||||
default: null,
|
||||
parseHTML: (element) => extractFootnoteIdentifier(element.getAttribute('id')),
|
||||
},
|
||||
label: {
|
||||
default: null,
|
||||
parseHTML: (element) => extractFootnoteIdentifier(element.getAttribute('id')),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
|
@ -15,7 +29,11 @@ export default Node.create({
|
|||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['li', mergeAttributes(HTMLAttributes), 0];
|
||||
renderHTML({ label, ...HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return new VueNodeViewRenderer(FootnoteDefinitionWrapper);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Node, mergeAttributes } from '@tiptap/core';
|
||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||
|
||||
const extractFootnoteIdentifier = (element) =>
|
||||
/^fnref-(\w+)-\d+$/.exec(element.querySelector('a')?.getAttribute('id'))?.[1];
|
||||
|
||||
export default Node.create({
|
||||
name: 'footnoteReference',
|
||||
|
||||
|
@ -16,13 +19,13 @@ export default Node.create({
|
|||
|
||||
addAttributes() {
|
||||
return {
|
||||
footnoteId: {
|
||||
identifier: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.querySelector('a').getAttribute('id'),
|
||||
parseHTML: extractFootnoteIdentifier,
|
||||
},
|
||||
footnoteNumber: {
|
||||
label: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.textContent,
|
||||
parseHTML: extractFootnoteIdentifier,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -31,7 +34,7 @@ export default Node.create({
|
|||
return [{ tag: 'sup.footnote-ref', priority: PARSE_HTML_PRIORITY_HIGHEST }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes: { footnoteNumber, footnoteId, ...HTMLAttributes } }) {
|
||||
return ['sup', mergeAttributes(HTMLAttributes), footnoteNumber];
|
||||
renderHTML({ HTMLAttributes: { label, ...HTMLAttributes } }) {
|
||||
return ['sup', mergeAttributes(HTMLAttributes), label];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,10 @@ export default Node.create({
|
|||
isolating: true,
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'section.footnotes > ol' }];
|
||||
return [
|
||||
{ tag: 'section.footnotes', skip: true },
|
||||
{ tag: 'section.footnotes > ol', skip: true },
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
|
|
|
@ -17,7 +17,6 @@ import Diagram from '../extensions/diagram';
|
|||
import Emoji from '../extensions/emoji';
|
||||
import Figure from '../extensions/figure';
|
||||
import FigureCaption from '../extensions/figure_caption';
|
||||
import FootnotesSection from '../extensions/footnotes_section';
|
||||
import FootnoteDefinition from '../extensions/footnote_definition';
|
||||
import FootnoteReference from '../extensions/footnote_reference';
|
||||
import Frontmatter from '../extensions/frontmatter';
|
||||
|
@ -154,15 +153,14 @@ const defaultSerializerConfig = {
|
|||
|
||||
state.write(`:${name}:`);
|
||||
},
|
||||
[FootnoteDefinition.name]: (state, node) => {
|
||||
[FootnoteDefinition.name]: preserveUnchanged((state, node) => {
|
||||
state.write(`[^${node.attrs.identifier}]: `);
|
||||
state.renderInline(node);
|
||||
},
|
||||
[FootnoteReference.name]: (state, node) => {
|
||||
state.write(`[^${node.attrs.footnoteNumber}]`);
|
||||
},
|
||||
[FootnotesSection.name]: (state, node) => {
|
||||
state.renderList(node, '', (index) => `[^${index + 1}]: `);
|
||||
},
|
||||
state.ensureNewLine();
|
||||
}),
|
||||
[FootnoteReference.name]: preserveUnchanged((state, node) => {
|
||||
state.write(`[^${node.attrs.identifier}]`);
|
||||
}),
|
||||
[Frontmatter.name]: (state, node) => {
|
||||
const { language } = node.attrs;
|
||||
const syntax = {
|
||||
|
|
|
@ -84,24 +84,21 @@ export default {
|
|||
<h1 class="page-title">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<hr />
|
||||
<div class="row gl-mt-3 gl-mb-3">
|
||||
<div class="col-lg-3">
|
||||
<h4 class="gl-mt-0">
|
||||
{{ $options.i18n.header }}
|
||||
</h4>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.helpMessage">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.helpPagePath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row col-12">
|
||||
<h4 class="gl-mt-0">
|
||||
{{ $options.i18n.header }}
|
||||
</h4>
|
||||
<p class="gl-w-full">
|
||||
<gl-sprintf :message="$options.i18n.helpMessage">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.helpPagePath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<gl-form
|
||||
id="new_environment"
|
||||
:aria-label="title"
|
||||
class="col-lg-9"
|
||||
class="gl-w-full"
|
||||
@submit.prevent="$emit('submit')"
|
||||
>
|
||||
<gl-form-group
|
||||
|
@ -144,7 +141,7 @@ export default {
|
|||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="gl-mr-6">
|
||||
<gl-button
|
||||
:loading="loading"
|
||||
type="submit"
|
||||
|
|
|
@ -81,6 +81,20 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
actionPrimary: {
|
||||
text: s__('FeatureFlags|Delete feature flag'),
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
},
|
||||
},
|
||||
actionSecondary: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
@ -193,11 +207,11 @@ export default {
|
|||
<gl-modal
|
||||
:ref="modalId"
|
||||
:title="modalTitle"
|
||||
:ok-title="s__('FeatureFlags|Delete feature flag')"
|
||||
:modal-id="modalId"
|
||||
title-tag="h4"
|
||||
ok-variant="danger"
|
||||
category="primary"
|
||||
size="sm"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-secondary="$options.modal.actionSecondary"
|
||||
@ok="onSubmit"
|
||||
>
|
||||
{{ deleteModalMessage }}
|
||||
|
|
|
@ -48,7 +48,7 @@ export default {
|
|||
|
||||
<gl-table :items="list" :fields="$options.tableFields" />
|
||||
|
||||
<gl-button :href="createUrl" category="primary" variant="info">
|
||||
<gl-button :href="createUrl" category="primary" variant="confirm">
|
||||
{{ $options.i18n.configureRegions }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
|
|
@ -63,6 +63,8 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
if (!this.iid) return { state: this.initialState };
|
||||
|
||||
if (this.initialState) {
|
||||
badgeState.state = this.initialState;
|
||||
}
|
||||
|
|
|
@ -866,7 +866,7 @@ export default {
|
|||
:export-csv-path="exportCsvPathWithQuery"
|
||||
:issuable-count="currentTabCount"
|
||||
/>
|
||||
<new-issue-dropdown v-if="showNewIssueDropdown" />
|
||||
<new-issue-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" />
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
<hr />
|
||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
|||
},
|
||||
inject: ['mergeRequestPath', 'sourceBranchPath', 'resolveConflictsPath'],
|
||||
i18n: {
|
||||
commitStatSummary: __('Showing %{conflict} between %{sourceBranch} and %{targetBranch}'),
|
||||
commitStatSummary: __('Showing %{conflict}'),
|
||||
resolveInfo: __(
|
||||
'You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}',
|
||||
),
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
<script>
|
||||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { isEqual, get, isEmpty } from 'lodash';
|
||||
import {
|
||||
CONTAINER_CLEANUP_POLICY_TITLE,
|
||||
CONTAINER_CLEANUP_POLICY_DESCRIPTION,
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
UNAVAILABLE_FEATURE_TITLE,
|
||||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
UNAVAILABLE_USER_FEATURE_TEXT,
|
||||
UNAVAILABLE_ADMIN_FEATURE_TEXT,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
|
||||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
|
||||
import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsBlock,
|
||||
GlAlert,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
ContainerExpirationPolicyForm,
|
||||
},
|
||||
inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
|
||||
i18n: {
|
||||
CONTAINER_CLEANUP_POLICY_TITLE,
|
||||
CONTAINER_CLEANUP_POLICY_DESCRIPTION,
|
||||
UNAVAILABLE_FEATURE_TITLE,
|
||||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
},
|
||||
apollo: {
|
||||
containerExpirationPolicy: {
|
||||
query: expirationPolicyQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update: (data) => data.project?.containerExpirationPolicy,
|
||||
result({ data }) {
|
||||
this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
|
||||
},
|
||||
error(e) {
|
||||
this.fetchSettingsError = e;
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetchSettingsError: false,
|
||||
containerExpirationPolicy: null,
|
||||
workingCopy: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDisabled() {
|
||||
return !(this.containerExpirationPolicy || this.enableHistoricEntries);
|
||||
},
|
||||
showDisabledFormMessage() {
|
||||
return this.isDisabled && !this.fetchSettingsError;
|
||||
},
|
||||
unavailableFeatureMessage() {
|
||||
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
|
||||
},
|
||||
isEdited() {
|
||||
if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
|
||||
return false;
|
||||
}
|
||||
return !isEqual(this.containerExpirationPolicy, this.workingCopy);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
restoreOriginal() {
|
||||
this.workingCopy = { ...this.containerExpirationPolicy };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<settings-block :collapsible="false">
|
||||
<template #title> {{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</template>
|
||||
<template #description>
|
||||
<span>
|
||||
<gl-sprintf :message="$options.i18n.CONTAINER_CLEANUP_POLICY_DESCRIPTION">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="helpPagePath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<container-expiration-policy-form
|
||||
v-if="!isDisabled"
|
||||
v-model="workingCopy"
|
||||
:is-loading="$apollo.queries.containerExpirationPolicy.loading"
|
||||
:is-edited="isEdited"
|
||||
@reset="restoreOriginal"
|
||||
/>
|
||||
<template v-else>
|
||||
<gl-alert
|
||||
v-if="showDisabledFormMessage"
|
||||
:dismissible="false"
|
||||
:title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
|
||||
variant="tip"
|
||||
>
|
||||
{{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
|
||||
|
||||
<gl-sprintf :message="unavailableFeatureMessage">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="adminSettingsPath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
<gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
|
||||
<gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
|
||||
</gl-alert>
|
||||
</template>
|
||||
</template>
|
||||
</settings-block>
|
||||
</template>
|
|
@ -104,7 +104,7 @@ export default {
|
|||
<span data-testid="description" class="gl-text-gray-400">
|
||||
<gl-sprintf :message="description">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link>
|
||||
<gl-link :href="tagsRegexHelpPagePath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
|
|
|
@ -1,128 +1,15 @@
|
|||
<script>
|
||||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { isEqual, get, isEmpty } from 'lodash';
|
||||
import {
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
UNAVAILABLE_FEATURE_TITLE,
|
||||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
UNAVAILABLE_USER_FEATURE_TEXT,
|
||||
UNAVAILABLE_ADMIN_FEATURE_TEXT,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
|
||||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
|
||||
import SettingsForm from './settings_form.vue';
|
||||
import ContainerExpirationPolicy from './container_expiration_policy.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsBlock,
|
||||
SettingsForm,
|
||||
GlAlert,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
|
||||
i18n: {
|
||||
UNAVAILABLE_FEATURE_TITLE,
|
||||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
},
|
||||
apollo: {
|
||||
containerExpirationPolicy: {
|
||||
query: expirationPolicyQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update: (data) => data.project?.containerExpirationPolicy,
|
||||
result({ data }) {
|
||||
this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
|
||||
},
|
||||
error(e) {
|
||||
this.fetchSettingsError = e;
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetchSettingsError: false,
|
||||
containerExpirationPolicy: null,
|
||||
workingCopy: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDisabled() {
|
||||
return !(this.containerExpirationPolicy || this.enableHistoricEntries);
|
||||
},
|
||||
showDisabledFormMessage() {
|
||||
return this.isDisabled && !this.fetchSettingsError;
|
||||
},
|
||||
unavailableFeatureMessage() {
|
||||
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
|
||||
},
|
||||
isEdited() {
|
||||
if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
|
||||
return false;
|
||||
}
|
||||
return !isEqual(this.containerExpirationPolicy, this.workingCopy);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
restoreOriginal() {
|
||||
this.workingCopy = { ...this.containerExpirationPolicy };
|
||||
},
|
||||
ContainerExpirationPolicy,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section data-testid="registry-settings-app">
|
||||
<settings-block :collapsible="false">
|
||||
<template #title> {{ __('Clean up image tags') }}</template>
|
||||
<template #description>
|
||||
<span data-testid="description">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__(
|
||||
'Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<settings-form
|
||||
v-if="!isDisabled"
|
||||
v-model="workingCopy"
|
||||
:is-loading="$apollo.queries.containerExpirationPolicy.loading"
|
||||
:is-edited="isEdited"
|
||||
@reset="restoreOriginal"
|
||||
/>
|
||||
<template v-else>
|
||||
<gl-alert
|
||||
v-if="showDisabledFormMessage"
|
||||
:dismissible="false"
|
||||
:title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
|
||||
variant="tip"
|
||||
>
|
||||
{{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
|
||||
|
||||
<gl-sprintf :message="unavailableFeatureMessage">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
<gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
|
||||
<gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
|
||||
</gl-alert>
|
||||
</template>
|
||||
</template>
|
||||
</settings-block>
|
||||
<container-expiration-policy />
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const CONTAINER_CLEANUP_POLICY_TITLE = s__(`ContainerRegistry|Clean up image tags`);
|
||||
export const CONTAINER_CLEANUP_POLICY_DESCRIPTION = s__(
|
||||
`ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}`,
|
||||
);
|
||||
export const SET_CLEANUP_POLICY_BUTTON = __('Save');
|
||||
export const UNAVAILABLE_FEATURE_TITLE = s__(
|
||||
`ContainerRegistry|Cleanup policy for tags is disabled`,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
|
||||
import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import { n__ } from '~/locale';
|
||||
|
@ -9,7 +9,7 @@ import TerraformPlan from './terraform_plan.vue';
|
|||
export default {
|
||||
name: 'MRWidgetTerraformContainer',
|
||||
components: {
|
||||
GlSkeletonLoading,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
MrWidgetExpanableSection,
|
||||
TerraformPlan,
|
||||
|
@ -100,7 +100,7 @@ export default {
|
|||
<template>
|
||||
<section class="mr-widget-section">
|
||||
<div v-if="loading" class="mr-widget-body">
|
||||
<gl-skeleton-loading />
|
||||
<gl-skeleton-loader />
|
||||
</div>
|
||||
|
||||
<mr-widget-expanable-section v-else>
|
||||
|
|
|
@ -82,9 +82,6 @@ export default {
|
|||
alertId: {
|
||||
default: '',
|
||||
},
|
||||
isThreatMonitoringPage: {
|
||||
default: false,
|
||||
},
|
||||
projectId: {
|
||||
default: '',
|
||||
},
|
||||
|
@ -223,9 +220,7 @@ export default {
|
|||
});
|
||||
},
|
||||
incidentPath(issueId) {
|
||||
return this.isThreatMonitoringPage
|
||||
? joinPaths(this.projectIssuesPath, issueId)
|
||||
: joinPaths(this.projectIssuesPath, 'incident', issueId);
|
||||
return joinPaths(this.projectIssuesPath, 'incident', issueId);
|
||||
},
|
||||
trackPageViews() {
|
||||
const { category, action } = this.trackAlertsDetailsViewsOptions;
|
||||
|
@ -372,7 +367,6 @@ export default {
|
|||
</gl-tab>
|
||||
|
||||
<metric-images-tab
|
||||
v-if="!isThreatMonitoringPage"
|
||||
:data-testid="$options.tabsConfig[1].id"
|
||||
:title="$options.tabsConfig[1].title"
|
||||
/>
|
||||
|
|
|
@ -30,13 +30,4 @@ export const PAGE_CONFIG = {
|
|||
label: 'Status',
|
||||
},
|
||||
},
|
||||
THREAT_MONITORING: {
|
||||
TITLE: 'THREAT_MONITORING',
|
||||
STATUSES: {
|
||||
TRIGGERED: s__('ThreatMonitoring|Unreviewed'),
|
||||
ACKNOWLEDGED: s__('ThreatMonitoring|In review'),
|
||||
RESOLVED: s__('ThreatMonitoring|Resolved'),
|
||||
IGNORED: s__('ThreatMonitoring|Dismissed'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -65,16 +65,12 @@ export default (selector) => {
|
|||
|
||||
const opsProperties = {};
|
||||
|
||||
if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
|
||||
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
|
||||
page
|
||||
];
|
||||
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
|
||||
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
|
||||
opsProperties.store = createStore({}, service);
|
||||
} else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
|
||||
provide.isThreatMonitoringPage = true;
|
||||
}
|
||||
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
|
||||
page
|
||||
];
|
||||
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
|
||||
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
|
||||
opsProperties.store = createStore({}, service);
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
|
|
|
@ -25,20 +25,22 @@ module StorageHelper
|
|||
end
|
||||
|
||||
def storage_enforcement_banner_info(namespace)
|
||||
return unless can?(current_user, :admin_namespace, namespace)
|
||||
return if namespace.paid?
|
||||
return unless namespace.storage_enforcement_date && namespace.storage_enforcement_date >= Date.today
|
||||
return if user_dismissed_storage_enforcement_banner?(namespace)
|
||||
root_ancestor = namespace.root_ancestor
|
||||
|
||||
return unless can?(current_user, :admin_namespace, root_ancestor)
|
||||
return if root_ancestor.paid?
|
||||
return unless future_enforcement_date?(root_ancestor)
|
||||
return if user_dismissed_storage_enforcement_banner?(root_ancestor)
|
||||
|
||||
{
|
||||
text: html_escape_once(s_("UsageQuota|From %{storage_enforcement_date} storage limits will apply to this namespace. " \
|
||||
"You are currently using %{used_storage} of namespace storage. " \
|
||||
"View and manage your usage from %{strong_start}%{namespace_type} settings > Usage quotas%{strong_end}.")).html_safe %
|
||||
{ storage_enforcement_date: namespace.storage_enforcement_date, used_storage: storage_counter(namespace.root_storage_statistics&.storage_size || 0), strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: namespace.type },
|
||||
{ storage_enforcement_date: root_ancestor.storage_enforcement_date, used_storage: storage_counter(root_ancestor.root_storage_statistics&.storage_size || 0), strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: root_ancestor.type },
|
||||
variant: 'warning',
|
||||
callouts_path: namespace.user_namespace? ? callouts_path : group_callouts_path,
|
||||
callouts_feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
|
||||
learn_more_link: link_to(_('Learn more.'), help_page_path('/'), rel: 'noopener noreferrer', target: '_blank') # TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
|
||||
callouts_path: root_ancestor.user_namespace? ? callouts_path : group_callouts_path,
|
||||
callouts_feature_name: storage_enforcement_banner_user_callouts_feature_name(root_ancestor),
|
||||
learn_more_link: link_to(_('Learn more.'), help_page_path('/'), rel: 'noopener noreferrer', target: '_blank')
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -63,8 +65,16 @@ module StorageHelper
|
|||
if namespace.user_namespace?
|
||||
current_user.dismissed_callout?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace))
|
||||
else
|
||||
current_user.dismissed_callout_for_group?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
|
||||
group: namespace)
|
||||
current_user.dismissed_callout_for_group?(
|
||||
feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
|
||||
group: namespace
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def future_enforcement_date?(namespace)
|
||||
return true if ::Feature.enabled?(:namespace_storage_limit_bypass_date_check, namespace)
|
||||
|
||||
namespace.storage_enforcement_date.present? && namespace.storage_enforcement_date >= Date.today
|
||||
end
|
||||
end
|
||||
|
|
|
@ -546,6 +546,8 @@ class Namespace < ApplicationRecord
|
|||
end
|
||||
|
||||
def storage_enforcement_date
|
||||
return Date.current if Feature.enabled?(:namespace_storage_limit_bypass_date_check, self)
|
||||
|
||||
# should return something like Date.new(2022, 02, 03)
|
||||
# TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
|
||||
nil
|
||||
|
|
|
@ -38,6 +38,6 @@
|
|||
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
|
||||
%div
|
||||
= f.gitlab_ui_checkbox_component :active, _('Active'), checkbox_options: { value: @schedule.active, required: false }
|
||||
.footer-block.row-content-block
|
||||
.footer-block
|
||||
= f.submit _('Save pipeline schedule'), class: 'btn gl-button btn-confirm'
|
||||
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn gl-button btn-default btn-cancel'
|
||||
|
|
|
@ -7,6 +7,5 @@
|
|||
|
||||
%h1.page-title
|
||||
= _("Schedule a new pipeline")
|
||||
%hr
|
||||
|
||||
= render "form"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -67,7 +67,7 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def get_urgency
|
||||
class_attributes[:urgency] || :low
|
||||
get_class_attribute(:urgency) || :low
|
||||
end
|
||||
|
||||
# Allows configuring worker's data_consistency.
|
||||
|
@ -98,13 +98,13 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def get_data_consistency
|
||||
class_attributes[:data_consistency] || DEFAULT_DATA_CONSISTENCY
|
||||
get_class_attribute(:data_consistency) || DEFAULT_DATA_CONSISTENCY
|
||||
end
|
||||
|
||||
def get_data_consistency_feature_flag_enabled?
|
||||
return true unless class_attributes[:data_consistency_feature_flag]
|
||||
return true unless get_class_attribute(:data_consistency_feature_flag)
|
||||
|
||||
Feature.enabled?(class_attributes[:data_consistency_feature_flag])
|
||||
Feature.enabled?(get_class_attribute(:data_consistency_feature_flag))
|
||||
end
|
||||
|
||||
# Set this attribute on a job when it will call to services outside of the
|
||||
|
@ -115,11 +115,11 @@ module WorkerAttributes
|
|||
set_class_attribute(:external_dependencies, true)
|
||||
end
|
||||
|
||||
# Returns a truthy value if the worker has external dependencies.
|
||||
# Returns true if the worker has external dependencies.
|
||||
# See doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies
|
||||
# for details
|
||||
def worker_has_external_dependencies?
|
||||
class_attributes[:external_dependencies]
|
||||
!!get_class_attribute(:external_dependencies)
|
||||
end
|
||||
|
||||
def worker_resource_boundary(boundary)
|
||||
|
@ -129,7 +129,7 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def get_worker_resource_boundary
|
||||
class_attributes[:resource_boundary] || :unknown
|
||||
get_class_attribute(:resource_boundary) || :unknown
|
||||
end
|
||||
|
||||
def idempotent!
|
||||
|
@ -137,7 +137,7 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def idempotent?
|
||||
class_attributes[:idempotent]
|
||||
!!get_class_attribute(:idempotent)
|
||||
end
|
||||
|
||||
def weight(value)
|
||||
|
@ -145,7 +145,7 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def get_weight
|
||||
class_attributes[:weight] ||
|
||||
get_class_attribute(:weight) ||
|
||||
NAMESPACE_WEIGHTS[queue_namespace] ||
|
||||
1
|
||||
end
|
||||
|
@ -155,7 +155,7 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def get_tags
|
||||
Array(class_attributes[:tags])
|
||||
Array(get_class_attribute(:tags))
|
||||
end
|
||||
|
||||
def deduplicate(strategy, options = {})
|
||||
|
@ -164,12 +164,12 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def get_deduplicate_strategy
|
||||
class_attributes[:deduplication_strategy] ||
|
||||
get_class_attribute(:deduplication_strategy) ||
|
||||
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
|
||||
end
|
||||
|
||||
def get_deduplication_options
|
||||
class_attributes[:deduplication_options] || {}
|
||||
get_class_attribute(:deduplication_options) || {}
|
||||
end
|
||||
|
||||
def deduplication_enabled?
|
||||
|
@ -183,7 +183,7 @@ module WorkerAttributes
|
|||
end
|
||||
|
||||
def big_payload?
|
||||
class_attributes[:big_payload]
|
||||
!!get_class_attribute(:big_payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,8 +51,16 @@
|
|||
- 1
|
||||
- - authorized_keys
|
||||
- 2
|
||||
- - authorized_project_update
|
||||
- - authorized_project_update:authorized_project_update_project_recalculate
|
||||
- 1
|
||||
- - authorized_project_update:authorized_project_update_project_recalculate_per_user
|
||||
- 1
|
||||
- - authorized_project_update:authorized_project_update_user_refresh_from_replica
|
||||
- 1
|
||||
- - authorized_project_update:authorized_project_update_user_refresh_over_user_range
|
||||
- 1
|
||||
- - authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
|
||||
- 2
|
||||
- - authorized_projects
|
||||
- 2
|
||||
- - auto_devops
|
||||
|
@ -420,7 +428,7 @@
|
|||
- - self_monitoring_project_delete
|
||||
- 2
|
||||
- - service_desk_email_receiver
|
||||
- 1
|
||||
- 2
|
||||
- - set_user_status_based_on_user_cap_setting
|
||||
- 1
|
||||
- - snippets_schedule_bulk_repository_shard_moves
|
||||
|
|
|
@ -34,8 +34,8 @@ To bring the former **primary** site up to date:
|
|||
|
||||
NOTE:
|
||||
If you [disabled the **primary** site permanently](index.md#step-2-permanently-disable-the-primary-site),
|
||||
you need to undo those steps now. For Debian/Ubuntu you just need to run
|
||||
`sudo systemctl enable gitlab-runsvdir`. For CentOS 6, you need to install
|
||||
you need to undo those steps now. For distributions with systemd, such as Debian/Ubuntu/CentOS7+, you must run
|
||||
`sudo systemctl enable gitlab-runsvdir`. For distributions without systemd, such as CentOS 6, you need to install
|
||||
the GitLab instance from scratch and set it up as a **secondary** site by
|
||||
following [Setup instructions](../setup/index.md). In this case, you don't need to follow the next step.
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ inside the Workload Identity Pool created in the previous step, using the follow
|
|||
such as `gitlab/gitlab`.
|
||||
- **Provider ID**: Unique ID in the pool for the Workload Identity Provider,
|
||||
such as `gitlab-gitlab`. This value is used to refer to the provider, and appears in URLs.
|
||||
- **Issuer (URL)**: The address of your GitLab instance, such as `https://gitlab.com` or
|
||||
`https://gitlab.example.com`.
|
||||
- **Issuer (URL)**: The address of your GitLab instance, such as `https://gitlab.com/` or
|
||||
`https://gitlab.example.com/`.
|
||||
- The address must use the `https://` protocol.
|
||||
- The address must end in a trailing slash.
|
||||
- **Audiences**: Manually set the allowed audiences list to the address of your
|
||||
|
|
|
@ -718,6 +718,51 @@ variables:
|
|||
| `CACHE_COMPRESSION_LEVEL` | To adjust compression ratio, set to `fastest`, `fast`, `default`, `slow`, or `slowest`. This setting works with the Fastzip archiver only, so the GitLab Runner feature flag [`FF_USE_FASTZIP`](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags) must also be enabled. |
|
||||
| `CACHE_REQUEST_TIMEOUT` | Configure the maximum duration of cache upload and download operations for a single job in minutes. Default is `10` minutes. |
|
||||
|
||||
### Staging directory
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3403) in GitLab Runner 15.0.
|
||||
|
||||
If you do not want to archive cache and artifacts in the system's default temporary directory, you can specify a different directory.
|
||||
|
||||
You might need to change the directory if your system's default temporary path has constraints.
|
||||
If you use a fast disk for the directory location, it can also improve performance.
|
||||
|
||||
To change the directory, set `ARCHIVER_STAGING_DIR` as a variable in your CI job, or use a runner variable when you register the runner (`gitlab register --env ARCHIVER_STAGING_DIR=<dir>`).
|
||||
|
||||
The directory you specify is used as the location for downloading artifacts prior to extraction. If the `fastzip` archiver is
|
||||
used, this location is also used as scratch space when archiving.
|
||||
|
||||
### Configure `fastzip` to improve performance
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3130) in GitLab Runner 15.0.
|
||||
|
||||
To tune `fastzip`, ensure the [`FF_USE_FASTZIP`](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags) flag is enabled.
|
||||
Then use any of the following environment variables.
|
||||
|
||||
| Variable | Description |
|
||||
|---------------------------------|--------------------------------------------------------|
|
||||
| `FASTZIP_ARCHIVER_CONCURRENCY` | The number of files to be concurrently compressed. Default is the number of CPUs available. |
|
||||
| `FASTZIP_ARCHIVER_BUFFER_SIZE` | The buffer size allocated per concurrency for each file. Data exceeding this number moves to scratch space. Default is 2 MiB. |
|
||||
| `FASTZIP_EXTRACTOR_CONCURRENCY` | The number of files to be concurrency decompressed. Default is the number of CPUs available. |
|
||||
|
||||
Files in a zip archive are appended sequentially. This makes concurrent compression challenging. `fastzip` works around
|
||||
this limitation by compressing files concurrently to disk first, and then copying the result back to zip archive
|
||||
sequentially.
|
||||
|
||||
To avoid writing to disk and reading the contents back for smaller files, a small buffer per concurrency is used. This setting
|
||||
can be controlled with `FASTZIP_ARCHIVER_BUFFER_SIZE`. The default size for this buffer is 2 MiB, therefore, a
|
||||
concurrency of 16 will allocate 32 MiB. Data that exceeds the buffer size will be written to and read back from disk.
|
||||
Therefore, using no buffer, `FASTZIP_ARCHIVER_BUFFER_SIZE: 0`, and only scratch space is a valid option.
|
||||
|
||||
`FASTZIP_ARCHIVER_CONCURRENCY` controls how many files are compressed concurrency. As mentioned above, this setting
|
||||
therefore can increase how much memory is being used, but also how much temporary data is written to the scratch space.
|
||||
The default is the number of CPUs available, but given the memory ramifications, this may not always be the best
|
||||
setting.
|
||||
|
||||
`FASTZIP_EXTRACTOR_CONCURRENCY` controls how many files are decompressed at once. Files from a zip archive can natively
|
||||
be read from concurrency, so no additional memory is allocated in additional to what the decompressor requires. This
|
||||
defaults to the number of CPUs available.
|
||||
|
||||
## Clean up stale runners
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363012) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `stale_runner_cleanup_for_namespace_development`. Disabled by default.
|
||||
|
|
|
@ -6,6 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Sidekiq worker attributes
|
||||
|
||||
Worker classes can define certain attributes to control their behavior and add metadata.
|
||||
|
||||
Child classes inheriting from other workers also inherit these attributes, so you only
|
||||
have to redefine them if you want to override their values.
|
||||
|
||||
## Job urgency
|
||||
|
||||
Jobs can have an `urgency` attribute set, which can be `:high`,
|
||||
|
|
|
@ -94,11 +94,13 @@ that can process jobs in the `background_migration` queue.
|
|||
|
||||
### Background migrations
|
||||
|
||||
#### Pending migrations
|
||||
|
||||
**For Omnibus installations:**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining'
|
||||
sudo gitlab-rails runner -e production 'puts Gitlab::Database::BackgroundMigrationJob.pending.count'
|
||||
sudo gitlab-rails runner -e production 'puts Gitlab::Database::BackgroundMigration::BatchedMigration.queued.count'
|
||||
```
|
||||
|
||||
**For installations from source:**
|
||||
|
@ -109,6 +111,38 @@ sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMi
|
|||
sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::Database::BackgroundMigrationJob.pending.count'
|
||||
```
|
||||
|
||||
#### Failed migrations
|
||||
|
||||
**For Omnibus installations:**
|
||||
|
||||
For GitLab 14.0-14.9:
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails runner -e production 'Gitlab::Database::BackgroundMigration::BatchedMigration.failed.count'
|
||||
```
|
||||
|
||||
For GitLab 14.10 and later:
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails runner -e production 'Gitlab::Database::BackgroundMigration::BatchedMigration.with_status(:failed).count'
|
||||
```
|
||||
|
||||
**For installations from source:**
|
||||
|
||||
For GitLab 14.0-14.9:
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rails runner -e production 'Gitlab::Database::BackgroundMigration::BatchedMigration.failed.count'
|
||||
```
|
||||
|
||||
For GitLab 14.10 and later:
|
||||
|
||||
```shell
|
||||
cd /home/git/gitlab
|
||||
sudo -u git -H bundle exec rails runner -e production 'Gitlab::Database::BackgroundMigration::BatchedMigration.with_status(:failed).count'
|
||||
```
|
||||
|
||||
### Batched background migrations
|
||||
|
||||
GitLab 14.0 introduced [batched background migrations](../user/admin_area/monitoring/background_migrations.md).
|
||||
|
@ -333,7 +367,7 @@ Find where your version sits in the upgrade path below, and upgrade GitLab
|
|||
accordingly, while also consulting the
|
||||
[version-specific upgrade instructions](#version-specific-upgrading-instructions):
|
||||
|
||||
`8.11.Z` -> `8.12.0` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> [`11.11.8`](#1200) -> `12.0.12` -> [`12.1.17`](#1210) -> [`12.10.14`](#12100) -> `13.0.14` -> [`13.1.11`](#1310) -> [`13.8.8`](#1388) -> [`13.12.15`](#13120) -> [`14.0.12`](#1400) -> [`14.9.0`](#1490) -> [`14.10.Z`](#1410) -> [`15.0.Z`](#1500) -> [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
|
||||
`8.11.Z` -> `8.12.0` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> [`11.11.8`](#1200) -> `12.0.12` -> [`12.1.17`](#1210) -> [`12.10.14`](#12100) -> `13.0.14` -> [`13.1.11`](#1310) -> [`13.8.8`](#1388) -> [`13.12.15`](#13120) -> [`14.0.12`](#1400) -> [`14.9.5`](#1490) -> [`14.10.Z`](#1410) -> [`15.0.Z`](#1500) -> [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
|
||||
|
||||
The following table, while not exhaustive, shows some examples of the supported
|
||||
upgrade paths.
|
||||
|
@ -341,8 +375,8 @@ Additional steps between the mentioned versions are possible. We list the minima
|
|||
|
||||
| Target version | Your version | Supported upgrade path | Note |
|
||||
| -------------- | ------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `15.1.0` | `14.6.2` | `14.6.2` -> `14.9.4` -> `14.10.3` -> `15.0.0` -> `15.1.0` | Three intermediate versions are required: `14.9` and `14.10`, `15.0`, then `15.1.0`. |
|
||||
| `15.0.0` | `14.6.2` | `14.6.2` -> `14.9.4` -> `14.10.3` -> `15.0.0` | Two intermediate versions are required: `14.9` and `14.10`, then `15.0.0`. |
|
||||
| `15.1.0` | `14.6.2` | `14.6.2` -> `14.9.5` -> `14.10.4` -> `15.0.2` -> `15.1.0` | Three intermediate versions are required: `14.9` and `14.10`, `15.0`, then `15.1.0`. |
|
||||
| `15.0.0` | `14.6.2` | `14.6.2` -> `14.9.5` -> `14.10.4` -> `15.0.2` | Two intermediate versions are required: `14.9` and `14.10`, then `15.0.0`. |
|
||||
| `14.6.2` | `13.10.2` | `13.10.2` -> `13.12.15` -> `14.0.12` -> `14.6.2` | Two intermediate versions are required: `13.12` and `14.0`, then `14.6.2`. |
|
||||
| `14.1.8` | `13.9.2` | `13.9.2` -> `13.12.15` -> `14.0.12` -> `14.1.8` | Two intermediate versions are required: `13.12` and `14.0`, then `14.1.8`. |
|
||||
| `13.12.15` | `12.9.2` | `12.9.2` -> `12.10.14` -> `13.0.14` -> `13.1.11` -> `13.8.8` -> `13.12.15` | Four intermediate versions are required: `12.10`, `13.0`, `13.1` and `13.8.8`, then `13.12.15`. |
|
||||
|
|
|
@ -18,6 +18,11 @@ pre-push:
|
|||
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
|
||||
glob: 'doc/*.md'
|
||||
run: yarn markdownlint {files}
|
||||
yamllint:
|
||||
tags: backend style
|
||||
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
|
||||
glob: '*.{yml,yaml}'
|
||||
run: scripts/lint-yaml.sh {files}
|
||||
stylelint:
|
||||
tags: stylesheet css style
|
||||
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
|
||||
|
|
|
@ -7966,9 +7966,6 @@ msgstr ""
|
|||
msgid "Clean up after running %{link_start}git filter-repo%{link_end} on the repository."
|
||||
msgstr ""
|
||||
|
||||
msgid "Clean up image tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cleanup policies are executed by background workers. This setting defines the maximum number of workers that can run concurrently. Set it to 0 to remove all workers and not execute the cleanup policies."
|
||||
msgstr ""
|
||||
|
||||
|
@ -9647,6 +9644,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|CLI Commands"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Clean up image tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Cleanup disabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9823,6 +9823,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Run cleanup:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Set up cleanup"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33245,9 +33248,6 @@ msgstr ""
|
|||
msgid "Save pipeline schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saving"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35234,7 +35234,7 @@ msgstr ""
|
|||
msgid "Show whitespace changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Showing %{conflict} between %{sourceBranch} and %{targetBranch}"
|
||||
msgid "Showing %{conflict}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Showing %{count} of %{total} projects"
|
||||
|
@ -39234,24 +39234,6 @@ msgstr ""
|
|||
msgid "Thread to reply to cannot be found"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Alert Details"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Dismissed"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|In review"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Threat Monitoring"
|
||||
msgstr ""
|
||||
|
||||
msgid "ThreatMonitoring|Unreviewed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Threshold in bytes at which to compress Sidekiq job arguments."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v yamllint > /dev/null; then
|
||||
echo "ERROR: yamllint is not installed. For more information, see https://yamllint.readthedocs.io/en/stable/index.html."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
yamllint --strict -f colored "$@"
|
|
@ -243,10 +243,17 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
|
|||
end
|
||||
|
||||
it 'expands multiple queue groups correctly' do
|
||||
expected_workers =
|
||||
if Gitlab.ee?
|
||||
[%w[chat_notification], %w[project_export project_template_export]]
|
||||
else
|
||||
[%w[chat_notification], %w[project_export]]
|
||||
end
|
||||
|
||||
expect(Gitlab::SidekiqCluster)
|
||||
.to receive(:start)
|
||||
.with([['chat_notification'], ['project_export']], default_options)
|
||||
.and_return([])
|
||||
.with(expected_workers, default_options)
|
||||
.and_return([])
|
||||
|
||||
cli.run(%w(--queue-selector feature_category=chatops&has_external_dependencies=true resource_boundary=memory&feature_category=importers))
|
||||
end
|
||||
|
|
|
@ -498,7 +498,9 @@ RSpec.describe 'Group' do
|
|||
let_it_be(:group) { create(:group) }
|
||||
let_it_be_with_refind(:user) { create(:user) }
|
||||
|
||||
before_all do
|
||||
before do
|
||||
stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
|
||||
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
|
|
@ -89,6 +89,10 @@ RSpec.describe 'User visits their profile' do
|
|||
end
|
||||
|
||||
describe 'storage_enforcement_banner', :js do
|
||||
before do
|
||||
stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
|
||||
end
|
||||
|
||||
context 'with storage_enforcement_date set' do
|
||||
let_it_be(:storage_enforcement_date) { Date.today + 30 }
|
||||
|
||||
|
|
|
@ -475,19 +475,19 @@
|
|||
markdown: |-
|
||||
A footnote reference tag looks like this: [^1]
|
||||
|
||||
This reference tag is a mix of letters and numbers. [^2]
|
||||
This reference tag is a mix of letters and numbers. [^footnote]
|
||||
|
||||
[^1]: This is the text inside a footnote.
|
||||
[^2]: This is another footnote.
|
||||
[^footnote]: This is another footnote.
|
||||
html: |-
|
||||
<p data-sourcepos="1:1-1:46" dir="auto">A footnote reference tag looks like this: <sup class="footnote-ref"><a href="#fn-1-2717" id="fnref-1-2717" data-footnote-ref="">1</a></sup></p>
|
||||
<p data-sourcepos="3:1-3:56" dir="auto">This reference tag is a mix of letters and numbers. <sup class="footnote-ref"><a href="#fn-2-2717" id="fnref-2-2717" data-footnote-ref="">2</a></sup></p>
|
||||
<p data-sourcepos="3:1-3:56" dir="auto">This reference tag is a mix of letters and numbers. <sup class="footnote-ref"><a href="#fn-footnote-2717" id="fnref-footnote-2717" data-footnote-ref="">2</a></sup></p>
|
||||
<section class="footnotes" data-footnotes><ol>
|
||||
<li id="fn-1-2717">
|
||||
<p data-sourcepos="5:7-5:41">This is the text inside a footnote. <a href="#fnref-1-2717" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
</li>
|
||||
<li id="fn-2-2717">
|
||||
<p data-sourcepos="6:7-6:31">This is another footnote. <a href="#fnref-2-2717" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
<li id="fn-footnote-2717">
|
||||
<p data-sourcepos="6:7-6:31">This is another footnote. <a href="#fnref-footnote-2717" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
</li>
|
||||
</ol></section>
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import FootnoteDefinitionWrapper from '~/content_editor/components/wrappers/footnote_definition.vue';
|
||||
|
||||
describe('content/components/wrappers/footnote_definition', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = async (node = {}) => {
|
||||
wrapper = shallowMountExtended(FootnoteDefinitionWrapper, {
|
||||
propsData: {
|
||||
node,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders footnote label as a readyonly element', () => {
|
||||
const label = 'footnote';
|
||||
|
||||
createWrapper({
|
||||
attrs: {
|
||||
label,
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain(label);
|
||||
expect(wrapper.findByTestId('footnote-label').attributes().contenteditable).toBe('false');
|
||||
});
|
||||
});
|
|
@ -13,7 +13,6 @@ import Figure from '~/content_editor/extensions/figure';
|
|||
import FigureCaption from '~/content_editor/extensions/figure_caption';
|
||||
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
|
||||
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
|
||||
import FootnotesSection from '~/content_editor/extensions/footnotes_section';
|
||||
import HardBreak from '~/content_editor/extensions/hard_break';
|
||||
import Heading from '~/content_editor/extensions/heading';
|
||||
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
|
||||
|
@ -53,7 +52,6 @@ const tiptapEditor = createTestEditor({
|
|||
Emoji,
|
||||
FootnoteDefinition,
|
||||
FootnoteReference,
|
||||
FootnotesSection,
|
||||
Figure,
|
||||
FigureCaption,
|
||||
HardBreak,
|
||||
|
@ -92,7 +90,6 @@ const {
|
|||
emoji,
|
||||
footnoteDefinition,
|
||||
footnoteReference,
|
||||
footnotesSection,
|
||||
figure,
|
||||
figureCaption,
|
||||
heading,
|
||||
|
@ -131,7 +128,6 @@ const {
|
|||
figureCaption: { nodeType: FigureCaption.name },
|
||||
footnoteDefinition: { nodeType: FootnoteDefinition.name },
|
||||
footnoteReference: { nodeType: FootnoteReference.name },
|
||||
footnotesSection: { nodeType: FootnotesSection.name },
|
||||
hardBreak: { nodeType: HardBreak.name },
|
||||
heading: { nodeType: Heading.name },
|
||||
horizontalRule: { nodeType: HorizontalRule.name },
|
||||
|
@ -1147,18 +1143,15 @@ there
|
|||
it('correctly serializes footnotes', () => {
|
||||
expect(
|
||||
serialize(
|
||||
paragraph(
|
||||
'Oranges are orange ',
|
||||
footnoteReference({ footnoteId: '1', footnoteNumber: '1' }),
|
||||
),
|
||||
footnotesSection(footnoteDefinition(paragraph('Oranges are fruits'))),
|
||||
paragraph('Oranges are orange ', footnoteReference({ label: '1', identifier: '1' })),
|
||||
footnoteDefinition({ label: '1', identifier: '1' }, 'Oranges are fruits'),
|
||||
),
|
||||
).toBe(
|
||||
`
|
||||
Oranges are orange [^1]
|
||||
|
||||
[^1]: Oranges are fruits
|
||||
`.trim(),
|
||||
`.trimLeft(),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ describe('Merge Conflict Resolver App', () => {
|
|||
const title = findConflictsCount();
|
||||
|
||||
expect(title.exists()).toBe(true);
|
||||
expect(title.text().trim()).toBe('Showing 3 conflicts between test-conflicts and main');
|
||||
expect(title.text().trim()).toBe('Showing 3 conflicts');
|
||||
});
|
||||
|
||||
it('shows a loading spinner while loading', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Settings Form Cadence matches snapshot 1`] = `
|
||||
exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
class="gl-mr-7 gl-mb-0!"
|
||||
data-testid="cadence-dropdown"
|
||||
|
@ -11,7 +11,7 @@ exports[`Settings Form Cadence matches snapshot 1`] = `
|
|||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Enable matches snapshot 1`] = `
|
||||
exports[`Container Expiration Policy Settings Form Enable matches snapshot 1`] = `
|
||||
<expiration-toggle-stub
|
||||
class="gl-mb-0!"
|
||||
data-testid="enable-toggle"
|
||||
|
@ -19,7 +19,7 @@ exports[`Settings Form Enable matches snapshot 1`] = `
|
|||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Keep N matches snapshot 1`] = `
|
||||
exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="keep-n-dropdown"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
|
@ -29,7 +29,7 @@ exports[`Settings Form Keep N matches snapshot 1`] = `
|
|||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Keep Regex matches snapshot 1`] = `
|
||||
exports[`Container Expiration Policy Settings Form Keep Regex matches snapshot 1`] = `
|
||||
<expiration-input-stub
|
||||
data-testid="keep-regex-input"
|
||||
description="Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}"
|
||||
|
@ -41,7 +41,7 @@ exports[`Settings Form Keep Regex matches snapshot 1`] = `
|
|||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form OlderThan matches snapshot 1`] = `
|
||||
exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1`] = `
|
||||
<expiration-dropdown-stub
|
||||
data-testid="older-than-dropdown"
|
||||
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
|
||||
|
@ -51,7 +51,7 @@ exports[`Settings Form OlderThan matches snapshot 1`] = `
|
|||
/>
|
||||
`;
|
||||
|
||||
exports[`Settings Form Remove regex matches snapshot 1`] = `
|
||||
exports[`Container Expiration Policy Settings Form Remove regex matches snapshot 1`] = `
|
||||
<expiration-input-stub
|
||||
data-testid="remove-regex-input"
|
||||
description="Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}"
|
|
@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
|
||||
import component from '~/packages_and_registries/settings/project/components/settings_form.vue';
|
||||
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
|
||||
import {
|
||||
UPDATE_SETTINGS_ERROR_MESSAGE,
|
||||
UPDATE_SETTINGS_SUCCESS_MESSAGE,
|
||||
|
@ -14,7 +14,7 @@ import expirationPolicyQuery from '~/packages_and_registries/settings/project/gr
|
|||
import Tracking from '~/tracking';
|
||||
import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
|
||||
|
||||
describe('Settings Form', () => {
|
||||
describe('Container Expiration Policy Settings Form', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
|
||||
import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
|
||||
import {
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
UNAVAILABLE_USER_FEATURE_TEXT,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
|
||||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
|
||||
import {
|
||||
expirationPolicyPayload,
|
||||
emptyExpirationPolicyPayload,
|
||||
containerExpirationPolicyData,
|
||||
} from '../mock_data';
|
||||
|
||||
describe('Container expiration policy project settings', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const defaultProvidedValues = {
|
||||
projectPath: 'path',
|
||||
isAdmin: false,
|
||||
adminSettingsPath: 'settingsPath',
|
||||
enableHistoricEntries: false,
|
||||
helpPagePath: 'helpPagePath',
|
||||
showCleanupPolicyLink: false,
|
||||
};
|
||||
|
||||
const findFormComponent = () => wrapper.find(ContainerExpirationPolicyForm);
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findSettingsBlock = () => wrapper.find(SettingsBlock);
|
||||
|
||||
const mountComponent = (provide = defaultProvidedValues, config) => {
|
||||
wrapper = shallowMount(component, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
SettingsBlock,
|
||||
},
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
provide,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const requestHandlers = [[expirationPolicyQuery, resolver]];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
mountComponent(provide, {
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('isEdited status', () => {
|
||||
it.each`
|
||||
description | apiResponse | workingCopy | result
|
||||
${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
|
||||
${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
|
||||
${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
|
||||
${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
|
||||
${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
|
||||
`('$description', async ({ apiResponse, workingCopy, result }) => {
|
||||
mountComponentWithApollo({
|
||||
provide: { ...defaultProvidedValues, enableHistoricEntries: true },
|
||||
resolver: jest.fn().mockResolvedValue(apiResponse),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
findFormComponent().vm.$emit('input', workingCopy);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findFormComponent().props('isEdited')).toBe(result);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the setting form', async () => {
|
||||
mountComponentWithApollo({
|
||||
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findFormComponent().exists()).toBe(true);
|
||||
expect(findSettingsBlock().props('collapsible')).toBe(false);
|
||||
});
|
||||
|
||||
describe('the form is disabled', () => {
|
||||
it('the form is hidden', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findFormComponent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows an alert', () => {
|
||||
mountComponent();
|
||||
|
||||
const text = findAlert().text();
|
||||
expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
|
||||
expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT);
|
||||
});
|
||||
|
||||
describe('an admin is visiting the page', () => {
|
||||
it('shows the admin part of the alert message', () => {
|
||||
mountComponent({ ...defaultProvidedValues, isAdmin: true });
|
||||
|
||||
const sprintf = findAlert().find(GlSprintf);
|
||||
expect(sprintf.text()).toBe('administration settings');
|
||||
expect(sprintf.find(GlLink).attributes('href')).toBe(
|
||||
defaultProvidedValues.adminSettingsPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSettingsError', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponentWithApollo({
|
||||
resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('the form is hidden', () => {
|
||||
expect(findFormComponent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows an alert', () => {
|
||||
expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty API response', () => {
|
||||
it.each`
|
||||
enableHistoricEntries | isShown
|
||||
${true} | ${true}
|
||||
${false} | ${false}
|
||||
`('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
|
||||
mountComponentWithApollo({
|
||||
provide: {
|
||||
...defaultProvidedValues,
|
||||
enableHistoricEntries,
|
||||
},
|
||||
resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findFormComponent().exists()).toBe(isShown);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,165 +1,19 @@
|
|||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
|
||||
import SettingsForm from '~/packages_and_registries/settings/project/components/settings_form.vue';
|
||||
import {
|
||||
FETCH_SETTINGS_ERROR_MESSAGE,
|
||||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
UNAVAILABLE_USER_FEATURE_TEXT,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
|
||||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
|
||||
|
||||
import {
|
||||
expirationPolicyPayload,
|
||||
emptyExpirationPolicyPayload,
|
||||
containerExpirationPolicyData,
|
||||
} from '../mock_data';
|
||||
|
||||
describe('Registry Settings App', () => {
|
||||
describe('Registry Settings app', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const defaultProvidedValues = {
|
||||
projectPath: 'path',
|
||||
isAdmin: false,
|
||||
adminSettingsPath: 'settingsPath',
|
||||
enableHistoricEntries: false,
|
||||
helpPagePath: 'helpPagePath',
|
||||
showCleanupPolicyLink: false,
|
||||
};
|
||||
|
||||
const findSettingsComponent = () => wrapper.find(SettingsForm);
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
|
||||
const mountComponent = (provide = defaultProvidedValues, config) => {
|
||||
wrapper = shallowMount(component, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
SettingsBlock,
|
||||
},
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
provide,
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const requestHandlers = [[expirationPolicyQuery, resolver]];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
mountComponent(provide, {
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
};
|
||||
const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('isEdited status', () => {
|
||||
it.each`
|
||||
description | apiResponse | workingCopy | result
|
||||
${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
|
||||
${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
|
||||
${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
|
||||
${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
|
||||
${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
|
||||
`('$description', async ({ apiResponse, workingCopy, result }) => {
|
||||
mountComponentWithApollo({
|
||||
provide: { ...defaultProvidedValues, enableHistoricEntries: true },
|
||||
resolver: jest.fn().mockResolvedValue(apiResponse),
|
||||
});
|
||||
await waitForPromises();
|
||||
it('renders container expiration policy component', () => {
|
||||
wrapper = shallowMount(component);
|
||||
|
||||
findSettingsComponent().vm.$emit('input', workingCopy);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSettingsComponent().props('isEdited')).toBe(result);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the setting form', async () => {
|
||||
mountComponentWithApollo({
|
||||
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSettingsComponent().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('the form is disabled', () => {
|
||||
it('the form is hidden', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findSettingsComponent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows an alert', () => {
|
||||
mountComponent();
|
||||
|
||||
const text = findAlert().text();
|
||||
expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
|
||||
expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT);
|
||||
});
|
||||
|
||||
describe('an admin is visiting the page', () => {
|
||||
it('shows the admin part of the alert message', () => {
|
||||
mountComponent({ ...defaultProvidedValues, isAdmin: true });
|
||||
|
||||
const sprintf = findAlert().find(GlSprintf);
|
||||
expect(sprintf.text()).toBe('administration settings');
|
||||
expect(sprintf.find(GlLink).attributes('href')).toBe(
|
||||
defaultProvidedValues.adminSettingsPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSettingsError', () => {
|
||||
beforeEach(async () => {
|
||||
mountComponentWithApollo({
|
||||
resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('the form is hidden', () => {
|
||||
expect(findSettingsComponent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows an alert', () => {
|
||||
expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty API response', () => {
|
||||
it.each`
|
||||
enableHistoricEntries | isShown
|
||||
${true} | ${true}
|
||||
${false} | ${false}
|
||||
`('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
|
||||
mountComponentWithApollo({
|
||||
provide: {
|
||||
...defaultProvidedValues,
|
||||
enableHistoricEntries,
|
||||
},
|
||||
resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSettingsComponent().exists()).toBe(isShown);
|
||||
});
|
||||
expect(findContainerExpirationPolicy().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
|
||||
import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
|
@ -51,7 +51,7 @@ describe('MrWidgetTerraformConainer', () => {
|
|||
});
|
||||
|
||||
it('diplays loading skeleton', () => {
|
||||
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
|
||||
expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -63,7 +63,7 @@ describe('MrWidgetTerraformConainer', () => {
|
|||
});
|
||||
|
||||
it('displays terraform content', () => {
|
||||
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
|
||||
expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(true);
|
||||
expect(findPlans()).toEqual(Object.values(plans));
|
||||
});
|
||||
|
@ -158,7 +158,7 @@ describe('MrWidgetTerraformConainer', () => {
|
|||
});
|
||||
|
||||
it('stops loading', () => {
|
||||
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('generates one broken plan', () => {
|
||||
|
|
|
@ -201,28 +201,6 @@ describe('AlertDetails', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Threat Monitoring details', () => {
|
||||
it('should not render the metrics tab', () => {
|
||||
mountComponent({
|
||||
data: { alert: mockAlert },
|
||||
provide: { isThreatMonitoringPage: true },
|
||||
});
|
||||
expect(findMetricsTab().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should display "View incident" button that links the issues page when incident exists', () => {
|
||||
const iid = '3';
|
||||
mountComponent({
|
||||
data: { alert: { ...mockAlert, issue: { iid } }, sidebarStatus: false },
|
||||
provide: { isThreatMonitoringPage: true },
|
||||
});
|
||||
|
||||
expect(findViewIncidentBtn().exists()).toBe(true);
|
||||
expect(findViewIncidentBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, iid));
|
||||
expect(findCreateIncidentBtn().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create incident from alert', () => {
|
||||
it('should display "View incident" button that links the incident page when incident exists', () => {
|
||||
const iid = '3';
|
||||
|
|
|
@ -60,4 +60,30 @@ describe('content_editor', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders footnote ids alongside the footnote definition', async () => {
|
||||
buildWrapper();
|
||||
|
||||
renderMarkdown.mockResolvedValue(`
|
||||
<p data-sourcepos="3:1-3:56" dir="auto">
|
||||
This reference tag is a mix of letters and numbers. <sup class="footnote-ref"><a href="#fn-footnote-2717" id="fnref-footnote-2717" data-footnote-ref="">2</a></sup>
|
||||
</p>
|
||||
<section class="footnotes" data-footnotes>
|
||||
<ol>
|
||||
<li id="fn-footnote-2717">
|
||||
<p data-sourcepos="6:7-6:31">This is another footnote. <a href="#fnref-footnote-2717" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
`);
|
||||
|
||||
await contentEditorService.setSerializedContent(`
|
||||
This reference tag is a mix of letters and numbers [^footnote].
|
||||
|
||||
[^footnote]: This is another footnote.
|
||||
`);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('footnote: This is another footnote');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,7 +51,7 @@ RSpec.describe StorageHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe "storage_enforcement_banner" do
|
||||
describe "storage_enforcement_banner", :saas do
|
||||
let_it_be_with_refind(:current_user) { create(:user) }
|
||||
let_it_be(:free_group) { create(:group) }
|
||||
let_it_be(:paid_group) { create(:group) }
|
||||
|
@ -60,8 +60,9 @@ RSpec.describe StorageHelper do
|
|||
allow(helper).to receive(:can?).with(current_user, :admin_namespace, free_group).and_return(true)
|
||||
allow(helper).to receive(:can?).with(current_user, :admin_namespace, paid_group).and_return(true)
|
||||
allow(helper).to receive(:current_user) { current_user }
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
allow(paid_group).to receive(:paid?).and_return(true)
|
||||
|
||||
stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
|
||||
end
|
||||
|
||||
describe "#storage_enforcement_banner_info" do
|
||||
|
@ -108,6 +109,28 @@ RSpec.describe StorageHelper do
|
|||
expect(helper.storage_enforcement_banner_info(free_group)[:text]).to eql("From #{storage_enforcement_date} storage limits will apply to this namespace. You are currently using 100 KB of namespace storage. View and manage your usage from <strong>Group settings > Usage quotas</strong>.")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the given group is a sub-group' do
|
||||
let_it_be(:sub_group) { build(:group) }
|
||||
|
||||
before do
|
||||
allow(sub_group).to receive(:root_ancestor).and_return(free_group)
|
||||
end
|
||||
|
||||
it 'returns the banner hash' do
|
||||
expect(helper.storage_enforcement_banner_info(sub_group).keys).to match_array(%i(text variant callouts_feature_name callouts_path learn_more_link))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the :storage_banner_bypass_date_check is enabled', :freeze_time do
|
||||
before do
|
||||
stub_feature_flags(namespace_storage_limit_bypass_date_check: true)
|
||||
end
|
||||
|
||||
it 'returns the enforcement info' do
|
||||
expect(helper.storage_enforcement_banner_info(free_group)[:text]).to include("From #{Date.current} storage limits will apply to this namespace.")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2258,10 +2258,24 @@ RSpec.describe Namespace do
|
|||
describe 'storage_enforcement_date' do
|
||||
let_it_be(:namespace) { create(:group) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(namespace_storage_limit_bypass_date_check: false)
|
||||
end
|
||||
|
||||
# Date TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
|
||||
it 'returns false' do
|
||||
it 'returns nil' do
|
||||
expect(namespace.storage_enforcement_date).to be(nil)
|
||||
end
|
||||
|
||||
context 'when :storage_banner_bypass_date_check is enabled' do
|
||||
before do
|
||||
stub_feature_flags(namespace_storage_limit_bypass_date_check: true)
|
||||
end
|
||||
|
||||
it 'returns the current date', :freeze_time do
|
||||
expect(namespace.storage_enforcement_date).to eq(Date.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'serialization' do
|
||||
|
|
|
@ -19,8 +19,7 @@ RSpec.shared_examples 'rejecting tags destruction for an importing repository on
|
|||
expect(find('.modal .modal-title')).to have_content _('Remove tag')
|
||||
find('.modal .modal-footer .btn-danger').click
|
||||
|
||||
alert_body = find('.gl-alert-body')
|
||||
expect(alert_body).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
|
||||
expect(alert_body).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
|
||||
expect(page).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
|
||||
expect(page).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkerAttributes do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:worker) do
|
||||
Class.new do
|
||||
def self.name
|
||||
|
@ -13,21 +15,64 @@ RSpec.describe WorkerAttributes do
|
|||
end
|
||||
end
|
||||
|
||||
let(:child_worker) do
|
||||
Class.new(worker) do
|
||||
def self.name
|
||||
"TestChildworker"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'class attributes' do
|
||||
# rubocop: disable Layout/LineLength
|
||||
where(:getter, :setter, :default, :values, :expected) do
|
||||
:get_feature_category | :feature_category | nil | [:foo] | :foo
|
||||
:get_urgency | :urgency | :low | [:high] | :high
|
||||
:get_data_consistency | :data_consistency | :always | [:sticky] | :sticky
|
||||
:get_worker_resource_boundary | :worker_resource_boundary | :unknown | [:cpu] | :cpu
|
||||
:get_weight | :weight | 1 | [3] | 3
|
||||
:get_tags | :tags | [] | [:foo, :bar] | [:foo, :bar]
|
||||
:get_deduplicate_strategy | :deduplicate | :until_executing | [:none] | :none
|
||||
:get_deduplication_options | :deduplicate | {} | [:none, including_scheduled: true] | { including_scheduled: true }
|
||||
:worker_has_external_dependencies? | :worker_has_external_dependencies! | false | [] | true
|
||||
:idempotent? | :idempotent! | false | [] | true
|
||||
:big_payload? | :big_payload! | false | [] | true
|
||||
end
|
||||
# rubocop: enable Layout/LineLength
|
||||
|
||||
with_them do
|
||||
context 'when the attribute is set' do
|
||||
before do
|
||||
worker.public_send(setter, *values)
|
||||
end
|
||||
|
||||
it 'returns the expected value' do
|
||||
expect(worker.public_send(getter)).to eq(expected)
|
||||
expect(child_worker.public_send(getter)).to eq(expected)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the attribute is not set' do
|
||||
it 'returns the default value' do
|
||||
expect(worker.public_send(getter)).to eq(default)
|
||||
expect(child_worker.public_send(getter)).to eq(default)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the attribute is set in the child worker' do
|
||||
before do
|
||||
child_worker.public_send(setter, *values)
|
||||
end
|
||||
|
||||
it 'returns the default value for the parent, and the expected value for the child' do
|
||||
expect(worker.public_send(getter)).to eq(default)
|
||||
expect(child_worker.public_send(getter)).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.data_consistency' do
|
||||
context 'with valid data_consistency' do
|
||||
it 'returns correct data_consistency' do
|
||||
worker.data_consistency(:sticky)
|
||||
|
||||
expect(worker.get_data_consistency).to eq(:sticky)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data_consistency is not provided' do
|
||||
it 'defaults to :always' do
|
||||
expect(worker.get_data_consistency).to eq(:always)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid data_consistency' do
|
||||
it 'raise exception' do
|
||||
expect { worker.data_consistency(:invalid) }
|
||||
|
@ -45,36 +90,12 @@ RSpec.describe WorkerAttributes do
|
|||
it 'returns correct feature flag value' do
|
||||
worker.data_consistency(:sticky, feature_flag: :test_feature_flag)
|
||||
|
||||
expect(worker.get_data_consistency_feature_flag_enabled?).not_to be_truthy
|
||||
expect(worker.get_data_consistency_feature_flag_enabled?).not_to be(true)
|
||||
expect(child_worker.get_data_consistency_feature_flag_enabled?).not_to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.idempotent?' do
|
||||
subject(:idempotent?) { worker.idempotent? }
|
||||
|
||||
context 'when the worker is idempotent' do
|
||||
before do
|
||||
worker.idempotent!
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when the worker is not idempotent' do
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.deduplicate' do
|
||||
it 'sets deduplication_strategy and deduplication_options' do
|
||||
worker.deduplicate(:until_executing, including_scheduled: true)
|
||||
|
||||
expect(worker.send(:class_attributes)[:deduplication_strategy]).to eq(:until_executing)
|
||||
expect(worker.send(:class_attributes)[:deduplication_options]).to eq(including_scheduled: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#deduplication_enabled?' do
|
||||
subject(:deduplication_enabled?) { worker.deduplication_enabled? }
|
||||
|
||||
|
@ -83,7 +104,10 @@ RSpec.describe WorkerAttributes do
|
|||
worker.deduplicate(:until_executing)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
it 'returns true' do
|
||||
expect(worker.deduplication_enabled?).to be(true)
|
||||
expect(child_worker.deduplication_enabled?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is set' do
|
||||
|
@ -99,7 +123,10 @@ RSpec.describe WorkerAttributes do
|
|||
stub_feature_flags(my_feature_flag: true)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
it 'returns true' do
|
||||
expect(worker.deduplication_enabled?).to be(true)
|
||||
expect(child_worker.deduplication_enabled?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the FF is disabled' do
|
||||
|
@ -107,7 +134,10 @@ RSpec.describe WorkerAttributes do
|
|||
stub_feature_flags(my_feature_flag: false)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
it 'returns false' do
|
||||
expect(worker.deduplication_enabled?).to be(false)
|
||||
expect(child_worker.deduplication_enabled?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue