Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-31 18:11:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-31 18:11:19 +0300
commit7f98cf51aa49426815fe943a5a8dae2a96b59c01 (patch)
tree89047dcbc3bdddcc28895c1ac950cf080b363d1b
parent74ecf758e30be848144df1672b5080a29fafbc0a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/blob/line_highlighter.js2
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue25
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql8
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql16
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue25
-rw-r--r--app/assets/javascripts/issues/list/constants.js32
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql12
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql28
-rw-r--r--app/assets/javascripts/issues/list/utils.js4
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue158
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue73
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue28
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue71
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue81
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_badge.vue41
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue (renamed from app/assets/javascripts/work_items/components/work_item_state.vue)71
-rw-r--r--app/helpers/issues_helper.rb3
-rw-r--r--app/models/ci/bridge.rb15
-rw-r--r--app/presenters/packages/npm/package_presenter.rb27
-rw-r--r--app/services/ci/retry_job_service.rb2
-rw-r--r--app/services/deployments/create_for_job_service.rb (renamed from app/services/deployments/create_for_build_service.rb)8
-rw-r--r--app/services/environments/create_for_job_service.rb (renamed from app/services/environments/create_for_build_service.rb)6
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb2
-rw-r--r--config/events/20211215022206_default_web_ide_click_consolidated_edit_ide.yml20
-rw-r--r--config/events/20230727180523_default_click_consolidated_edit.yml (renamed from config/events/20211215022206_default_edit_click_consolidated_edit.yml)16
-rw-r--r--config/feature_flags/development/issue_date_filter.yml8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/post_migrate/20230724123547_cleanup_conversion_big_int_ci_build_needs_self_managed.rb31
-rw-r--r--db/post_migrate/20230731100513_add_index_on_vulnerability_reads_for_filtering.rb23
-rw-r--r--db/schema_migrations/202307241235471
-rw-r--r--db/schema_migrations/202307311005131
-rw-r--r--db/structure.sql2
-rw-r--r--doc/.vale/gitlab/CommandStringsQuoted.yml14
-rw-r--r--doc/.vale/gitlab/CurlStringsQuoted.yml13
-rw-r--r--doc/administration/settings/sign_up_restrictions.md54
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/ci/docker/using_docker_build.md6
-rw-r--r--doc/development/database/database_lab.md6
-rw-r--r--doc/tutorials/install_gitlab_single_node/index.md2
-rw-r--r--doc/user/project/ml/experiment_tracking/index.md2
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb9
-rw-r--r--lib/gitlab/ci/pipeline/chain/ensure_environments.rb2
-rw-r--r--lib/gitlab/redis/cache.rb5
-rw-r--r--lib/gitlab/redis/feature_flag.rb1
-rw-r--r--lib/gitlab/redis/repository_cache.rb1
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/factories/ci/bridge.rb4
-rw-r--r--spec/factories/ci/builds.rb134
-rw-r--r--spec/factories/ci/deployable.rb141
-rw-r--r--spec/features/projects/work_items/work_item_spec.rb2
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js5
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js5
-rw-r--r--spec/frontend/super_sidebar/components/menu_section_spec.js21
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js49
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js65
-rw-r--r--spec/frontend/work_items/components/item_state_spec.js66
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js8
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js79
-rw-r--r--spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js10
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js25
-rw-r--r--spec/frontend/work_items/components/work_item_state_badge_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_state_toggle_button_spec.js (renamed from spec/frontend/work_items/components/work_item_state_spec.js)69
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/redis/cache_spec.rb4
-rw-r--r--spec/migrations/cleanup_conversion_big_int_ci_build_needs_self_managed_spec.rb107
-rw-r--r--spec/models/ci/bridge_spec.rb6
-rw-r--r--spec/models/ci/build_spec.rb5
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb33
-rw-r--r--spec/services/deployments/create_for_job_service_spec.rb18
-rw-r--r--spec/services/environments/create_for_job_service_spec.rb19
-rw-r--r--spec/support/shared_examples/ci/deployable_shared_examples.rb166
-rw-r--r--spec/support/shared_examples/deployments/create_for_job_shared_examples.rb (renamed from spec/services/deployments/create_for_build_service_spec.rb)39
-rw-r--r--spec/support/shared_examples/environments/create_for_job_shared_examples.rb (renamed from spec/services/environments/create_for_build_service_spec.rb)19
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb10
84 files changed, 1361 insertions, 839 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 3a55d0f6747..e20f211bd33 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -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:
diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js
index 1ec204b4034..4258d16b69f 100644
--- a/app/assets/javascripts/blob/line_highlighter.js
+++ b/app/assets/javascripts/blob/line_highlighter.js
@@ -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));
}
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index eb73f8e0182..9febebf7e55 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -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));
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
index 5c331fe95e2..51e38d44c85 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -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
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql
index b36f546e4ab..a91f15f0c04 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues_counts.query.graphql
@@ -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
}
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index db3008c17a4..c4e906be94f 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -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) {
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index a7933803ed4..85e300b6474 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -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',
+ },
+ },
+ },
};
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index 1018848fb53..23410ea0f81 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -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 {
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
index fdb0eeb5970..7953dc423b6 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
@@ -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
}
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 04ce9a4969b..37df0c8f9ff 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -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)) {
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 37a6ab0122b..4e26750efc2 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
new file mode 100644
index 00000000000..a57034b5bb7
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 52a2f42f8ec..db48e68e8f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -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);
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 5b98af8c732..39fd3d62c3b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -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';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue
new file mode 100644
index 00000000000..4446886dc88
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/date_token.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 756d9b42e99..79d14b5f2d0 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -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)"
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
deleted file mode 100644
index 2100cc67c8c..00000000000
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ /dev/null
@@ -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>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index c330eccb186..66ad3d50287 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -265,6 +265,7 @@ export default {
:comment-button-text="commentButtonText"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
+ @error="$emit('error', $event)"
/>
<textarea
v-else
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index c317ec48732..b143c529014 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -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>
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index c727075eaac..139f0f7919c 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -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"
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 78a86aa49a4..af5293ebe91 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -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" />
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index dc4065f9812..238df236b6b 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -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"
diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
new file mode 100644
index 00000000000..1d1bc7352b1
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -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>
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
index 3880ae25c8c..0ea30845466 100644
--- a/app/assets/javascripts/work_items/components/work_item_state.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
@@ -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>
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index bbd8d2dd0a9..6a58a453a19 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -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
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 77fb7238298..895ff937abe 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -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
diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb
deleted file mode 100644
index 42f61182ab8..00000000000
--- a/app/presenters/packages/npm/package_presenter.rb
+++ /dev/null
@@ -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
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index e3cbba6de23..14ea09f17a0 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -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)
diff --git a/app/services/deployments/create_for_build_service.rb b/app/services/deployments/create_for_job_service.rb
index b58aa50a66f..fe632794d99 100644
--- a/app/services/deployments/create_for_build_service.rb
+++ b/app/services/deployments/create_for_job_service.rb
@@ -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(
diff --git a/app/services/environments/create_for_build_service.rb b/app/services/environments/create_for_job_service.rb
index ff4da212002..2a225c1ac91 100644
--- a/app/services/environments/create_for_build_service.rb
+++ b/app/services/environments/create_for_job_service.rb
@@ -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)
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index 52a4f075cf0..067dbb7492f 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -32,7 +32,7 @@ module Ci
end
def create_deployment(build)
- ::Deployments::CreateForBuildService.new.execute(build)
+ ::Deployments::CreateForJobService.new.execute(build)
end
end
end
diff --git a/config/events/20211215022206_default_web_ide_click_consolidated_edit_ide.yml b/config/events/20211215022206_default_web_ide_click_consolidated_edit_ide.yml
deleted file mode 100644
index 45b621cb404..00000000000
--- a/config/events/20211215022206_default_web_ide_click_consolidated_edit_ide.yml
+++ /dev/null
@@ -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
diff --git a/config/events/20211215022206_default_edit_click_consolidated_edit.yml b/config/events/20230727180523_default_click_consolidated_edit.yml
index ffa88b0a384..06da6ac1257 100644
--- a/config/events/20211215022206_default_edit_click_consolidated_edit.yml
+++ b/config/events/20230727180523_default_click_consolidated_edit.yml
@@ -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
+
diff --git a/config/feature_flags/development/issue_date_filter.yml b/config/feature_flags/development/issue_date_filter.yml
new file mode 100644
index 00000000000..1b6cb2c4bed
--- /dev/null
+++ b/config/feature_flags/development/issue_date_filter.yml
@@ -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
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 31b058e9e85..bc5a2ec1d24 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -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
diff --git a/db/post_migrate/20230724123547_cleanup_conversion_big_int_ci_build_needs_self_managed.rb b/db/post_migrate/20230724123547_cleanup_conversion_big_int_ci_build_needs_self_managed.rb
new file mode 100644
index 00000000000..5fbb5bd47cd
--- /dev/null
+++ b/db/post_migrate/20230724123547_cleanup_conversion_big_int_ci_build_needs_self_managed.rb
@@ -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
diff --git a/db/post_migrate/20230731100513_add_index_on_vulnerability_reads_for_filtering.rb b/db/post_migrate/20230731100513_add_index_on_vulnerability_reads_for_filtering.rb
new file mode 100644
index 00000000000..bbed1e60d35
--- /dev/null
+++ b/db/post_migrate/20230731100513_add_index_on_vulnerability_reads_for_filtering.rb
@@ -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
diff --git a/db/schema_migrations/20230724123547 b/db/schema_migrations/20230724123547
new file mode 100644
index 00000000000..b29f625de7e
--- /dev/null
+++ b/db/schema_migrations/20230724123547
@@ -0,0 +1 @@
+658cb25d5add4ad4e26d7baef6759f8512fa0244dd347b0522ad75ac496c9965 \ No newline at end of file
diff --git a/db/schema_migrations/20230731100513 b/db/schema_migrations/20230731100513
new file mode 100644
index 00000000000..4f83eaeb294
--- /dev/null
+++ b/db/schema_migrations/20230731100513
@@ -0,0 +1 @@
+a85f3b493021cc27079dc07fe0ba5f11eeeca9798cf6ccdc60f7f7f7eae049af \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index ba104d2623d..2a297259356 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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));
diff --git a/doc/.vale/gitlab/CommandStringsQuoted.yml b/doc/.vale/gitlab/CommandStringsQuoted.yml
new file mode 100644
index 00000000000..531595ed10d
--- /dev/null
+++ b/doc/.vale/gitlab/CommandStringsQuoted.yml
@@ -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]*'
diff --git a/doc/.vale/gitlab/CurlStringsQuoted.yml b/doc/.vale/gitlab/CurlStringsQuoted.yml
deleted file mode 100644
index efe7aa23832..00000000000
--- a/doc/.vale/gitlab/CurlStringsQuoted.yml
+++ /dev/null
@@ -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 [^"]+://.*'
diff --git a/doc/administration/settings/sign_up_restrictions.md b/doc/administration/settings/sign_up_restrictions.md
index f213794eb75..719f205ba32 100644
--- a/doc/administration/settings/sign_up_restrictions.md
+++ b/doc/administration/settings/sign_up_restrictions.md
@@ -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`).
+
+### Set a user cap
+
+Set a user cap to restrict the number of users who can sign up without administrator approval.
-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.
+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`).
-### Set the user cap number
+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 so that the number of new users who can sign up without
+administrator approval is not restricted.
-## Remove the user cap
+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
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 21c984a135f..a4f548d5e2a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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`. |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index f55ba5516f9..2ba1332e339 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -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"
```
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 5e8f2b36620..97b4b6a428b 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -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" \
diff --git a/doc/development/database/database_lab.md b/doc/development/database/database_lab.md
index 357133d8bca..7edb8ab4de5 100644
--- a/doc/development/database/database_lab.md
+++ b/doc/development/database/database_lab.md
@@ -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
diff --git a/doc/tutorials/install_gitlab_single_node/index.md b/doc/tutorials/install_gitlab_single_node/index.md
index cdc05c1f1a9..5ed98ccbefc 100644
--- a/doc/tutorials/install_gitlab_single_node/index.md
+++ b/doc/tutorials/install_gitlab_single_node/index.md
@@ -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>.
diff --git a/doc/user/project/ml/experiment_tracking/index.md b/doc/user/project/ml/experiment_tracking/index.md
index 4e9e736f067..4f4f05be126 100644
--- a/doc/user/project/ml/experiment_tracking/index.md
+++ b/doc/user/project/ml/experiment_tracking/index.md
@@ -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**.
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index ec20440f013..a045a3d4828 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -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
diff --git a/lib/gitlab/ci/pipeline/chain/ensure_environments.rb b/lib/gitlab/ci/pipeline/chain/ensure_environments.rb
index ebea6a538ef..3ff9aff68ac 100644
--- a/lib/gitlab/ci/pipeline/chain/ensure_environments.rb
+++ b/lib/gitlab/ci/pipeline/chain/ensure_environments.rb
@@ -16,7 +16,7 @@ module Gitlab
private
def ensure_environment(build)
- ::Environments::CreateForBuildService.new.execute(build)
+ ::Environments::CreateForJobService.new.execute(build)
end
end
end
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index 60944268f91..6121555e435 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -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
diff --git a/lib/gitlab/redis/feature_flag.rb b/lib/gitlab/redis/feature_flag.rb
index 441ff669035..395805792d7 100644
--- a/lib/gitlab/redis/feature_flag.rb
+++ b/lib/gitlab/redis/feature_flag.rb
@@ -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
diff --git a/lib/gitlab/redis/repository_cache.rb b/lib/gitlab/redis/repository_cache.rb
index 966c6584aa5..6d0c35a6829 100644
--- a/lib/gitlab/redis/repository_cache.rb
+++ b/lib/gitlab/redis/repository_cache.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 96fb9d44511..f714e09878c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
index 49ac74f6f86..c5c0634994d 100644
--- a/spec/factories/ci/bridge.rb
+++ b/spec/factories/ci/bridge.rb
@@ -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 }
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index dc75e17499c..7325ab30989 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -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
diff --git a/spec/factories/ci/deployable.rb b/spec/factories/ci/deployable.rb
new file mode 100644
index 00000000000..15b37e44e07
--- /dev/null
+++ b/spec/factories/ci/deployable.rb
@@ -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
diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb
index e996a76b1c5..618d3e2efd0 100644
--- a/spec/features/projects/work_items/work_item_spec.rb
+++ b/spec/features/projects/work_items/work_item_spec.rb
@@ -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
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 148c6230b9f..4686a4fe0c4 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -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 },
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 2a0b002ae1f..de027a21c8f 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -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 },
diff --git a/spec/frontend/super_sidebar/components/menu_section_spec.js b/spec/frontend/super_sidebar/components/menu_section_spec.js
index dd729d8fd6a..288e317d4c6 100644
--- a/spec/frontend/super_sidebar/components/menu_section_spec.js
+++ b/spec/frontend/super_sidebar/components/menu_section_spec.js
@@ -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);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 8d3bf3dd3be..d5e04c666e0 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -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',
);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js
new file mode 100644
index 00000000000..56a59790210
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js
@@ -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']],
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 063f1d793ef..56d89d428f7 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -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);
+ });
+
+ 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(getDropdownItemsAsData()).toEqual(expectedActions);
+ 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 () => {
diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js
deleted file mode 100644
index c3bdbfe030e..00000000000
--- a/spec/frontend/work_items/components/item_state_spec.js
+++ /dev/null
@@ -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);
- });
- });
- });
-});
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index e6d20dcb0d9..4b1b7b27ad9 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -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]]);
+ });
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index 6c00d52aac5..dd88f34ae4f 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -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,
+ });
+ findWorkItemToggleStateButton().vm.$emit(
+ 'error',
+ 'Something went wrong while updating the task. Please try again.',
);
- 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();
+ await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([
- ['Something went wrong while updating the task. Please try again.'],
- ]);
- });
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
});
});
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
index ba9af7b2b68..8b7e04854af 100644
--- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -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();
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 7ceae935d2d..84d9dba93ae 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -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 () => {
diff --git a/spec/frontend/work_items/components/work_item_state_badge_spec.js b/spec/frontend/work_items/components/work_item_state_badge_spec.js
new file mode 100644
index 00000000000..888d712cc5a
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_state_badge_spec.js
@@ -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);
+ },
+ );
+});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
index d1262057c73..c0b206e5da4 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
@@ -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();
-
- 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);
- });
- });
+ 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(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', {
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7dc969f274a..952f3bb6ad8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -441,6 +441,7 @@ bridges:
- needs
- resource
- sourced_pipeline
+- deployment
- resource_group
- metadata
- trigger_request
diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb
index b7b4ba0eb2f..a48bde5e4ab 100644
--- a/spec/lib/gitlab/redis/cache_spec.rb
+++ b/spec/lib/gitlab/redis/cache_spec.rb
@@ -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
diff --git a/spec/migrations/cleanup_conversion_big_int_ci_build_needs_self_managed_spec.rb b/spec/migrations/cleanup_conversion_big_int_ci_build_needs_self_managed_spec.rb
new file mode 100644
index 00000000000..03a8356c721
--- /dev/null
+++ b/spec/migrations/cleanup_conversion_big_int_ci_build_needs_self_managed_spec.rb
@@ -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
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index b284cacf354..a5366a1a67a 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -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)
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index bfd6360527f..351010fca18 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -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) }
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
deleted file mode 100644
index fe4773a9cad..00000000000
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/services/deployments/create_for_job_service_spec.rb b/spec/services/deployments/create_for_job_service_spec.rb
new file mode 100644
index 00000000000..f5a30078b2d
--- /dev/null
+++ b/spec/services/deployments/create_for_job_service_spec.rb
@@ -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
diff --git a/spec/services/environments/create_for_job_service_spec.rb b/spec/services/environments/create_for_job_service_spec.rb
new file mode 100644
index 00000000000..0d459301f19
--- /dev/null
+++ b/spec/services/environments/create_for_job_service_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb
index 682e408566b..367e939e69c 100644
--- a/spec/support/shared_examples/ci/deployable_shared_examples.rb
+++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb
@@ -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
diff --git a/spec/services/deployments/create_for_build_service_spec.rb b/spec/support/shared_examples/deployments/create_for_job_shared_examples.rb
index c07fc07cfbf..52a44cdefef 100644
--- a/spec/services/deployments/create_for_build_service_spec.rb
+++ b/spec/support/shared_examples/deployments/create_for_job_shared_examples.rb
@@ -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
diff --git a/spec/services/environments/create_for_build_service_spec.rb b/spec/support/shared_examples/environments/create_for_job_shared_examples.rb
index 223401a243d..3acdc8c142f 100644
--- a/spec/services/environments/create_for_build_service_spec.rb
+++ b/spec/support/shared_examples/environments/create_for_job_shared_examples.rb
@@ -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 }
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 4c15b682458..97407d93cb0 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -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