Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
74ecf758e3
commit
7f98cf51aa
|
|
@ -264,7 +264,7 @@
|
|||
|
||||
.zoekt-services:
|
||||
services:
|
||||
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.1
|
||||
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.2
|
||||
alias: zoekt-ci-image
|
||||
|
||||
.use-pg12:
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ LineHighlighter.prototype.highlightRange = function (range) {
|
|||
const results = [];
|
||||
const ref = range[0] <= range[1] ? range : range.reverse();
|
||||
|
||||
for (let lineNumber = ref[0]; lineNumber <= ref[1]; lineNumber += 1) {
|
||||
for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
|
||||
results.push(this.highlightLine(lineNumber));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { getParameterByName } from '~/lib/utils/url_utility';
|
|||
import {
|
||||
OPERATORS_IS,
|
||||
OPERATORS_IS_NOT_OR,
|
||||
OPERATORS_AFTER_BEFORE,
|
||||
TOKEN_TITLE_ASSIGNEE,
|
||||
TOKEN_TITLE_AUTHOR,
|
||||
TOKEN_TITLE_CONFIDENTIAL,
|
||||
|
|
@ -44,6 +45,8 @@ import {
|
|||
TOKEN_TITLE_MY_REACTION,
|
||||
TOKEN_TITLE_SEARCH_WITHIN,
|
||||
TOKEN_TITLE_TYPE,
|
||||
TOKEN_TITLE_CREATED,
|
||||
TOKEN_TITLE_CLOSED,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
|
|
@ -52,6 +55,8 @@ import {
|
|||
TOKEN_TYPE_MY_REACTION,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_TYPE,
|
||||
TOKEN_TYPE_CREATED,
|
||||
TOKEN_TYPE_CLOSED,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
|
||||
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
|
||||
|
|
@ -63,6 +68,7 @@ const EmojiToken = () =>
|
|||
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
|
||||
const LabelToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
|
||||
const DateToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/date_token.vue');
|
||||
const MilestoneToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
|
||||
|
||||
|
|
@ -89,6 +95,7 @@ export default {
|
|||
'emptyStateWithoutFilterSvgPath',
|
||||
'hasBlockedIssuesFeature',
|
||||
'hasIssuableHealthStatusFeature',
|
||||
'hasIssueDateFilterFeature',
|
||||
'hasIssueWeightsFeature',
|
||||
'hasScopedLabelsFeature',
|
||||
'initialSort',
|
||||
|
|
@ -318,6 +325,24 @@ export default {
|
|||
fetchEmojis: this.fetchEmojis,
|
||||
recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-my_reaction',
|
||||
});
|
||||
|
||||
if (this.hasIssueDateFilterFeature) {
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_CREATED,
|
||||
title: TOKEN_TITLE_CREATED,
|
||||
icon: 'history',
|
||||
token: DateToken,
|
||||
operators: OPERATORS_AFTER_BEFORE,
|
||||
});
|
||||
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_CLOSED,
|
||||
title: TOKEN_TITLE_CLOSED,
|
||||
icon: 'history',
|
||||
token: DateToken,
|
||||
operators: OPERATORS_AFTER_BEFORE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tokens.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ query getDashboardIssues(
|
|||
$beforeCursor: String
|
||||
$firstPageSize: Int
|
||||
$lastPageSize: Int
|
||||
$createdAfter: Time
|
||||
$createdBefore: Time
|
||||
$closedAfter: Time
|
||||
$closedBefore: Time
|
||||
) {
|
||||
issues(
|
||||
search: $search
|
||||
|
|
@ -44,6 +48,10 @@ query getDashboardIssues(
|
|||
before: $beforeCursor
|
||||
first: $firstPageSize
|
||||
last: $lastPageSize
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) @persist {
|
||||
nodes {
|
||||
__persist
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ query getDashboardIssuesCount(
|
|||
$in: [IssuableSearchableField!]
|
||||
$not: NegatedIssueFilterInput
|
||||
$or: UnionedIssueFilterInput
|
||||
$createdAfter: Time
|
||||
$createdBefore: Time
|
||||
$closedAfter: Time
|
||||
$closedBefore: Time
|
||||
) {
|
||||
openedIssues: issues(
|
||||
state: opened
|
||||
|
|
@ -28,6 +32,10 @@ query getDashboardIssuesCount(
|
|||
in: $in
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -46,6 +54,10 @@ query getDashboardIssuesCount(
|
|||
in: $in
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -64,6 +76,10 @@ query getDashboardIssuesCount(
|
|||
in: $in
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
OPERATORS_IS,
|
||||
OPERATORS_IS_NOT,
|
||||
OPERATORS_IS_NOT_OR,
|
||||
OPERATORS_AFTER_BEFORE,
|
||||
TOKEN_TITLE_ASSIGNEE,
|
||||
TOKEN_TITLE_AUTHOR,
|
||||
TOKEN_TITLE_CONFIDENTIAL,
|
||||
|
|
@ -51,6 +52,8 @@ import {
|
|||
TOKEN_TITLE_RELEASE,
|
||||
TOKEN_TITLE_SEARCH_WITHIN,
|
||||
TOKEN_TITLE_TYPE,
|
||||
TOKEN_TITLE_CREATED,
|
||||
TOKEN_TITLE_CLOSED,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
|
|
@ -62,6 +65,8 @@ import {
|
|||
TOKEN_TYPE_RELEASE,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_TYPE,
|
||||
TOKEN_TYPE_CREATED,
|
||||
TOKEN_TYPE_CLOSED,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
|
||||
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
|
||||
|
|
@ -125,6 +130,7 @@ const CrmContactToken = () =>
|
|||
import('~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue');
|
||||
const CrmOrganizationToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue');
|
||||
const DateToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/date_token.vue');
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
|
|
@ -166,6 +172,7 @@ export default {
|
|||
'hasAnyProjects',
|
||||
'hasBlockedIssuesFeature',
|
||||
'hasIssuableHealthStatusFeature',
|
||||
'hasIssueDateFilterFeature',
|
||||
'hasIssueWeightsFeature',
|
||||
'hasScopedLabelsFeature',
|
||||
'initialEmail',
|
||||
|
|
@ -460,6 +467,24 @@ export default {
|
|||
{ icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo },
|
||||
],
|
||||
});
|
||||
|
||||
if (this.hasIssueDateFilterFeature) {
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_CREATED,
|
||||
title: TOKEN_TITLE_CREATED,
|
||||
icon: 'history',
|
||||
token: DateToken,
|
||||
operators: OPERATORS_AFTER_BEFORE,
|
||||
});
|
||||
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_CLOSED,
|
||||
title: TOKEN_TITLE_CLOSED,
|
||||
icon: 'history',
|
||||
token: DateToken,
|
||||
operators: OPERATORS_AFTER_BEFORE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.canReadCrmContact) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
OPERATOR_IS,
|
||||
OPERATOR_NOT,
|
||||
OPERATOR_OR,
|
||||
OPERATOR_AFTER,
|
||||
OPERATOR_BEFORE,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
|
|
@ -24,6 +26,8 @@ import {
|
|||
TOKEN_TYPE_TYPE,
|
||||
TOKEN_TYPE_WEIGHT,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_CREATED,
|
||||
TOKEN_TYPE_CLOSED,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import {
|
||||
WORK_ITEM_TYPE_ENUM_INCIDENT,
|
||||
|
|
@ -416,4 +420,32 @@ export const filtersMap = {
|
|||
},
|
||||
},
|
||||
},
|
||||
[TOKEN_TYPE_CREATED]: {
|
||||
[API_PARAM]: {
|
||||
[NORMAL_FILTER]: 'createdBefore',
|
||||
[ALTERNATIVE_FILTER]: 'createdAfter',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_AFTER]: {
|
||||
[ALTERNATIVE_FILTER]: 'created_after',
|
||||
},
|
||||
[OPERATOR_BEFORE]: {
|
||||
[NORMAL_FILTER]: 'created_before',
|
||||
},
|
||||
},
|
||||
},
|
||||
[TOKEN_TYPE_CLOSED]: {
|
||||
[API_PARAM]: {
|
||||
[NORMAL_FILTER]: 'closedBefore',
|
||||
[ALTERNATIVE_FILTER]: 'closedAfter',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_AFTER]: {
|
||||
[ALTERNATIVE_FILTER]: 'closed_after',
|
||||
},
|
||||
[OPERATOR_BEFORE]: {
|
||||
[NORMAL_FILTER]: 'closed_before',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ query getIssues(
|
|||
$afterCursor: String
|
||||
$firstPageSize: Int
|
||||
$lastPageSize: Int
|
||||
$createdAfter: Time
|
||||
$createdBefore: Time
|
||||
$closedAfter: Time
|
||||
$closedBefore: Time
|
||||
) {
|
||||
group(fullPath: $fullPath) @skip(if: $isProject) @persist {
|
||||
id
|
||||
|
|
@ -57,6 +61,10 @@ query getIssues(
|
|||
after: $afterCursor
|
||||
first: $firstPageSize
|
||||
last: $lastPageSize
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
__persist
|
||||
pageInfo {
|
||||
|
|
@ -96,6 +104,10 @@ query getIssues(
|
|||
after: $afterCursor
|
||||
first: $firstPageSize
|
||||
last: $lastPageSize
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
__persist
|
||||
pageInfo {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ query getIssuesCount(
|
|||
$crmOrganizationId: String
|
||||
$not: NegatedIssueFilterInput
|
||||
$or: UnionedIssueFilterInput
|
||||
$createdAfter: Time
|
||||
$createdBefore: Time
|
||||
$closedAfter: Time
|
||||
$closedBefore: Time
|
||||
) {
|
||||
group(fullPath: $fullPath) @skip(if: $isProject) {
|
||||
id
|
||||
|
|
@ -39,6 +43,10 @@ query getIssuesCount(
|
|||
crmOrganizationId: $crmOrganizationId
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -60,6 +68,10 @@ query getIssuesCount(
|
|||
crmOrganizationId: $crmOrganizationId
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -81,6 +93,10 @@ query getIssuesCount(
|
|||
crmOrganizationId: $crmOrganizationId
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -106,6 +122,10 @@ query getIssuesCount(
|
|||
crmOrganizationId: $crmOrganizationId
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -128,6 +148,10 @@ query getIssuesCount(
|
|||
crmOrganizationId: $crmOrganizationId
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -150,6 +174,10 @@ query getIssuesCount(
|
|||
crmOrganizationId: $crmOrganizationId
|
||||
not: $not
|
||||
or: $or
|
||||
createdAfter: $createdAfter
|
||||
createdBefore: $createdBefore
|
||||
closedAfter: $closedAfter
|
||||
closedBefore: $closedBefore
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
FILTERED_SEARCH_TERM,
|
||||
OPERATOR_NOT,
|
||||
OPERATOR_OR,
|
||||
OPERATOR_AFTER,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
|
|
@ -246,8 +247,9 @@ export const isSpecialFilter = (type, data) => {
|
|||
const getFilterType = ({ type, value: { data, operator } }) => {
|
||||
const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR;
|
||||
const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR;
|
||||
const isAfter = operator === OPERATOR_AFTER;
|
||||
|
||||
if (isUnionedAuthor || isUnionedLabel) {
|
||||
if (isUnionedAuthor || isUnionedLabel || isAfter) {
|
||||
return ALTERNATIVE_FILTER;
|
||||
}
|
||||
if (isSpecialFilter(type, data)) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export default {
|
|||
isExpanded: Boolean(this.expanded || this.item.is_active),
|
||||
isMouseOverSection: false,
|
||||
isMouseOverFlyout: false,
|
||||
keepFlyoutClosed: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -77,12 +78,17 @@ export default {
|
|||
watch: {
|
||||
isExpanded(newIsExpanded) {
|
||||
this.$emit('collapse-toggle', newIsExpanded);
|
||||
this.keepFlyoutClosed = !this.newIsExpanded;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePointerover(e) {
|
||||
this.isMouseOverSection = e.pointerType === 'mouse';
|
||||
},
|
||||
handlePointerleave() {
|
||||
this.isMouseOverSection = false;
|
||||
this.keepFlyoutClosed = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -99,7 +105,7 @@ export default {
|
|||
v-bind="buttonProps"
|
||||
@click="isExpanded = !isExpanded"
|
||||
@pointerover="handlePointerover"
|
||||
@pointerleave="isMouseOverSection = false"
|
||||
@pointerleave="handlePointerleave"
|
||||
>
|
||||
<span
|
||||
:class="[isActive ? 'gl-bg-blue-500' : 'gl-bg-transparent']"
|
||||
|
|
@ -124,7 +130,7 @@ export default {
|
|||
|
||||
<flyout-menu
|
||||
v-if="hasFlyout"
|
||||
v-show="isMouseOver && !isExpanded"
|
||||
v-show="isMouseOver && !isExpanded && !keepFlyoutClosed"
|
||||
:target-id="`menu-section-button-${itemId}`"
|
||||
:items="item.items"
|
||||
@mouseover="isMouseOverFlyout = true"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
<script>
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
|
||||
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
|
||||
import { i18n, codeQualityPrefixes } from './constants';
|
||||
|
||||
const translations = i18n;
|
||||
|
||||
export default {
|
||||
name: 'WidgetCodeQuality',
|
||||
components: {
|
||||
MrWidget,
|
||||
},
|
||||
i18n: translations,
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pollingFinished: false,
|
||||
hasError: false,
|
||||
collapsedData: {},
|
||||
poll: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
summary() {
|
||||
const { new_errors, resolved_errors } = this.collapsedData;
|
||||
|
||||
if (!this.pollingFinished) {
|
||||
return { title: i18n.loading };
|
||||
} else if (this.hasError) {
|
||||
return { title: i18n.error };
|
||||
} else if (
|
||||
this.collapsedData?.new_errors?.length >= 1 &&
|
||||
this.collapsedData?.resolved_errors?.length >= 1
|
||||
) {
|
||||
return {
|
||||
title: i18n.improvementAndDegradationCopy(
|
||||
i18n.findings(resolved_errors, codeQualityPrefixes.fixed),
|
||||
i18n.findings(new_errors, codeQualityPrefixes.new),
|
||||
),
|
||||
};
|
||||
} else if (this.collapsedData?.resolved_errors?.length >= 1) {
|
||||
return {
|
||||
title: i18n.singularCopy(i18n.findings(resolved_errors, codeQualityPrefixes.fixed)),
|
||||
};
|
||||
} else if (this.collapsedData?.new_errors?.length >= 1) {
|
||||
return { title: i18n.singularCopy(i18n.findings(new_errors, codeQualityPrefixes.new)) };
|
||||
}
|
||||
return { title: i18n.noChanges };
|
||||
},
|
||||
expandedData() {
|
||||
const fullData = [];
|
||||
this.collapsedData?.new_errors?.forEach((e) => {
|
||||
fullData.push({
|
||||
text: e.check_name
|
||||
? `${capitalizeFirstCharacter(e.severity)} - ${e.check_name} - ${e.description}`
|
||||
: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
|
||||
link: {
|
||||
href: e.web_url,
|
||||
text: `${i18n.prependText} ${e.file_path}:${e.line}`,
|
||||
},
|
||||
icon: {
|
||||
name: SEVERITY_ICONS_MR_WIDGET[e.severity],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.collapsedData?.resolved_errors?.forEach((e) => {
|
||||
fullData.push({
|
||||
text: e.check_name
|
||||
? `${capitalizeFirstCharacter(e.severity)} - ${e.check_name} - ${e.description}`
|
||||
: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
|
||||
supportingText: `${i18n.prependText} ${e.file_path}:${e.line}`,
|
||||
icon: {
|
||||
name: SEVERITY_ICONS_MR_WIDGET[e.severity],
|
||||
},
|
||||
badge: {
|
||||
variant: 'neutral',
|
||||
text: i18n.fixed,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return fullData;
|
||||
},
|
||||
statusIcon() {
|
||||
if (this.collapsedData?.new_errors?.length >= 1) {
|
||||
return EXTENSION_ICONS.warning;
|
||||
} else if (this.collapsedData?.resolved_errors?.length >= 1) {
|
||||
return EXTENSION_ICONS.success;
|
||||
}
|
||||
return EXTENSION_ICONS.neutral;
|
||||
},
|
||||
shouldCollapse() {
|
||||
const { new_errors: newErrors, resolved_errors: resolvedErrors } = this.collapsedData;
|
||||
|
||||
if ((newErrors?.length === 0 && resolvedErrors?.length === 0) || this.hasError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
apiCodeQualityPath() {
|
||||
return this.mr.codequalityReportsPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setCollapsedError(err) {
|
||||
this.hasError = true;
|
||||
|
||||
Sentry.captureException(err);
|
||||
},
|
||||
fetchCodeQuality() {
|
||||
return axios
|
||||
.get(this.apiCodeQualityPath)
|
||||
.then(({ data, headers = {}, status }) => {
|
||||
if (status === HTTP_STATUS_OK) {
|
||||
this.pollingFinished = true;
|
||||
}
|
||||
if (data) {
|
||||
this.collapsedData = data;
|
||||
}
|
||||
return {
|
||||
headers,
|
||||
status,
|
||||
data,
|
||||
};
|
||||
})
|
||||
.catch((e) => {
|
||||
return this.setCollapsedError(e);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<mr-widget
|
||||
:fetch-collapsed-data="fetchCodeQuality"
|
||||
:error-text="$options.i18n.error"
|
||||
:has-error="hasError"
|
||||
:content="expandedData"
|
||||
:loading-text="$options.i18n.loading"
|
||||
data-testid="new-cq-widget"
|
||||
:summary="summary"
|
||||
:widget-name="$options.name"
|
||||
:status-icon-name="statusIcon"
|
||||
:is-collapsible="shouldCollapse"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -56,7 +56,6 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
|
|||
import getStateQuery from './queries/get_state.query.graphql';
|
||||
import getStateSubscription from './queries/get_state.subscription.graphql';
|
||||
import accessibilityExtension from './extensions/accessibility';
|
||||
import codeQualityExtension from './extensions/code_quality';
|
||||
import testReportExtension from './extensions/test_report';
|
||||
import ReportWidgetContainer from './components/report_widget_container.vue';
|
||||
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
|
||||
|
|
@ -215,9 +214,6 @@ export default {
|
|||
|
||||
return !hasCI && mergeRequestAddCiConfigPath && !isDismissedSuggestPipeline;
|
||||
},
|
||||
shouldRenderCodeQuality() {
|
||||
return this.mr?.codequalityReportsPath;
|
||||
},
|
||||
shouldRenderCollaborationStatus() {
|
||||
return this.mr.allowCollaboration && this.mr.isOpen;
|
||||
},
|
||||
|
|
@ -280,11 +276,6 @@ export default {
|
|||
this.initPostMergeDeploymentsPolling();
|
||||
}
|
||||
},
|
||||
shouldRenderCodeQuality(newVal) {
|
||||
if (newVal) {
|
||||
this.registerCodeQualityExtension();
|
||||
}
|
||||
},
|
||||
shouldShowAccessibilityReport(newVal) {
|
||||
if (newVal) {
|
||||
this.registerAccessibilityExtension();
|
||||
|
|
@ -534,11 +525,6 @@ export default {
|
|||
registerExtension(accessibilityExtension);
|
||||
}
|
||||
},
|
||||
registerCodeQualityExtension() {
|
||||
if (this.shouldRenderCodeQuality) {
|
||||
registerExtension(codeQualityExtension);
|
||||
}
|
||||
},
|
||||
registerTestReportExtension() {
|
||||
if (this.shouldRenderTestReport) {
|
||||
registerExtension(testReportExtension);
|
||||
|
|
|
|||
|
|
@ -17,12 +17,19 @@ export const OPERATOR_NOT = '!=';
|
|||
export const OPERATOR_NOT_TEXT = __('is not one of');
|
||||
export const OPERATOR_OR = '||';
|
||||
export const OPERATOR_OR_TEXT = __('is one of');
|
||||
export const OPERATOR_AFTER = '≥';
|
||||
export const OPERATOR_AFTER_TEXT = __('on or after');
|
||||
export const OPERATOR_BEFORE = '<';
|
||||
export const OPERATOR_BEFORE_TEXT = __('before');
|
||||
|
||||
export const OPERATORS_IS = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
|
||||
export const OPERATORS_NOT = [{ value: OPERATOR_NOT, description: OPERATOR_NOT_TEXT }];
|
||||
export const OPERATORS_OR = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
|
||||
export const OPERATORS_AFTER = [{ value: OPERATOR_AFTER, description: OPERATOR_AFTER_TEXT }];
|
||||
export const OPERATORS_BEFORE = [{ value: OPERATOR_BEFORE, description: OPERATOR_BEFORE_TEXT }];
|
||||
export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
|
||||
export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
|
||||
export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE];
|
||||
|
||||
export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
|
||||
export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
|
||||
|
|
@ -62,6 +69,8 @@ export const TOKEN_TITLE_STATUS = __('Status');
|
|||
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
|
||||
export const TOKEN_TITLE_TYPE = __('Type');
|
||||
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
|
||||
export const TOKEN_TITLE_CREATED = __('Created date');
|
||||
export const TOKEN_TITLE_CLOSED = __('Closed date');
|
||||
|
||||
export const TOKEN_TYPE_APPROVED_BY = 'approved-by';
|
||||
export const TOKEN_TYPE_ASSIGNEE = 'assignee';
|
||||
|
|
@ -88,3 +97,5 @@ export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
|
|||
export const TOKEN_TYPE_TYPE = 'type';
|
||||
export const TOKEN_TYPE_WEIGHT = 'weight';
|
||||
export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
|
||||
export const TOKEN_TYPE_CREATED = 'created';
|
||||
export const TOKEN_TYPE_CLOSED = 'closed';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<script>
|
||||
import { GlDatepicker, GlFilteredSearchToken } from '@gitlab/ui';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDatepicker,
|
||||
GlFilteredSearchToken,
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedDate: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectValue(value) {
|
||||
this.selectedDate = formatDate(value, 'yyyy-mm-dd');
|
||||
},
|
||||
close(submitValue) {
|
||||
if (this.selectedDate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitValue(this.selectedDate);
|
||||
},
|
||||
handle() {
|
||||
const listeners = { ...this.$listeners };
|
||||
// If we don't remove this, clicking the month/year in the datepicker will deactivate
|
||||
delete listeners.deactivate;
|
||||
return listeners;
|
||||
},
|
||||
},
|
||||
dataSegmentInputAttributes: {
|
||||
id: 'glfs-datepicker',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-filtered-search-token
|
||||
:config="config"
|
||||
:value="value"
|
||||
:active="active"
|
||||
:data-segment-input-attributes="$options.dataSegmentInputAttributes"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
v-on="handle()"
|
||||
>
|
||||
<template #before-data-segment-input="{ submitValue }">
|
||||
<gl-datepicker
|
||||
class="gl-display-none!"
|
||||
target="#glfs-datepicker"
|
||||
:container="null"
|
||||
@input="selectValue($event)"
|
||||
@close="close(submitValue)"
|
||||
/>
|
||||
</template>
|
||||
</gl-filtered-search-token>
|
||||
</template>
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '@gitlab/ui';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import Tracking from '~/tracking';
|
||||
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
|
||||
import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
|
||||
|
||||
|
|
@ -28,6 +29,8 @@ export const i18n = {
|
|||
toggleText: __('Edit'),
|
||||
};
|
||||
|
||||
const TRACKING_ACTION_NAME = 'click_consolidated_edit';
|
||||
|
||||
export default {
|
||||
name: 'CEWebIdeLink',
|
||||
components: {
|
||||
|
|
@ -40,6 +43,7 @@ export default {
|
|||
ConfirmForkModal,
|
||||
},
|
||||
i18n,
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
isFork: {
|
||||
type: Boolean,
|
||||
|
|
@ -181,9 +185,9 @@ export default {
|
|||
key: KEY_EDIT,
|
||||
text: __('Edit single file'),
|
||||
secondaryText: __('Edit this file only.'),
|
||||
attrs: {
|
||||
'data-track-action': 'click_consolidated_edit',
|
||||
'data-track-label': 'edit',
|
||||
tracking: {
|
||||
action: TRACKING_ACTION_NAME,
|
||||
label: 'single_file',
|
||||
},
|
||||
...handleOptions,
|
||||
};
|
||||
|
|
@ -223,9 +227,9 @@ export default {
|
|||
key: KEY_WEB_IDE,
|
||||
text: this.webIdeActionText,
|
||||
secondaryText: this.$options.i18n.webIdeText,
|
||||
attrs: {
|
||||
'data-track-action': 'click_consolidated_edit_ide',
|
||||
'data-track-label': 'web_ide',
|
||||
tracking: {
|
||||
action: TRACKING_ACTION_NAME,
|
||||
label: 'web_ide',
|
||||
},
|
||||
...handleOptions,
|
||||
};
|
||||
|
|
@ -253,9 +257,9 @@ export default {
|
|||
text: __('Edit in pipeline editor'),
|
||||
secondaryText,
|
||||
href: this.pipelineEditorUrl,
|
||||
attrs: {
|
||||
'data-track-action': 'click_consolidated_pipeline_editor',
|
||||
'data-track-label': 'pipeline_editor',
|
||||
tracking: {
|
||||
action: TRACKING_ACTION_NAME,
|
||||
label: 'pipeline_editor',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -277,6 +281,10 @@ export default {
|
|||
key: KEY_GITPOD,
|
||||
text: this.gitpodActionText,
|
||||
secondaryText,
|
||||
tracking: {
|
||||
action: TRACKING_ACTION_NAME,
|
||||
label: 'gitpod',
|
||||
},
|
||||
...handleOptions,
|
||||
};
|
||||
},
|
||||
|
|
@ -311,6 +319,7 @@ export default {
|
|||
this[dataKey] = true;
|
||||
},
|
||||
executeAction(action) {
|
||||
this.track(action.tracking.action, { label: action.tracking.label });
|
||||
action.handle?.();
|
||||
},
|
||||
},
|
||||
|
|
@ -335,7 +344,6 @@ export default {
|
|||
<gl-disclosure-dropdown-item
|
||||
v-for="action in actions"
|
||||
:key="action.key"
|
||||
v-bind="action.attrs"
|
||||
:item="action"
|
||||
:data-qa-selector="`${action.key}_menu_item`"
|
||||
@action="executeAction(action)"
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { STATE_OPEN, STATE_CLOSED } from '../constants';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
status: __('Status'),
|
||||
},
|
||||
states: [
|
||||
{
|
||||
value: STATE_OPEN,
|
||||
text: __('Open'),
|
||||
},
|
||||
{
|
||||
value: STATE_CLOSED,
|
||||
text: __('Closed'),
|
||||
},
|
||||
],
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlFormSelect,
|
||||
},
|
||||
props: {
|
||||
state: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentState() {
|
||||
return this.$options.states[this.state];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setState(newState) {
|
||||
if (newState !== this.state) {
|
||||
this.$emit('changed', newState);
|
||||
}
|
||||
},
|
||||
},
|
||||
labelId: 'work-item-state-select',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.status"
|
||||
:label-for="$options.labelId"
|
||||
label-cols="3"
|
||||
label-cols-lg="2"
|
||||
label-class="gl-pb-0! gl-overflow-wrap-break work-item-field-label"
|
||||
class="gl-align-items-center"
|
||||
>
|
||||
<gl-form-select
|
||||
:id="$options.labelId"
|
||||
:value="state"
|
||||
:options="$options.states"
|
||||
:disabled="disabled"
|
||||
data-testid="work-item-state-select"
|
||||
class="hide-unfocused-input-decoration work-item-field-value gl-w-auto gl-pl-4 gl-my-1"
|
||||
:class="{ 'gl-bg-transparent! gl-cursor-text!': disabled }"
|
||||
@change="setState"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
|
@ -265,6 +265,7 @@ export default {
|
|||
:comment-button-text="commentButtonText"
|
||||
@submitForm="updateWorkItem"
|
||||
@cancelEditing="cancelEditing"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
<script>
|
||||
import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { s__, __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
sprintfWorkItem,
|
||||
STATE_OPEN,
|
||||
STATE_EVENT_REOPEN,
|
||||
STATE_EVENT_CLOSE,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
i18n,
|
||||
} from '~/work_items/constants';
|
||||
import { STATE_OPEN, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
|
||||
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
|
|
@ -25,6 +16,7 @@ export default {
|
|||
'Notes|Internal notes are only visible to members with the role of Reporter or higher',
|
||||
),
|
||||
addInternalNote: __('Add internal note'),
|
||||
cancelButtonText: __('Cancel'),
|
||||
},
|
||||
constantOptions: {
|
||||
markdownDocsPath: helpPagePath('user/markdown'),
|
||||
|
|
@ -34,6 +26,7 @@ export default {
|
|||
MarkdownEditor,
|
||||
GlFormCheckbox,
|
||||
GlIcon,
|
||||
WorkItemStateToggleButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -123,14 +116,6 @@ export default {
|
|||
isWorkItemOpen() {
|
||||
return this.workItemState === STATE_OPEN;
|
||||
},
|
||||
toggleWorkItemStateText() {
|
||||
return this.isWorkItemOpen
|
||||
? sprintf(__('Close %{workItemType}'), { workItemType: this.workItemType.toLowerCase() })
|
||||
: sprintf(__('Reopen %{workItemType}'), { workItemType: this.workItemType.toLowerCase() });
|
||||
},
|
||||
cancelButtonText() {
|
||||
return this.isNewDiscussion ? this.toggleWorkItemStateText : __('Cancel');
|
||||
},
|
||||
commentButtonTextComputed() {
|
||||
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
|
||||
},
|
||||
|
|
@ -166,48 +151,6 @@ export default {
|
|||
this.$emit('cancelEditing');
|
||||
clearDraft(this.autosaveKey);
|
||||
},
|
||||
async toggleWorkItemState() {
|
||||
const input = {
|
||||
id: this.workItemId,
|
||||
stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
|
||||
};
|
||||
|
||||
this.updateInProgress = true;
|
||||
|
||||
try {
|
||||
this.track('updated_state');
|
||||
|
||||
const { mutation, variables } = getUpdateWorkItemMutation({
|
||||
workItemParentId: this.workItemParentId,
|
||||
input,
|
||||
});
|
||||
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation,
|
||||
variables,
|
||||
});
|
||||
|
||||
const errors = data.workItemUpdate?.errors;
|
||||
|
||||
if (errors?.length) {
|
||||
this.$emit('error', i18n.updateError);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
|
||||
this.$emit('error', msg);
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
this.updateInProgress = false;
|
||||
},
|
||||
cancelButtonAction() {
|
||||
if (this.isNewDiscussion) {
|
||||
this.toggleWorkItemState();
|
||||
} else {
|
||||
this.cancelEditing();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -257,13 +200,23 @@ export default {
|
|||
@click="$emit('submitForm', { commentText, isNoteInternal })"
|
||||
>{{ commentButtonTextComputed }}
|
||||
</gl-button>
|
||||
<work-item-state-toggle-button
|
||||
v-if="isNewDiscussion"
|
||||
class="gl-ml-3"
|
||||
:work-item-id="workItemId"
|
||||
:work-item-state="workItemState"
|
||||
:work-item-type="workItemType"
|
||||
can-update
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<gl-button
|
||||
v-else
|
||||
data-testid="cancel-button"
|
||||
category="primary"
|
||||
class="gl-ml-3"
|
||||
:loading="updateInProgress"
|
||||
@click="cancelButtonAction"
|
||||
>{{ cancelButtonText }}
|
||||
@click="cancelEditing"
|
||||
>{{ $options.i18n.cancelButtonText }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
} from '../constants';
|
||||
import WorkItemState from './work_item_state.vue';
|
||||
import WorkItemDueDate from './work_item_due_date.vue';
|
||||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
import WorkItemLabels from './work_item_labels.vue';
|
||||
|
|
@ -23,7 +22,6 @@ export default {
|
|||
WorkItemMilestone,
|
||||
WorkItemAssignees,
|
||||
WorkItemDueDate,
|
||||
WorkItemState,
|
||||
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
|
||||
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
|
||||
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
|
||||
|
|
@ -97,12 +95,6 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="work-item-attributes-wrapper">
|
||||
<work-item-state
|
||||
:work-item="workItem"
|
||||
:work-item-parent-id="workItemParentId"
|
||||
:can-update="canUpdate"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-assignees
|
||||
v-if="workItemAssignees"
|
||||
:can-update="canUpdate"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { GlAvatarLink, GlSprintf } from '@gitlab/ui';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
|
||||
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
|
||||
|
||||
export default {
|
||||
|
|
@ -9,6 +10,7 @@ export default {
|
|||
GlAvatarLink,
|
||||
GlSprintf,
|
||||
TimeAgoTooltip,
|
||||
WorkItemStateBadge,
|
||||
},
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
|
|
@ -31,6 +33,9 @@ export default {
|
|||
authorId() {
|
||||
return getIdFromGraphQLId(this.author.id);
|
||||
},
|
||||
workItemState() {
|
||||
return this.workItem?.state;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
workItem: {
|
||||
|
|
@ -54,7 +59,8 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="gl-mb-3">
|
||||
<span data-testid="work-item-created">
|
||||
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
|
||||
<span data-testid="work-item-created" class="gl-vertical-align-middle">
|
||||
<gl-sprintf v-if="author.name" :message="__('Created %{timeAgo} by %{author}')">
|
||||
<template #timeAgo>
|
||||
<time-ago-tooltip :time="createdAt" />
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import WorkItemDescription from './work_item_description.vue';
|
|||
import WorkItemNotes from './work_item_notes.vue';
|
||||
import WorkItemDetailModal from './work_item_detail_modal.vue';
|
||||
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
|
||||
import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
|
|
@ -57,6 +58,7 @@ export default {
|
|||
},
|
||||
isLoggedIn: isLoggedIn(),
|
||||
components: {
|
||||
WorkItemStateToggleButton,
|
||||
GlAlert,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
|
|
@ -445,6 +447,14 @@ export default {
|
|||
class="gl-mr-3 gl-cursor-help"
|
||||
>{{ __('Confidential') }}</gl-badge
|
||||
>
|
||||
<work-item-state-toggle-button
|
||||
v-if="canUpdate"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-state="workItem.state"
|
||||
:work-item-parent-id="workItemParentId"
|
||||
:work-item-type="workItemType"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
<work-item-todos
|
||||
v-if="showWorkItemCurrentUserTodos"
|
||||
:work-item-id="workItem.id"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { STATE_OPEN } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
},
|
||||
props: {
|
||||
workItemState: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isWorkItemOpen() {
|
||||
return this.workItemState === STATE_OPEN;
|
||||
},
|
||||
stateText() {
|
||||
return this.isWorkItemOpen ? __('Open') : __('Closed');
|
||||
},
|
||||
workItemStateIcon() {
|
||||
return this.isWorkItemOpen ? 'issue-open-m' : 'issue-close';
|
||||
},
|
||||
workItemStateVariant() {
|
||||
return this.isWorkItemOpen ? 'success' : 'info';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-badge
|
||||
:icon="workItemStateIcon"
|
||||
:variant="workItemStateVariant"
|
||||
class="gl-mr-2 gl-vertical-align-middle"
|
||||
>
|
||||
{{ stateText }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
@ -1,26 +1,35 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Tracking from '~/tracking';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
STATE_OPEN,
|
||||
STATE_CLOSED,
|
||||
STATE_EVENT_CLOSE,
|
||||
STATE_EVENT_REOPEN,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
} from '../constants';
|
||||
import { getUpdateWorkItemMutation } from './update_work_item';
|
||||
import ItemState from './item_state.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ItemState,
|
||||
GlButton,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
workItem: {
|
||||
type: Object,
|
||||
workItemState: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemParentId: {
|
||||
|
|
@ -28,11 +37,6 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -40,8 +44,16 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
workItemType() {
|
||||
return this.workItem.workItemType?.name;
|
||||
isWorkItemOpen() {
|
||||
return this.workItemState === STATE_OPEN;
|
||||
},
|
||||
toggleWorkItemStateText() {
|
||||
const baseText = this.isWorkItemOpen
|
||||
? __('Close %{workItemType}')
|
||||
: __('Reopen %{workItemType}');
|
||||
return capitalizeFirstCharacter(
|
||||
sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
|
||||
);
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
|
|
@ -52,25 +64,10 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
updateWorkItemState(newState) {
|
||||
const stateEventMap = {
|
||||
[STATE_OPEN]: STATE_EVENT_REOPEN,
|
||||
[STATE_CLOSED]: STATE_EVENT_CLOSE,
|
||||
};
|
||||
|
||||
const stateEvent = stateEventMap[newState];
|
||||
|
||||
this.updateWorkItem(stateEvent);
|
||||
},
|
||||
|
||||
async updateWorkItem(updatedState) {
|
||||
if (!updatedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
async updateWorkItem() {
|
||||
const input = {
|
||||
id: this.workItem.id,
|
||||
stateEvent: updatedState,
|
||||
id: this.workItemId,
|
||||
stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
|
||||
};
|
||||
|
||||
this.updateInProgress = true;
|
||||
|
|
@ -107,10 +104,10 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<item-state
|
||||
v-if="workItem.state"
|
||||
:state="workItem.state"
|
||||
:disabled="updateInProgress || !canUpdate"
|
||||
@changed="updateWorkItemState"
|
||||
/>
|
||||
<gl-button
|
||||
:loading="updateInProgress"
|
||||
data-testid="work-item-state-toggle"
|
||||
@click="updateWorkItem"
|
||||
>{{ toggleWorkItemStateText }}</gl-button
|
||||
>
|
||||
</template>
|
||||
|
|
@ -195,7 +195,8 @@ module IssuesHelper
|
|||
is_signed_in: current_user.present?.to_s,
|
||||
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
|
||||
rss_path: url_for(safe_params.merge(rss_url_options)),
|
||||
sign_in_path: new_user_session_path
|
||||
sign_in_path: new_user_session_path,
|
||||
has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, namespace)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module Ci
|
|||
class Bridge < Ci::Processable
|
||||
include Ci::Contextable
|
||||
include Ci::Metadatable
|
||||
include Ci::Deployable
|
||||
include Importable
|
||||
include AfterCommitQueue
|
||||
include Ci::HasRef
|
||||
|
|
@ -180,20 +181,6 @@ module Ci
|
|||
false
|
||||
end
|
||||
|
||||
def outdated_deployment?
|
||||
false
|
||||
end
|
||||
|
||||
def expanded_environment_name
|
||||
end
|
||||
|
||||
def persisted_environment
|
||||
end
|
||||
|
||||
def deployment_job?
|
||||
false
|
||||
end
|
||||
|
||||
def execute_hooks
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Npm
|
||||
class PackagePresenter
|
||||
def initialize(metadata)
|
||||
@metadata = metadata
|
||||
end
|
||||
|
||||
def name
|
||||
metadata[:name]
|
||||
end
|
||||
|
||||
def versions
|
||||
metadata[:versions]
|
||||
end
|
||||
|
||||
def dist_tags
|
||||
metadata[:dist_tags]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :metadata
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -39,7 +39,7 @@ module Ci
|
|||
|
||||
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
|
||||
|
||||
::Deployments::CreateForBuildService.new.execute(new_job)
|
||||
::Deployments::CreateForJobService.new.execute(new_job)
|
||||
|
||||
::MergeRequests::AddTodoWhenBuildFailsService
|
||||
.new(project: project)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Deployments
|
||||
# This class creates a deployment record for a build (a pipeline job).
|
||||
class CreateForBuildService
|
||||
# This class creates a deployment record for a pipeline job.
|
||||
class CreateForJobService
|
||||
DeploymentCreationError = Class.new(StandardError)
|
||||
|
||||
def execute(build)
|
||||
return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
|
||||
return unless build.is_a?(::Ci::Processable) && build.persisted_environment.present?
|
||||
|
||||
environment = build.actual_persisted_environment
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ module Deployments
|
|||
# non-environment job.
|
||||
return unless deployment.valid? && deployment.environment.persisted?
|
||||
|
||||
if cluster = deployment.environment.deployment_platform&.cluster
|
||||
if cluster = deployment.environment.deployment_platform&.cluster # rubocop: disable Lint/AssignmentInCondition
|
||||
# double write cluster_id until 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
|
||||
deployment.cluster_id = cluster.id
|
||||
deployment.deployment_cluster = ::DeploymentCluster.new(
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Environments
|
||||
# This class creates an environment record for a build (a pipeline job).
|
||||
class CreateForBuildService
|
||||
# This class creates an environment record for a pipeline job.
|
||||
class CreateForJobService
|
||||
def execute(build)
|
||||
return unless build.instance_of?(::Ci::Build) && build.has_environment_keyword?
|
||||
return unless build.is_a?(::Ci::Processable) && build.has_environment_keyword?
|
||||
|
||||
environment = to_resource(build)
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ module Ci
|
|||
end
|
||||
|
||||
def create_deployment(build)
|
||||
::Deployments::CreateForBuildService.new.execute(build)
|
||||
::Deployments::CreateForJobService.new.execute(build)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
description: "Edit multiple files with Web IDE"
|
||||
category: default
|
||||
action: click_consolidated_edit_ide
|
||||
label_description: "`web_ide`"
|
||||
property_description: ""
|
||||
value_description: ""
|
||||
extra_properties:
|
||||
identifiers:
|
||||
product_section: dev
|
||||
product_stage: create
|
||||
product_group: group::editor
|
||||
milestone: "14.1"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64179
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
description: "Edit a single file"
|
||||
---
|
||||
description: "Selects an editor in the Edit dropdown menu"
|
||||
category: default
|
||||
action: click_consolidated_edit
|
||||
label_description: "`edit`"
|
||||
property_description: ""
|
||||
value_description: ""
|
||||
label_description: "The editor selected in the Edit dropdown menu"
|
||||
property_description:
|
||||
value_description:
|
||||
extra_properties:
|
||||
identifiers:
|
||||
product_section: dev
|
||||
product_stage: create
|
||||
product_group: group::editor
|
||||
milestone: "14.1"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64179
|
||||
product_group: group::ide
|
||||
milestone: "16.3"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127163
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
|
|
@ -18,3 +19,4 @@ tiers:
|
|||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: issue_date_filter
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120160
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420173
|
||||
milestone: '16.2'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
||||
|
|
@ -565,6 +565,8 @@
|
|||
- 1
|
||||
- - search_wiki_elastic_delete_group_wiki
|
||||
- 1
|
||||
- - search_zoekt_delete_project
|
||||
- 1
|
||||
- - search_zoekt_namespace_indexer
|
||||
- 1
|
||||
- - security_auto_fix
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CleanupConversionBigIntCiBuildNeedsSelfManaged < Gitlab::Database::Migration[2.1]
|
||||
include Gitlab::Database::MigrationHelpers::ConvertToBigint
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
TABLE = :ci_build_needs
|
||||
|
||||
def up
|
||||
return if should_skip?
|
||||
return unless column_exists?(TABLE, :id_convert_to_bigint)
|
||||
|
||||
# rubocop:disable Migration/WithLockRetriesDisallowedMethod
|
||||
with_lock_retries do
|
||||
cleanup_conversion_of_integer_to_bigint(TABLE, :id)
|
||||
end
|
||||
# rubocop:enable Migration/WithLockRetriesDisallowedMethod
|
||||
end
|
||||
|
||||
def down
|
||||
return if should_skip?
|
||||
return if column_exists?(TABLE, :id_convert_to_bigint)
|
||||
|
||||
restore_conversion_of_integer_to_bigint(TABLE, :id)
|
||||
end
|
||||
|
||||
def should_skip?
|
||||
com_or_dev_or_test_but_not_jh?
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnVulnerabilityReadsForFiltering < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = "idx_vuln_reads_for_filtering"
|
||||
|
||||
def up
|
||||
add_concurrent_index(
|
||||
:vulnerability_reads,
|
||||
%i[project_id state dismissal_reason severity vulnerability_id],
|
||||
order: { severity: :desc, vulnerability_id: "DESC NULLS LAST" },
|
||||
name: INDEX_NAME
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(
|
||||
:vulnerability_reads,
|
||||
INDEX_NAME
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
658cb25d5add4ad4e26d7baef6759f8512fa0244dd347b0522ad75ac496c9965
|
||||
|
|
@ -0,0 +1 @@
|
|||
a85f3b493021cc27079dc07fe0ba5f11eeeca9798cf6ccdc60f7f7f7eae049af
|
||||
|
|
@ -30191,6 +30191,8 @@ CREATE UNIQUE INDEX idx_uniq_analytics_dashboards_pointers_on_project_id ON anal
|
|||
|
||||
CREATE INDEX idx_user_details_on_provisioned_by_group_id_user_id ON user_details USING btree (provisioned_by_group_id, user_id);
|
||||
|
||||
CREATE INDEX idx_vuln_reads_for_filtering ON vulnerability_reads USING btree (project_id, state, dismissal_reason, severity DESC, vulnerability_id DESC NULLS LAST);
|
||||
|
||||
CREATE UNIQUE INDEX idx_vuln_signatures_uniqueness_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, algorithm_type, signature_sha);
|
||||
|
||||
CREATE INDEX idx_vulnerabilities_on_project_id_and_id_active_cis_dft_branch ON vulnerabilities USING btree (project_id, id) WHERE ((report_type = 7) AND (state = ANY (ARRAY[1, 4])) AND (present_on_default_branch IS TRUE));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
# Error: gitlab.CommandStringsQuoted
|
||||
#
|
||||
# Ensures all code blocks wrap URL strings in quotation marks.
|
||||
#
|
||||
# For a list of all options, see https://vale.sh/docs/topics/styles/
|
||||
extends: existence
|
||||
message: "For the command example, use double quotes around the URL: %s"
|
||||
link: https://docs.gitlab.com/ee/development/documentation/restful_api_styleguide.html#curl-commands
|
||||
level: error
|
||||
scope: raw
|
||||
nonword: true
|
||||
tokens:
|
||||
- '(curl|--url)[^"\]\n]+?https?:\/\/[^ \n]*'
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
# Error: gitlab.CurlStringsQuoted
|
||||
#
|
||||
# Ensures all code blocks using `curl` wrap URL strings in quotation marks.
|
||||
#
|
||||
# For a list of all options, see https://vale.sh/docs/topics/styles/
|
||||
extends: existence
|
||||
message: "For the cURL example, use double quotes around the URL: %s"
|
||||
link: https://docs.gitlab.com/ee/development/documentation/restful_api_styleguide.html#curl-commands
|
||||
level: error
|
||||
scope: code
|
||||
raw:
|
||||
- 'curl [^"]+://.*'
|
||||
|
|
@ -80,28 +80,32 @@ The following settings are available:
|
|||
|
||||
## User cap
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4315) in GitLab 13.7.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292600) in GitLab 13.9.
|
||||
The user cap is the maximum number of billable users who can sign up or be added to a subscription
|
||||
without administrator approval. After the user cap is reached, users who sign up or are
|
||||
added must be [approved](../../administration/moderate_users.md#approve-or-reject-a-user-sign-up)
|
||||
by an administrator. Users can use their account only after they have been approved by an administrator.
|
||||
|
||||
When the number of billable users reaches the user cap, any user who is added or requests access must be
|
||||
[approved](../../administration/moderate_users.md#approve-or-reject-a-user-sign-up) by an administrator before they can start using
|
||||
their account.
|
||||
|
||||
If an administrator [increases](#set-the-user-cap-number) or [removes](#remove-the-user-cap) the
|
||||
user cap, the users in pending approval state are automatically approved in a background job.
|
||||
If an administrator increases or removes the user cap, users pending approval are automatically approved.
|
||||
|
||||
NOTE:
|
||||
The amount of billable users [is updated once a day](../../subscriptions/self_managed/index.md#billable-users).
|
||||
This means the user cap might apply only retrospectively after the cap has already been exceeded.
|
||||
To ensure the cap is enabled immediately, set it to a low value below the current number of
|
||||
billable users, for example: `1`.
|
||||
For instances that use LDAP or OmniAuth, when [administrator approval for new sign-ups](#require-administrator-approval-for-new-sign-ups)
|
||||
is enabled or disabled, downtime might occur due to changes in the Rails configuration.
|
||||
You can set a user cap to enforce approvals for new users. To ensure the user cap applies immediately, set the cap to a value below the current number of billable users (for example, `1`).
|
||||
|
||||
On instances that use LDAP or OmniAuth, enabling and disabling
|
||||
[administrator approval for new sign ups](#require-administrator-approval-for-new-sign-ups)
|
||||
involves changing the Rails configuration, and may require downtime.
|
||||
User cap can be used instead. As noted above, set the cap to value that ensures it is enforced immediately.
|
||||
### Set a user cap
|
||||
|
||||
### Set the user cap number
|
||||
Set a user cap to restrict the number of users who can sign up without administrator approval.
|
||||
|
||||
The number of [billable users](../../subscriptions/self_managed/index.md#billable-users) is updated once a day.
|
||||
The user cap might apply only retrospectively after the cap has already been exceeded.
|
||||
To ensure the cap is enabled immediately, set the cap to a value below the current number of
|
||||
billable users (for example, `1`).
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- You must be an administrator.
|
||||
|
||||
To set a user cap:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
|
|
@ -110,9 +114,18 @@ User cap can be used instead. As noted above, set the cap to value that ensures
|
|||
1. Enter a number in **User cap**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
New user sign ups are subject to the user cap restriction.
|
||||
### Remove the user cap
|
||||
|
||||
## Remove the user cap
|
||||
Remove the user cap so that the number of new users who can sign up without
|
||||
administrator approval is not restricted.
|
||||
|
||||
After you remove the user cap, users pending approval are automatically approved.
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- You must be an administrator.
|
||||
|
||||
To remove the user cap:
|
||||
|
||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||
1. Select **Admin Area**.
|
||||
|
|
@ -121,9 +134,6 @@ New user sign ups are subject to the user cap restriction.
|
|||
1. Remove the number from **User cap**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
New users sign ups are not subject to the user cap restriction. Users in pending approval state are
|
||||
automatically approved in a background job.
|
||||
|
||||
## Minimum password length limit
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20661) in GitLab 12.6
|
||||
|
|
|
|||
|
|
@ -783,6 +783,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="queryvulnerabilitiesclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
|
||||
| <a id="queryvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
|
||||
| <a id="queryvulnerabilitiesdismissalreason"></a>`dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. Only dismissed Vulnerabilities will be included with the filter. |
|
||||
| <a id="queryvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. |
|
||||
| <a id="queryvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
|
||||
| <a id="queryvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
|
||||
|
|
@ -17249,6 +17250,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="groupvulnerabilitiesclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
|
||||
| <a id="groupvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
|
||||
| <a id="groupvulnerabilitiesdismissalreason"></a>`dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. Only dismissed Vulnerabilities will be included with the filter. |
|
||||
| <a id="groupvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. |
|
||||
| <a id="groupvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
|
||||
| <a id="groupvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
|
||||
|
|
@ -22068,6 +22070,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="projectvulnerabilitiesclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
|
||||
| <a id="projectvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
|
||||
| <a id="projectvulnerabilitiesdismissalreason"></a>`dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. Only dismissed Vulnerabilities will be included with the filter. |
|
||||
| <a id="projectvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. |
|
||||
| <a id="projectvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
|
||||
| <a id="projectvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
|
||||
|
|
|
|||
|
|
@ -1487,7 +1487,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your-token>" \
|
|||
--header "Content-Type: application/json" --data '{
|
||||
"name": "new_project", "description": "New Project", "path": "new_project",
|
||||
"namespace_id": "42", "initialize_with_readme": "true"}' \
|
||||
--url 'https://gitlab.example.com/api/v4/projects/'
|
||||
--url "https://gitlab.example.com/api/v4/projects/"
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -1671,7 +1671,7 @@ For example, to toggle the setting for
|
|||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your-token>" \
|
||||
--url 'https://gitlab.com/api/v4/projects/<your-project-ID>' \
|
||||
--url "https://gitlab.com/api/v4/projects/<your-project-ID>" \
|
||||
--data "shared_runners_enabled=true" # to turn off: "shared_runners_enabled=false"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ the Docker commands, but needs permission to do so.
|
|||
|
||||
```shell
|
||||
sudo gitlab-runner register -n \
|
||||
--url https://gitlab.com/ \
|
||||
--url "https://gitlab.com/" \
|
||||
--registration-token REGISTRATION_TOKEN \
|
||||
--executor shell \
|
||||
--description "My Runner"
|
||||
|
|
@ -117,7 +117,7 @@ To use Docker-in-Docker with TLS enabled:
|
|||
|
||||
```shell
|
||||
sudo gitlab-runner register -n \
|
||||
--url https://gitlab.com/ \
|
||||
--url "https://gitlab.com/" \
|
||||
--registration-token REGISTRATION_TOKEN \
|
||||
--executor docker \
|
||||
--description "My Docker Runner" \
|
||||
|
|
@ -381,7 +381,7 @@ To mount `/var/run/docker.sock` while registering your runner, include the follo
|
|||
|
||||
```shell
|
||||
sudo gitlab-runner register -n \
|
||||
--url https://gitlab.com/ \
|
||||
--url "https://gitlab.com/" \
|
||||
--registration-token REGISTRATION_TOKEN \
|
||||
--executor docker \
|
||||
--description "My Docker Runner" \
|
||||
|
|
|
|||
|
|
@ -146,13 +146,13 @@ the `pgai` Gem:
|
|||
|
||||
1. To get started, you need to gather some values from the [Postgres.ai instances page](https://console.postgres.ai/gitlab/instances):
|
||||
|
||||
1. Navigate to the instance that you want to configure and the on right side of the screen.
|
||||
1. Go to the instance that you want to configure and the on right side of the screen.
|
||||
1. Under **Connection**, select **Connect**. The menu might be collapsed.
|
||||
|
||||
A pop-up with everything that's needed for configuration appears, using this format:
|
||||
A dialog with everything that's needed for configuration appears, using this format:
|
||||
|
||||
```shell
|
||||
dblab init --url http://127.0.0.1:1234 --token TOKEN --environment-id <environment-id>
|
||||
dblab init --url "http://127.0.0.1:1234" --token TOKEN --environment-id <environment-id>
|
||||
```
|
||||
|
||||
```shell
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ Now that your server is set up, install GitLab:
|
|||
1. Add the GitLab package repository and install the package:
|
||||
|
||||
```shell
|
||||
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash
|
||||
curl "https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh" | sudo bash
|
||||
```
|
||||
|
||||
To see the contents of the script, visit <https://packages.gitlab.com/gitlab/gitlab-ee/install>.
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ on how to use GitLab as a backend for the MLflow Client.
|
|||
To list the current active experiments, either go to `https/-/ml/experiments` or:
|
||||
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. Select **Deploy > Model experiments**.
|
||||
1. Select **Analyze > Model experiments**.
|
||||
1. To display all candidates that have been logged, along with their metrics, parameters, and metadata, select an experiment.
|
||||
1. To display details for a candidate, select **Details**.
|
||||
|
||||
|
|
|
|||
|
|
@ -96,9 +96,8 @@ module API
|
|||
|
||||
track_package_event(:list_tags, :npm, project: project, namespace: project.namespace)
|
||||
|
||||
metadata = generate_metadata_service(packages).execute(only_dist_tags: true)
|
||||
present ::Packages::Npm::PackagePresenter.new(metadata),
|
||||
with: ::API::Entities::NpmPackageTag
|
||||
metadata = generate_metadata_service(packages).execute(only_dist_tags: true).payload
|
||||
present metadata, with: ::API::Entities::NpmPackageTag
|
||||
end
|
||||
|
||||
params do
|
||||
|
|
@ -229,8 +228,8 @@ module API
|
|||
enqueue_sync_metadata_cache_worker(project, package_name)
|
||||
end
|
||||
|
||||
present ::Packages::Npm::PackagePresenter.new(generate_metadata_service(packages).execute),
|
||||
with: ::API::Entities::NpmPackage
|
||||
metadata = generate_metadata_service(packages).execute.payload
|
||||
present metadata, with: ::API::Entities::NpmPackage
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def ensure_environment(build)
|
||||
::Environments::CreateForBuildService.new.execute(build)
|
||||
::Environments::CreateForJobService.new.execute(build)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@ module Gitlab
|
|||
class << self
|
||||
# Full list of options:
|
||||
# https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
|
||||
# pool argument event not documented in the link above is handled by RedisCacheStore see:
|
||||
# https://github.com/rails/rails/blob/593893c901f87b4ed205751f72df41519b4d2da3/activesupport/lib/active_support/cache/redis_cache_store.rb#L165
|
||||
# and
|
||||
# https://github.com/rails/rails/blob/ad790cb2f6bc724a89e4266b505b3c57d5089dae/activesupport/lib/active_support/cache.rb#L206
|
||||
def active_support_config
|
||||
{
|
||||
redis: pool,
|
||||
pool: false,
|
||||
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
|
||||
namespace: CACHE_NAMESPACE,
|
||||
expires_in: default_ttl_seconds
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module Gitlab
|
|||
def cache_store
|
||||
@cache_store ||= FeatureFlagStore.new(
|
||||
redis: pool,
|
||||
pool: false,
|
||||
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
|
||||
namespace: Cache::CACHE_NAMESPACE,
|
||||
expires_in: 1.hour
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ module Gitlab
|
|||
def cache_store
|
||||
@cache_store ||= RepositoryCacheStore.new(
|
||||
redis: pool,
|
||||
pool: false,
|
||||
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
|
||||
namespace: Cache::CACHE_NAMESPACE,
|
||||
expires_in: Cache.default_ttl_seconds
|
||||
|
|
|
|||
|
|
@ -49857,6 +49857,9 @@ msgstr ""
|
|||
msgid "UsageQuota|%{linkTitle} help link"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|%{percentageRemaining}%% namespace storage remaining."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|%{storage_limit_link_start}A namespace storage limit%{link_end} will soon be enforced for the %{strong_start}%{namespace_name}%{strong_end} namespace. %{extra_message}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -49968,9 +49971,6 @@ msgstr ""
|
|||
msgid "UsageQuota|Registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|Seats"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54343,6 +54343,9 @@ msgstr ""
|
|||
msgid "banned user already exists"
|
||||
msgstr ""
|
||||
|
||||
msgid "before"
|
||||
msgstr ""
|
||||
|
||||
msgid "beta"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -55886,6 +55889,9 @@ msgstr ""
|
|||
msgid "nounSeries|%{item}, and %{lastItem}"
|
||||
msgstr ""
|
||||
|
||||
msgid "on or after"
|
||||
msgstr ""
|
||||
|
||||
msgid "only available on top-level groups."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'deployable'
|
||||
|
||||
FactoryBot.define do
|
||||
factory :ci_bridge, class: 'Ci::Bridge', parent: :ci_processable do
|
||||
instance_eval ::Factories::Ci::Deployable.traits
|
||||
|
||||
name { 'bridge' }
|
||||
created_at { '2013-10-29 09:50:00 CET' }
|
||||
status { :created }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'deployable'
|
||||
|
||||
FactoryBot.define do
|
||||
factory :ci_build, class: 'Ci::Build', parent: :ci_processable do
|
||||
instance_eval ::Factories::Ci::Deployable.traits
|
||||
|
||||
name { 'test' }
|
||||
add_attribute(:protected) { false }
|
||||
created_at { 'Di 29. Okt 09:50:00 CET 2013' }
|
||||
|
|
@ -137,122 +141,6 @@ FactoryBot.define do
|
|||
self.when { 'manual' }
|
||||
end
|
||||
|
||||
trait :teardown_environment do
|
||||
environment { 'staging' }
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging',
|
||||
action: 'stop',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :environment_with_deployment_tier do
|
||||
environment { 'test_portal' }
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'test_portal',
|
||||
action: 'start',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME',
|
||||
deployment_tier: 'testing' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :deploy_to_production do
|
||||
environment { 'production' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'production',
|
||||
url: 'http://prd.example.com/$CI_JOB_NAME' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :start_review_app do
|
||||
environment { 'review/$CI_COMMIT_REF_NAME' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'review/$CI_COMMIT_REF_NAME',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME',
|
||||
on_stop: 'stop_review_app' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :stop_review_app do
|
||||
name { 'stop_review_app' }
|
||||
environment { 'review/$CI_COMMIT_REF_NAME' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'review/$CI_COMMIT_REF_NAME',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME',
|
||||
action: 'stop' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :prepare_staging do
|
||||
name { 'prepare staging' }
|
||||
environment { 'staging' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging', action: 'prepare' }
|
||||
}
|
||||
end
|
||||
|
||||
set_expanded_environment_name
|
||||
end
|
||||
|
||||
trait :start_staging do
|
||||
name { 'start staging' }
|
||||
environment { 'staging' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging', action: 'start' }
|
||||
}
|
||||
end
|
||||
|
||||
set_expanded_environment_name
|
||||
end
|
||||
|
||||
trait :stop_staging do
|
||||
name { 'stop staging' }
|
||||
environment { 'staging' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging', action: 'stop' }
|
||||
}
|
||||
end
|
||||
|
||||
set_expanded_environment_name
|
||||
end
|
||||
|
||||
trait :set_expanded_environment_name do
|
||||
after(:build) do |build, evaluator|
|
||||
build.assign_attributes(
|
||||
metadata_attributes: {
|
||||
expanded_environment_name: build.expanded_environment_name
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
trait :allowed_to_fail do
|
||||
allow_failure { true }
|
||||
end
|
||||
|
|
@ -311,20 +199,6 @@ FactoryBot.define do
|
|||
trigger_request factory: :ci_trigger_request
|
||||
end
|
||||
|
||||
trait :with_deployment do
|
||||
after(:build) do |build, evaluator|
|
||||
##
|
||||
# Build deployment/environment relations if environment name is set
|
||||
# to the job. If `build.deployment` has already been set, it doesn't
|
||||
# build a new instance.
|
||||
Environments::CreateForBuildService.new.execute(build)
|
||||
end
|
||||
|
||||
after(:create) do |build, evaluator|
|
||||
Deployments::CreateForBuildService.new.execute(build)
|
||||
end
|
||||
end
|
||||
|
||||
trait :tag do
|
||||
tag { true }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Factories
|
||||
module Ci
|
||||
module Deployable
|
||||
def self.traits
|
||||
<<-RUBY
|
||||
trait :teardown_environment do
|
||||
environment { 'staging' }
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging',
|
||||
action: 'stop',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :environment_with_deployment_tier do
|
||||
environment { 'test_portal' }
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'test_portal',
|
||||
action: 'start',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME',
|
||||
deployment_tier: 'testing' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :deploy_to_production do
|
||||
environment { 'production' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'production',
|
||||
url: 'http://prd.example.com/$CI_JOB_NAME' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :start_review_app do
|
||||
environment { 'review/$CI_COMMIT_REF_NAME' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'review/$CI_COMMIT_REF_NAME',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME',
|
||||
on_stop: 'stop_review_app' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :stop_review_app do
|
||||
name { 'stop_review_app' }
|
||||
environment { 'review/$CI_COMMIT_REF_NAME' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'review/$CI_COMMIT_REF_NAME',
|
||||
url: 'http://staging.example.com/$CI_JOB_NAME',
|
||||
action: 'stop' }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
trait :prepare_staging do
|
||||
name { 'prepare staging' }
|
||||
environment { 'staging' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging', action: 'prepare' }
|
||||
}
|
||||
end
|
||||
|
||||
set_expanded_environment_name
|
||||
end
|
||||
|
||||
trait :start_staging do
|
||||
name { 'start staging' }
|
||||
environment { 'staging' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging', action: 'start' }
|
||||
}
|
||||
end
|
||||
|
||||
set_expanded_environment_name
|
||||
end
|
||||
|
||||
trait :stop_staging do
|
||||
name { 'stop staging' }
|
||||
environment { 'staging' }
|
||||
|
||||
options do
|
||||
{
|
||||
script: %w(ls),
|
||||
environment: { name: 'staging', action: 'stop' }
|
||||
}
|
||||
end
|
||||
|
||||
set_expanded_environment_name
|
||||
end
|
||||
|
||||
trait :set_expanded_environment_name do
|
||||
after(:build) do |job, evaluator|
|
||||
job.assign_attributes(
|
||||
metadata_attributes: {
|
||||
expanded_environment_name: job.expanded_environment_name
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_deployment do
|
||||
after(:build) do |job, evaluator|
|
||||
##
|
||||
# Build deployment/environment relations if environment name is set
|
||||
# to the job. If `job.deployment` has already been set, it doesn't
|
||||
# build a new instance.
|
||||
Environments::CreateForJobService.new.execute(job)
|
||||
end
|
||||
|
||||
after(:create) do |job, evaluator|
|
||||
Deployments::CreateForJobService.new.execute(job)
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -40,7 +40,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
|
|||
end
|
||||
|
||||
it_behaves_like 'work items title'
|
||||
it_behaves_like 'work items status'
|
||||
it_behaves_like 'work items toggle status button'
|
||||
it_behaves_like 'work items assignees'
|
||||
it_behaves_like 'work items labels'
|
||||
it_behaves_like 'work items comments', :issue
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import {
|
|||
TOKEN_TYPE_MY_REACTION,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_TYPE,
|
||||
TOKEN_TYPE_CREATED,
|
||||
TOKEN_TYPE_CLOSED,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
|
||||
import {
|
||||
|
|
@ -61,6 +63,7 @@ describe('IssuesDashboardApp component', () => {
|
|||
emptyStateWithFilterSvgPath: 'empty/state/with/filter/svg/path.svg',
|
||||
emptyStateWithoutFilterSvgPath: 'empty/state/with/filter/svg/path.svg',
|
||||
hasBlockedIssuesFeature: true,
|
||||
hasIssueDateFilterFeature: true,
|
||||
hasIssuableHealthStatusFeature: true,
|
||||
hasIssueWeightsFeature: true,
|
||||
hasScopedLabelsFeature: true,
|
||||
|
|
@ -365,7 +368,9 @@ describe('IssuesDashboardApp component', () => {
|
|||
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_CLOSED },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
{ type: TOKEN_TYPE_CREATED },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ import {
|
|||
TOKEN_TYPE_RELEASE,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_TYPE,
|
||||
TOKEN_TYPE_CREATED,
|
||||
TOKEN_TYPE_CLOSED,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
|
||||
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
|
||||
|
|
@ -109,6 +111,7 @@ describe('CE IssuesListApp component', () => {
|
|||
hasAnyIssues: true,
|
||||
hasAnyProjects: true,
|
||||
hasBlockedIssuesFeature: true,
|
||||
hasIssueDateFilterFeature: true,
|
||||
hasIssuableHealthStatusFeature: true,
|
||||
hasIssueWeightsFeature: true,
|
||||
hasIterationsFeature: true,
|
||||
|
|
@ -653,8 +656,10 @@ describe('CE IssuesListApp component', () => {
|
|||
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_CLOSED },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
{ type: TOKEN_TYPE_CONTACT },
|
||||
{ type: TOKEN_TYPE_CREATED },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe('MenuSection component', () => {
|
|||
const findNavItems = () => wrapper.findAllComponents(NavItem);
|
||||
const createWrapper = (item, otherProps) => {
|
||||
wrapper = shallowMountExtended(MenuSection, {
|
||||
propsData: { item, ...otherProps },
|
||||
propsData: { item: { items: [], ...item }, ...otherProps },
|
||||
stubs: {
|
||||
GlCollapse: stubComponent(GlCollapse, {
|
||||
props: ['visible'],
|
||||
|
|
@ -101,6 +101,25 @@ describe('MenuSection component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when section gets closed', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper({ title: 'Asdf' }, { expanded: true, 'has-flyout': true });
|
||||
await findButton().trigger('click');
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
});
|
||||
|
||||
it('shows the flyout only after section title gets hovered out and in again', async () => {
|
||||
expect(findCollapse().props('visible')).toBe(false);
|
||||
expect(findFlyout().isVisible()).toBe(false);
|
||||
|
||||
await findButton().trigger('pointerleave');
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
|
||||
expect(findCollapse().props('visible')).toBe(false);
|
||||
expect(findFlyout().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import { trimText } from 'helpers/text_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
|
||||
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
|
||||
import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
|
||||
import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality/index.vue';
|
||||
import {
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
HTTP_STATUS_NO_CONTENT,
|
||||
|
|
@ -26,10 +24,7 @@ import {
|
|||
describe('Code Quality extension', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
|
||||
registerExtension(codeQualityExtension);
|
||||
|
||||
const endpoint = '/root/repo/-/merge_requests/4/accessibility_reports.json';
|
||||
const endpoint = '/root/repo/-/merge_requests/4/codequality_reports.json';
|
||||
|
||||
const mockApi = (statusCode, data) => {
|
||||
mock.onGet(endpoint).reply(statusCode, data);
|
||||
|
|
@ -43,10 +38,11 @@ describe('Code Quality extension', () => {
|
|||
const getSuccessIcon = () => wrapper.findByTestId('status-success-icon').exists();
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = mountExtended(extensionsContainer, {
|
||||
wrapper = mountExtended(codeQualityExtension, {
|
||||
propsData: {
|
||||
mr: {
|
||||
codeQuality: endpoint,
|
||||
codequality: endpoint,
|
||||
codequalityReportsPath: endpoint,
|
||||
blobPath: {
|
||||
head_path: 'example/path',
|
||||
base_path: 'example/path',
|
||||
|
|
@ -198,7 +194,7 @@ describe('Code Quality extension', () => {
|
|||
"Minor - Parsing error: 'return' outside of function in index.js:12",
|
||||
);
|
||||
expect(text.resolvedError).toContain(
|
||||
"Minor - Parsing error: 'return' outside of function Fixed in index.js:12",
|
||||
"Minor - Parsing error: 'return' outside of function in index.js:12 Fixed",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -212,7 +208,7 @@ describe('Code Quality extension', () => {
|
|||
'Minor - Rubocop/Metrics/ParameterLists - Avoid parameter lists longer than 5 parameters. [12/5] in main.rb:3',
|
||||
);
|
||||
expect(text.resolvedError).toContain(
|
||||
'Minor - Rubocop/Metrics/ParameterLists - Avoid parameter lists longer than 5 parameters. [12/5] Fixed in main.rb:3',
|
||||
'Minor - Rubocop/Metrics/ParameterLists - Avoid parameter lists longer than 5 parameters. [12/5] in main.rb:3 Fixed',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { GlDatepicker, GlFilteredSearchToken } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import DateToken from '~/vue_shared/components/filtered_search_bar/tokens/date_token.vue';
|
||||
|
||||
const propsData = {
|
||||
active: true,
|
||||
config: {},
|
||||
value: { operator: '>', data: null },
|
||||
};
|
||||
|
||||
function createComponent() {
|
||||
return mount(DateToken, {
|
||||
propsData,
|
||||
provide: {
|
||||
portalName: 'fake target',
|
||||
alignSuggestions: function fakeAlignSuggestions() {},
|
||||
termsAsTokens: () => false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('DateToken', () => {
|
||||
let wrapper;
|
||||
|
||||
const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
|
||||
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
it('renders GlDatepicker', () => {
|
||||
expect(findDatepicker().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders GlFilteredSearchToken', () => {
|
||||
expect(findGlFilteredSearchToken().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('emits `complete` and `select` with the formatted date when a value is selected', () => {
|
||||
findDatepicker().vm.$emit('input', new Date('October 13, 2014 11:13:00'));
|
||||
findDatepicker().vm.$emit('close');
|
||||
|
||||
expect(findGlFilteredSearchToken().emitted()).toEqual({
|
||||
complete: [[]],
|
||||
select: [['2014-10-13']],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { GlModal, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
import { omit } from 'lodash';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
||||
|
|
@ -6,13 +7,14 @@ import getWritableForksResponse from 'test_fixtures/graphql/vue_shared/component
|
|||
import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue';
|
||||
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
|
||||
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import {
|
||||
shallowMountExtended,
|
||||
mountExtended,
|
||||
extendedWrapper,
|
||||
} from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import getWritableForksQuery from '~/vue_shared/components/web_ide/get_writable_forks.query.graphql';
|
||||
|
|
@ -34,8 +36,10 @@ const ACTION_EDIT = {
|
|||
secondaryText: 'Edit this file only.',
|
||||
attrs: {
|
||||
'data-qa-selector': 'edit_menu_item',
|
||||
'data-track-action': 'click_consolidated_edit',
|
||||
'data-track-label': 'edit',
|
||||
},
|
||||
tracking: {
|
||||
action: 'click_consolidated_edit',
|
||||
label: 'single_file',
|
||||
},
|
||||
};
|
||||
const ACTION_EDIT_CONFIRM_FORK = {
|
||||
|
|
@ -48,11 +52,13 @@ const ACTION_WEB_IDE = {
|
|||
text: 'Web IDE',
|
||||
attrs: {
|
||||
'data-qa-selector': 'webide_menu_item',
|
||||
'data-track-action': 'click_consolidated_edit_ide',
|
||||
'data-track-label': 'web_ide',
|
||||
},
|
||||
href: undefined,
|
||||
handle: expect.any(Function),
|
||||
tracking: {
|
||||
action: 'click_consolidated_edit',
|
||||
label: 'web_ide',
|
||||
},
|
||||
};
|
||||
const ACTION_WEB_IDE_CONFIRM_FORK = {
|
||||
...ACTION_WEB_IDE,
|
||||
|
|
@ -67,6 +73,10 @@ const ACTION_GITPOD = {
|
|||
attrs: {
|
||||
'data-qa-selector': 'gitpod_menu_item',
|
||||
},
|
||||
tracking: {
|
||||
action: 'click_consolidated_edit',
|
||||
label: 'gitpod',
|
||||
},
|
||||
};
|
||||
const ACTION_GITPOD_ENABLE = {
|
||||
...ACTION_GITPOD,
|
||||
|
|
@ -79,8 +89,10 @@ const ACTION_PIPELINE_EDITOR = {
|
|||
text: 'Edit in pipeline editor',
|
||||
attrs: {
|
||||
'data-qa-selector': 'pipeline_editor_menu_item',
|
||||
'data-track-action': 'click_consolidated_pipeline_editor',
|
||||
'data-track-label': 'pipeline_editor',
|
||||
},
|
||||
tracking: {
|
||||
action: 'click_consolidated_edit',
|
||||
label: 'pipeline_editor',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -88,6 +100,7 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
||||
function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
|
||||
const fakeApollo = createMockApollo([
|
||||
|
|
@ -116,6 +129,8 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
},
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
}
|
||||
|
||||
const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
|
|
@ -135,13 +150,12 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
handle: props.item.handle,
|
||||
attrs: {
|
||||
'data-qa-selector': attributes['data-qa-selector'],
|
||||
'data-track-action': attributes['data-track-action'],
|
||||
'data-track-label': attributes['data-track-label'],
|
||||
},
|
||||
};
|
||||
});
|
||||
const omitTrackingParams = (actions) => actions.map((action) => omit(action, 'tracking'));
|
||||
|
||||
it.each([
|
||||
describe.each([
|
||||
{
|
||||
props: {},
|
||||
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT],
|
||||
|
|
@ -231,10 +245,27 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
props: { showEditButton: false },
|
||||
expectedActions: [ACTION_WEB_IDE],
|
||||
},
|
||||
])('renders actions with appropriately for given props', ({ props, expectedActions }) => {
|
||||
createComponent(props);
|
||||
])('for a set of props', ({ props, expectedActions }) => {
|
||||
beforeEach(() => {
|
||||
createComponent(props);
|
||||
});
|
||||
|
||||
expect(getDropdownItemsAsData()).toEqual(expectedActions);
|
||||
it('renders the appropiate actions', () => {
|
||||
// omit tracking property because it is not included in the dropdown item
|
||||
expect(getDropdownItemsAsData()).toEqual(omitTrackingParams(expectedActions));
|
||||
});
|
||||
|
||||
describe('when an action is clicked', () => {
|
||||
it('tracks event', () => {
|
||||
expectedActions.forEach((action, index) => {
|
||||
findDisclosureDropdownItems().at(index).vm.$emit('action');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, action.tracking.action, {
|
||||
label: action.tracking.label,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('bubbles up shown and hidden events triggered by actions button component', () => {
|
||||
|
|
@ -272,11 +303,9 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
});
|
||||
|
||||
it('displays Pipeline Editor as the first action', () => {
|
||||
expect(getDropdownItemsAsData()).toEqual([
|
||||
ACTION_PIPELINE_EDITOR,
|
||||
ACTION_WEB_IDE,
|
||||
ACTION_GITPOD,
|
||||
]);
|
||||
expect(getDropdownItemsAsData()).toEqual(
|
||||
omitTrackingParams([ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_GITPOD]),
|
||||
);
|
||||
});
|
||||
|
||||
it('when web ide button is clicked it opens in a new tab', async () => {
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
import { GlFormSelect } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
|
||||
import ItemState from '~/work_items/components/item_state.vue';
|
||||
|
||||
describe('ItemState', () => {
|
||||
let wrapper;
|
||||
|
||||
const findLabel = () => wrapper.find('label').text();
|
||||
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
|
||||
const selectedValue = () => wrapper.find('option:checked').element.value;
|
||||
|
||||
const clickOpen = () => wrapper.findAll('option').at(0).setSelected();
|
||||
|
||||
const createComponent = ({ state = STATE_OPEN, disabled = false } = {}) => {
|
||||
wrapper = mount(ItemState, {
|
||||
propsData: {
|
||||
state,
|
||||
disabled,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('renders label and dropdown', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findLabel()).toBe('Status');
|
||||
expect(selectedValue()).toBe(STATE_OPEN);
|
||||
});
|
||||
|
||||
it('renders dropdown for closed', () => {
|
||||
createComponent({ state: STATE_CLOSED });
|
||||
|
||||
expect(selectedValue()).toBe(STATE_CLOSED);
|
||||
});
|
||||
|
||||
it('emits changed event', async () => {
|
||||
createComponent({ state: STATE_CLOSED });
|
||||
|
||||
await clickOpen();
|
||||
|
||||
expect(wrapper.emitted('changed')).toEqual([[STATE_OPEN]]);
|
||||
});
|
||||
|
||||
it('does not emits changed event if clicking selected value', async () => {
|
||||
createComponent({ state: STATE_OPEN });
|
||||
|
||||
await clickOpen();
|
||||
|
||||
expect(wrapper.emitted('changed')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('form select disabled prop', () => {
|
||||
describe.each`
|
||||
description | disabled | value
|
||||
${'when not disabled'} | ${false} | ${undefined}
|
||||
${'when disabled'} | ${true} | ${'disabled'}
|
||||
`('$description', ({ disabled, value }) => {
|
||||
it(`renders form select component with disabled=${value}`, () => {
|
||||
createComponent({ disabled });
|
||||
|
||||
expect(findFormSelect().attributes('disabled')).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -247,6 +247,14 @@ describe('Work item add note', () => {
|
|||
|
||||
expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
|
||||
});
|
||||
|
||||
it('emits error to parent when the comment form emits error', async () => {
|
||||
await createComponent({ isEditing: true, signedIn: true });
|
||||
const error = 'error';
|
||||
findCommentForm().vm.$emit('error', error);
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[error]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,11 @@ import { createMockDirective } from 'helpers/vue_mock_directive';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import * as autosave from '~/lib/utils/autosave';
|
||||
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
|
||||
import {
|
||||
STATE_OPEN,
|
||||
STATE_CLOSED,
|
||||
STATE_EVENT_REOPEN,
|
||||
STATE_EVENT_CLOSE,
|
||||
} from '~/work_items/constants';
|
||||
import { STATE_OPEN } from '~/work_items/constants';
|
||||
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
|
||||
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import { updateWorkItemMutationResponse, workItemQueryResponse } from 'jest/work_items/mock_data';
|
||||
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
@ -44,8 +37,7 @@ describe('Work item comment form component', () => {
|
|||
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
|
||||
const findInternalNoteCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findInternalNoteTooltipIcon = () => wrapper.findComponent(GlIcon);
|
||||
|
||||
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||
const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggleButton);
|
||||
|
||||
const createComponent = ({
|
||||
isSubmitting = false,
|
||||
|
|
@ -53,10 +45,8 @@ describe('Work item comment form component', () => {
|
|||
isNewDiscussion = false,
|
||||
workItemState = STATE_OPEN,
|
||||
workItemType = 'Task',
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemCommentForm, {
|
||||
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
|
||||
propsData: {
|
||||
workItemState,
|
||||
workItemId,
|
||||
|
|
@ -205,61 +195,20 @@ describe('Work item comment form component', () => {
|
|||
});
|
||||
|
||||
describe('when used as a top level/is a new discussion', () => {
|
||||
describe('cancel button text', () => {
|
||||
it.each`
|
||||
workItemState | workItemType | buttonText
|
||||
${STATE_OPEN} | ${'Task'} | ${'Close task'}
|
||||
${STATE_CLOSED} | ${'Task'} | ${'Reopen task'}
|
||||
${STATE_OPEN} | ${'Objective'} | ${'Close objective'}
|
||||
${STATE_CLOSED} | ${'Objective'} | ${'Reopen objective'}
|
||||
${STATE_OPEN} | ${'Key result'} | ${'Close key result'}
|
||||
${STATE_CLOSED} | ${'Key result'} | ${'Reopen key result'}
|
||||
`(
|
||||
'is "$buttonText" when "$workItemType" state is "$workItemState"',
|
||||
({ workItemState, workItemType, buttonText }) => {
|
||||
createComponent({ isNewDiscussion: true, workItemState, workItemType });
|
||||
|
||||
expect(findCancelButton().text()).toBe(buttonText);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Close/reopen button click', () => {
|
||||
it.each`
|
||||
workItemState | stateEvent
|
||||
${STATE_OPEN} | ${STATE_EVENT_CLOSE}
|
||||
${STATE_CLOSED} | ${STATE_EVENT_REOPEN}
|
||||
`(
|
||||
'calls mutation with "$stateEvent" when workItemState is "$workItemState"',
|
||||
async ({ workItemState, stateEvent }) => {
|
||||
createComponent({ isNewDiscussion: true, workItemState });
|
||||
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mutationSuccessHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemQueryResponse.data.workItem.id,
|
||||
stateEvent,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('emits an error message when the mutation was unsuccessful', async () => {
|
||||
createComponent({
|
||||
isNewDiscussion: true,
|
||||
mutationHandler: jest.fn().mockRejectedValue('Error!'),
|
||||
});
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
it('emits an error message when the mutation was unsuccessful', async () => {
|
||||
createComponent({
|
||||
isNewDiscussion: true,
|
||||
});
|
||||
findWorkItemToggleStateButton().vm.$emit(
|
||||
'error',
|
||||
'Something went wrong while updating the task. Please try again.',
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
['Something went wrong while updating the task. Please try again.'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
|
||||
import WorkItemState from '~/work_items/components/work_item_state.vue';
|
||||
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
||||
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
|
||||
|
||||
|
|
@ -13,7 +12,6 @@ describe('WorkItemAttributesWrapper component', () => {
|
|||
|
||||
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
|
||||
|
||||
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
|
||||
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
|
||||
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
|
||||
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
|
||||
|
|
@ -40,14 +38,6 @@ describe('WorkItemAttributesWrapper component', () => {
|
|||
});
|
||||
};
|
||||
|
||||
describe('work item state', () => {
|
||||
it('renders the work item state', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findWorkItemState().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignees widget', () => {
|
||||
it('renders assignees component when widget is returned from the API', () => {
|
||||
createComponent();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
|||
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
|
||||
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
|
||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
|
|
@ -47,6 +48,10 @@ describe('WorkItemDetail component', () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true });
|
||||
const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({
|
||||
canUpdate: false,
|
||||
canDelete: false,
|
||||
});
|
||||
const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({
|
||||
parent: null,
|
||||
canUpdate: true,
|
||||
|
|
@ -82,6 +87,7 @@ describe('WorkItemDetail component', () => {
|
|||
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
|
||||
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
|
||||
const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
|
||||
const findWorkItemStateToggleButton = () => wrapper.findComponent(WorkItemStateToggleButton);
|
||||
|
||||
const createComponent = ({
|
||||
isModal = false,
|
||||
|
|
@ -194,6 +200,25 @@ describe('WorkItemDetail component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('work item state toggle button', () => {
|
||||
describe.each`
|
||||
description | canUpdate
|
||||
${'when user cannot update'} | ${false}
|
||||
${'when user can update'} | ${true}
|
||||
`('$description', ({ canUpdate }) => {
|
||||
it(`${canUpdate ? 'is rendered' : 'is not rendered'}`, async () => {
|
||||
createComponent({
|
||||
handler: canUpdate
|
||||
? jest.fn().mockResolvedValue(workItemQueryResponse)
|
||||
: jest.fn().mockResolvedValue(workItemQueryResponseWithCannotUpdate),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemStateToggleButton().exists()).toBe(canUpdate);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close button', () => {
|
||||
describe('when isModal prop is false', () => {
|
||||
it('does not render', async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
|
||||
import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
|
||||
|
||||
describe('WorkItemStateBadge', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ workItemState = STATE_OPEN } = {}) => {
|
||||
wrapper = shallowMount(WorkItemStateBadge, {
|
||||
propsData: {
|
||||
workItemState,
|
||||
},
|
||||
});
|
||||
};
|
||||
const findStatusBadge = () => wrapper.findComponent(GlBadge);
|
||||
|
||||
it.each`
|
||||
state | icon | stateText | variant
|
||||
${STATE_OPEN} | ${'issue-open-m'} | ${'Open'} | ${'success'}
|
||||
${STATE_CLOSED} | ${'issue-close'} | ${'Closed'} | ${'info'}
|
||||
`(
|
||||
'renders icon as "$icon" and text as "$stateText" when the work item state is "$state"',
|
||||
({ state, icon, stateText, variant }) => {
|
||||
createComponent({ workItemState: state });
|
||||
|
||||
expect(findStatusBadge().props('icon')).toBe(icon);
|
||||
expect(findStatusBadge().props('variant')).toBe(variant);
|
||||
expect(findStatusBadge().text()).toBe(stateText);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { GlButton } 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 { mockTracking } from 'helpers/tracking_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ItemState from '~/work_items/components/item_state.vue';
|
||||
import WorkItemState from '~/work_items/components/work_item_state.vue';
|
||||
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
|
||||
import {
|
||||
STATE_OPEN,
|
||||
STATE_CLOSED,
|
||||
|
|
@ -16,59 +16,58 @@ import {
|
|||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
|
||||
|
||||
describe('WorkItemState component', () => {
|
||||
describe('Work Item State toggle button component', () => {
|
||||
let wrapper;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||
|
||||
const findItemState = () => wrapper.findComponent(ItemState);
|
||||
const findStateToggleButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
const { id } = workItemQueryResponse.data.workItem;
|
||||
|
||||
const createComponent = ({
|
||||
state = STATE_OPEN,
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
canUpdate = true,
|
||||
workItemState = STATE_OPEN,
|
||||
workItemType = 'Task',
|
||||
} = {}) => {
|
||||
const { id, workItemType } = workItemQueryResponse.data.workItem;
|
||||
wrapper = shallowMount(WorkItemState, {
|
||||
wrapper = shallowMount(WorkItemStateToggleButton, {
|
||||
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
|
||||
propsData: {
|
||||
workItem: {
|
||||
id,
|
||||
state,
|
||||
workItemType,
|
||||
},
|
||||
workItemId: id,
|
||||
workItemState,
|
||||
workItemType,
|
||||
canUpdate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('renders state', () => {
|
||||
createComponent();
|
||||
describe('work item State button text', () => {
|
||||
it.each`
|
||||
workItemState | workItemType | buttonText
|
||||
${STATE_OPEN} | ${'Task'} | ${'Close task'}
|
||||
${STATE_CLOSED} | ${'Task'} | ${'Reopen task'}
|
||||
${STATE_OPEN} | ${'Objective'} | ${'Close objective'}
|
||||
${STATE_CLOSED} | ${'Objective'} | ${'Reopen objective'}
|
||||
${STATE_OPEN} | ${'Key result'} | ${'Close key result'}
|
||||
${STATE_CLOSED} | ${'Key result'} | ${'Reopen key result'}
|
||||
`(
|
||||
'is "$buttonText" when "$workItemType" state is "$workItemState"',
|
||||
({ workItemState, workItemType, buttonText }) => {
|
||||
createComponent({ workItemState, workItemType });
|
||||
|
||||
expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state);
|
||||
});
|
||||
|
||||
describe('item state disabled prop', () => {
|
||||
describe.each`
|
||||
description | canUpdate | value
|
||||
${'when cannot update'} | ${false} | ${true}
|
||||
${'when can update'} | ${true} | ${false}
|
||||
`('$description', ({ canUpdate, value }) => {
|
||||
it(`renders item state component with disabled=${value}`, () => {
|
||||
createComponent({ canUpdate });
|
||||
|
||||
expect(findItemState().props('disabled')).toBe(value);
|
||||
});
|
||||
});
|
||||
expect(findStateToggleButton().text()).toBe(buttonText);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('when updating the state', () => {
|
||||
it('calls a mutation', () => {
|
||||
createComponent();
|
||||
|
||||
findItemState().vm.$emit('changed', STATE_CLOSED);
|
||||
findStateToggleButton().vm.$emit('click');
|
||||
|
||||
expect(mutationSuccessHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
|
|
@ -80,10 +79,10 @@ describe('WorkItemState component', () => {
|
|||
|
||||
it('calls a mutation with REOPEN', () => {
|
||||
createComponent({
|
||||
state: STATE_CLOSED,
|
||||
workItemState: STATE_CLOSED,
|
||||
});
|
||||
|
||||
findItemState().vm.$emit('changed', STATE_OPEN);
|
||||
findStateToggleButton().vm.$emit('click');
|
||||
|
||||
expect(mutationSuccessHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
|
|
@ -96,7 +95,7 @@ describe('WorkItemState component', () => {
|
|||
it('emits an error message when the mutation was unsuccessful', async () => {
|
||||
createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
|
||||
|
||||
findItemState().vm.$emit('changed', STATE_CLOSED);
|
||||
findStateToggleButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([
|
||||
|
|
@ -109,7 +108,7 @@ describe('WorkItemState component', () => {
|
|||
|
||||
createComponent();
|
||||
|
||||
findItemState().vm.$emit('changed', STATE_CLOSED);
|
||||
findStateToggleButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_state', {
|
||||
|
|
@ -441,6 +441,7 @@ bridges:
|
|||
- needs
|
||||
- resource
|
||||
- sourced_pipeline
|
||||
- deployment
|
||||
- resource_group
|
||||
- metadata
|
||||
- trigger_request
|
||||
|
|
|
|||
|
|
@ -17,5 +17,9 @@ RSpec.describe Gitlab::Redis::Cache do
|
|||
|
||||
expect(described_class.active_support_config[:expires_in]).to eq(1.day)
|
||||
end
|
||||
|
||||
it 'has a pool set to false' do
|
||||
expect(described_class.active_support_config[:pool]).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe CleanupConversionBigIntCiBuildNeedsSelfManaged, feature_category: :database do
|
||||
after do
|
||||
connection = described_class.new.connection
|
||||
connection.execute('ALTER TABLE ci_build_needs DROP COLUMN IF EXISTS id_convert_to_bigint')
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
context 'when it is GitLab.com, dev, or test but not JiHu' do
|
||||
before do
|
||||
# As we call `schema_migrate_down!` before each example, and for this migration
|
||||
# `#down` is same as `#up`, we need to ensure we start from the expected state.
|
||||
connection = described_class.new.connection
|
||||
connection.execute('ALTER TABLE ci_build_needs DROP COLUMN IF EXISTS id_convert_to_bigint')
|
||||
end
|
||||
|
||||
it 'does nothing' do
|
||||
# rubocop: disable RSpec/AnyInstanceOf
|
||||
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
|
||||
# rubocop: enable RSpec/AnyInstanceOf
|
||||
|
||||
ci_build_needs = table(:ci_build_needs)
|
||||
|
||||
disable_migrations_output do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
ci_build_needs.reset_column_information
|
||||
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
ci_build_needs.reset_column_information
|
||||
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a self-managed instance with the temporary column already dropped' do
|
||||
before do
|
||||
# As we call `schema_migrate_down!` before each example, and for this migration
|
||||
# `#down` is same as `#up`, we need to ensure we start from the expected state.
|
||||
connection = described_class.new.connection
|
||||
connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id TYPE bigint')
|
||||
connection.execute('ALTER TABLE ci_build_needs DROP COLUMN IF EXISTS id_convert_to_bigint')
|
||||
end
|
||||
|
||||
it 'does nothing' do
|
||||
# rubocop: disable RSpec/AnyInstanceOf
|
||||
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
|
||||
# rubocop: enable RSpec/AnyInstanceOf
|
||||
|
||||
ci_build_needs = table(:ci_build_needs)
|
||||
|
||||
migrate!
|
||||
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a self-managed instance with the temporary columns' do
|
||||
before do
|
||||
# As we call `schema_migrate_down!` before each example, and for this migration
|
||||
# `#down` is same as `#up`, we need to ensure we start from the expected state.
|
||||
connection = described_class.new.connection
|
||||
connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id TYPE bigint')
|
||||
connection.execute('ALTER TABLE ci_build_needs ADD COLUMN IF NOT EXISTS id_convert_to_bigint integer')
|
||||
end
|
||||
|
||||
it 'drops the temporary column' do
|
||||
# rubocop: disable RSpec/AnyInstanceOf
|
||||
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
|
||||
# rubocop: enable RSpec/AnyInstanceOf
|
||||
|
||||
ci_build_needs = table(:ci_build_needs)
|
||||
|
||||
disable_migrations_output do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
ci_build_needs.reset_column_information
|
||||
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
|
||||
expect(ci_build_needs.columns.find do |c|
|
||||
c.name == 'id_convert_to_bigint'
|
||||
end.sql_type).to eq('integer')
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
ci_build_needs.reset_column_information
|
||||
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
|
||||
expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
|
||||
let_it_be(:project) { create(:project, :in_group) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository, :in_group) }
|
||||
let_it_be(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
|
|
@ -27,6 +27,10 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
|
|||
|
||||
it_behaves_like 'a retryable job'
|
||||
|
||||
it_behaves_like 'a deployable job' do
|
||||
let(:job) { bridge }
|
||||
end
|
||||
|
||||
it 'has one downstream pipeline' do
|
||||
expect(bridge).to have_one(:sourced_pipeline)
|
||||
expect(bridge).to have_one(:downstream_pipeline)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
|
|||
it_behaves_like 'has ID tokens', :ci_build
|
||||
|
||||
it_behaves_like 'a retryable job'
|
||||
it_behaves_like 'a deployable job'
|
||||
|
||||
it_behaves_like 'a deployable job' do
|
||||
let(:job) { build }
|
||||
end
|
||||
|
||||
describe '.manual_actions' do
|
||||
let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Npm::PackagePresenter, feature_category: :package_registry do
|
||||
let_it_be(:metadata) do
|
||||
{
|
||||
name: 'foo',
|
||||
versions: { '1.0.0' => { 'dist' => { 'tarball' => 'http://localhost/tarball.tgz' } } },
|
||||
dist_tags: { 'latest' => '1.0.0' }
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.new(metadata) }
|
||||
|
||||
describe '#name' do
|
||||
it 'returns the name' do
|
||||
expect(subject.name).to eq('foo')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#versions' do
|
||||
it 'returns the versions' do
|
||||
expect(subject.versions).to eq({ '1.0.0' => { 'dist' => { 'tarball' => 'http://localhost/tarball.tgz' } } })
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dist_tags' do
|
||||
it 'returns the dist_tags' do
|
||||
expect(subject.dist_tags).to eq({ 'latest' => '1.0.0' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Deployments::CreateForJobService, feature_category: :continuous_delivery do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:service) { described_class.new }
|
||||
|
||||
it_behaves_like 'create deployment for job' do
|
||||
let(:factory_type) { :ci_build }
|
||||
end
|
||||
|
||||
it_behaves_like 'create deployment for job' do
|
||||
let(:factory_type) { :ci_bridge }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Environments::CreateForJobService, feature_category: :continuous_delivery do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
let(:service) { described_class.new }
|
||||
|
||||
it_behaves_like 'create environment for job' do
|
||||
let(:factory_type) { :ci_build }
|
||||
end
|
||||
|
||||
it_behaves_like 'create environment for job' do
|
||||
let(:factory_type) { :ci_bridge }
|
||||
end
|
||||
end
|
||||
|
|
@ -7,6 +7,8 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
shared_examples 'calling proper BuildFinishedWorker' do
|
||||
it 'calls Ci::BuildFinishedWorker' do
|
||||
skip unless described_class == ::Ci::Build
|
||||
|
||||
expect(Ci::BuildFinishedWorker).to receive(:perform_async)
|
||||
|
||||
subject
|
||||
|
|
@ -14,12 +16,12 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#outdated_deployment?' do
|
||||
subject { build.outdated_deployment? }
|
||||
subject { job.outdated_deployment? }
|
||||
|
||||
let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') }
|
||||
let(:job) { create(factory_type, :created, :with_deployment, project: project, pipeline: pipeline, environment: 'production') }
|
||||
|
||||
context 'when build has no environment' do
|
||||
let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) }
|
||||
context 'when job has no environment' do
|
||||
let(:job) { create(factory_type, :created, pipeline: pipeline, environment: nil) }
|
||||
|
||||
it { expect(subject).to be_falsey }
|
||||
end
|
||||
|
|
@ -32,27 +34,27 @@ RSpec.shared_examples 'a deployable job' do
|
|||
it { expect(subject).to be_falsey }
|
||||
end
|
||||
|
||||
context 'when build is not an outdated deployment' do
|
||||
context 'when job is not an outdated deployment' do
|
||||
before do
|
||||
allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
|
||||
allow(job.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
|
||||
end
|
||||
|
||||
it { expect(subject).to be_falsey }
|
||||
end
|
||||
|
||||
context 'when build is older than the latest deployment and still pending status' do
|
||||
context 'when job is older than the latest deployment and still pending status' do
|
||||
before do
|
||||
allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
|
||||
allow(job.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
|
||||
end
|
||||
|
||||
it { expect(subject).to be_truthy }
|
||||
it { expect(subject).to be_truthy} # rubocop: disable Layout/SpaceInsideBlockBraces
|
||||
end
|
||||
|
||||
context 'when build is older than the latest deployment but succeeded once' do
|
||||
let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') }
|
||||
context 'when job is older than the latest deployment but succeeded once' do
|
||||
let(:job) { create(factory_type, :success, :with_deployment, pipeline: pipeline, environment: 'production') }
|
||||
|
||||
before do
|
||||
allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
|
||||
allow(job.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns false for allowing rollback' do
|
||||
|
|
@ -72,10 +74,10 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe 'state transition as a deployable' do
|
||||
subject { build.send(event) }
|
||||
subject { job.send(event) }
|
||||
|
||||
let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) }
|
||||
let(:deployment) { build.deployment }
|
||||
let!(:job) { create(factory_type, :with_deployment, :start_review_app, status: :pending, pipeline: pipeline) }
|
||||
let(:deployment) { job.deployment }
|
||||
let(:environment) { deployment.environment }
|
||||
|
||||
before do
|
||||
|
|
@ -116,13 +118,13 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
context 'when deployment is already running state' do
|
||||
before do
|
||||
build.deployment.success!
|
||||
job.deployment.success!
|
||||
end
|
||||
|
||||
it 'does not change deployment status and tracks an error' do
|
||||
expect(Gitlab::ErrorTracking)
|
||||
.to receive(:track_exception).with(
|
||||
instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
|
||||
instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: job.id)
|
||||
|
||||
with_cross_database_modification_prevented do
|
||||
expect { subject }.not_to change { deployment.reload.status }
|
||||
|
|
@ -199,7 +201,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
# `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502
|
||||
context 'when transits from skipped to created to running' do
|
||||
before do
|
||||
build.skip!
|
||||
job.skip!
|
||||
end
|
||||
|
||||
context 'during skipped to created' do
|
||||
|
|
@ -216,8 +218,8 @@ RSpec.shared_examples 'a deployable job' do
|
|||
let(:event) { :run! }
|
||||
|
||||
before do
|
||||
build.process!
|
||||
build.enqueue!
|
||||
job.process!
|
||||
job.enqueue!
|
||||
end
|
||||
|
||||
it 'transitions to running and calls webhook' do
|
||||
|
|
@ -235,10 +237,10 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#on_stop' do
|
||||
subject { build.on_stop }
|
||||
subject { job.on_stop }
|
||||
|
||||
context 'when a job has a specification that it can be stopped from the other job' do
|
||||
let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) }
|
||||
let(:job) { create(factory_type, :start_review_app, pipeline: pipeline) }
|
||||
|
||||
it 'returns the other job name' do
|
||||
is_expected.to eq('stop_review_app')
|
||||
|
|
@ -246,7 +248,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
context 'when a job does not have environment information' do
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
let(:job) { create(factory_type, pipeline: pipeline) }
|
||||
|
||||
it 'returns nil' do
|
||||
is_expected.to be_nil
|
||||
|
|
@ -255,9 +257,9 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#environment_tier_from_options' do
|
||||
subject { build.environment_tier_from_options }
|
||||
subject { job.environment_tier_from_options }
|
||||
|
||||
let(:build) { Ci::Build.new(options: options) }
|
||||
let(:job) { Ci::Build.new(options: options) }
|
||||
let(:options) { { environment: { deployment_tier: 'production' } } }
|
||||
|
||||
it { is_expected.to eq('production') }
|
||||
|
|
@ -270,11 +272,11 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#environment_tier' do
|
||||
subject { build.environment_tier }
|
||||
subject { job.environment_tier }
|
||||
|
||||
let(:options) { { environment: { deployment_tier: 'production' } } }
|
||||
let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) }
|
||||
let(:build) { Ci::Build.new(options: options, environment: 'production', project: project) }
|
||||
let(:job) { Ci::Build.new(options: options, environment: 'production', project: project) }
|
||||
|
||||
it { is_expected.to eq('production') }
|
||||
|
||||
|
|
@ -295,11 +297,11 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
describe 'environment' do
|
||||
describe '#has_environment_keyword?' do
|
||||
subject { build.has_environment_keyword? }
|
||||
subject { job.has_environment_keyword? }
|
||||
|
||||
context 'when environment is defined' do
|
||||
before do
|
||||
build.update!(environment: 'review')
|
||||
job.update!(environment: 'review')
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
|
@ -307,7 +309,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
context 'when environment is not defined' do
|
||||
before do
|
||||
build.update!(environment: nil)
|
||||
job.update!(environment: nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
|
@ -315,12 +317,12 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#expanded_environment_name' do
|
||||
subject { build.expanded_environment_name }
|
||||
subject { job.expanded_environment_name }
|
||||
|
||||
context 'when environment uses $CI_COMMIT_REF_NAME' do
|
||||
let(:build) do
|
||||
let(:job) do
|
||||
create(
|
||||
:ci_build,
|
||||
factory_type,
|
||||
ref: 'master',
|
||||
environment: 'review/$CI_COMMIT_REF_NAME',
|
||||
pipeline: pipeline
|
||||
|
|
@ -331,9 +333,9 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
context 'when environment uses yaml_variables containing symbol keys' do
|
||||
let(:build) do
|
||||
let(:job) do
|
||||
create(
|
||||
:ci_build,
|
||||
factory_type,
|
||||
yaml_variables: [{ key: :APP_HOST, value: 'host' }],
|
||||
environment: 'review/$APP_HOST',
|
||||
pipeline: pipeline
|
||||
|
|
@ -344,13 +346,13 @@ RSpec.shared_examples 'a deployable job' do
|
|||
is_expected.to eq('review/host')
|
||||
end
|
||||
|
||||
context 'when build metadata has already persisted the expanded environment name' do
|
||||
context 'when job metadata has already persisted the expanded environment name' do
|
||||
before do
|
||||
build.metadata.expanded_environment_name = 'review/foo'
|
||||
job.metadata.expanded_environment_name = 'review/foo'
|
||||
end
|
||||
|
||||
it 'returns a persisted expanded environment name without a list of variables' do
|
||||
expect(build).not_to receive(:simple_variables)
|
||||
expect(job).not_to receive(:simple_variables)
|
||||
|
||||
is_expected.to eq('review/foo')
|
||||
end
|
||||
|
|
@ -358,8 +360,8 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
context 'when using persisted variables' do
|
||||
let(:build) do
|
||||
create(:ci_build, environment: 'review/x$CI_JOB_ID', pipeline: pipeline)
|
||||
let(:job) do
|
||||
create(factory_type, environment: 'review/x$CI_JOB_ID', pipeline: pipeline)
|
||||
end
|
||||
|
||||
it { is_expected.to eq('review/x') }
|
||||
|
|
@ -372,9 +374,9 @@ RSpec.shared_examples 'a deployable job' do
|
|||
]
|
||||
end
|
||||
|
||||
let(:build) do
|
||||
let(:job) do
|
||||
create(
|
||||
:ci_build,
|
||||
factory_type,
|
||||
ref: 'master',
|
||||
yaml_variables: yaml_variables,
|
||||
environment: 'review/$ENVIRONMENT_NAME',
|
||||
|
|
@ -387,9 +389,9 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#expanded_kubernetes_namespace' do
|
||||
let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) }
|
||||
let(:job) { create(factory_type, environment: environment, options: options, pipeline: pipeline) }
|
||||
|
||||
subject { build.expanded_kubernetes_namespace }
|
||||
subject { job.expanded_kubernetes_namespace }
|
||||
|
||||
context 'environment and namespace are not set' do
|
||||
let(:environment) { nil }
|
||||
|
|
@ -435,11 +437,11 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#deployment_job?' do
|
||||
subject { build.deployment_job? }
|
||||
subject { job.deployment_job? }
|
||||
|
||||
context 'when environment is defined' do
|
||||
before do
|
||||
build.update!(environment: 'review')
|
||||
job.update!(environment: 'review')
|
||||
end
|
||||
|
||||
context 'no action is defined' do
|
||||
|
|
@ -448,7 +450,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
context 'and start action is defined' do
|
||||
before do
|
||||
build.update!(options: { environment: { action: 'start' } })
|
||||
job.update!(options: { environment: { action: 'start' } })
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
|
@ -457,7 +459,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
context 'when environment is not defined' do
|
||||
before do
|
||||
build.update!(environment: nil)
|
||||
job.update!(environment: nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
|
@ -465,11 +467,11 @@ RSpec.shared_examples 'a deployable job' do
|
|||
end
|
||||
|
||||
describe '#stops_environment?' do
|
||||
subject { build.stops_environment? }
|
||||
subject { job.stops_environment? }
|
||||
|
||||
context 'when environment is defined' do
|
||||
before do
|
||||
build.update!(environment: 'review')
|
||||
job.update!(environment: 'review')
|
||||
end
|
||||
|
||||
context 'no action is defined' do
|
||||
|
|
@ -478,7 +480,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
context 'and stop action is defined' do
|
||||
before do
|
||||
build.update!(options: { environment: { action: 'stop' } })
|
||||
job.update!(options: { environment: { action: 'stop' } })
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
|
@ -487,7 +489,7 @@ RSpec.shared_examples 'a deployable job' do
|
|||
|
||||
context 'when environment is not defined' do
|
||||
before do
|
||||
build.update!(environment: nil)
|
||||
job.update!(environment: nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
|
@ -500,19 +502,19 @@ RSpec.shared_examples 'a deployable job' do
|
|||
create(:environment, project: project, name: "foo-#{project.default_branch}")
|
||||
end
|
||||
|
||||
subject { build.persisted_environment }
|
||||
subject { job.persisted_environment }
|
||||
|
||||
context 'when referenced literally' do
|
||||
let(:build) do
|
||||
create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}")
|
||||
let(:job) do
|
||||
create(factory_type, pipeline: pipeline, environment: "foo-#{project.default_branch}")
|
||||
end
|
||||
|
||||
it { is_expected.to eq(environment) }
|
||||
end
|
||||
|
||||
context 'when referenced with a variable' do
|
||||
let(:build) do
|
||||
create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
|
||||
let(:job) do
|
||||
create(factory_type, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
|
||||
end
|
||||
|
||||
it { is_expected.to eq(environment) }
|
||||
|
|
@ -522,11 +524,11 @@ RSpec.shared_examples 'a deployable job' do
|
|||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when build has a stop environment' do
|
||||
let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
|
||||
context 'when job has a stop environment' do
|
||||
let(:job) { create(factory_type, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
|
||||
|
||||
it 'expands environment name' do
|
||||
expect(build).to receive(:expanded_environment_name).and_call_original
|
||||
expect(job).to receive(:expanded_environment_name).and_call_original
|
||||
|
||||
is_expected.to eq(environment)
|
||||
end
|
||||
|
|
@ -538,39 +540,43 @@ RSpec.shared_examples 'a deployable job' do
|
|||
allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf
|
||||
end
|
||||
|
||||
context 'when build is a last deployment' do
|
||||
let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: build.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
|
||||
context 'when job is a last deployment' do
|
||||
let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: job.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
|
||||
|
||||
it { expect(build.deployment_status).to eq(:last) }
|
||||
it { expect(job.deployment_status).to eq(:last) }
|
||||
end
|
||||
|
||||
context 'when there is a newer build with deployment' do
|
||||
let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: build.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
|
||||
context 'when there is a newer job with deployment' do
|
||||
let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: job.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
|
||||
let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
|
||||
|
||||
it { expect(build.deployment_status).to eq(:out_of_date) }
|
||||
it { expect(job.deployment_status).to eq(:out_of_date) }
|
||||
end
|
||||
|
||||
context 'when build with deployment has failed' do
|
||||
let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: build.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
|
||||
context 'when job with deployment has failed' do
|
||||
let(:job) { create(factory_type, :failed, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: job.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
|
||||
|
||||
it { expect(build.deployment_status).to eq(:failed) }
|
||||
it { expect(job.deployment_status).to eq(:failed) }
|
||||
end
|
||||
|
||||
context 'when build with deployment is running' do
|
||||
let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: build.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
|
||||
context 'when job with deployment is running' do
|
||||
let(:job) { create(factory_type, environment: 'production', pipeline: pipeline) }
|
||||
let(:environment) { create(:environment, name: 'production', project: job.project) }
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
|
||||
|
||||
it { expect(build.deployment_status).to eq(:creating) }
|
||||
it { expect(job.deployment_status).to eq(:creating) }
|
||||
end
|
||||
end
|
||||
|
||||
def factory_type
|
||||
described_class.name.underscore.tr('/', '_')
|
||||
end
|
||||
end
|
||||
# rubocop:enable Layout/LineLength
|
||||
# rubocop:enable RSpec/ContextWording
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous_delivery do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:service) { described_class.new }
|
||||
|
||||
RSpec.shared_examples 'create deployment for job' do
|
||||
describe '#execute' do
|
||||
subject { service.execute(build) }
|
||||
|
||||
context 'with a deployment job' do
|
||||
let!(:build) { create(:ci_build, :start_review_app, project: project) }
|
||||
let!(:build) { create(factory_type, :start_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
|
||||
|
||||
it 'creates a deployment record' do
|
||||
|
|
@ -43,7 +36,7 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
end
|
||||
|
||||
context 'when the corresponding environment does not exist' do
|
||||
let!(:environment) {}
|
||||
let!(:environment) {} # rubocop:disable Lint/EmptyBlock
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
|
@ -54,7 +47,7 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
end
|
||||
|
||||
context 'with a teardown job' do
|
||||
let!(:build) { create(:ci_build, :stop_review_app, project: project) }
|
||||
let!(:build) { create(factory_type, :stop_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, name: build.expanded_environment_name) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
|
|
@ -65,7 +58,7 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
end
|
||||
|
||||
context 'with a normal job' do
|
||||
let!(:build) { create(:ci_build, project: project) }
|
||||
let!(:build) { create(factory_type, project: project) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
|
@ -74,18 +67,10 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
end
|
||||
end
|
||||
|
||||
context 'with a bridge' do
|
||||
let!(:build) { create(:ci_bridge, project: project) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build has environment attribute' do
|
||||
let!(:build) do
|
||||
create(:ci_build, environment: 'production', project: project,
|
||||
options: { environment: { name: 'production', **kubernetes_options } })
|
||||
create(factory_type, environment: 'production', project: project,
|
||||
options: { environment: { name: 'production', **kubernetes_options } }) # rubocop:disable Layout/ArgumentAlignment
|
||||
end
|
||||
|
||||
let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
|
||||
|
|
@ -129,8 +114,8 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
end
|
||||
|
||||
context 'when build already has deployment' do
|
||||
let!(:build) { create(:ci_build, :with_deployment, project: project, environment: 'production') }
|
||||
let!(:environment) {}
|
||||
let!(:build) { create(factory_type, :with_deployment, project: project, environment: 'production') }
|
||||
let!(:environment) {} # rubocop:disable Lint/EmptyBlock
|
||||
|
||||
it 'returns the persisted deployment' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
|
@ -147,8 +132,8 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
|
||||
with_them do
|
||||
let!(:build) do
|
||||
create(:ci_build, environment: 'production', project: project,
|
||||
options: { environment: { name: 'production', action: action } })
|
||||
create(factory_type, environment: 'production', project: project,
|
||||
options: { environment: { name: 'production', action: action } }) # rubocop:disable Layout/ArgumentAlignment
|
||||
end
|
||||
|
||||
it 'returns nothing' do
|
||||
|
|
@ -158,7 +143,7 @@ RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous
|
|||
end
|
||||
|
||||
context 'when build does not have environment attribute' do
|
||||
let!(:build) { create(:ci_build, project: project) }
|
||||
let!(:build) { create(factory_type, project: project) }
|
||||
|
||||
it 'returns nothing' do
|
||||
is_expected.to be_nil
|
||||
|
|
@ -1,15 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Environments::CreateForBuildService, feature_category: :continuous_delivery do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
let!(:job) { build(:ci_build, project: project, pipeline: pipeline, **attributes) }
|
||||
let(:service) { described_class.new }
|
||||
let(:merge_request) {}
|
||||
RSpec.shared_examples 'create environment for job' do
|
||||
let!(:job) { build(factory_type, project: project, pipeline: pipeline, **attributes) }
|
||||
let(:merge_request) {} # rubocop:disable Lint/EmptyBlock
|
||||
|
||||
describe '#execute' do
|
||||
subject { service.execute(job) }
|
||||
|
|
@ -218,7 +211,7 @@ RSpec.describe Environments::CreateForBuildService, feature_category: :continuou
|
|||
|
||||
context 'when a pipeline contains a deployment job' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project, merge_request: merge_request) }
|
||||
let!(:job) { build(:ci_build, :start_review_app, project: project, pipeline: pipeline) }
|
||||
let!(:job) { build(factory_type, :start_review_app, project: project, pipeline: pipeline) }
|
||||
|
||||
context 'and the environment does not exist' do
|
||||
it 'creates the environment specified by the job' do
|
||||
|
|
@ -280,7 +273,7 @@ RSpec.describe Environments::CreateForBuildService, feature_category: :continuou
|
|||
end
|
||||
|
||||
context 'when a pipeline contains a teardown job' do
|
||||
let!(:job) { build(:ci_build, :stop_review_app, project: project) }
|
||||
let!(:job) { build(factory_type, :stop_review_app, project: project) }
|
||||
|
||||
it 'ensures environment existence for the job' do
|
||||
expect { subject }.to change { Environment.count }.by(1)
|
||||
|
|
@ -292,7 +285,7 @@ RSpec.describe Environments::CreateForBuildService, feature_category: :continuou
|
|||
end
|
||||
|
||||
context 'when a pipeline does not contain a deployment job' do
|
||||
let!(:job) { build(:ci_build, project: project) }
|
||||
let!(:job) { build(factory_type, project: project) }
|
||||
|
||||
it 'does not create any environments' do
|
||||
expect { subject }.not_to change { Environment.count }
|
||||
|
|
@ -15,17 +15,17 @@ RSpec.shared_examples 'work items title' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'work items status' do
|
||||
let(:state_selector) { '[data-testid="work-item-state-select"]' }
|
||||
RSpec.shared_examples 'work items toggle status button' do
|
||||
let(:state_button) { '[data-testid="work-item-state-toggle"]' }
|
||||
|
||||
it 'successfully shows and changes the status of the work item' do
|
||||
expect(find(state_selector)).to have_content 'Open'
|
||||
expect(find(state_button, match: :first)).to have_content 'Close'
|
||||
|
||||
find(state_selector).select("Closed")
|
||||
find(state_button, match: :first).click
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find(state_selector)).to have_content 'Closed'
|
||||
expect(find(state_button, match: :first)).to have_content 'Reopen'
|
||||
expect(work_item.reload.state).to eq('closed')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue