Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-08 15:08:15 +00:00
parent cdda3d117c
commit e18e22ce4c
53 changed files with 1287 additions and 1001 deletions

View File

@ -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>

View File

@ -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);
},
});

View File

@ -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];
},
});

View File

@ -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 }) {

View File

@ -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 = {

View File

@ -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"

View File

@ -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 }}

View File

@ -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>

View File

@ -63,6 +63,8 @@ export default {
},
},
data() {
if (!this.iid) return { state: this.initialState };
if (this.initialState) {
badgeState.state = this.initialState;
}

View File

@ -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 />

View File

@ -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}',
),

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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`,

View File

@ -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>

View File

@ -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"
/>

View File

@ -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'),
},
},
};

View File

@ -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({

View File

@ -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 &gt; 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

View File

@ -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

View File

@ -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'

View File

@ -7,6 +7,5 @@
%h1.page-title
= _("Schedule a new pipeline")
%hr
= render "form"

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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`,

View File

@ -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`. |

View File

@ -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

View File

@ -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 ""

10
scripts/lint-yaml.sh Executable file
View File

@ -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 "$@"

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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>

View File

@ -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');
});
});

View File

@ -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(),
);
});

View File

@ -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', () => {

View File

@ -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}"

View File

@ -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;

View File

@ -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);
});
});
});

View File

@ -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);
});
});

View File

@ -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', () => {

View File

@ -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';

View File

@ -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');
});
});

View File

@ -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 &gt; 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

View File

@ -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

View File

@ -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

View File

@ -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