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-03-21 15:08:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-21 15:08:46 +0300
commit7f521d27811b472c43203ed3d1bde4460a617f89 (patch)
tree47f1a10b776991e86c6db002bc6e03e83acc356a
parent83e3316a189d3b709b23af30647b5f9ea5377bac (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml7
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue8
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/index.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue40
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue2
-rw-r--r--app/assets/javascripts/lib/utils/error_message.js31
-rw-r--r--app/assets/javascripts/milestones/index.js3
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue14
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue102
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js30
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/search_results.vue74
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/mock_data.js54
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue3
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue27
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss4
-rw-r--r--app/assets/stylesheets/framework/sortable.scss9
-rw-r--r--app/assets/stylesheets/framework/system_messages.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss36
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sidebars_helper.rb41
-rw-r--r--app/models/pages/lookup_path.rb6
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml11
-rw-r--r--app/views/authentication/_register.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml5
-rw-r--r--app/views/projects/_flash_messages.html.haml2
-rw-r--r--app/views/projects/_terraform_banner.html.haml2
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/empty.html.haml1
-rw-r--r--app/views/projects/harbor/repositories/index.html.haml1
-rw-r--r--app/views/projects/hook_logs/show.html.haml1
-rw-r--r--app/views/projects/hooks/edit.html.haml1
-rw-r--r--app/views/projects/hooks/index.html.haml1
-rw-r--r--app/views/projects/imports/show.html.haml1
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/packages/infrastructure_registry/index.html.haml1
-rw-r--r--app/views/projects/packages/infrastructure_registry/show.html.haml1
-rw-r--r--app/views/projects/packages/packages/index.html.haml1
-rw-r--r--app/views/projects/registry/repositories/index.html.haml1
-rw-r--r--app/views/projects/security/configuration/show.html.haml1
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml1
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml1
-rw-r--r--app/views/projects/settings/integrations/edit.html.haml1
-rw-r--r--app/views/projects/settings/integrations/index.html.haml1
-rw-r--r--app/views/projects/settings/members/show.html.haml2
-rw-r--r--app/views/projects/settings/merge_requests/show.html.haml1
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--app/views/projects/settings/packages_and_registries/show.html.haml1
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/projects/snippets/edit.html.haml1
-rw-r--r--app/views/projects/snippets/new.html.haml1
-rw-r--r--app/views/shared/milestones/_delete_button.html.haml8
-rw-r--r--app/views/shared/milestones/_header.html.haml73
-rw-r--r--db/migrate/20230316093433_insert_daily_invites_trial_plan_limits.rb19
-rw-r--r--db/schema_migrations/202303160934331
-rw-r--r--doc/administration/instance_limits.md2
-rw-r--r--doc/administration/pages/index.md4
-rw-r--r--doc/administration/pages/source.md4
-rw-r--r--doc/administration/wikis/index.md4
-rw-r--r--doc/api/group_wikis.md4
-rw-r--r--doc/api/pages.md4
-rw-r--r--doc/api/pages_domains.md4
-rw-r--r--doc/api/wikis.md4
-rw-r--r--doc/development/api_styleguide.md3
-rw-r--r--doc/development/documentation/index.md37
-rw-r--r--doc/development/documentation/redirects.md24
-rw-r--r--doc/development/fe_guide/content_editor.md4
-rw-r--r--doc/development/fe_guide/style/javascript.md20
-rw-r--r--doc/development/pages/index.md4
-rw-r--r--doc/development/wikis.md4
-rw-r--r--doc/integration/partner_marketplace.md14
-rw-r--r--doc/operations/incident_management/slack.md9
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md1
-rw-r--r--doc/user/project/integrations/webhooks.md14
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md4
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/index.md4
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md4
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md4
-rw-r--r--doc/user/project/pages/getting_started/pages_ci_cd_template.md4
-rw-r--r--doc/user/project/pages/getting_started/pages_forked_sample_project.md4
-rw-r--r--doc/user/project/pages/getting_started/pages_from_scratch.md4
-rw-r--r--doc/user/project/pages/getting_started/pages_new_project_template.md4
-rw-r--r--doc/user/project/pages/getting_started_part_one.md4
-rw-r--r--doc/user/project/pages/index.md4
-rw-r--r--doc/user/project/pages/introduction.md4
-rw-r--r--doc/user/project/pages/pages_access_control.md4
-rw-r--r--doc/user/project/pages/public_folder.md4
-rw-r--r--doc/user/project/pages/redirects.md4
-rw-r--r--doc/user/project/wiki/group.md4
-rw-r--r--doc/user/project/wiki/index.md4
-rw-r--r--lib/api/entities/internal/pages/lookup_path.rb2
-rw-r--r--lib/gitlab/slash_commands/incident_management/incident_new.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb5
-rwxr-xr-xscripts/review_apps/automated_cleanup.rb19
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb473
-rw-r--r--spec/fixtures/api/schemas/internal/pages/lookup_path.json2
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js13
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js48
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js17
-rw-r--r--spec/frontend/lib/utils/error_message_spec.js102
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js3
-rw-r--r--spec/frontend/notes/components/note_body_spec.js8
-rw-r--r--spec/frontend/notes/components/note_form_spec.js108
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js26
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js14
-rw-r--r--spec/frontend/super_sidebar/components/search_results_spec.js16
-rw-r--r--spec/frontend/super_sidebar/mock_data.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js4
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js19
-rw-r--r--spec/helpers/search_helper_spec.rb40
-rw-r--r--spec/helpers/sidebars_helper_spec.rb32
-rw-r--r--spec/migrations/insert_daily_invites_trial_plan_limits_spec.rb51
-rw-r--r--spec/models/pages/lookup_path_spec.rb6
-rw-r--r--spec/requests/api/internal/pages_spec.rb8
-rw-r--r--spec/scripts/review_apps/automated_cleanup_spec.rb42
-rw-r--r--spec/tooling/lib/tooling/kubernetes_client_spec.rb483
-rw-r--r--spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb274
-rwxr-xr-xtooling/bin/partial_to_views_mappings9
-rw-r--r--tooling/lib/tooling/kubernetes_client.rb156
-rw-r--r--tooling/lib/tooling/mappings/partial_to_views_mappings.rb105
132 files changed, 1701 insertions, 1307 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index afb2e2e88c6..3b2f56cb5c0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -165,6 +165,7 @@ variables:
RSPEC_FOSS_IMPACT_PIPELINE_TEMPLATE_YML: .gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
RSPEC_LAST_RUN_RESULTS_FILE: rspec/rspec_last_run_results.txt
RSPEC_MATCHING_JS_FILES_PATH: rspec/js_matching_files.txt
+ RSPEC_VIEWS_INCLUDING_PARTIALS_PATH: rspec/views_including_partials.txt
RSPEC_MATCHING_TESTS_PATH: rspec/matching_tests.txt
RSPEC_MATCHING_TESTS_FOSS_PATH: rspec/matching_tests-foss.txt
RSPEC_MATCHING_TESTS_EE_PATH: rspec/matching_tests-ee.txt
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index f0e87e0161a..3582b584f66 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -16,7 +16,7 @@ review-cleanup:
- install_gitlab_gem
- setup_gcloud
script:
- - scripts/review_apps/automated_cleanup.rb || (scripts/slack review-apps-monitoring "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL} - <https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/runbooks/review-apps.md#review-cleanup-job-failed|📗 RUNBOOK 📕>" warning "GitLab Bot" && exit 1);
+ - scripts/review_apps/automated_cleanup.rb --dry-run="${DRY_RUN}" || (scripts/slack review-apps-monitoring "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL} - <https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/runbooks/review-apps.md#review-cleanup-job-failed|📗 RUNBOOK 📕>" warning "GitLab Bot" && exit 1);
review-stop:
extends:
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index 76c7af2753e..9620f0b87bd 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -120,6 +120,8 @@ detect-tests:
mkdir -p $(dirname "$RSPEC_CHANGED_FILES_PATH")
tooling/bin/find_changes ${RSPEC_CHANGED_FILES_PATH};
tooling/bin/find_tests ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH};
+ tooling/bin/partial_to_views_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_VIEWS_INCLUDING_PARTIALS_PATH};
+ tooling/bin/find_tests ${RSPEC_VIEWS_INCLUDING_PARTIALS_PATH} ${RSPEC_MATCHING_TESTS_PATH};
tooling/bin/js_to_system_specs_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH};
tooling/bin/find_changes ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH} ${FRONTEND_FIXTURES_MAPPING_PATH};
filter_rspec_matched_foss_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_FOSS_PATH};
@@ -136,9 +138,10 @@ detect-tests:
- ${FRONTEND_FIXTURES_MAPPING_PATH}
- ${RSPEC_CHANGED_FILES_PATH}
- ${RSPEC_MATCHING_JS_FILES_PATH}
- - ${RSPEC_MATCHING_TESTS_PATH}
- - ${RSPEC_MATCHING_TESTS_FOSS_PATH}
- ${RSPEC_MATCHING_TESTS_EE_PATH}
+ - ${RSPEC_MATCHING_TESTS_FOSS_PATH}
+ - ${RSPEC_MATCHING_TESTS_PATH}
+ - ${RSPEC_VIEWS_INCLUDING_PARTIALS_PATH}
detect-previous-failed-tests:
extends:
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
index 98ed2a31730..907b68e6ffc 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
@@ -7,7 +7,6 @@ export const i18n = {
currentPassword: __('Current password'),
confirmTitle: __('Are you sure?'),
confirmWebAuthn: __('This will invalidate your registered applications and WebAuthn devices.'),
- confirm: __('This will invalidate your registered applications and WebAuthn devices.'),
disableTwoFactor: __('Disable two-factor authentication'),
disable: __('Disable'),
cancel: __('Cancel'),
@@ -41,7 +40,6 @@ export default {
GlModal,
},
inject: [
- 'webauthnEnabled',
'isCurrentPasswordRequired',
'profileTwoFactorAuthPath',
'profileTwoFactorAuthMethod',
@@ -59,11 +57,7 @@ export default {
},
computed: {
confirmText() {
- if (this.webauthnEnabled) {
- return i18n.confirmWebAuthn;
- }
-
- return i18n.confirm;
+ return i18n.confirmWebAuthn;
},
},
methods: {
diff --git a/app/assets/javascripts/authentication/two_factor_auth/index.js b/app/assets/javascripts/authentication/two_factor_auth/index.js
index 7d21c19ac4c..cec80335ba0 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/index.js
+++ b/app/assets/javascripts/authentication/two_factor_auth/index.js
@@ -13,7 +13,6 @@ export const initManageTwoFactorForm = () => {
}
const {
- webauthnEnabled = false,
currentPasswordRequired,
profileTwoFactorAuthPath = '',
profileTwoFactorAuthMethod = '',
@@ -26,7 +25,6 @@ export const initManageTwoFactorForm = () => {
return new Vue({
el,
provide: {
- webauthnEnabled,
isCurrentPasswordRequired,
profileTwoFactorAuthPath,
profileTwoFactorAuthMethod,
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 56461165588..92f461c72d7 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -23,7 +23,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="last-pipeline-status">
<ci-badge-link
v-if="hasPipeline"
:status="lastPipelineStatus"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
index 48d59bf6e7c..9c0fc148dac 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
@@ -23,7 +23,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="next-run-cell">
<time-ago-tooltip v-if="showTimeAgo" :time="realNextRunTime" />
<span v-else data-testid="pipeline-schedule-inactive">
{{ s__('PipelineSchedules|Inactive') }}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
index 0b95e2037e8..b97914f8c26 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
@@ -68,7 +68,12 @@ export default {
</script>
<template>
- <gl-table-lite :fields="$options.fields" :items="schedules" stacked="md">
+ <gl-table-lite
+ :fields="$options.fields"
+ :items="schedules"
+ :tbody-tr-attr="{ 'data-testid': 'pipeline-schedule-table-row' }"
+ stacked="md"
+ >
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index f63ab1bb067..43ba527dad8 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -8,7 +8,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import { commentLineOptions, formatLineRange } from '~/notes/components/multiline_comment_utils';
import NoteForm from '~/notes/components/note_form.vue';
-import autosave from '~/notes/mixins/autosave';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import {
DIFF_NOTE_TYPE,
INLINE_DIFF_LINES_KEY,
@@ -21,7 +21,7 @@ export default {
NoteForm,
MultilineCommentForm,
},
- mixins: [autosave, diffLineNoteFormMixin, glFeatureFlagsMixin()],
+ mixins: [diffLineNoteFormMixin, glFeatureFlagsMixin()],
props: {
diffFileHash: {
type: String,
@@ -146,6 +146,27 @@ export default {
return lines;
},
+ autosaveKey() {
+ if (!this.isLoggedIn) return '';
+
+ const {
+ id,
+ noteable_type: noteableTypeUnderscored,
+ noteableType,
+ diff_head_sha: diffHeadSha,
+ source_project_id: sourceProjectId,
+ } = this.noteableData;
+
+ return [
+ s__('Autosave|Note'),
+ capitalizeFirstCharacter(noteableTypeUnderscored || noteableType),
+ id,
+ diffHeadSha,
+ DIFF_NOTE_TYPE,
+ sourceProjectId,
+ this.line.line_code,
+ ].join('/');
+ },
},
created() {
if (this.range) {
@@ -155,17 +176,6 @@ export default {
}
},
mounted() {
- if (this.isLoggedIn) {
- const keys = [
- this.noteableData.diff_head_sha,
- DIFF_NOTE_TYPE,
- this.noteableData.source_project_id,
- this.line.line_code,
- ];
-
- this.initAutoSave(this.noteableData, keys);
- }
-
if (this.selectedCommentPosition) {
this.commentLineStart = this.selectedCommentPosition.start;
}
@@ -196,9 +206,6 @@ export default {
lineCode: this.line.line_code,
fileHash: this.diffFileHash,
});
- this.$nextTick(() => {
- this.resetAutoSave();
- });
}),
handleSaveNote(note) {
return this.saveDiffDiscussion({ note, formData: this.formData }).then(() =>
@@ -232,6 +239,7 @@ export default {
:diff-file="diffFile"
:show-suggest-popover="showSuggestPopover"
:save-button-title="__('Comment')"
+ :autosave-key="autosaveKey"
class="diff-comment-form gl-mt-3"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index e8b4ff16aec..7de8eff7863 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -76,7 +76,7 @@ export default {
class="diff-stats-group gl-text-red-500 gl-display-flex gl-align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
- <span>-</span>
+ <span>−</span>
<span data-testid="js-file-deletion-line">{{ removedLines }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/lib/utils/error_message.js b/app/assets/javascripts/lib/utils/error_message.js
index 4cea4257e7b..61e1a762330 100644
--- a/app/assets/javascripts/lib/utils/error_message.js
+++ b/app/assets/javascripts/lib/utils/error_message.js
@@ -1,20 +1,17 @@
export const USER_FACING_ERROR_MESSAGE_PREFIX = 'UF:';
-const getMessageFromError = (error = '') => {
- return error.message || error;
-};
-
-export const parseErrorMessage = (error = '') => {
- const messageString = getMessageFromError(error);
-
- if (messageString.startsWith(USER_FACING_ERROR_MESSAGE_PREFIX)) {
- return {
- message: messageString.replace(USER_FACING_ERROR_MESSAGE_PREFIX, '').trim(),
- userFacing: true,
- };
- }
- return {
- message: messageString,
- userFacing: false,
- };
+/**
+ * Utility to parse an error object returned from API.
+ *
+ *
+ * @param { Object } error - An error object directly from API response
+ * @param { string } error.message - The error message, returned from API.
+ * @param { string } defaultMessage - Default user-facing error message
+ * @returns { string } - A transformed user-facing error message, or defaultMessage
+ */
+export const parseErrorMessage = (error = {}, defaultMessage = '') => {
+ const messageString = error.message || '';
+ return messageString.startsWith(USER_FACING_ERROR_MESSAGE_PREFIX)
+ ? messageString.replace(USER_FACING_ERROR_MESSAGE_PREFIX, '').trim()
+ : defaultMessage;
};
diff --git a/app/assets/javascripts/milestones/index.js b/app/assets/javascripts/milestones/index.js
index f90fdb04923..9d210f7a6ec 100644
--- a/app/assets/javascripts/milestones/index.js
+++ b/app/assets/javascripts/milestones/index.js
@@ -64,8 +64,6 @@ export function initDeleteMilestoneModal() {
if (!successful) {
button.removeAttribute('disabled');
}
-
- button.querySelector('.js-loading-icon').classList.add('hidden');
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
@@ -75,7 +73,6 @@ export function initDeleteMilestoneModal() {
`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
);
button.setAttribute('disabled', '');
- button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index eef011db7d2..b4e5129ca0e 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -5,7 +5,6 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-import autosave from '../mixins/autosave';
import NoteAttachment from './note_attachment.vue';
import NoteAwardsList from './note_awards_list.vue';
import NoteEditedText from './note_edited_text.vue';
@@ -22,7 +21,6 @@ export default {
directives: {
SafeHtml,
},
- mixins: [autosave],
props: {
note: {
type: Object,
@@ -96,21 +94,9 @@ export default {
},
mounted() {
this.renderGFM();
-
- if (this.isEditing) {
- this.initAutoSave(this.note);
- }
},
updated() {
this.renderGFM();
-
- if (this.isEditing) {
- if (!this.autosave) {
- this.initAutoSave(this.note);
- } else {
- this.setAutoSave();
- }
- }
},
methods: {
...mapActions([
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index b6ede10d02b..bde645a4f41 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,10 +1,10 @@
<script>
import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
-import { getDraft, updateDraft } from '~/lib/utils/autosave';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
@@ -15,13 +15,13 @@ export default {
i18n: COMMENT_FORM,
name: 'NoteForm',
components: {
- MarkdownField,
+ MarkdownEditor,
CommentFieldLayout,
GlButton,
GlSprintf,
GlLink,
},
- mixins: [issuableStateMixin, resolvable],
+ mixins: [issuableStateMixin, resolvable, glFeaturesFlagMixin()],
props: {
noteBody: {
type: String,
@@ -95,20 +95,22 @@ export default {
},
},
data() {
- let updatedNoteBody = this.noteBody;
-
- if (!updatedNoteBody && this.autosaveKey) {
- updatedNoteBody = getDraft(this.autosaveKey) || '';
- }
-
return {
- updatedNoteBody,
+ updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
isResolving: this.resolveDiscussion,
isUnresolving: !this.resolveDiscussion,
resolveAsThread: true,
isSubmittingWithKeydown: false,
+ formFieldProps: {
+ id: 'note_note',
+ name: 'note[note]',
+ 'aria-label': __('Reply to comment'),
+ placeholder: this.$options.i18n.bodyPlaceholder,
+ class: 'note-textarea js-gfm-input js-note-text markdown-area js-vue-issue-note-form',
+ 'data-qa-selector': 'reply_field',
+ },
};
},
computed: {
@@ -135,11 +137,6 @@ export default {
.some((n) => n.current_user?.can_resolve_discussion) || this.isDraft
);
},
- textareaPlaceholder() {
- return this.discussionNote?.internal
- ? this.$options.i18n.bodyPlaceholderInternal
- : this.$options.i18n.bodyPlaceholder;
- },
noteHash() {
if (this.noteId) {
return `#note_${this.noteId}`;
@@ -214,6 +211,9 @@ export default {
placeholder: { link: ['startTag', 'endTag'] },
};
},
+ enableContentEditor() {
+ return Boolean(this.glFeatures.contentEditorOnIssues);
+ },
},
watch: {
noteBody() {
@@ -225,7 +225,7 @@ export default {
},
},
mounted() {
- this.$refs.textarea.focus();
+ this.updatePlaceholder();
},
methods: {
...mapActions(['toggleResolveNote']),
@@ -252,19 +252,21 @@ export default {
},
cancelHandler(shouldConfirm = false) {
// check if any dropdowns are active before sending the cancelation event
- if (!this.$refs.textarea.classList.contains('at-who-active')) {
+ if (
+ !this.$refs.markdownEditor.$el
+ .querySelector('textarea')
+ ?.classList.contains('at-who-active')
+ ) {
this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody);
}
},
- onInput() {
- if (this.isSubmittingWithKeydown) {
- return;
- }
-
- if (this.autosaveKey) {
- const { autosaveKey, updatedNoteBody: text } = this;
- updateDraft(autosaveKey, text);
- }
+ updatePlaceholder() {
+ this.formFieldProps.placeholder = this.discussionNote?.internal
+ ? this.$options.i18n.bodyPlaceholderInternal
+ : this.$options.i18n.bodyPlaceholder;
+ },
+ onInput(value) {
+ this.updatedNoteBody = value;
},
handleKeySubmit() {
if (this.showBatchCommentsActions) {
@@ -273,6 +275,7 @@ export default {
this.isSubmittingWithKeydown = true;
this.handleUpdate();
}
+ this.updatedNoteBody = '';
},
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
@@ -333,41 +336,32 @@ export default {
:noteable-data="getNoteableData"
:is-internal-note="discussion.internal"
>
- <markdown-field
- :markdown-preview-path="markdownPreviewPath"
+ <markdown-editor
+ ref="markdownEditor"
+ :enable-content-editor="enableContentEditor"
+ :value="updatedNoteBody"
+ :render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
:line="line"
:lines="lines"
- :note="discussionNote"
:can-suggest="canSuggest"
:add-spacing-classes="false"
:help-page-path="helpPagePath"
+ :note="discussionNote"
+ :form-field-props="formFieldProps"
:show-suggest-popover="showSuggestPopover"
- :textarea-value="updatedNoteBody"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :autosave-key="autosaveKey"
+ :disabled="isSubmitting"
+ supports-quick-actions
+ autofocus
+ @keydown.meta.enter="handleKeySubmit()"
+ @keydown.ctrl.enter="handleKeySubmit()"
+ @keydown.exact.up="editMyLastNote()"
+ @keydown.exact.esc="cancelHandler(true)"
+ @input="onInput"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
- >
- <template #textarea>
- <textarea
- id="note_note"
- ref="textarea"
- v-model="updatedNoteBody"
- :disabled="isSubmitting"
- data-supports-quick-actions="true"
- name="note[note]"
- class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form"
- data-qa-selector="reply_field"
- dir="auto"
- :aria-label="__('Reply to comment')"
- :placeholder="textareaPlaceholder"
- @keydown.meta.enter="handleKeySubmit()"
- @keydown.ctrl.enter="handleKeySubmit()"
- @keydown.exact.up="editMyLastNote()"
- @keydown.exact.esc="cancelHandler(true)"
- @input="onInput"
- ></textarea>
- </template>
- </markdown-field>
+ />
</comment-field-layout>
<div class="note-form-actions">
<template v-if="showBatchCommentsActions">
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 80025d6f98a..d9340556012 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -294,7 +294,6 @@ export default {
this.isRequesting = false;
this.oldContent = null;
renderGFM(this.$refs.noteBody.$el);
- this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
formUpdateHandler({ noteText, callback, resolveDiscussion }) {
@@ -383,7 +382,6 @@ export default {
});
if (!confirmed) return;
}
- this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
// eslint-disable-next-line vue/no-mutating-props
this.note.note_html = this.oldContent;
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
deleted file mode 100644
index 17272d5abef..00000000000
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { s__ } from '~/locale';
-import Autosave from '~/autosave';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-
-export default {
- methods: {
- initAutoSave(noteable, extraKeys = []) {
- let keys = [
- s__('Autosave|Note'),
- capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType),
- noteable.id,
- ];
-
- if (extraKeys) {
- keys = keys.concat(extraKeys);
- }
-
- this.autosave = new Autosave(this.$refs.noteForm.$refs.textarea, keys);
- },
- resetAutoSave() {
- this.autosave.reset();
- },
- setAutoSave() {
- this.autosave.save();
- },
- disposeAutoSave() {
- this.autosave.dispose();
- },
- },
-};
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index ccfaa678201..e96f71981e5 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,7 +1,6 @@
<script>
import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { parseErrorMessage } from '~/lib/utils/error_message';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
@@ -34,9 +33,6 @@ export const i18n = {
'SecurityConfiguration|Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability.',
),
securityTrainingDoc: s__('SecurityConfiguration|Learn more about vulnerability training'),
- genericErrorText: s__(
- `SecurityConfiguration|Something went wrong. Please refresh the page, or try again later.`,
- ),
};
export default {
@@ -128,9 +124,8 @@ export default {
dismissedProjects.add(this.projectFullPath);
this.autoDevopsEnabledAlertDismissedProjects = Array.from(dismissedProjects);
},
- onError(error) {
- const { message, userFacing } = parseErrorMessage(error);
- this.errorMessage = userFacing ? message : i18n.genericErrorText;
+ onError(message) {
+ this.errorMessage = message;
},
dismissAlert() {
this.errorMessage = '';
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
index b3d4ecdda47..fe33ad9ce4b 100644
--- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue
+++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
@@ -4,7 +4,6 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { s__ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import searchUserProjectsAndGroups from '../graphql/queries/search_user_groups_and_projects.query.graphql';
-import { contextSwitcherItems } from '../mock_data';
import { trackContextAccess, formatContextSwitcherItems } from '../utils';
import NavItem from './nav_item.vue';
import ProjectsList from './projects_list.vue';
@@ -59,6 +58,10 @@ export default {
GroupsList,
},
props: {
+ persistentLinks: {
+ type: Array,
+ required: true,
+ },
username: {
type: String,
required: true,
@@ -89,7 +92,6 @@ export default {
return Boolean(this.searchString);
},
},
- contextSwitcherItems,
created() {
if (this.currentContext.namespace) {
trackContextAccess(this.username, this.currentContext);
@@ -115,8 +117,7 @@ export default {
{{ $options.i18n.switchTo }}
</div>
<ul :aria-label="$options.i18n.switchTo" class="gl-p-0">
- <nav-item :item="$options.contextSwitcherItems.yourWork" />
- <nav-item :item="$options.contextSwitcherItems.explore" />
+ <nav-item v-for="item in persistentLinks" :key="item.link" :item="item" />
</ul>
</li>
<projects-list
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index cd5363ad7a5..b101cbce22a 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -110,9 +110,9 @@ export default {
<gl-icon v-if="item.icon" :name="item.icon" class="gl-ml-2" />
</slot>
</div>
- <div class="gl-pr-3 gl-text-gray-900">
+ <div class="gl-pr-3 gl-text-gray-900 gl-truncate-end">
{{ item.title }}
- <div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500">
+ <div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500 gl-truncate-end">
{{ item.subtitle }}
</div>
</div>
diff --git a/app/assets/javascripts/super_sidebar/components/search_results.vue b/app/assets/javascripts/super_sidebar/components/search_results.vue
index 7c172110bad..cfd6184bc47 100644
--- a/app/assets/javascripts/super_sidebar/components/search_results.vue
+++ b/app/assets/javascripts/super_sidebar/components/search_results.vue
@@ -1,10 +1,17 @@
<script>
+import { GlCollapse, GlCollapseToggleDirective, GlIcon } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import ItemsList from './items_list.vue';
export default {
components: {
+ GlCollapse,
+ GlIcon,
ItemsList,
},
+ directives: {
+ CollapseToggle: GlCollapseToggleDirective,
+ },
props: {
title: {
type: String,
@@ -20,30 +27,69 @@ export default {
default: () => [],
},
},
+ data() {
+ return {
+ expanded: true,
+ };
+ },
computed: {
isEmpty() {
return !this.searchResults.length;
},
+ collapseIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
+ },
+ },
+ created() {
+ this.collapseId = uniqueId('expandable-section-');
},
+ buttonClasses: [
+ // Reset user agent styles
+ 'gl-appearance-none',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ // Text styles
+ 'gl-text-left',
+ 'gl-text-transform-uppercase',
+ 'gl-text-secondary',
+ 'gl-font-weight-bold',
+ 'gl-font-xs',
+ 'gl-line-height-12',
+ 'gl-letter-spacing-06em',
+ // Border
+ 'gl-border-t',
+ 'gl-border-gray-50',
+ // Spacing
+ 'gl-my-3',
+ 'gl-pt-2',
+ 'gl-w-full',
+ // Layout
+ 'gl-display-flex',
+ 'gl-justify-content-space-between',
+ 'gl-align-items-center',
+ ],
};
</script>
<template>
- <li class="gl-border-t gl-border-gray-50 gl-mx-3 gl-py-3">
- <div
- data-testid="list-title"
- aria-hidden="true"
- class="gl-text-transform-uppercase gl-text-secondary gl-font-weight-bold gl-font-xs gl-line-height-12 gl-letter-spacing-06em gl-my-3"
+ <li class="gl-border-t gl-border-gray-50 gl-mx-3">
+ <button
+ v-collapse-toggle="collapseId"
+ :class="$options.buttonClasses"
+ data-testid="search-results-toggle"
>
{{ title }}
- </div>
- <div v-if="isEmpty" data-testid="empty-text" class="gl-text-gray-500 gl-font-sm gl-my-3">
- {{ noResultsText }}
- </div>
- <items-list :aria-label="title" :items="searchResults">
- <template #view-all-items>
- <slot name="view-all-items"></slot>
- </template>
- </items-list>
+ <gl-icon :name="collapseIcon" :size="16" />
+ </button>
+ <gl-collapse :id="collapseId" v-model="expanded">
+ <div v-if="isEmpty" data-testid="empty-text" class="gl-text-gray-500 gl-font-sm gl-my-3">
+ {{ noResultsText }}
+ </div>
+ <items-list :aria-label="title" :items="searchResults">
+ <template #view-all-items>
+ <slot name="view-all-items"></slot>
+ </template>
+ </items-list>
+ </gl-collapse>
</li>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index e8df534346b..6b9b152e290 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -76,6 +76,7 @@ export default {
/>
<gl-collapse id="context-switcher" v-model="contextSwitcherOpened">
<context-switcher
+ :persistent-links="sidebarData.context_switcher_links"
:username="sidebarData.username"
:projects-path="sidebarData.projects_path"
:groups-path="sidebarData.groups_path"
diff --git a/app/assets/javascripts/super_sidebar/mock_data.js b/app/assets/javascripts/super_sidebar/mock_data.js
deleted file mode 100644
index 5e5ad97eb68..00000000000
--- a/app/assets/javascripts/super_sidebar/mock_data.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { s__ } from '~/locale';
-
-export const contextSwitcherItems = {
- yourWork: { title: s__('Navigation|Your work'), link: '/', icon: 'work' },
- explore: { title: s__('Navigation|Explore'), link: '/explore', icon: 'compass' },
- recentProjects: [
- {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- title: 'Orange',
- subtitle: 'tropical-tree',
- link: '/tropical-tree',
- avatar:
- 'https://gitlab.com/uploads/-/system/project/avatar/4456656/pajamas-logo.png?width=64',
- },
- {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- title: 'Lemon',
- subtitle: 'tropical-tree',
- link: '/tropical-tree',
- avatar: 'https://gitlab.com/uploads/-/system/project/avatar/7071551/GitLab_UI.png?width=64',
- },
- {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- title: 'Coconut',
- subtitle: 'tropical-tree',
- link: '/tropical-tree',
- avatar:
- 'https://gitlab.com/uploads/-/system/project/avatar/4149988/SVGs_project.png?width=64',
- },
- ],
- recentGroups: [
- {
- title: 'Developer Evangelism at GitLab',
- subtitle: 'tropical-tree',
- link: '/tropical-tree',
- avatar:
- 'https://gitlab.com/uploads/-/system/group/avatar/10087220/rainbow_tanuki.jpg?width=64',
- },
- {
- title: 'security-products',
- subtitle: 'tropical-tree',
- link: '/tropical-tree',
- avatar:
- 'https://gitlab.com/uploads/-/system/group/avatar/11932235/gitlab-icon-rgb.png?width=64',
- },
- {
- title: 'Tanuki-Workshops',
- subtitle: 'tropical-tree',
- link: '/tropical-tree',
- avatar:
- 'https://gitlab.com/uploads/-/system/group/avatar/5085244/Screenshot_2019-04-29_at_16.13.07.png?width=64',
- },
- ],
-};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 93583907a11..5bfc4e3c7b5 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -97,6 +97,7 @@ export default {
mounted() {
this.autofocusTextarea();
+ this.$emit('input', this.markdown);
this.saveDraft();
},
methods: {
@@ -176,8 +177,8 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:show-content-editor-switcher="enableContentEditor"
:drawio-enabled="drawioEnabled"
- class="bordered-box"
@enableContentEditor="onEditingModeChange('contentEditor')"
+ @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
>
<template #textarea>
<textarea
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index 3afd1f9410b..e16d62bfdf7 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants';
+import { parseErrorMessage, USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
import { redirectTo } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
import apolloProvider from '../provider';
@@ -9,6 +10,16 @@ function mutationSettingsForFeatureType(type) {
return featureToMutationMap[type];
}
+export const i18n = {
+ buttonLabel: s__('SecurityConfiguration|Configure with a merge request'),
+ noSuccessPathError: s__(
+ 'SecurityConfiguration|%{featureName} merge request creation mutation failed',
+ ),
+ genericErrorText: s__(
+ `SecurityConfiguration|Something went wrong. Please refresh the page, or try again later.`,
+ ),
+};
+
export default {
apolloProvider,
components: {
@@ -55,15 +66,20 @@ export default {
throw new Error(errors[0]);
}
+ // Sending USER_FACING_ERROR_MESSAGE_PREFIX prefixed messages should happen only in
+ // the backend. Hence the code below is an anti-pattern.
+ // The issue to refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/397714
if (!successPath) {
throw new Error(
- sprintf(this.$options.i18n.noSuccessPathError, { featureName: this.feature.name }),
+ `${USER_FACING_ERROR_MESSAGE_PREFIX} ${sprintf(this.$options.i18n.noSuccessPathError, {
+ featureName: this.feature.name,
+ })}`,
);
}
redirectTo(successPath);
} catch (e) {
- this.$emit('error', e.message);
+ this.$emit('error', parseErrorMessage(e, this.$options.i18n.genericErrorText));
this.isLoading = false;
}
},
@@ -84,12 +100,7 @@ export default {
Boolean(mutationSettingsForFeatureType(type))
);
},
- i18n: {
- buttonLabel: s__('SecurityConfiguration|Configure with a merge request'),
- noSuccessPathError: s__(
- 'SecurityConfiguration|%{featureName} merge request creation mutation failed',
- ),
- },
+ i18n,
};
</script>
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 0b30b4c3ef0..4d07e4259a0 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -61,10 +61,6 @@ $item-remove-button-space: 42px;
color: $orange-500;
}
- .item-title-wrapper {
- max-width: calc(100% - #{$item-remove-button-space});
- }
-
.item-title {
flex-basis: 100%;
font-size: $gl-font-size-small;
diff --git a/app/assets/stylesheets/framework/sortable.scss b/app/assets/stylesheets/framework/sortable.scss
index f9e95d16f63..91781bfe539 100644
--- a/app/assets/stylesheets/framework/sortable.scss
+++ b/app/assets/stylesheets/framework/sortable.scss
@@ -51,3 +51,12 @@
cursor: no-drop !important;
}
}
+
+.tree-item.is-dragging {
+ border-top: 0;
+
+ .item-body {
+ background-color: $white;
+ border: 2px solid $gray-200;
+ }
+}
diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss
index 590a66ff28e..fc6b1181575 100644
--- a/app/assets/stylesheets/framework/system_messages.scss
+++ b/app/assets/stylesheets/framework/system_messages.scss
@@ -87,6 +87,7 @@
.nav-sidebar,
.super-sidebar,
.right-sidebar,
+ .review-bar-component,
// navless pages' footer eg: login page
// navless pages' footer border eg: login page
&.devise-layout-html body .footer-container,
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index 708d1a2895e..8dc07715989 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -131,42 +131,6 @@
}
}
-.milestone-page-header {
- display: flex;
- flex-flow: row;
- align-items: center;
- flex-wrap: wrap;
-
- .milestone-buttons {
- margin-left: auto;
- order: 2;
-
- .verbose {
- display: none;
- }
- }
-
- .header-text-content {
- order: 3;
- width: 100%;
- }
-
- @include media-breakpoint-up(xs) {
- .milestone-buttons .verbose {
- display: inline;
- }
-
- .header-text-content {
- order: 2;
- width: auto;
- }
-
- .milestone-buttons {
- order: 3;
- }
- }
-}
-
.issuable-row {
background-color: var(--white, $white);
}
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index d62dc038388..9d14784f086 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -341,7 +341,7 @@ module SearchHelper
# Autocomplete results for the current user's projects
# rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)
- current_user.authorized_projects.order_id_desc.search_by_title(term)
+ current_user.authorized_projects.order_id_desc.search(term, include_namespace: true)
.sorted_by_stars_desc.non_archived.limit(limit).map do |p|
{
category: "Projects",
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 6c9688b0f9d..1efbd4acdd9 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -50,15 +50,7 @@ module SidebarsHelper
has_link_to_profile: current_user_menu?(:profile),
link_to_profile: user_url(user),
logo_url: current_appearance&.header_logo_path,
- status: {
- can_update: can?(current_user, :update_user_status, current_user),
- busy: user.status&.busy?,
- customized: user.status&.customized?,
- availability: user.status&.availability.to_s,
- emoji: user.status&.emoji,
- message: user.status&.message_html&.html_safe,
- clear_after: user_clear_status_at(user)
- },
+ status: user_status_menu_data(user),
trial: {
has_start_trial: current_user_menu?(:start_trial),
url: trials_link_url
@@ -88,7 +80,8 @@ module SidebarsHelper
gitlab_com_but_not_canary: Gitlab.com_but_not_canary?,
gitlab_com_and_canary: Gitlab.com_and_canary?,
canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url,
- current_context: super_sidebar_current_context(project: project, group: group)
+ current_context: super_sidebar_current_context(project: project, group: group),
+ context_switcher_links: context_switcher_links
}
end
@@ -119,6 +112,18 @@ module SidebarsHelper
private
+ def user_status_menu_data(user)
+ {
+ can_update: can?(user, :update_user_status, user),
+ busy: user.status&.busy?,
+ customized: user.status&.customized?,
+ availability: user.status&.availability.to_s,
+ emoji: user.status&.emoji,
+ message: user.status&.message_html&.html_safe,
+ clear_after: user_clear_status_at(user)
+ }
+ end
+
def create_new_menu_groups(group:, project:)
new_dropdown_sections = new_dropdown_view_model(group: group, project: project)[:menu_sections]
show_headers = new_dropdown_sections.length > 1
@@ -260,6 +265,22 @@ module SidebarsHelper
{}
end
+
+ def context_switcher_links
+ links = [
+ # We should probably not return "You work" when used is not logged-in
+ { title: s_('Navigation|Your work'), link: root_path, icon: 'work' },
+ { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' }
+ ]
+
+ if current_user&.can_admin_all_resources?
+ links.append(
+ { title: s_('Navigation|Admin'), link: admin_root_path, icon: 'admin' }
+ )
+ end
+
+ links
+ end
end
SidebarsHelper.prepend_mod_with('SidebarsHelper')
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 222cde19da7..96c20ab03d4 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -54,12 +54,12 @@ module Pages
end
strong_memoize_attr :prefix
- def unique_domain
+ def unique_url
return unless project.project_setting.pages_unique_domain_enabled?
- project.project_setting.pages_unique_domain
+ project.pages_unique_url
end
- strong_memoize_attr :unique_domain
+ strong_memoize_attr :unique_url
private
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index d92d13260fe..c079dd6b581 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -1,10 +1,9 @@
- page_title _('DevOps Reports')
- add_page_specific_style 'page_bundles/dev_ops_reports'
-.container
- .gl-mt-3
- - if show_adoption?
- = render_if_exists 'admin/dev_ops_report/devops_tabs'
- - else
- = render 'score'
+.gl-mt-3
+ - if show_adoption?
+ = render_if_exists 'admin/dev_ops_report/devops_tabs'
+ - else
+ = render 'score'
diff --git a/app/views/authentication/_register.html.haml b/app/views/authentication/_register.html.haml
index dc4511a8159..f8a03f085ff 100644
--- a/app/views/authentication/_register.html.haml
+++ b/app/views/authentication/_register.html.haml
@@ -1,4 +1,4 @@
-- if Feature.enabled?(:webauthn) && Feature.enabled?(:webauthn_without_totp)
+- if Feature.enabled?(:webauthn_without_totp)
#js-device-registration{ data: device_registration_data(current_password_required: current_password_required?, target_path: target_path, webauthn_error: @webauthn_error) }
- else
#js-register-token-2fa
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 61fe6ba8e47..5c3c24f5467 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -2,7 +2,6 @@
- page_title _('Two-Factor Authentication'), _('Account')
- add_to_breadcrumbs _('Account'), profile_account_path
- @content_class = "limit-container-width" unless fluid_layout
-- webauthn_enabled = Feature.enabled?(:webauthn)
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.gl-mt-3
@@ -68,7 +67,7 @@
%p
= _('Set up a hardware device to enable two-factor authentication (2FA).')
%p
- - if webauthn_enabled && Feature.enabled?(:webauthn_without_totp)
+ - if Feature.enabled?(:webauthn_without_totp)
= _("Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser.")
- else
= _("Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in, even from an unsupported browser.")
@@ -134,7 +133,7 @@
dismissible: false) do |c|
= c.body do
= link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
- .js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
+ .js-manage-two-factor-form{ data: { current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
%p
= _("Register a one-time password authenticator or a WebAuthn device first.")
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index 2d9f7e49ddc..dc0c9547901 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -10,5 +10,5 @@
- if show_auto_devops_callout?(@project)
= render 'shared/auto_devops_callout'
= render_if_exists 'projects/above_size_limit_warning', project: project
- = render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
+ = render_if_exists 'shared/shared_runners_minutes_limit', project: project
= render_if_exists 'projects/terraform_banner', project: project
diff --git a/app/views/projects/_terraform_banner.html.haml b/app/views/projects/_terraform_banner.html.haml
index 881e4ccd9df..24711fc39d8 100644
--- a/app/views/projects/_terraform_banner.html.haml
+++ b/app/views/projects/_terraform_banner.html.haml
@@ -1,5 +1,3 @@
-- @content_class = "container-limited limit-container-width" unless fluid_layout
-
- if show_terraform_banner?(project)
.container-fluid{ class: @content_class }
.js-terraform-notification{ data: { terraform_image_path: image_path('illustrations/third-party-logos/ci_cd-template-logos/terraform.svg') } }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b2270e0faf7..b0eef923411 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,7 +1,6 @@
- breadcrumb_title _("General Settings")
- page_title _("General")
- add_page_specific_style 'page_bundles/projects_edit'
-- @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default?
- reduce_visibility_form_id = 'reduce-visibility-form'
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index ca3f49bae95..b6c21588193 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,4 +1,3 @@
-- @content_class = "limit-container-width" unless fluid_layout
- default_branch_name = @project.default_branch_or_main
- escaped_default_branch_name = default_branch_name.shellescape
- @skip_current_level_breadcrumb = true
diff --git a/app/views/projects/harbor/repositories/index.html.haml b/app/views/projects/harbor/repositories/index.html.haml
index e6f0e3e950c..b6f6fb64451 100644
--- a/app/views/projects/harbor/repositories/index.html.haml
+++ b/app/views/projects/harbor/repositories/index.html.haml
@@ -1,5 +1,4 @@
- page_title _("Harbor Registry")
-- @content_class = "limit-container-width" unless fluid_layout
#js-harbor-registry-list-project{ data: { endpoint: project_harbor_repositories_path(@project),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index d610ef21400..0f4dc4b5e32 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -1,4 +1,3 @@
-- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
- page_title _('Webhook Logs')
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index 3e63faaf448..b553249c4b8 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -1,4 +1,3 @@
-- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Webhook Settings'), project_hooks_path(@project)
- page_title _('Webhook')
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 15cb7869dc5..35214ad38dc 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,4 +1,3 @@
-- @content_class = 'limit-container-width' unless fluid_layout
- breadcrumb_title _('Webhook Settings')
- page_title _('Webhooks')
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 9fe541c5912..7f509aee07c 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,5 +1,4 @@
- page_title import_in_progress_title
-- @content_class = "limit-container-width" unless fluid_layout
.save-project-loader
.center
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 7e8bf4ae57f..5896309c765 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,4 +1,3 @@
-- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
diff --git a/app/views/projects/packages/infrastructure_registry/index.html.haml b/app/views/projects/packages/infrastructure_registry/index.html.haml
index 5a118997ff9..9577f6383e9 100644
--- a/app/views/projects/packages/infrastructure_registry/index.html.haml
+++ b/app/views/projects/packages/infrastructure_registry/index.html.haml
@@ -1,5 +1,4 @@
- page_title _("Infrastructure Registry")
-- @content_class = "limit-container-width" unless fluid_layout
.row
.col-12
diff --git a/app/views/projects/packages/infrastructure_registry/show.html.haml b/app/views/projects/packages/infrastructure_registry/show.html.haml
index e7c77478170..7343d552d54 100644
--- a/app/views/projects/packages/infrastructure_registry/show.html.haml
+++ b/app/views/projects/packages/infrastructure_registry/show.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs @package.name, project_infrastructure_registry_index_path(@project)
- breadcrumb_title @package.version
- page_title _("Infrastructure Registry")
-- @content_class = "limit-container-width" unless fluid_layout
.row
.col-12
diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml
index 4ab16f25dd2..e120e8ccb18 100644
--- a/app/views/projects/packages/packages/index.html.haml
+++ b/app/views/projects/packages/packages/index.html.haml
@@ -1,5 +1,4 @@
- page_title _("Package Registry")
-- @content_class = "limit-container-width" unless fluid_layout
.row
.col-12
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 910aab6da72..644aca2477b 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,5 +1,4 @@
- page_title _("Container Registry")
-- @content_class = "limit-container-width" unless fluid_layout
- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil})
%section
diff --git a/app/views/projects/security/configuration/show.html.haml b/app/views/projects/security/configuration/show.html.haml
index 2904fb81afe..63e175f96e5 100644
--- a/app/views/projects/security/configuration/show.html.haml
+++ b/app/views/projects/security/configuration/show.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title _("Security configuration")
- page_title _("Security configuration")
-- @content_class = "limit-container-width" unless fluid_layout
#js-security-configuration{ data: { **@configuration.to_html_data_attribute,
vulnerability_training_docs_path: vulnerability_training_docs_path,
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index f6c5c4e2950..b581ccaceec 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -2,7 +2,6 @@
- page_title _('Project Access Tokens')
- type = _('project access token')
- type_plural = _('project access tokens')
-- @content_class = 'limit-container-width' unless fluid_layout
.row.gl-mt-3.js-search-settings-section
.col-lg-4
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index b27f5a0e5ed..eb32d5c7c82 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,4 +1,3 @@
-- @content_class = "limit-container-width" unless fluid_layout
- page_title _("CI/CD Settings")
- page_title _("CI/CD")
diff --git a/app/views/projects/settings/integrations/edit.html.haml b/app/views/projects/settings/integrations/edit.html.haml
index 46276e6c6c9..84d3ac2ded9 100644
--- a/app/views/projects/settings/integrations/edit.html.haml
+++ b/app/views/projects/settings/integrations/edit.html.haml
@@ -1,7 +1,6 @@
- breadcrumb_title @integration.title
- add_to_breadcrumbs _('Integration Settings'), project_settings_integrations_path(@project)
- page_title @integration.title, _('Integrations')
-- @content_class = 'limit-container-width' unless fluid_layout
= render 'form', integration: @integration
diff --git a/app/views/projects/settings/integrations/index.html.haml b/app/views/projects/settings/integrations/index.html.haml
index c316b4e9cac..ed65cce5acb 100644
--- a/app/views/projects/settings/integrations/index.html.haml
+++ b/app/views/projects/settings/integrations/index.html.haml
@@ -1,4 +1,3 @@
-- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title _('Integration Settings')
- page_title _('Integrations')
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index 5fca734222b..5c9389c9c1c 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,5 +1,3 @@
-- @content_class = "limit-container-width" unless fluid_layout
-
- page_title _("Members")
= render "projects/project_members/index"
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index 7dfd304e07b..6cc5dfd8c90 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title _('Merge requests')
- page_title _('Merge requests')
-- @content_class = 'limit-container-width' unless fluid_layout
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings.expanded{ class: [('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 90e0ccce8b4..2aae408b88f 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -1,4 +1,3 @@
-- @content_class = 'limit-container-width' unless fluid_layout
- page_title _('Monitor Settings')
- breadcrumb_title _('Monitor Settings')
diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml
index c81b38f44dd..22385677192 100644
--- a/app/views/projects/settings/packages_and_registries/show.html.haml
+++ b/app/views/projects/settings/packages_and_registries/show.html.haml
@@ -1,5 +1,4 @@
- breadcrumb_title _('Packages and registries settings')
- page_title _('Packages and registries settings')
-- @content_class = 'limit-container-width' unless fluid_layout
#js-registry-settings{ data: settings_data }
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index de171a25e8d..c532c19e0d1 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title _("Repository Settings")
- page_title _("Repository")
-- @content_class = "limit-container-width" unless fluid_layout
- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to packages, your repository, and registry images.')
= render "projects/branch_defaults/show"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index f47f4ebc7ee..ab2f6745dfd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,4 @@
- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
-- @content_class = "limit-container-width" unless fluid_layout
- @skip_current_level_breadcrumb = true
- add_page_specific_style 'page_bundles/project'
- add_page_specific_style 'page_bundles/tree'
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index d9bf064ad24..6e1ebdeedf0 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,7 +1,6 @@
- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-- @content_class = "limit-container-width" unless fluid_layout
%h1.page-title.gl-font-size-h-display
= _("Edit Snippet")
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 5086b5eaa3d..59b2536c5d0 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,7 +1,6 @@
- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title _("New")
- page_title _("New Snippet")
-- @content_class = "limit-container-width" unless fluid_layout
%h1.page-title.gl-font-size-h-display
= _("New Snippet")
diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml
index 432d2efc36e..caab7710fa8 100644
--- a/app/views/shared/milestones/_delete_button.html.haml
+++ b/app/views/shared/milestones/_delete_button.html.haml
@@ -1,8 +1,6 @@
- milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone)
-= render Pajamas::ButtonComponent.new(variant: :danger,
- button_options: { class: 'js-delete-milestone-button btn-grouped', data: { milestone_id: @milestone.id, milestone_title: markdown_field(@milestone, :title), milestone_url: milestone_url, milestone_issue_count: @milestone.issues.count, milestone_merge_request_count: @milestone.merge_requests.count }, disabled: true }) do
- = gl_loading_icon(inline: true, css_class: "gl-mr-2 js-loading-icon hidden")
- = _('Delete')
-
+%button.gl-button.btn.btn-link.menu-item.js-delete-milestone-button{ data: { milestone_id: @milestone.id, milestone_title: markdown_field(@milestone, :title), milestone_url: milestone_url, milestone_issue_count: @milestone.issues.count, milestone_merge_request_count: @milestone.merge_requests.count }, disabled: true }
+ .gl-dropdown-item-text-wrapper.gl-text-red-500
+ = _('Delete')
#js-delete-milestone-modal
diff --git a/app/views/shared/milestones/_header.html.haml b/app/views/shared/milestones/_header.html.haml
index 900c71675d9..0b017678ef7 100644
--- a/app/views/shared/milestones/_header.html.haml
+++ b/app/views/shared/milestones/_header.html.haml
@@ -1,30 +1,57 @@
-.detail-page-header.milestone-page-header
- = gl_badge_tag milestone_status_string(milestone), { variant: milestone_badge_variant(milestone) }, { class: 'gl-mr-3' }
+.detail-page-header
+ .detail-page-header-body.gl-flex-wrap-wrap
+ = gl_badge_tag milestone_status_string(milestone), { variant: milestone_badge_variant(milestone) }, { class: 'gl-mr-3' }
- .header-text-content
- %span.identifier
- %strong
- = _('Milestone')
- - if milestone.due_date || milestone.start_date
- = milestone_date_range(milestone)
-
- .milestone-buttons
- - if can?(current_user, :admin_milestone, @group || @project)
- = render Pajamas::ButtonComponent.new(href: edit_milestone_path(milestone), button_options: { class: 'btn-grouped' }) do
- = _('Edit')
-
- - if milestone.project_milestone? && milestone.project.group
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-promote-project-milestone-button btn-grouped', data: { milestone_title: milestone.title, group_name: milestone.project.group.name, url: promote_project_milestone_path(milestone.project, milestone) }, disabled: true }) do
- = _('Promote')
- #promote-milestone-modal
+ .header-text-content
+ %span.identifier
+ %strong
+ = _('Milestone')
+ - if milestone.due_date || milestone.start_date
+ = milestone_date_range(milestone)
+ = render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left', button_options: { 'aria-label' => _('Toggle sidebar'), class: 'btn-grouped gl-float-right! gl-sm-display-none js-sidebar-toggle' })
+ - if can?(current_user, :admin_milestone, @group || @project)
+ .milestone-buttons.detail-page-header-actions.gl-display-flex.gl-align-self-start
- if milestone.active?
- = render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :close }), method: :put, button_options: { class: 'btn-grouped btn-close' }) do
+ = render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :close }), method: :put, button_options: { class: 'btn-close gl-display-none gl-md-display-inline-block' }) do
= _('Close milestone')
- else
- = render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :activate }), method: :put, button_options: { class: 'btn-grouped' }) do
+ = render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :activate }), method: :put, button_options: { class: 'gl-display-none gl-md-display-inline-block' }) do
= _('Reopen milestone')
- = render 'shared/milestones/delete_button'
-
- = render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left', button_options: { 'aria-label' => _('Toggle sidebar'), class: 'btn-grouped gl-float-right! gl-sm-display-none js-sidebar-toggle' })
+ .btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Milestone actions'), testid: 'milestone-actions', 'aria-label': _('Milestone actions') }, aria: { label: _('Milestone actions') } do
+ = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
+ %span.gl-dropdown-button-text= _('Milestone actions')
+ = sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
+ .dropdown-menu.dropdown-menu-right
+ .gl-dropdown-inner
+ .gl-dropdown-contents
+ %ul
+ %li.gl-dropdown-item
+ = link_to edit_milestone_path(milestone), class: 'menu-item' do
+ .gl-dropdown-item-text-wrapper
+ = _('Edit')
+ - if milestone.project_milestone? && milestone.project.group
+ %li.gl-dropdown-item
+ %button.gl-button.btn.btn-link.menu-item.js-promote-project-milestone-button{ data: { milestone_title: milestone.title,
+ group_name: milestone.project.group.name,
+ url: promote_project_milestone_path(milestone.project, milestone)},
+ disabled: true,
+ type: 'button' }
+ .gl-dropdown-item-text-wrapper
+ = _('Promote')
+ #promote-milestone-modal
+ - if milestone.active?
+ %li.gl-dropdown-item{ class: "gl-md-display-none!" }
+ = link_to update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'menu-item' do
+ .gl-dropdown-item-text-wrapper
+ = _('Close milestone')
+ - else
+ %li.gl-dropdown-item{ class: "gl-md-display-none!" }
+ = link_to update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'menu-item' do
+ .gl-dropdown-item-text-wrapper
+ = _('Reopen milestone')
+ %li.gl-dropdown-item
+ = render 'shared/milestones/delete_button'
diff --git a/db/migrate/20230316093433_insert_daily_invites_trial_plan_limits.rb b/db/migrate/20230316093433_insert_daily_invites_trial_plan_limits.rb
new file mode 100644
index 00000000000..f6254cad192
--- /dev/null
+++ b/db/migrate/20230316093433_insert_daily_invites_trial_plan_limits.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class InsertDailyInvitesTrialPlanLimits < Gitlab::Database::Migration[2.1]
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ return unless Gitlab.com?
+
+ create_or_update_plan_limit('daily_invites', 'premium_trial', 50)
+ create_or_update_plan_limit('daily_invites', 'ultimate_trial', 50)
+ end
+
+ def down
+ return unless Gitlab.com?
+
+ create_or_update_plan_limit('daily_invites', 'premium_trial', 0)
+ create_or_update_plan_limit('daily_invites', 'ultimate_trial', 0)
+ end
+end
diff --git a/db/schema_migrations/20230316093433 b/db/schema_migrations/20230316093433
new file mode 100644
index 00000000000..eaf1434bb2b
--- /dev/null
+++ b/db/schema_migrations/20230316093433
@@ -0,0 +1 @@
+5e3c28caac0cc43d28c7f279ad001234ec6f81e2522c087fc303a6e3355b5a33 \ No newline at end of file
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 2b2eefdb17c..e276d3b25af 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -126,7 +126,7 @@ Read more about [import/export rate limits](../user/admin_area/settings/import_e
Limit the maximum daily member invitations allowed per group hierarchy.
-- GitLab.com: Free members may invite 20 members per day.
+- GitLab.com: Free members may invite 20 members per day, Premium trial and Ultimate trial members may invite 50 members per day.
- Self-managed: Invites are not limited.
### Webhook rate limit
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index efeb19a8d5e..b5a41f0e442 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: 'Learn how to administer GitLab Pages.'
---
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index db76d15ec58..457e92d96bd 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/administration/wikis/index.md b/doc/administration/wikis/index.md
index f3442e23160..330e41ee880 100644
--- a/doc/administration/wikis/index.md
+++ b/doc/administration/wikis/index.md
@@ -1,7 +1,7 @@
---
type: reference, howto
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/group_wikis.md b/doc/api/group_wikis.md
index 6fb74ea00b7..c03224373de 100644
--- a/doc/api/group_wikis.md
+++ b/doc/api/group_wikis.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments"
type: reference, api
---
diff --git a/doc/api/pages.md b/doc/api/pages.md
index ed63090b9be..2821f5d510c 100644
--- a/doc/api/pages.md
+++ b/doc/api/pages.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index e83811d0415..7d52c803c88 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index 046901af56b..17317b7c594 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 828ef21ba9a..34e043a30d6 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -338,12 +338,13 @@ Also see [verifying N+1 performance](#verifying-with-tests) in tests.
When throwing an error with a message that is meant to be user-facing, you should
use the error message utility function contained in `lib/gitlab/utils/error_message.rb`.
It adds a prefix to the error message, making it distinguishable from non-user-facing error messages.
-Please make sure that the Frontend is aware of the prefix usage and is using the according utils.
```ruby
Gitlab::Utils::ErrorMessage.to_user_facing('Example user-facing error-message')
```
+Please make sure that the Frontend is aware of the prefix usage and is using the according utils. See [Error handling](fe_guide/style/javascript.md#error-handling) in JavaScript style guide for more information.
+
## Include a changelog entry
All client-facing changes **must** include a [changelog entry](changelog.md).
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index d1f5ee8f9f3..c9f31b36e3f 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -114,21 +114,6 @@ The following metadata should be added when a page is moved to another location:
- `redirect_to`: The relative path and filename (with an `.md` extension) of the
location to which visitors should be redirected for a moved page.
[Learn more](redirects.md).
-- `disqus_identifier`: Identifier for Disqus commenting system. Used to keep
- comments with a page that has been moved to a new URL.
- [Learn more](redirects.md#redirections-for-pages-with-disqus-comments).
-
-### Comments metadata
-
-The [docs website](site_architecture/index.md) has comments (provided by Disqus)
-enabled by default. In case you want to disable them (for example in index pages),
-set it to `false`:
-
-```yaml
----
-comments: false
----
-```
### Additional page metadata
@@ -353,28 +338,6 @@ feedback: false
The default is to leave it there. If you want to omit it from a document, you
must check with a technical writer before doing so.
-## Disqus
-
-We have integrated the docs site with Disqus (introduced by
-[!151](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/151)),
-allowing our users to post comments.
-
-To omit only the comments from the feedback section, use the following key in
-the front matter:
-
-```yaml
----
-comments: false
----
-```
-
-We're hiding comments only in main index pages, such as [the main documentation index](../../index.md),
-since its content is too broad to comment on. Before omitting Disqus, you must
-check with a technical writer.
-
-Note that after adding `feedback: false` to the front matter, it will omit
-Disqus, therefore, don't add both keys to the same document.
-
The click events in the feedback section are tracked with Google Tag Manager.
The conversions can be viewed on Google Analytics by navigating to
**Behavior > Events > Top events > docs**.
diff --git a/doc/development/documentation/redirects.md b/doc/development/documentation/redirects.md
index 4cfe8be713a..068c1e84a0f 100644
--- a/doc/development/documentation/redirects.md
+++ b/doc/development/documentation/redirects.md
@@ -81,7 +81,6 @@ To redirect a page to another page in the same repository:
- Replace both instances of `../newpath/to/file/index.md` with the new file path.
- Replace both instances of `YYYY-MM-DD` with the expiration date, as explained in the template.
-1. If the page has Disqus comments, follow [the steps for pages with Disqus comments](#redirections-for-pages-with-disqus-comments).
1. If the page had images that aren't used on any other pages, delete them.
After your changes are committed, search for and update all links that point to the old file:
@@ -158,26 +157,3 @@ If you create a new page and then rename it before it's added to a release on th
Instead of following that procedure, ask a Technical Writer to manually add the redirect
to [`redirects.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/content/_data/redirects.yaml).
-
-## Redirections for pages with Disqus comments
-
-If the documentation page being relocated already has Disqus comments,
-we need to preserve the Disqus thread.
-
-Disqus uses an identifier per page, and for <https://docs.gitlab.com>, the page identifier
-is configured to be the page URL. Therefore, when we change the document location,
-we need to preserve the old URL as the same Disqus identifier.
-
-To do that, add to the front matter the variable `disqus_identifier`,
-using the old URL as value. For example, let's say we moved the document
-available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
-`https://docs.gitlab.com/my-new-location/index.html`.
-
-Into the **new document** front matter, we add the following information. You must
-include the filename in the `disqus_identifier` URL, even if it's `index.html` or `README.html`.
-
-```yaml
----
-disqus_identifier: 'https://docs.gitlab.com/my-old-location/README.html'
----
-```
diff --git a/doc/development/fe_guide/content_editor.md b/doc/development/fe_guide/content_editor.md
index 5c7fe68fec5..25140a067ca 100644
--- a/doc/development/fe_guide/content_editor.md
+++ b/doc/development/fe_guide/content_editor.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/development/fe_guide/style/javascript.md b/doc/development/fe_guide/style/javascript.md
index b35ffdd8669..be5208e9b55 100644
--- a/doc/development/fe_guide/style/javascript.md
+++ b/doc/development/fe_guide/style/javascript.md
@@ -332,19 +332,23 @@ Only export the constants as a collection (array, or object) when there is a nee
## Error handling
-When catching a server-side error you should use the error message
+When catching a server-side error, you should use the error message
utility function contained in `app/assets/javascripts/lib/utils/error_message.js`.
-This utility parses the received error message and checks for a prefix that indicates
-whether the message is meant to be user-facing or not. The utility returns
-an object with the message, and a boolean indicating whether the message is meant to be user-facing or not. Please make sure that the Backend is aware of the utils usage and is adding the prefix
-to the error message accordingly.
+This utility accepts two parameters: the error object received from the server response and a
+default error message. The utility examines the message in the error object for a prefix that
+indicates whether the message is meant to be user-facing or not. If the message is intended
+to be user-facing, the utility returns it as is. Otherwise, it returns the default error
+message passed as a parameter.
```javascript
import { parseErrorMessage } from '~/lib/utils/error_message';
onError(error) {
- const { message, userFacing } = parseErrorMessage(error);
-
- const errorMessage = userFacing ? message : genericErrorText;
+ const errorMessage = parseErrorMessage(error, genericErrorText);
}
```
+
+To benefit from this parsing mechanism, the utility user should ensure that the server-side
+code is aware of this utility's usage and prefixes the error messages where appropriate
+before sending them back to the user. See
+[Error handling for API](../../api_styleguide.md#error-handling) for more information.
diff --git a/doc/development/pages/index.md b/doc/development/pages/index.md
index e71d7df642c..d5b18ee80fb 100644
--- a/doc/development/pages/index.md
+++ b/doc/development/pages/index.md
@@ -1,7 +1,7 @@
---
type: reference, dev
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "GitLab's development guidelines for GitLab Pages"
---
diff --git a/doc/development/wikis.md b/doc/development/wikis.md
index 2f97931f924..8da38ba6121 100644
--- a/doc/development/wikis.md
+++ b/doc/development/wikis.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "GitLab's development guidelines for Wikis"
---
diff --git a/doc/integration/partner_marketplace.md b/doc/integration/partner_marketplace.md
index 5ed131145f4..76988af99b2 100644
--- a/doc/integration/partner_marketplace.md
+++ b/doc/integration/partner_marketplace.md
@@ -60,7 +60,7 @@ To access the Marketplace API you need to:
- Retrieve an OAuth access token.
Marketplace API endpoints are secured with [OAuth 2.0](https://oauth.net/2/). OAuth is an authorization framework
-that grants 3rd party or client applications, like a GitLab Partner application, limited access to resources on an
+that grants 3rd party or client applications, like a Marketplace partner application, limited access to resources on an
HTTP service, like the Customers Portal.
OAuth 2.0 uses _grant types_ (or _flows_) that describe how a client application gets authorization in
@@ -72,14 +72,14 @@ own resources, instead of accessing resources on behalf of a user.
### Step 1: Request access
-Before you can use the Marketplace API, you must contact your GitLab Partner Manager or email [`partnerorderops`](mailto:partnerorderops@gitlab.com)
+Before you can use the Marketplace API, you must contact your Marketplace partner Manager or email [`partnerorderops`](mailto:partnerorderops@gitlab.com)
to request access. After you request access, GitLab configures the following accounts and credentials for you:
1. Client credentials. Marketplace APIs are secured with OAuth 2.0. The client credentials include the client ID and client secret
that you need to retrieve the OAuth access token.
1. Invoice owner account in Zuora system. Required for invoice processing.
1. Distributor account in Salesforce system.
-1. Trading partner account in Salesforce system.
+1. Trading partner account in Salesforce system. GitLab adds the Trading Partner ID to a permitted list to pass the validations.
### Step 2: Retrieve an access token
@@ -121,7 +121,7 @@ curl \
## Create a new customer subscription
-To create a new customer subscription from a GitLab Partner client application,
+To create a new customer subscription from a Marketplace partner client application,
- Make an authorized POST request to the
[`/api/v1/marketplace/subscriptions`](https://customers.staging.gitlab.com/openapi_docs/marketplace#/marketplace/post_api_v1_marketplace_subscriptions)
@@ -129,8 +129,8 @@ endpoint in the Customers Portal with the following parameters in JSON format:
| Parameter | Type | Required | Description |
|--------------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `externalSubscriptionId` | string | yes | ID of the subscription on the GitLab Partner system. |
-| `tradingPartnerId` | string | yes | ID of the GitLab Partner account on the Customers Portal. |
+| `externalSubscriptionId` | string | yes | ID of the subscription on the Marketplace partner system. |
+| `tradingPartnerId` | string | yes | ID of the Trading Partner account in Salesforce. Received from GitLab. |
| `customer` | object | yes | Information about the customer. Must include company name. Contact must include `firstName`, `lastName` and `email`. Address must include `country`. |
| `orderLines` | array | yes | Specifies the product purchased. Must include `quantity` and `productId`. |
@@ -147,7 +147,7 @@ To get the status of a given subscription,
[`/api/v1/marketplace/subscriptions/{external_subscription_id}`](https://customers.staging.gitlab.com/openapi_docs/marketplace#/marketplace/get_api_v1_marketplace_subscriptions__external_subscription_id_)
endpoint in the Customers Portal.
-The request must include the GitLab partner system ID of the subscription to fetch the status for.
+The request must include the Marketplace partner system ID of the subscription to fetch the status for.
If the request is successful, the response body contains the status of the subscription provision. The status can be:
diff --git a/doc/operations/incident_management/slack.md b/doc/operations/incident_management/slack.md
index 434c481900c..68c6804f947 100644
--- a/doc/operations/incident_management/slack.md
+++ b/doc/operations/incident_management/slack.md
@@ -4,13 +4,14 @@ group: Respond
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Incident management for Slack **(FREE SAAS)**
+# Incident management for Slack (Beta) **(FREE SAAS)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344856) in GitLab 15.7 [with a flag](../../administration/feature_flags.md) named `incident_declare_slash_command`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344856) in GitLab 15.7 [with a flag](../../administration/feature_flags.md) named `incident_declare_slash_command`. Disabled by default.
+> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/378072) in GitLab 15.10 in [Open Beta](../../policy/alpha-beta-support.md#open-beta-features).
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `incident_declare_slash_command`.
-On GitLab.com, this feature is not available.
+On self-managed GitLab, this feature is not available.
+On GitLab.com, this feature is available.
The feature is not ready for production use.
Many teams receive alerts and collaborate in real time during incidents in Slack.
diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md
index 649b0c51818..e13e0e60277 100644
--- a/doc/user/project/integrations/gitlab_slack_application.md
+++ b/doc/user/project/integrations/gitlab_slack_application.md
@@ -63,6 +63,7 @@ Replace `<project>` with the project full path, or create a shorter [project ali
| `/gitlab <project> issue comment <id> <shift+return> <comment>` | Adds a new comment with the comment body `<comment>` to the issue with the ID `<id>`. |
| `/gitlab <project> deploy <from> to <to>` | [Deploys](#the-deploy-slash-command) from the `<from>` environment to the `<to>` environment. |
| `/gitlab <project> run <job name> <arguments>` | Executes the [ChatOps](../../../ci/chatops/index.md) job `<job name>` on the default branch. |
+| `/gitlab incident declare` | **Beta** Opens modal to [create a new incident](../../../operations/incident_management/slack.md). |
### The deploy slash command
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 1511e37e31e..004dfbc6953 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -99,7 +99,7 @@ You can define URL variables directly using the REST API.
## Configure your webhook receiver endpoint
Webhook receiver endpoints should be fast and stable.
-Slow and unstable receivers can be [disabled automatically](#failing-webhooks) to ensure system reliability. Webhooks that fail can lead to retries, [which cause duplicate events](#webhook-fails-or-multiple-webhook-requests-are-triggered).
+Slow and unstable receivers can be [disabled automatically](#failing-webhooks) to ensure system reliability. Webhooks that fail might lead to [duplicate events](#webhook-fails-or-multiple-webhook-requests-are-triggered).
Endpoints should follow these best practices:
@@ -129,11 +129,11 @@ FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `auto_disabling_web_hooks`.
On GitLab.com, this feature is available.
-If a webhook fails repeatedly, it may be disabled automatically.
+Webhooks that fail four consecutive times are automatically disabled.
Webhooks that return response codes in the `5xx` range are understood to be failing
intermittently and are temporarily disabled. These webhooks are initially disabled
-for 1 minute, which is extended on each retry up to a maximum of 24 hours.
+for one minute, which is extended on each subsequent failure up to a maximum of 24 hours.
Webhooks that return response codes in the `4xx` range are understood to be
misconfigured and are permanently disabled until you manually re-enable
@@ -293,8 +293,9 @@ To view the table:
1. Scroll down to the webhooks.
1. Each [failing webhook](#failing-webhooks) has a badge listing it as:
- - **Failed to connect** if it is misconfigured, and needs manual intervention to re-enable it.
- - **Fails to connect** if it is temporarily disabled and is retrying later.
+ - **Failed to connect** if it's misconfigured and must be manually re-enabled.
+ - **Fails to connect** if it's temporarily disabled and is automatically
+ re-enabled after the timeout limit has elapsed.
![Badges on failing webhooks](img/failed_badges.png)
@@ -330,8 +331,7 @@ Missing intermediate certificates are common causes of verification failure.
### Webhook fails or multiple webhook requests are triggered
-If you are receiving multiple webhook requests, the webhook might have timed out and
-been retried.
+If you're receiving multiple webhook requests, the webhook might have timed out.
GitLab expects a response in [10 seconds](../../../user/gitlab_com/index.md#other-limits). On self-managed GitLab instances, you can [change the webhook timeout limit](../../../administration/instance_limits.md#webhook-timeout).
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
index a92f65b064e..5cdf493fe6f 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
@@ -1,7 +1,7 @@
---
type: concepts
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
index 3bb62921979..849d478f922 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
@@ -1,7 +1,7 @@
---
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pages/getting_started_part_three.html'
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
index 34a2f221327..130709de3a5 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
@@ -1,8 +1,8 @@
---
type: reference
description: "Automatic Let's Encrypt SSL certificates for GitLab Pages."
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
index 0ba00177659..484dc784fdb 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
@@ -1,7 +1,7 @@
---
type: concepts
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/getting_started/pages_ci_cd_template.md b/doc/user/project/pages/getting_started/pages_ci_cd_template.md
index 37f74d18d4c..f0d591d02c9 100644
--- a/doc/user/project/pages/getting_started/pages_ci_cd_template.md
+++ b/doc/user/project/pages/getting_started/pages_ci_cd_template.md
@@ -1,7 +1,7 @@
---
type: reference, howto
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/getting_started/pages_forked_sample_project.md b/doc/user/project/pages/getting_started/pages_forked_sample_project.md
index a864c114b3e..008c63a0b2d 100644
--- a/doc/user/project/pages/getting_started/pages_forked_sample_project.md
+++ b/doc/user/project/pages/getting_started/pages_forked_sample_project.md
@@ -1,7 +1,7 @@
---
type: reference, howto
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/getting_started/pages_from_scratch.md b/doc/user/project/pages/getting_started/pages_from_scratch.md
index a3d6c8f75f9..2d511b34514 100644
--- a/doc/user/project/pages/getting_started/pages_from_scratch.md
+++ b/doc/user/project/pages/getting_started/pages_from_scratch.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/getting_started/pages_new_project_template.md b/doc/user/project/pages/getting_started/pages_new_project_template.md
index 7b6efaa7af4..859fd891fed 100644
--- a/doc/user/project/pages/getting_started/pages_new_project_template.md
+++ b/doc/user/project/pages/getting_started/pages_new_project_template.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index b286c59916a..6eb996db210 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 691757e3eca..f3b94bbf666 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,7 +1,7 @@
---
description: 'Learn how to use GitLab Pages to deploy a static website at no additional cost.'
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 0a8348f468e..36baff2341f 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/pages_access_control.md b/doc/user/project/pages/pages_access_control.md
index 248a74a8abc..1b046d03f59 100644
--- a/doc/user/project/pages/pages_access_control.md
+++ b/doc/user/project/pages/pages_access_control.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/public_folder.md b/doc/user/project/pages/public_folder.md
index 3007d1a10d1..019bec3e39c 100644
--- a/doc/user/project/pages/public_folder.md
+++ b/doc/user/project/pages/public_folder.md
@@ -1,8 +1,8 @@
---
description: 'Learn how to configure the build output folder for the most
common static site generators'
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/pages/redirects.md b/doc/user/project/pages/redirects.md
index 75cb1474dc2..bd8206b3bda 100644
--- a/doc/user/project/pages/redirects.md
+++ b/doc/user/project/pages/redirects.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/wiki/group.md b/doc/user/project/wiki/group.md
index de6aed52fd8..a3a6cad65e1 100644
--- a/doc/user/project/wiki/group.md
+++ b/doc/user/project/wiki/group.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index f01331ac784..eb13814f2ad 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -1,6 +1,6 @@
---
-stage: Create
-group: Editor
+stage: Plan
+group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
diff --git a/lib/api/entities/internal/pages/lookup_path.rb b/lib/api/entities/internal/pages/lookup_path.rb
index 1ea41e129b2..20326983fc7 100644
--- a/lib/api/entities/internal/pages/lookup_path.rb
+++ b/lib/api/entities/internal/pages/lookup_path.rb
@@ -10,7 +10,7 @@ module API
:prefix,
:project_id,
:source,
- :unique_domain
+ :unique_url
end
end
end
diff --git a/lib/gitlab/slash_commands/incident_management/incident_new.rb b/lib/gitlab/slash_commands/incident_management/incident_new.rb
index ce91edfd51a..a43235bdeb6 100644
--- a/lib/gitlab/slash_commands/incident_management/incident_new.rb
+++ b/lib/gitlab/slash_commands/incident_management/incident_new.rb
@@ -5,7 +5,7 @@ module Gitlab
module IncidentManagement
class IncidentNew < IncidentCommand
def self.help_message
- 'incident declare'
+ 'incident declare *(Beta)*'
end
def self.allowed?(_project, _user)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 97f8eef8f0d..5700229b309 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27674,6 +27674,9 @@ msgid_plural "Milestones"
msgstr[0] ""
msgstr[1] ""
+msgid "Milestone actions"
+msgstr ""
+
msgid "Milestone due date"
msgstr ""
@@ -28399,6 +28402,9 @@ msgstr ""
msgid "NavigationTheme|Red"
msgstr ""
+msgid "Navigation|Admin"
+msgstr ""
+
msgid "Navigation|Context navigation"
msgstr ""
diff --git a/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb b/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb
index a6cdd737341..b480a1f41cf 100644
--- a/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb
@@ -112,7 +112,10 @@ module QA
let(:disabled_after) { 4 }
it 'hook is auto-disabled',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/389595' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/389595', quarantine: {
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/393274',
+ type: :investigating
+ } do
Resource::ProjectWebHook.setup(fail_mock, session: session, issues: true) do |webhook, smocker|
hook_trigger_times.times do
Resource::Issue.fabricate_via_api! do |issue_init|
diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb
index 5fff7f4ff88..0fe6867483d 100755
--- a/scripts/review_apps/automated_cleanup.rb
+++ b/scripts/review_apps/automated_cleanup.rb
@@ -50,16 +50,12 @@ module ReviewApps
end
end
- def review_apps_namespace
- 'review-apps'
- end
-
def helm
@helm ||= Tooling::Helm3Client.new
end
def kubernetes
- @kubernetes ||= Tooling::KubernetesClient.new(namespace: review_apps_namespace)
+ @kubernetes ||= Tooling::KubernetesClient.new
end
def perform_gitlab_environment_cleanup!(days_for_delete:)
@@ -164,14 +160,14 @@ module ReviewApps
def perform_stale_namespace_cleanup!(days:)
puts "Dry-run mode." if dry_run
- kubernetes_client = Tooling::KubernetesClient.new(namespace: nil)
- kubernetes_client.cleanup_review_app_namespaces(created_before: threshold_time(days: days), wait: false) unless dry_run
+ kubernetes.cleanup_namespaces_by_created_at(created_before: threshold_time(days: days)) unless dry_run
end
def perform_stale_pvc_cleanup!(days:)
puts "Dry-run mode." if dry_run
- kubernetes.cleanup_by_created_at(resource_type: 'pvc', created_before: threshold_time(days: days), wait: false) unless dry_run
+
+ kubernetes.cleanup_pvcs_by_created_at(created_before: threshold_time(days: days)) unless dry_run
end
private
@@ -245,8 +241,7 @@ module ReviewApps
releases_names = releases.map(&:name)
unless dry_run
helm.delete(release_name: releases_names)
- kubernetes.cleanup_by_release(release_name: releases_names, wait: false)
- kubernetes.delete_namespaces_by_exact_names(resource_names: releases_names, wait: false)
+ kubernetes.delete_namespaces(releases_names)
end
rescue Tooling::Helm3Client::CommandFailedError => ex
@@ -289,8 +284,8 @@ if $PROGRAM_NAME == __FILE__
}
OptionParser.new do |opts|
- opts.on("-d", "--dry-run", "Whether to perform a dry-run or not.") do |value|
- options[:dry_run] = true
+ opts.on("-d", "--dry-run BOOLEAN", String, "Whether to perform a dry-run or not.") do |value|
+ options[:dry_run] = true if value == 'true'
end
opts.on("-h", "--help", "Prints this help") do
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 141e626c6f3..a7f2457de04 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe "User deletes milestone", :js, feature_category: :team_planning d
project.add_developer(user)
visit(project_milestones_path(project))
click_link(milestone.title)
+ click_button("Milestone actions")
click_button("Delete")
click_button("Delete milestone")
@@ -38,6 +39,7 @@ RSpec.describe "User deletes milestone", :js, feature_category: :team_planning d
visit(group_milestones_path(group))
click_link(milestone_to_be_deleted.title)
+ click_button("Milestone actions")
click_button("Delete")
click_button("Delete milestone")
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index acb2af07e50..f7e2adb7829 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -7,295 +7,394 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
let!(:project) { create(:project, :repository) }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
- let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
+ let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule, project: project) }
let(:scope) { nil }
let!(:user) { create(:user) }
+ let!(:maintainer) { create(:user) }
- before do
- stub_feature_flags(pipeline_schedules_vue: false)
- end
-
- context 'logged in as the pipeline schedule owner' do
+ context 'with pipeline_schedules_vue feature flag turned off' do
before do
- project.add_developer(user)
- pipeline_schedule.update!(owner: user)
- gitlab_sign_in(user)
+ stub_feature_flags(pipeline_schedules_vue: false)
end
- describe 'GET /projects/pipeline_schedules' do
+ context 'logged in as the pipeline schedule owner' do
before do
- visit_pipelines_schedules
+ project.add_developer(user)
+ pipeline_schedule.update!(owner: user)
+ gitlab_sign_in(user)
end
- it 'edits the pipeline' do
- page.within('.pipeline-schedule-table-row') do
- click_link 'Edit'
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
end
- expect(page).to have_content('Edit Pipeline Schedule')
- end
- end
+ it 'edits the pipeline' do
+ page.within('.pipeline-schedule-table-row') do
+ click_link 'Edit'
+ end
- describe 'PATCH /projects/pipelines_schedules/:id/edit' do
- before do
- edit_pipeline_schedule
+ expect(page).to have_content('Edit Pipeline Schedule')
+ end
end
- it 'displays existing properties' do
- description = find_field('schedule_description').value
- expect(description).to eq('pipeline schedule')
- expect(page).to have_button('master')
- expect(page).to have_button('Select timezone')
- end
+ describe 'PATCH /projects/pipelines_schedules/:id/edit' do
+ before do
+ edit_pipeline_schedule
+ end
- it 'edits the scheduled pipeline' do
- fill_in 'schedule_description', with: 'my brand new description'
+ it 'displays existing properties' do
+ description = find_field('schedule_description').value
+ expect(description).to eq('pipeline schedule')
+ expect(page).to have_button('master')
+ expect(page).to have_button('Select timezone')
+ end
- save_pipeline_schedule
+ it 'edits the scheduled pipeline' do
+ fill_in 'schedule_description', with: 'my brand new description'
- expect(page).to have_content('my brand new description')
- end
+ save_pipeline_schedule
- context 'when ref is nil' do
- before do
- pipeline_schedule.update_attribute(:ref, nil)
- edit_pipeline_schedule
+ expect(page).to have_content('my brand new description')
end
- it 'shows the pipeline schedule with default ref' do
- page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-button-text').text).to eq('master')
+ context 'when ref is nil' do
+ before do
+ pipeline_schedule.update_attribute(:ref, nil)
+ edit_pipeline_schedule
end
- end
- end
- context 'when ref is empty' do
- before do
- pipeline_schedule.update_attribute(:ref, '')
- edit_pipeline_schedule
+ it 'shows the pipeline schedule with default ref' do
+ page.within('[data-testid="schedule-target-ref"]') do
+ expect(first('.gl-button-text').text).to eq('master')
+ end
+ end
end
- it 'shows the pipeline schedule with default ref' do
- page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-button-text').text).to eq('master')
+ context 'when ref is empty' do
+ before do
+ pipeline_schedule.update_attribute(:ref, '')
+ edit_pipeline_schedule
+ end
+
+ it 'shows the pipeline schedule with default ref' do
+ page.within('[data-testid="schedule-target-ref"]') do
+ expect(first('.gl-button-text').text).to eq('master')
+ end
end
end
end
end
- end
- context 'logged in as a project maintainer' do
- before do
- project.add_maintainer(user)
- gitlab_sign_in(user)
- end
-
- describe 'GET /projects/pipeline_schedules' do
+ context 'logged in as a project maintainer' do
before do
- visit_pipelines_schedules
+ project.add_maintainer(user)
+ gitlab_sign_in(user)
end
- describe 'The view' do
- it 'displays the required information description' do
- page.within('.pipeline-schedule-table-row') do
- expect(page).to have_content('pipeline schedule')
- expect(find("[data-testid='next-run-cell'] time")['title'])
- .to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
- expect(page).to have_link('master')
- expect(page).to have_link("##{pipeline.id}")
- end
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
end
- it 'creates a new scheduled pipeline' do
- click_link 'New schedule'
+ describe 'The view' do
+ it 'displays the required information description' do
+ page.within('.pipeline-schedule-table-row') do
+ expect(page).to have_content('pipeline schedule')
+ expect(find("[data-testid='next-run-cell'] time")['title'])
+ .to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
+ expect(page).to have_link('master')
+ expect(page).to have_link("##{pipeline.id}")
+ end
+ end
- expect(page).to have_content('Schedule a new pipeline')
- end
+ it 'creates a new scheduled pipeline' do
+ click_link 'New schedule'
+
+ expect(page).to have_content('Schedule a new pipeline')
+ end
- it 'changes ownership of the pipeline' do
- click_button 'Take ownership'
+ it 'changes ownership of the pipeline' do
+ click_button 'Take ownership'
- page.within('#pipeline-take-ownership-modal') do
- click_link 'Take ownership'
+ page.within('#pipeline-take-ownership-modal') do
+ click_link 'Take ownership'
+ end
+
+ page.within('.pipeline-schedule-table-row') do
+ expect(page).not_to have_content('No owner')
+ expect(page).to have_link('Sidney Jones')
+ end
end
- page.within('.pipeline-schedule-table-row') do
- expect(page).not_to have_content('No owner')
- expect(page).to have_link('Sidney Jones')
+ it 'deletes the pipeline' do
+ click_link 'Delete'
+
+ accept_gl_confirm(button_text: 'Delete pipeline schedule')
+
+ expect(page).not_to have_css(".pipeline-schedule-table-row")
end
end
- it 'deletes the pipeline' do
- click_link 'Delete'
+ context 'when ref is nil' do
+ before do
+ pipeline_schedule.update_attribute(:ref, nil)
+ visit_pipelines_schedules
+ end
+
+ it 'shows a list of the pipeline schedules with empty ref column' do
+ expect(first('.branch-name-cell').text).to eq('')
+ end
+ end
- accept_gl_confirm(button_text: 'Delete pipeline schedule')
+ context 'when ref is empty' do
+ before do
+ pipeline_schedule.update_attribute(:ref, '')
+ visit_pipelines_schedules
+ end
- expect(page).not_to have_css(".pipeline-schedule-table-row")
+ it 'shows a list of the pipeline schedules with empty ref column' do
+ expect(first('.branch-name-cell').text).to eq('')
+ end
end
end
- context 'when ref is nil' do
+ describe 'POST /projects/pipeline_schedules/new' do
before do
- pipeline_schedule.update_attribute(:ref, nil)
- visit_pipelines_schedules
+ visit_new_pipeline_schedule
+ end
+
+ it 'sets defaults for timezone and target branch' do
+ expect(page).to have_button('master')
+ expect(page).to have_button('Select timezone')
end
- it 'shows a list of the pipeline schedules with empty ref column' do
- expect(first('.branch-name-cell').text).to eq('')
+ it 'creates a new scheduled pipeline' do
+ fill_in_schedule_form
+ save_pipeline_schedule
+
+ expect(page).to have_content('my fancy description')
+ end
+
+ it 'prevents an invalid form from being submitted' do
+ save_pipeline_schedule
+
+ expect(page).to have_content('This field is required')
end
end
- context 'when ref is empty' do
+ context 'when user creates a new pipeline schedule with variables' do
before do
- pipeline_schedule.update_attribute(:ref, '')
visit_pipelines_schedules
+ click_link 'New schedule'
+ fill_in_schedule_form
+ all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
+ all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
+ all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
+ all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
+ save_pipeline_schedule
end
- it 'shows a list of the pipeline schedules with empty ref column' do
- expect(first('.branch-name-cell').text).to eq('')
+ it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
+ expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
+ expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
+ end
end
end
- end
- describe 'POST /projects/pipeline_schedules/new' do
- before do
- visit_new_pipeline_schedule
- end
+ context 'when user edits a variable of a pipeline schedule' do
+ before do
+ create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
+ create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
+ end
- it 'sets defaults for timezone and target branch' do
- expect(page).to have_button('master')
- expect(page).to have_button('Select timezone')
+ visit_pipelines_schedules
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ find('.js-ci-variable-list-section .js-secret-value-reveal-button').click
+ first('.js-ci-variable-input-key').set('foo')
+ first('.js-ci-variable-input-value').set('bar')
+ click_button 'Save pipeline schedule'
+ end
+
+ it 'user sees the updated variable in edit window' do
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar')
+ end
+ end
end
- it 'creates a new scheduled pipeline' do
- fill_in_schedule_form
- save_pipeline_schedule
+ context 'when user removes a variable of a pipeline schedule' do
+ before do
+ create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
+ create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
+ end
- expect(page).to have_content('my fancy description')
+ visit_pipelines_schedules
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ find('.ci-variable-list .ci-variable-row-remove-button').click
+ click_button 'Save pipeline schedule'
+ end
+
+ it 'user does not see the removed variable in edit window' do
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('')
+ end
+ end
end
- it 'prevents an invalid form from being submitted' do
- save_pipeline_schedule
+ context 'when active is true and next_run_at is NULL' do
+ before do
+ create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
+ pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
+ end
+ end
+
+ it 'user edit and recover the problematic pipeline schedule' do
+ visit_pipelines_schedules
+ find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
+ fill_in 'schedule_cron', with: '* 1 2 3 4'
+ click_button 'Save pipeline schedule'
- expect(page).to have_content('This field is required')
+ page.within('.pipeline-schedule-table-row:nth-child(1)') do
+ expect(page).to have_css("[data-testid='next-run-cell'] time")
+ end
+ end
end
end
- context 'when user creates a new pipeline schedule with variables' do
+ context 'logged in as non-member' do
before do
- visit_pipelines_schedules
- click_link 'New schedule'
- fill_in_schedule_form
- all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
- all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
- all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
- all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
- save_pipeline_schedule
+ gitlab_sign_in(user)
end
- it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
- find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.ci-variable-list') do
- expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
- expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
- expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
- expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
+ end
+
+ describe 'The view' do
+ it 'does not show create schedule button' do
+ expect(page).not_to have_link('New schedule')
+ end
end
end
end
- context 'when user edits a variable of a pipeline schedule' do
- before do
- create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
- create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
+ context 'not logged in' do
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
end
- visit_pipelines_schedules
- find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
-
- find('.js-ci-variable-list-section .js-secret-value-reveal-button').click
- first('.js-ci-variable-input-key').set('foo')
- first('.js-ci-variable-input-value').set('bar')
- click_button 'Save pipeline schedule'
- end
-
- it 'user sees the updated variable in edit window' do
- find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.ci-variable-list') do
- expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
- expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar')
+ describe 'The view' do
+ it 'does not show create schedule button' do
+ expect(page).not_to have_link('New schedule')
+ end
end
end
end
+ end
- context 'when user removes a variable of a pipeline schedule' do
+ context 'with pipeline_schedules_vue feature flag turned on' do
+ context 'logged in as a project maintainer' do
before do
- create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
- create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
- end
-
- visit_pipelines_schedules
- find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- find('.ci-variable-list .ci-variable-row-remove-button').click
- click_button 'Save pipeline schedule'
+ project.add_maintainer(maintainer)
+ pipeline_schedule.update!(owner: user)
+ gitlab_sign_in(maintainer)
end
- it 'user does not see the removed variable in edit window' do
- find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.ci-variable-list') do
- expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
- expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('')
- end
- end
- end
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
- context 'when active is true and next_run_at is NULL' do
- before do
- create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
- pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
+ wait_for_requests
end
- end
- it 'user edit and recover the problematic pipeline schedule' do
- visit_pipelines_schedules
- find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- fill_in 'schedule_cron', with: '* 1 2 3 4'
- click_button 'Save pipeline schedule'
+ describe 'The view' do
+ it 'displays the required information description' do
+ page.within('[data-testid="pipeline-schedule-table-row"]') do
+ expect(page).to have_content('pipeline schedule')
+ expect(find("[data-testid='next-run-cell'] time")['title'])
+ .to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
+ expect(page).to have_link('master')
+ expect(find("[data-testid='last-pipeline-status'] a")['href']).to include(pipeline.id.to_s)
+ end
+ end
+
+ it 'changes ownership of the pipeline' do
+ click_button 'Take ownership'
+
+ page.within('#pipeline-take-ownership-modal') do
+ click_button 'Take ownership'
+
+ wait_for_requests
+ end
+
+ page.within('[data-testid="pipeline-schedule-table-row"]') do
+ expect(page).not_to have_content('No owner')
+ expect(page).to have_link('Sidney Jones')
+ end
+ end
- page.within('.pipeline-schedule-table-row:nth-child(1)') do
- expect(page).to have_css("[data-testid='next-run-cell'] time")
+ it 'runs the pipeline' do
+ click_button 'Run pipeline schedule'
+
+ wait_for_requests
+
+ expect(page).to have_content("Successfully scheduled a pipeline to run. Go to the Pipelines page for details.")
+ end
+
+ it 'deletes the pipeline' do
+ click_button 'Delete pipeline schedule'
+
+ accept_gl_confirm(button_text: 'Delete pipeline schedule')
+
+ expect(page).not_to have_css('[data-testid="pipeline-schedule-table-row"]')
+ end
end
end
end
- end
-
- context 'logged in as non-member' do
- before do
- gitlab_sign_in(user)
- end
- describe 'GET /projects/pipeline_schedules' do
+ context 'logged in as non-member' do
before do
- visit_pipelines_schedules
+ gitlab_sign_in(user)
end
- describe 'The view' do
- it 'does not show create schedule button' do
- expect(page).not_to have_link('New schedule')
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
+
+ wait_for_requests
+ end
+
+ describe 'The view' do
+ it 'does not show create schedule button' do
+ expect(page).not_to have_link('New schedule')
+ end
end
end
end
- end
- context 'not logged in' do
- describe 'GET /projects/pipeline_schedules' do
- before do
- visit_pipelines_schedules
- end
+ context 'not logged in' do
+ describe 'GET /projects/pipeline_schedules' do
+ before do
+ visit_pipelines_schedules
+
+ wait_for_requests
+ end
- describe 'The view' do
- it 'does not show create schedule button' do
- expect(page).not_to have_link('New schedule')
+ describe 'The view' do
+ it 'does not show create schedule button' do
+ expect(page).not_to have_link('New schedule')
+ end
end
end
end
diff --git a/spec/fixtures/api/schemas/internal/pages/lookup_path.json b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
index 8ca71870911..c6aa2122058 100644
--- a/spec/fixtures/api/schemas/internal/pages/lookup_path.json
+++ b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
@@ -24,7 +24,7 @@
"additionalProperties": false
},
"prefix": { "type": "string" },
- "unique_domain": { "type": ["string", "null"] }
+ "unique_url": { "type": ["string", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
index 694c16a85c4..66f1ca2b32a 100644
--- a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -19,7 +19,6 @@ describe('ManageTwoFactorForm', () => {
wrapper = mountExtended(ManageTwoFactorForm, {
provide: {
...defaultProvide,
- webauthnEnabled: options?.webauthnEnabled ?? false,
isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
},
stubs: {
@@ -91,17 +90,7 @@ describe('ManageTwoFactorForm', () => {
describe('when clicked', () => {
itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findDisableButton);
- itShowsConfirmationModal(i18n.confirm);
-
- describe('when webauthnEnabled', () => {
- beforeEach(() => {
- createComponent({
- webauthnEnabled: true,
- });
- });
-
- itShowsConfirmationModal(i18n.confirmWebAuthn);
- });
+ itShowsConfirmationModal(i18n.confirmWebAuthn);
it('modifies the form action and method when submitted through the button', async () => {
const form = findForm();
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 2807fe7727f..3eb47fdb97e 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
@@ -16,22 +16,20 @@ describe('Design overlay component', () => {
const mockDimensions = { width: 100, height: 100 };
- const findOverlay = () => wrapper.find('[data-testid="design-overlay"]');
- const findAllNotes = () => wrapper.findAll('[data-testid="note-pin"]');
- const findCommentBadge = () => wrapper.find('[data-testid="comment-badge"]');
+ const findOverlay = () => wrapper.findByTestId('design-overlay');
+ const findAllNotes = () => wrapper.findAllByTestId('note-pin');
+ const findCommentBadge = () => wrapper.findByTestId('comment-badge');
const findBadgeAtIndex = (noteIndex) => findAllNotes().at(noteIndex);
const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
- const clickAndDragBadge = async (elem, fromPoint, toPoint) => {
+ const clickAndDragBadge = (elem, fromPoint, toPoint) => {
elem.vm.$emit(
'mousedown',
new MouseEvent('click', { clientX: fromPoint.x, clientY: fromPoint.y }),
);
findOverlay().trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
- await nextTick();
elem.vm.$emit('mouseup', new MouseEvent('click', { clientX: toPoint.x, clientY: toPoint.y }));
- await nextTick();
};
function createComponent(props = {}, data = {}) {
@@ -47,7 +45,7 @@ describe('Design overlay component', () => {
},
});
- wrapper = shallowMount(DesignOverlay, {
+ wrapper = shallowMountExtended(DesignOverlay, {
apolloProvider,
propsData: {
dimensions: mockDimensions,
@@ -80,7 +78,7 @@ describe('Design overlay component', () => {
expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;');
});
- it('should emit `openCommentForm` when clicking on overlay', async () => {
+ it('should emit `openCommentForm` when clicking on overlay', () => {
createComponent();
const newCoordinates = {
x: 10,
@@ -90,7 +88,7 @@ describe('Design overlay component', () => {
wrapper
.find('[data-qa-selector="design_image_button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
- await nextTick();
+
expect(wrapper.emitted('openCommentForm')).toEqual([
[{ x: newCoordinates.x, y: newCoordinates.y }],
]);
@@ -175,25 +173,15 @@ describe('Design overlay component', () => {
});
});
- it('should recalculate badges positions on window resize', async () => {
+ it('should calculate badges positions based on dimensions', () => {
createComponent({
notes,
dimensions: {
- width: 400,
- height: 400,
- },
- });
-
- expect(findFirstBadge().props('position')).toEqual({ left: '40px', top: '60px' });
-
- wrapper.setProps({
- dimensions: {
width: 200,
height: 200,
},
});
- await nextTick();
expect(findFirstBadge().props('position')).toEqual({ left: '20px', top: '30px' });
});
@@ -216,7 +204,6 @@ describe('Design overlay component', () => {
new MouseEvent('click', { clientX: position.x, clientY: position.y }),
);
- await nextTick();
findFirstBadge().vm.$emit(
'mouseup',
new MouseEvent('click', { clientX: position.x, clientY: position.y }),
@@ -290,7 +277,7 @@ describe('Design overlay component', () => {
});
describe('when moving the comment badge', () => {
- it('should update badge style when note-moving action ends', async () => {
+ it('should update badge style when note-moving action ends', () => {
const { position } = notes[0];
createComponent({
currentCommentForm: {
@@ -298,19 +285,15 @@ describe('Design overlay component', () => {
},
});
- const commentBadge = findCommentBadge();
+ expect(findCommentBadge().props('position')).toEqual({ left: '10px', top: '15px' });
+
const toPoint = { x: 20, y: 20 };
- await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint);
- commentBadge.vm.$emit('mouseup', new MouseEvent('click'));
- // simulates the currentCommentForm being updated in index.vue component, and
- // propagated back down to this prop
- wrapper.setProps({
+ createComponent({
currentCommentForm: { height: position.height, width: position.width, ...toPoint },
});
- await nextTick();
- expect(commentBadge.props('position')).toEqual({ left: '20px', top: '20px' });
+ expect(findCommentBadge().props('position')).toEqual({ left: '20px', top: '20px' });
});
it('should emit `openCommentForm` event when mouseleave fired on overlay element', async () => {
@@ -330,8 +313,7 @@ describe('Design overlay component', () => {
newCoordinates,
);
- wrapper.trigger('mouseleave');
- await nextTick();
+ findOverlay().vm.$emit('mouseleave');
expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
});
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index bd0e3455872..eb895bd9057 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import Autosave from '~/autosave';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createModules } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
@@ -11,7 +10,6 @@ import { noteableDataMock } from 'jest/notes/mock_data';
import { getDiffFileMock } from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-jest.mock('~/autosave');
describe('DiffLineNoteForm', () => {
let wrapper;
@@ -77,7 +75,6 @@ describe('DiffLineNoteForm', () => {
const findCommentForm = () => wrapper.findComponent(MultilineCommentForm);
beforeEach(() => {
- Autosave.mockClear();
createComponent();
});
@@ -100,19 +97,6 @@ describe('DiffLineNoteForm', () => {
});
});
- it('should init autosave', () => {
- // we're using shallow mount here so there's no element to pass to Autosave
- expect(Autosave).toHaveBeenCalledWith(undefined, [
- 'Note',
- 'Issue',
- 98,
- undefined,
- 'DiffNote',
- undefined,
- '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
- ]);
- });
-
describe('when cancelling form', () => {
afterEach(() => {
confirmAction.mockReset();
@@ -146,7 +130,6 @@ describe('DiffLineNoteForm', () => {
await nextTick();
expect(getSelectedLine().hasForm).toBe(false);
- expect(Autosave.mock.instances[0].reset).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/lib/utils/error_message_spec.js b/spec/frontend/lib/utils/error_message_spec.js
index 17b5168c32f..54c630b8ba0 100644
--- a/spec/frontend/lib/utils/error_message_spec.js
+++ b/spec/frontend/lib/utils/error_message_spec.js
@@ -1,65 +1,43 @@
import { parseErrorMessage, USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
-const defaultErrorMessage = 'Something caused this error';
-const userFacingErrorMessage = 'User facing error message';
-const nonUserFacingErrorMessage = 'NonUser facing error message';
-const genericErrorMessage = 'Some error message';
-
-describe('error message', () => {
- describe('when given an errormessage object', () => {
- const errorMessageObject = {
- options: {
- cause: defaultErrorMessage,
- },
- filename: 'error.js',
- linenumber: 7,
- };
-
- it('returns the correct values for userfacing errors', () => {
- const userFacingObject = errorMessageObject;
- userFacingObject.message = `${USER_FACING_ERROR_MESSAGE_PREFIX} ${userFacingErrorMessage}`;
-
- expect(parseErrorMessage(userFacingObject)).toEqual({
- message: userFacingErrorMessage,
- userFacing: true,
- });
- });
-
- it('returns the correct values for non userfacing errors', () => {
- const nonUserFacingObject = errorMessageObject;
- nonUserFacingObject.message = nonUserFacingErrorMessage;
-
- expect(parseErrorMessage(nonUserFacingObject)).toEqual({
- message: nonUserFacingErrorMessage,
- userFacing: false,
- });
- });
- });
-
- describe('when given an errormessage string', () => {
- it('returns the correct values for userfacing errors', () => {
- expect(
- parseErrorMessage(`${USER_FACING_ERROR_MESSAGE_PREFIX} ${genericErrorMessage}`),
- ).toEqual({
- message: genericErrorMessage,
- userFacing: true,
- });
- });
-
- it('returns the correct values for non userfacing errors', () => {
- expect(parseErrorMessage(genericErrorMessage)).toEqual({
- message: genericErrorMessage,
- userFacing: false,
- });
- });
- });
-
- describe('when given nothing', () => {
- it('returns an empty error message', () => {
- expect(parseErrorMessage()).toEqual({
- message: '',
- userFacing: false,
- });
- });
- });
+const defaultErrorMessage = 'Default error message';
+const errorMessage = 'Returned error message';
+
+const generateErrorWithMessage = (message) => {
+ return {
+ message,
+ };
+};
+
+describe('parseErrorMessage', () => {
+ it.each`
+ error | expectedResult
+ ${`${USER_FACING_ERROR_MESSAGE_PREFIX} ${errorMessage}`} | ${errorMessage}
+ ${`${errorMessage} ${USER_FACING_ERROR_MESSAGE_PREFIX}`} | ${defaultErrorMessage}
+ ${errorMessage} | ${defaultErrorMessage}
+ ${undefined} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage}
+ `(
+ 'properly parses "$error" error object and returns "$expectedResult"',
+ ({ error, expectedResult }) => {
+ const errorObject = generateErrorWithMessage(error);
+ expect(parseErrorMessage(errorObject, defaultErrorMessage)).toEqual(expectedResult);
+ },
+ );
+
+ it.each`
+ error | defaultMessage | expectedResult
+ ${undefined} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${{}} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${undefined} | ${''}
+ ${generateErrorWithMessage(`${USER_FACING_ERROR_MESSAGE_PREFIX} ${errorMessage}`)} | ${undefined} | ${errorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${''} | ${''}
+ ${generateErrorWithMessage(`${USER_FACING_ERROR_MESSAGE_PREFIX} ${errorMessage}`)} | ${''} | ${errorMessage}
+ `(
+ 'properly handles the edge case of error="$error" and defaultMessage="$defaultMessage"',
+ ({ error, defaultMessage, expectedResult }) => {
+ expect(parseErrorMessage(error, defaultMessage)).toEqual(expectedResult);
+ },
+ );
});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 062cd098640..891b5c751fb 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
@@ -27,6 +28,8 @@ jest.mock('~/alert');
Vue.use(Vuex);
describe('issue_comment_form component', () => {
+ useLocalStorageSpy();
+
let store;
let wrapper;
let axiosMock;
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index b4f185004bb..c4f8e50b969 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -7,10 +7,7 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
-import Autosave from '~/autosave';
-
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
-
import { noteableDataMock, notesDataMock, note } from '../mock_data';
jest.mock('~/autosave');
@@ -82,11 +79,6 @@ describe('issue_note_body component', () => {
expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
});
- it('adds autosave', () => {
- // passing undefined instead of an element because of shallowMount
- expect(Autosave).toHaveBeenCalledWith(undefined, ['Note', note.noteable_type, note.id]);
- });
-
describe('isInternalNote', () => {
beforeEach(() => {
wrapper.setProps({ isInternalNote: true });
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 59362e18098..12c3b154fc7 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -2,7 +2,6 @@ import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
-import { getDraft, updateDraft } from '~/lib/utils/autosave';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@@ -12,30 +11,25 @@ import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_d
jest.mock('~/lib/utils/autosave');
describe('issue_note_form component', () => {
- const dummyAutosaveKey = 'some-autosave-key';
- const dummyDraft = 'dummy draft content';
-
let store;
let wrapper;
let props;
+ let features;
const createComponentWrapper = () => {
return mount(NoteForm, {
store,
propsData: props,
+ provide: {
+ glFeatures: features || {},
+ },
});
};
const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
beforeEach(() => {
- getDraft.mockImplementation((key) => {
- if (key === dummyAutosaveKey) {
- return dummyDraft;
- }
-
- return null;
- });
+ features = {};
store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
@@ -68,6 +62,20 @@ describe('issue_note_form component', () => {
});
});
+ it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
+ features = { contentEditorOnIssues: false };
+ wrapper = createComponentWrapper();
+
+ expect(wrapper.text()).not.toContain('Rich text');
+ });
+
+ it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
+ features = { contentEditorOnIssues: true };
+ wrapper = createComponentWrapper();
+
+ expect(wrapper.text()).toContain('Rich text');
+ });
+
describe('conflicts editing', () => {
beforeEach(() => {
wrapper = createComponentWrapper();
@@ -117,13 +125,15 @@ describe('issue_note_form component', () => {
${true} | ${'Write an internal note or drag your files here…'}
`(
'should set correct textarea placeholder text when discussion confidentiality is $internal',
- ({ internal, placeholder }) => {
+ async ({ internal, placeholder }) => {
props.note = {
...note,
internal,
};
wrapper = createComponentWrapper();
+ await nextTick();
+
expect(wrapper.find('textarea').attributes('placeholder')).toBe(placeholder);
},
);
@@ -204,7 +214,7 @@ describe('issue_note_form component', () => {
});
await nextTick();
- const textareaEl = wrapper.vm.$refs.textarea;
+ const textareaEl = wrapper.vm.$refs.markdownEditor.$el.querySelector('textarea');
const cancelButton = findCancelButton();
textareaEl.classList.add(AT_WHO_ACTIVE_CLASS);
cancelButton.vm.$emit('click');
@@ -229,78 +239,6 @@ describe('issue_note_form component', () => {
});
});
- describe('with autosaveKey', () => {
- describe('with draft', () => {
- beforeEach(() => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- });
- wrapper = createComponentWrapper();
-
- return nextTick();
- });
-
- it('displays the draft in textarea', () => {
- const textarea = wrapper.find('textarea');
-
- expect(textarea.element.value).toBe(dummyDraft);
- });
- });
-
- describe('without draft', () => {
- beforeEach(() => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: 'some key without draft',
- });
- wrapper = createComponentWrapper();
-
- return nextTick();
- });
-
- it('leaves the textarea empty', () => {
- const textarea = wrapper.find('textarea');
-
- expect(textarea.element.value).toBe('');
- });
- });
-
- it('updates the draft if textarea content changes', () => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- });
- wrapper = createComponentWrapper();
- const textarea = wrapper.find('textarea');
- const dummyContent = 'some new content';
-
- textarea.setValue(dummyContent);
-
- expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
- });
-
- it('does not save draft when ctrl+enter is pressed', () => {
- const options = {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- };
-
- props = { ...props, ...options };
- wrapper = createComponentWrapper();
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isSubmittingWithKeydown: true });
-
- const textarea = wrapper.find('textarea');
- textarea.setValue('some content');
- textarea.trigger('keydown.enter', { metaKey: true });
-
- expect(updateDraft).not.toHaveBeenCalled();
- });
- });
-
describe('with batch comments', () => {
beforeEach(() => {
store.registerModule('batchComments', batchComments());
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 0ca350f9ed7..ae5316eb12f 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -26,8 +26,6 @@ import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
-import { USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
-import { manageViaMRErrorMessage } from '../constants';
const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@@ -202,21 +200,20 @@ describe('App component', () => {
});
});
- describe('when user facing error occurs', () => {
+ describe('when error occurs', () => {
+ const errorMessage = 'There was a manage via MR error';
+
it('should show Alert with error Message', async () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
- // Prefixed with USER_FACING_ERROR_MESSAGE_PREFIX as used in lib/gitlab/utils/error_message.rb to indicate a user facing error
- findFeatureCards()
- .at(1)
- .vm.$emit('error', `${USER_FACING_ERROR_MESSAGE_PREFIX} ${manageViaMRErrorMessage}`);
+ findFeatureCards().at(1).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
- expect(findManageViaMRErrorAlert().text()).toEqual(manageViaMRErrorMessage);
+ expect(findManageViaMRErrorAlert().text()).toBe(errorMessage);
});
it('should hide Alert when it is dismissed', async () => {
- findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage);
+ findFeatureCards().at(1).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
@@ -226,17 +223,6 @@ describe('App component', () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
});
});
-
- describe('when non-user facing error occurs', () => {
- it('should show Alert with generic error Message', async () => {
- expect(findManageViaMRErrorAlert().exists()).toBe(false);
- findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage);
-
- await nextTick();
- expect(findManageViaMRErrorAlert().exists()).toBe(true);
- expect(findManageViaMRErrorAlert().text()).toEqual(i18n.genericErrorText);
- });
- });
});
describe('Auto DevOps hint alert', () => {
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
index 538e87cf843..92df8129799 100644
--- a/spec/frontend/super_sidebar/components/context_switcher_spec.js
+++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js
@@ -5,6 +5,7 @@ import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
import ProjectsList from '~/super_sidebar/components/projects_list.vue';
import GroupsList from '~/super_sidebar/components/groups_list.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -23,6 +24,7 @@ jest.mock('~/super_sidebar/utils', () => ({
trackContextAccess: jest.fn(),
}));
+const persistentLinks = [{ title: 'Explore', link: '/explore', icon: 'compass' }];
const username = 'root';
const projectsPath = 'projectsPath';
const groupsPath = 'groupsPath';
@@ -33,6 +35,7 @@ describe('ContextSwitcher component', () => {
let wrapper;
let mockApollo;
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findProjectsList = () => wrapper.findComponent(ProjectsList);
const findGroupsList = () => wrapper.findComponent(GroupsList);
@@ -60,6 +63,7 @@ describe('ContextSwitcher component', () => {
wrapper = shallowMountExtended(ContextSwitcher, {
apolloProvider: mockApollo,
propsData: {
+ persistentLinks,
username,
projectsPath,
groupsPath,
@@ -84,6 +88,12 @@ describe('ContextSwitcher component', () => {
createWrapper();
});
+ it('renders the persistent links', () => {
+ const navItems = findNavItems();
+ expect(navItems.length).toBe(persistentLinks.length);
+ expect(navItems.at(0).props('item')).toBe(persistentLinks[0]);
+ });
+
it('passes the placeholder to the search box', () => {
expect(findSearchBox().props('placeholder')).toBe(
s__('Navigation|Search for projects or groups'),
@@ -138,6 +148,10 @@ describe('ContextSwitcher component', () => {
return triggerSearchQuery();
});
+ it('hides persistent links', () => {
+ expect(findNavItems().length).toBe(0);
+ });
+
it('triggers the search query on search', () => {
expect(searchUserProjectsAndGroupsHandlerSuccess).toHaveBeenCalled();
});
diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js
index dd48935c138..daec5c2a9b4 100644
--- a/spec/frontend/super_sidebar/components/search_results_spec.js
+++ b/spec/frontend/super_sidebar/components/search_results_spec.js
@@ -1,7 +1,9 @@
+import { GlCollapse } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import SearchResults from '~/super_sidebar/components/search_results.vue';
import ItemsList from '~/super_sidebar/components/items_list.vue';
+import { stubComponent } from 'helpers/stub_component';
const title = s__('Navigation|PROJECTS');
const noResultsText = s__('Navigation|No project matches found');
@@ -9,7 +11,8 @@ const noResultsText = s__('Navigation|No project matches found');
describe('SearchResults component', () => {
let wrapper;
- const findListTitle = () => wrapper.findByTestId('list-title');
+ const findSearchResultsToggle = () => wrapper.findByTestId('search-results-toggle');
+ const findCollapsibleSection = () => wrapper.findComponent(GlCollapse);
const findItemsList = () => wrapper.findComponent(ItemsList);
const findEmptyText = () => wrapper.findByTestId('empty-text');
@@ -20,6 +23,11 @@ describe('SearchResults component', () => {
noResultsText,
...props,
},
+ stubs: {
+ GlCollapse: stubComponent(GlCollapse, {
+ props: ['visible'],
+ }),
+ },
});
};
@@ -29,7 +37,11 @@ describe('SearchResults component', () => {
});
it("renders the list's title", () => {
- expect(findListTitle().text()).toBe(title);
+ expect(findSearchResultsToggle().text()).toBe(title);
+ });
+
+ it('is expanded', () => {
+ expect(findCollapsibleSection().props('visible')).toBe(true);
});
it('renders the empty text', () => {
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index b540f85d9fe..8c70693465f 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -86,6 +86,7 @@ export const sidebarData = {
gitlab_version_check: { severity: 'success' },
gitlab_com_and_canary: false,
canary_toggle_com_url: 'https://next.gitlab.com',
+ context_switcher_links: [],
};
export const userMenuMockStatus = {
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 681ff6c8dd3..7bda37bcaa8 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -275,7 +275,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await findTextarea().setValue(newValue);
- expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ expect(wrapper.emitted('input')).toEqual([[value], [newValue]]);
});
it('autosaves the markdown value to local storage', async () => {
@@ -370,7 +370,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await findContentEditor().vm.$emit('change', { markdown: newValue });
- expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ expect(wrapper.emitted('input')).toEqual([[value], [newValue]]);
});
it('autosaves the content editor value to local storage', async () => {
diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
index 6345393951c..646b37d334b 100644
--- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
@@ -8,7 +8,9 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { humanize } from '~/lib/utils/text_utility';
import { redirectTo } from '~/lib/utils/url_utility';
-import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
+import ManageViaMr, {
+ i18n,
+} from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks';
@@ -77,10 +79,11 @@ describe('ManageViaMr component', () => {
buildConfigureSecurityFeatureMock({
successPath: '',
});
- const errorHandler = async () =>
- buildConfigureSecurityFeatureMock({
- errors: ['foo'],
+ const errorHandler = async (message = 'foo') => {
+ return buildConfigureSecurityFeatureMock({
+ errors: [message],
});
+ };
const pendingHandler = () => new Promise(() => {});
describe('when feature is configured', () => {
@@ -147,9 +150,11 @@ describe('ManageViaMr component', () => {
});
describe.each`
- handler | message
- ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
- ${errorHandler} | ${'foo'}
+ handler | message
+ ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
+ ${errorHandler.bind(null, 'UF: message')} | ${'message'}
+ ${errorHandler.bind(null, 'message')} | ${i18n.genericErrorText}
+ ${errorHandler} | ${i18n.genericErrorText}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const apolloProvider = createMockApolloProvider(mutation, handler);
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index ba703914049..2cea577a852 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -306,6 +306,46 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
end
+ describe 'projects_autocomplete' do
+ let_it_be(:user) { create(:user, name: "madelein") }
+ let_it_be(:project_1) { create(:project, name: 'test 1') }
+ let_it_be(:project_2) { create(:project, name: 'test 2') }
+ let(:search_term) { 'test' }
+
+ before do
+ allow(self).to receive(:current_user).and_return(user)
+ end
+
+ context 'when the user does not have access to projects' do
+ it 'does not return any results' do
+ expect(projects_autocomplete(search_term)).to eq([])
+ end
+ end
+
+ context 'when the user has access to one project' do
+ before do
+ project_2.add_developer(user)
+ end
+
+ it 'returns the project' do
+ expect(projects_autocomplete(search_term).pluck(:id)).to eq([project_2.id])
+ end
+
+ context 'when a project namespace matches the search term but the project does not' do
+ let_it_be(:group) { create(:group, name: 'test group') }
+ let_it_be(:project_3) { create(:project, name: 'nothing', namespace: group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'returns all projects matching the term' do
+ expect(projects_autocomplete(search_term).pluck(:id)).to match_array([project_2.id, project_3.id])
+ end
+ end
+ end
+ end
+
describe 'search_entries_info' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index dbb6f9bd9f3..ea48246fabe 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -246,6 +246,38 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
end
end
end
+
+ describe 'context switcher persistent links' do
+ let_it_be(:public_link) do
+ [
+ { title: s_('Navigation|Your work'), link: '/', icon: 'work' },
+ { title: s_('Navigation|Explore'), link: '/explore', icon: 'compass' }
+ ]
+ end
+
+ subject do
+ helper.super_sidebar_context(user, group: nil, project: nil, panel: panel)
+ end
+
+ context 'when user is not an admin' do
+ it 'returns only the public links' do
+ expect(subject[:context_switcher_links]).to eq(public_link)
+ end
+ end
+
+ context 'when user is an admin' do
+ before do
+ allow(user).to receive(:can_admin_all_resources?).and_return(true)
+ end
+
+ it 'returns public links and admin area link' do
+ expect(subject[:context_switcher_links]).to eq([
+ *public_link,
+ { title: s_('Navigation|Admin'), link: '/admin', icon: 'admin' }
+ ])
+ end
+ end
+ end
end
describe '#super_sidebar_nav_panel' do
diff --git a/spec/migrations/insert_daily_invites_trial_plan_limits_spec.rb b/spec/migrations/insert_daily_invites_trial_plan_limits_spec.rb
new file mode 100644
index 00000000000..ea1476b94a9
--- /dev/null
+++ b/spec/migrations/insert_daily_invites_trial_plan_limits_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe InsertDailyInvitesTrialPlanLimits, feature_category: :subgroups do
+ let(:plans) { table(:plans) }
+ let(:plan_limits) { table(:plan_limits) }
+ let!(:premium_trial_plan) { plans.create!(name: 'premium_trial') }
+ let!(:ultimate_trial_plan) { plans.create!(name: 'ultimate_trial') }
+
+ context 'when on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ trial_plan_ids = [premium_trial_plan.id, ultimate_trial_plan.id]
+ expect(plan_limits.where(plan_id: trial_plan_ids).where.not(daily_invites: 0)).to be_empty
+ }
+
+ migration.after -> {
+ expect(plan_limits.pluck(:plan_id, :daily_invites))
+ .to contain_exactly([premium_trial_plan.id, 50], [ultimate_trial_plan.id, 50])
+ }
+ end
+ end
+ end
+
+ context 'when on self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ trial_plan_ids = [premium_trial_plan.id, ultimate_trial_plan.id]
+
+ migration.before -> {
+ expect(plan_limits.where(plan_id: trial_plan_ids).where.not(daily_invites: 0)).to be_empty
+ }
+
+ migration.after -> {
+ expect(plan_limits.where(plan_id: trial_plan_ids).where.not(daily_invites: 0)).to be_empty
+ }
+ end
+ end
+ end
+end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 38ff1bb090e..26b57e2e58e 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -138,14 +138,14 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
end
end
- describe '#unique_domain' do
+ describe '#unique_url' do
let(:project) { build(:project) }
context 'when unique domain is disabled' do
it 'returns nil' do
project.project_setting.pages_unique_domain_enabled = false
- expect(lookup_path.unique_domain).to be_nil
+ expect(lookup_path.unique_url).to be_nil
end
end
@@ -154,7 +154,7 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
project.project_setting.pages_unique_domain_enabled = true
project.project_setting.pages_unique_domain = 'unique-domain'
- expect(lookup_path.unique_domain).to eq('unique-domain')
+ expect(lookup_path.unique_url).to eq('http://unique-domain.example.com')
end
end
end
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 20fb9100ebb..70ca1abc819 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'file_size' => deployment.size,
'file_count' => deployment.file_count
},
- 'unique_domain' => nil
+ 'unique_url' => nil
}
]
)
@@ -206,7 +206,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'file_size' => deployment.size,
'file_count' => deployment.file_count
},
- 'unique_domain' => 'unique-domain'
+ 'unique_url' => 'http://unique-domain.example.com'
}
]
)
@@ -262,7 +262,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'file_size' => deployment.size,
'file_count' => deployment.file_count
},
- 'unique_domain' => nil
+ 'unique_url' => nil
}
]
)
@@ -310,7 +310,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'file_size' => deployment.size,
'file_count' => deployment.file_count
},
- 'unique_domain' => nil
+ 'unique_url' => nil
}
]
)
diff --git a/spec/scripts/review_apps/automated_cleanup_spec.rb b/spec/scripts/review_apps/automated_cleanup_spec.rb
index 546bf55a934..4b6016760dc 100644
--- a/spec/scripts/review_apps/automated_cleanup_spec.rb
+++ b/spec/scripts/review_apps/automated_cleanup_spec.rb
@@ -30,10 +30,8 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
allow(Tooling::Helm3Client).to receive(:new).and_return(helm_client)
allow(Tooling::KubernetesClient).to receive(:new).and_return(kubernetes_client)
- allow(kubernetes_client).to receive(:cleanup_by_created_at)
- allow(kubernetes_client).to receive(:cleanup_by_release)
- allow(kubernetes_client).to receive(:cleanup_review_app_namespaces)
- allow(kubernetes_client).to receive(:delete_namespaces_by_exact_names)
+ allow(kubernetes_client).to receive(:cleanup_pvcs_by_created_at)
+ allow(kubernetes_client).to receive(:cleanup_namespaces_by_created_at)
end
shared_examples 'the days argument is an integer in the correct range' do
@@ -86,11 +84,7 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
it_behaves_like 'the days argument is an integer in the correct range'
it 'performs Kubernetes cleanup by created at' do
- expect(kubernetes_client).to receive(:cleanup_by_created_at).with(
- resource_type: 'pvc',
- created_before: two_days_ago,
- wait: false
- )
+ expect(kubernetes_client).to receive(:cleanup_pvcs_by_created_at).with(created_before: two_days_ago)
subject
end
@@ -99,7 +93,7 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
let(:dry_run) { true }
it 'does not delete anything' do
- expect(kubernetes_client).not_to receive(:cleanup_by_created_at)
+ expect(kubernetes_client).not_to receive(:cleanup_pvcs_by_created_at)
end
end
end
@@ -112,10 +106,7 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
it_behaves_like 'the days argument is an integer in the correct range'
it 'performs Kubernetes cleanup for review apps namespaces' do
- expect(kubernetes_client).to receive(:cleanup_review_app_namespaces).with(
- created_before: two_days_ago,
- wait: false
- )
+ expect(kubernetes_client).to receive(:cleanup_namespaces_by_created_at).with(created_before: two_days_ago)
subject
end
@@ -124,7 +115,7 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
let(:dry_run) { true }
it 'does not delete anything' do
- expect(kubernetes_client).not_to receive(:cleanup_review_app_namespaces)
+ expect(kubernetes_client).not_to receive(:cleanup_namespaces_by_created_at)
end
end
end
@@ -147,8 +138,7 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
before do
allow(helm_client).to receive(:delete)
- allow(kubernetes_client).to receive(:cleanup_by_release)
- allow(kubernetes_client).to receive(:delete_namespaces_by_exact_names)
+ allow(kubernetes_client).to receive(:delete_namespaces)
end
it 'deletes the helm release' do
@@ -157,16 +147,8 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
subject
end
- it 'empties the k8s resources in the k8s namespace for the release' do
- expect(kubernetes_client).to receive(:cleanup_by_release).with(release_name: releases_names, wait: false)
-
- subject
- end
-
it 'deletes the associated k8s namespace' do
- expect(kubernetes_client).to receive(:delete_namespaces_by_exact_names).with(
- resource_names: releases_names, wait: false
- )
+ expect(kubernetes_client).to receive(:delete_namespaces).with(releases_names)
subject
end
@@ -179,14 +161,8 @@ RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
subject
end
- it 'does not empty the k8s resources in the k8s namespace for the release' do
- expect(kubernetes_client).not_to receive(:cleanup_by_release)
-
- subject
- end
-
it 'does not delete the associated k8s namespace' do
- expect(kubernetes_client).not_to receive(:delete_namespaces_by_exact_names)
+ expect(kubernetes_client).not_to receive(:delete_namespaces)
subject
end
diff --git a/spec/tooling/lib/tooling/kubernetes_client_spec.rb b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
index 50d33182a42..20eb78c2f4f 100644
--- a/spec/tooling/lib/tooling/kubernetes_client_spec.rb
+++ b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
@@ -1,286 +1,373 @@
# frozen_string_literal: true
+require 'time'
require_relative '../../../../tooling/lib/tooling/kubernetes_client'
RSpec.describe Tooling::KubernetesClient do
- let(:namespace) { 'review-apps' }
- let(:release_name) { 'my-release' }
- let(:pod_for_release) { "pod-my-release-abcd" }
- let(:raw_resource_names_str) { "NAME\nfoo\n#{pod_for_release}\nbar" }
- let(:raw_resource_names) { raw_resource_names_str.lines.map(&:strip) }
-
- subject { described_class.new(namespace: namespace) }
+ let(:instance) { described_class.new }
+ let(:one_day_ago) { Time.now - 3600 * 24 * 1 }
+ let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
+ let(:three_days_ago) { Time.now - 3600 * 24 * 3 }
+
+ before do
+ # Global mock to ensure that no kubectl commands are run by accident in a test.
+ allow(instance).to receive(:run_command)
+ end
- describe 'RESOURCE_LIST' do
- it 'returns the correct list of resources separated by commas' do
- expect(described_class::RESOURCE_LIST).to eq('ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd')
+ describe '#cleanup_pvcs_by_created_at' do
+ let(:pvc_1_created_at) { three_days_ago }
+ let(:pvc_2_created_at) { three_days_ago }
+ let(:pvc_1_namespace) { 'review-first-review-app' }
+ let(:pvc_2_namespace) { 'review-second-review-app' }
+ let(:kubectl_pvcs_json) do
+ <<~JSON
+ {
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "PersistentVolumeClaim",
+ "metadata": {
+ "creationTimestamp": "#{pvc_1_created_at.utc.iso8601}",
+ "name": "pvc1",
+ "namespace": "#{pvc_1_namespace}"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "PersistentVolumeClaim",
+ "metadata": {
+ "creationTimestamp": "#{pvc_2_created_at.utc.iso8601}",
+ "name": "pvc2",
+ "namespace": "#{pvc_2_namespace}"
+ }
+ }
+ ]
+ }
+ JSON
end
- end
- describe '#cleanup_by_release' do
+ subject { instance.cleanup_pvcs_by_created_at(created_before: two_days_ago) }
+
before do
- allow(subject).to receive(:raw_resource_names).and_return(raw_resource_names)
+ allow(instance).to receive(:run_command).with(
+ "kubectl get pvc --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json"
+ ).and_return(kubectl_pvcs_json)
end
- shared_examples 'a kubectl command to delete resources' do
- let(:wait) { true }
- let(:release_names_in_command) { release_name.respond_to?(:join) ? %(-l 'release in (#{release_name.join(', ')})') : %(-l release="#{release_name}") }
-
- specify do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --wait=#{wait} #{release_names_in_command})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ context 'when no pvcs are stale' do
+ let(:pvc_1_created_at) { one_day_ago }
+ let(:pvc_2_created_at) { one_day_ago }
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ it 'does not delete any PVC' do
+ expect(instance).not_to receive(:run_command).with(/kubectl delete pvc/)
- # We're not verifying the output here, just silencing it
- expect { subject.cleanup_by_release(release_name: release_name) }.to output.to_stdout
+ subject
end
end
- it 'raises an error if the Kubernetes command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --wait=true -l release="#{release_name}")])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+ context 'when some pvcs are stale' do
+ let(:pvc_1_created_at) { three_days_ago }
+ let(:pvc_2_created_at) { three_days_ago }
- expect { subject.cleanup_by_release(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
- end
+ context 'when some pvcs are not in a review app namespaces' do
+ let(:pvc_1_namespace) { 'review-my-review-app' }
+ let(:pvc_2_namespace) { 'review-apps' } # This is not a review apps namespace, so we should not delete PVCs inside it
- it_behaves_like 'a kubectl command to delete resources'
+ it 'deletes the stale pvcs inside of review-apps namespaces only' do
+ expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_1_namespace} --now --ignore-not-found pvc1")
+ expect(instance).not_to receive(:run_command).with(/kubectl delete pvc --namespace=#{pvc_2_namespace}/)
- context 'with multiple releases' do
- let(:release_name) { %w[my-release my-release-2] }
+ subject
+ end
+ end
- it_behaves_like 'a kubectl command to delete resources'
- end
+ context 'when all pvcs are in review-apps namespaces' do
+ let(:pvc_1_namespace) { 'review-my-review-app' }
+ let(:pvc_2_namespace) { 'review-another-review-app' }
- context 'with `wait: false`' do
- let(:wait) { false }
+ it 'deletes all of the stale pvcs' do
+ expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_1_namespace} --now --ignore-not-found pvc1")
+ expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_2_namespace} --now --ignore-not-found pvc2")
- it_behaves_like 'a kubectl command to delete resources'
+ subject
+ end
+ end
end
end
- describe '#cleanup_by_created_at' do
- let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
- let(:resource_type) { 'pvc' }
- let(:resource_names) { [pod_for_release] }
+ describe '#cleanup_namespaces_by_created_at' do
+ let(:namespace_1_created_at) { three_days_ago }
+ let(:namespace_2_created_at) { three_days_ago }
+ let(:namespace_1_name) { 'review-first-review-app' }
+ let(:namespace_2_name) { 'review-second-review-app' }
+ let(:kubectl_namespaces_json) do
+ <<~JSON
+ {
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "namespace",
+ "metadata": {
+ "creationTimestamp": "#{namespace_1_created_at.utc.iso8601}",
+ "name": "#{namespace_1_name}"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "namespace",
+ "metadata": {
+ "creationTimestamp": "#{namespace_2_created_at.utc.iso8601}",
+ "name": "#{namespace_2_name}"
+ }
+ }
+ ]
+ }
+ JSON
+ end
+
+ subject { instance.cleanup_namespaces_by_created_at(created_before: two_days_ago) }
before do
- allow(subject).to receive(:resource_names_created_before).with(resource_type: resource_type, created_before: two_days_ago).and_return(resource_names)
+ allow(instance).to receive(:run_command).with(
+ "kubectl get namespace --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json"
+ ).and_return(kubectl_namespaces_json)
end
- shared_examples 'a kubectl command to delete resources by older than given creation time' do
- let(:wait) { true }
- let(:release_names_in_command) { resource_names.join(' ') }
+ context 'when no namespaces are stale' do
+ let(:namespace_1_created_at) { one_day_ago }
+ let(:namespace_2_created_at) { one_day_ago }
- specify do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{resource_type} ".squeeze(' ') +
- %(--namespace "#{namespace}" --now --ignore-not-found --wait=#{wait} #{release_names_in_command})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ it 'does not delete any namespace' do
+ expect(instance).not_to receive(:run_command).with(/kubectl delete namespace/)
- # We're not verifying the output here, just silencing it
- expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to output.to_stdout
+ subject
end
end
- it 'raises an error if the Kubernetes command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete #{resource_type} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --wait=true #{pod_for_release})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+ context 'when some namespaces are stale' do
+ let(:namespace_1_created_at) { three_days_ago }
+ let(:namespace_2_created_at) { three_days_ago }
- expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to raise_error(described_class::CommandFailedError)
- end
+ context 'when some namespaces are not review app namespaces' do
+ let(:namespace_1_name) { 'review-my-review-app' }
+ let(:namespace_2_name) { 'review-apps' } # This is not a review apps namespace, so we should not try to delete it
- it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ it 'only deletes the review app namespaces' do
+ expect(instance).to receive(:run_command).with("kubectl delete namespace --now --ignore-not-found #{namespace_1_name}")
- context 'with multiple resource names' do
- let(:resource_names) { %w[pod-1 pod-2] }
+ subject
+ end
+ end
- it_behaves_like 'a kubectl command to delete resources by older than given creation time'
- end
+ context 'when all namespaces are review app namespaces' do
+ let(:namespace_1_name) { 'review-my-review-app' }
+ let(:namespace_2_name) { 'review-another-review-app' }
- context 'with `wait: false`' do
- let(:wait) { false }
+ it 'deletes all of the stale namespaces' do
+ expect(instance).to receive(:run_command).with("kubectl delete namespace --now --ignore-not-found #{namespace_1_name} #{namespace_2_name}")
- it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ subject
+ end
+ end
end
+ end
- context 'with no resource_type given' do
- let(:resource_type) { nil }
+ describe '#delete_pvc' do
+ let(:pvc_name) { 'my-pvc' }
- it_behaves_like 'a kubectl command to delete resources by older than given creation time'
- end
+ subject { instance.delete_pvc(pvc_name, pvc_namespace) }
+
+ context 'when the namespace is not a review app namespace' do
+ let(:pvc_namespace) { 'not-a-review-app-namespace' }
- context 'with multiple resource_type given' do
- let(:resource_type) { 'pvc,service' }
+ it 'does not delete the pvc' do
+ expect(instance).not_to receive(:run_command).with(/kubectl delete pvc/)
- it_behaves_like 'a kubectl command to delete resources by older than given creation time'
+ subject
+ end
end
- context 'with no resources found' do
- let(:resource_names) { [] }
+ context 'when the namespace is a review app namespace' do
+ let(:pvc_namespace) { 'review-apple-test' }
- it 'does not call #delete_by_exact_names' do
- expect(subject).not_to receive(:delete_by_exact_names)
+ it 'deletes the pvc' do
+ expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_namespace} --now --ignore-not-found #{pvc_name}")
- subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago)
+ subject
end
end
end
- describe '#cleanup_review_app_namespaces' do
- let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
- let(:namespaces) { %w[review-abc-123 review-xyz-789] }
+ describe '#delete_namespaces' do
+ subject { instance.delete_namespaces(namespaces) }
- subject { described_class.new(namespace: nil) }
+ context 'when at least one namespace is not a review app namespace' do
+ let(:namespaces) { %w[review-ns-1 default] }
- before do
- allow(subject).to receive(:review_app_namespaces_created_before).with(created_before: two_days_ago).and_return(namespaces)
+ it 'does not delete any namespace' do
+ expect(instance).not_to receive(:run_command).with(/kubectl delete namespace/)
+
+ subject
+ end
end
- shared_examples 'a kubectl command to delete namespaces older than given creation time' do
- let(:wait) { true }
+ context 'when all namespaces are review app namespaces' do
+ let(:namespaces) { %w[review-ns-1 review-ns-2] }
- specify do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete namespace " +
- %(--now --ignore-not-found --wait=#{wait} #{namespaces.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ it 'deletes the namespaces' do
+ expect(instance).to receive(:run_command).with("kubectl delete namespace --now --ignore-not-found #{namespaces.join(' ')}")
- # We're not verifying the output here, just silencing it
- expect { subject.cleanup_review_app_namespaces(created_before: two_days_ago) }.to output.to_stdout
+ subject
end
end
+ end
- it_behaves_like 'a kubectl command to delete namespaces older than given creation time'
-
- it 'raises an error if the Kubernetes command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl delete namespace " +
- %(--now --ignore-not-found --wait=true #{namespaces.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
-
- expect { subject.cleanup_review_app_namespaces(created_before: two_days_ago) }.to raise_error(described_class::CommandFailedError)
+ describe '#pvcs_created_before' do
+ subject { instance.pvcs_created_before(created_before: two_days_ago) }
+
+ let(:pvc_1_created_at) { three_days_ago }
+ let(:pvc_2_created_at) { three_days_ago }
+ let(:pvc_1_namespace) { 'review-first-review-app' }
+ let(:pvc_2_namespace) { 'review-second-review-app' }
+ let(:kubectl_pvcs_json) do
+ <<~JSON
+ {
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "PersistentVolumeClaim",
+ "metadata": {
+ "creationTimestamp": "#{pvc_1_created_at.utc.iso8601}",
+ "name": "pvc1",
+ "namespace": "#{pvc_1_namespace}"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "PersistentVolumeClaim",
+ "metadata": {
+ "creationTimestamp": "#{pvc_2_created_at.utc.iso8601}",
+ "name": "pvc2",
+ "namespace": "#{pvc_2_namespace}"
+ }
+ }
+ ]
+ }
+ JSON
end
- context 'with no namespaces found' do
- let(:namespaces) { [] }
+ it 'calls #resource_created_before with the correct parameters' do
+ expect(instance).to receive(:resource_created_before).with(resource_type: 'pvc', created_before: two_days_ago)
- it 'does not call #delete_namespaces_by_exact_names' do
- expect(subject).not_to receive(:delete_namespaces_by_exact_names)
+ subject
+ end
- subject.cleanup_review_app_namespaces(created_before: two_days_ago)
- end
+ it 'returns a hash with two keys' do
+ allow(instance).to receive(:run_command).with(
+ "kubectl get pvc --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json"
+ ).and_return(kubectl_pvcs_json)
+
+ expect(subject).to match_array([
+ {
+ resource_name: 'pvc1',
+ namespace: 'review-first-review-app'
+ },
+ {
+ resource_name: 'pvc2',
+ namespace: 'review-second-review-app'
+ }
+ ])
end
end
- describe '#raw_resource_names' do
- it 'calls kubectl to retrieve the resource names' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl get #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" -o name)])
- .and_return(Gitlab::Popen::Result.new([], raw_resource_names_str, '', double(success?: true)))
-
- expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names)
+ describe '#namespaces_created_before' do
+ subject { instance.namespaces_created_before(created_before: two_days_ago) }
+
+ let(:namespace_1_created_at) { three_days_ago }
+ let(:namespace_2_created_at) { three_days_ago }
+ let(:namespace_1_name) { 'review-first-review-app' }
+ let(:namespace_2_name) { 'review-second-review-app' }
+ let(:kubectl_namespaces_json) do
+ <<~JSON
+ {
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "namespace",
+ "metadata": {
+ "creationTimestamp": "#{namespace_1_created_at.utc.iso8601}",
+ "name": "#{namespace_1_name}"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "namespace",
+ "metadata": {
+ "creationTimestamp": "#{namespace_2_created_at.utc.iso8601}",
+ "name": "#{namespace_2_name}"
+ }
+ }
+ ]
+ }
+ JSON
end
- end
- describe '#resource_names_created_before' do
- let(:three_days_ago) { Time.now - 3600 * 24 * 3 }
- let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
- let(:pvc_created_three_days_ago) { 'pvc-created-three-days-ago' }
- let(:resource_type) { 'pvc' }
- let(:raw_resources) do
- {
- items: [
- {
- apiVersion: "v1",
- kind: "PersistentVolumeClaim",
- metadata: {
- creationTimestamp: three_days_ago,
- name: pvc_created_three_days_ago
- }
- },
- {
- apiVersion: "v1",
- kind: "PersistentVolumeClaim",
- metadata: {
- creationTimestamp: Time.now,
- name: 'another-pvc'
- }
- }
- ]
- }.to_json
+ it 'calls #resource_created_before with the correct parameters' do
+ expect(instance).to receive(:resource_created_before).with(resource_type: 'namespace', created_before: two_days_ago)
+
+ subject
end
- shared_examples 'a kubectl command to retrieve resource names sorted by creationTimestamp' do
- specify do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl get #{resource_type} ".squeeze(' ') +
- %(--namespace "#{namespace}" ) +
- "--sort-by='{.metadata.creationTimestamp}' -o json"])
- .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true)))
+ it 'returns an array of namespaces' do
+ allow(instance).to receive(:run_command).with(
+ "kubectl get namespace --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json"
+ ).and_return(kubectl_namespaces_json)
- expect(subject.__send__(:resource_names_created_before, resource_type: resource_type, created_before: two_days_ago)).to contain_exactly(pvc_created_three_days_ago)
- end
+ expect(subject).to match_array(%w[review-first-review-app review-second-review-app])
end
+ end
- it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp'
+ describe '#run_command' do
+ subject { instance.run_command(command) }
- context 'with no resource_type given' do
- let(:resource_type) { nil }
+ before do
+ # We undo the global mock just for this method
+ allow(instance).to receive(:run_command).and_call_original
- it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp'
+ # Mock stdout
+ allow(instance).to receive(:puts)
end
- context 'with multiple resource_type given' do
- let(:resource_type) { 'pvc,service' }
+ context 'when executing a successful command' do
+ let(:command) { 'true' } # https://linux.die.net/man/1/true
- it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp'
- end
- end
+ it 'displays the name of the command to stdout' do
+ expect(instance).to receive(:puts).with("Running command: `#{command}`")
+
+ subject
+ end
- describe '#review_app_namespaces_created_before' do
- let(:three_days_ago) { Time.now - 3600 * 24 * 3 }
- let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
- let(:namespace_created_three_days_ago) { 'review-ns-created-three-days-ago' }
- let(:resource_type) { 'namespace' }
- let(:raw_resources) do
- {
- items: [
- {
- apiVersion: "v1",
- kind: "Namespace",
- metadata: {
- creationTimestamp: three_days_ago,
- name: namespace_created_three_days_ago
- }
- },
- {
- apiVersion: "v1",
- kind: "Namespace",
- metadata: {
- creationTimestamp: Time.now,
- name: 'another-namespace'
- }
- }
- ]
- }.to_json
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
end
- specify do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl get namespace --sort-by='{.metadata.creationTimestamp}' -o json"])
- .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true)))
+ context 'when executing an unsuccessful command' do
+ let(:command) { 'false' } # https://linux.die.net/man/1/false
+
+ it 'displays the name of the command to stdout' do
+ expect(instance).to receive(:puts).with("Running command: `#{command}`")
- expect(subject.__send__(:review_app_namespaces_created_before, created_before: two_days_ago)).to eq([namespace_created_three_days_ago])
+ expect { subject }.to raise_error(described_class::CommandFailedError)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::CommandFailedError)
+ end
end
end
end
diff --git a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
new file mode 100644
index 00000000000..69dddb0ae3d
--- /dev/null
+++ b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
@@ -0,0 +1,274 @@
+# frozen_string_literal: true
+
+require 'tempfile'
+require 'fileutils'
+require_relative '../../../../../tooling/lib/tooling/mappings/partial_to_views_mappings'
+
+RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :tooling do
+ attr_accessor :view_base_folder, :changes_file, :output_file
+
+ let(:instance) { described_class.new(changes_file, output_file, view_base_folder: view_base_folder) }
+ let(:changes_file_content) { "changed_file1 changed_file2" }
+ let(:output_file_content) { "previously_added_view.html.haml" }
+
+ around do |example|
+ self.changes_file = Tempfile.new('changes')
+ self.output_file = Tempfile.new('output_file')
+
+ # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
+ # Tempfile.html#class-Tempfile-label-Explicit+close
+ begin
+ Dir.mktmpdir do |tmp_views_base_folder|
+ self.view_base_folder = tmp_views_base_folder
+ example.run
+ end
+ ensure
+ changes_file.close
+ output_file.close
+ changes_file.unlink
+ output_file.unlink
+ end
+ end
+
+ before do
+ # We write into the temp files initially, to check how the code modified those files
+ File.write(changes_file, changes_file_content)
+ File.write(output_file, output_file_content)
+ end
+
+ describe '#execute' do
+ subject { instance.execute }
+
+ let(:changed_files) { ["#{view_base_folder}/my_view.html.haml"] }
+ let(:changes_file_content) { changed_files.join(" ") }
+
+ before do
+ # We create all of the changed_files, so that they are part of the filtered files
+ changed_files.each { |changed_file| FileUtils.touch(changed_file) }
+ end
+
+ it 'does not modify the content of the input file' do
+ expect { subject }.not_to change { File.read(changes_file) }
+ end
+
+ context 'when no partials were modified' do
+ it 'empties the output file' do
+ expect { subject }.to change { File.read(output_file) }.from(output_file_content).to('')
+ end
+ end
+
+ context 'when some partials were modified' do
+ let(:changed_files) do
+ [
+ "#{view_base_folder}/my_view.html.haml",
+ "#{view_base_folder}/_my_partial.html.haml"
+ ]
+ end
+
+ before do
+ # We create a red-herring partial to have a more convincing test suite
+ FileUtils.touch("#{view_base_folder}/_another_partial.html.haml")
+ end
+
+ context 'when the partials are not included in any views' do
+ before do
+ File.write("#{view_base_folder}/my_view.html.haml", "render 'another_partial'")
+ end
+
+ it 'empties the output file' do
+ expect { subject }.to change { File.read(output_file) }.from(output_file_content).to('')
+ end
+ end
+
+ context 'when the partials are included in views' do
+ before do
+ File.write("#{view_base_folder}/my_view.html.haml", "render 'my_partial'")
+ end
+
+ it 'writes the view including the partial to the output' do
+ expect { subject }.to change { File.read(output_file) }
+ .from(output_file_content)
+ .to("#{view_base_folder}/my_view.html.haml")
+ end
+ end
+ end
+ end
+
+ describe '#filter_files' do
+ subject { instance.filter_files }
+
+ let(:changes_file_content) { file_path }
+
+ context 'when the file does not exist on disk' do
+ let(:file_path) { "#{view_base_folder}/_index.html.erb" }
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when the file exists on disk' do
+ before do
+ File.write(file_path, "I am a partial!")
+ end
+
+ context 'when the file is not in the view base folders' do
+ let(:file_path) { "/tmp/_index.html.haml" }
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when the filename does not start with an underscore' do
+ let(:file_path) { "#{view_base_folder}/index.html.haml" }
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when the filename does not have the correct extension' do
+ let(:file_path) { "#{view_base_folder}/_index.html.erb" }
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when the file is a partial' do
+ let(:file_path) { "#{view_base_folder}/_index.html.haml" }
+
+ it 'returns the file' do
+ expect(subject).to match_array(file_path)
+ end
+ end
+ end
+ end
+
+ describe '#extract_partial_keyword' do
+ subject { instance.extract_partial_keyword('ee/app/views/shared/_new_project_item_vue_select.html.haml') }
+
+ it 'returns the correct partial keyword' do
+ expect(subject).to eq('new_project_item_vue_select')
+ end
+ end
+
+ describe '#view_includes_modified_partial?' do
+ subject { instance.view_includes_modified_partial?(view_file, included_partial_name) }
+
+ context 'when the included partial name is relative to the view file' do
+ let(:view_file) { "#{view_base_folder}/components/my_view.html.haml" }
+ let(:included_partial_name) { 'subfolder/relative_partial' }
+
+ before do
+ FileUtils.mkdir_p("#{view_base_folder}/components/subfolder")
+ File.write(changes_file_content, "I am a partial!")
+ end
+
+ context 'when the partial is not part of the changed files' do
+ let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_not_the_partial.html.haml" }
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when the partial is part of the changed files' do
+ let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_relative_partial.html.haml" }
+
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ context 'when the included partial name is relative to the base views folder' do
+ let(:view_file) { "#{view_base_folder}/components/my_view.html.haml" }
+ let(:included_partial_name) { 'shared/absolute_partial' }
+
+ before do
+ FileUtils.mkdir_p("#{view_base_folder}/components")
+ FileUtils.mkdir_p("#{view_base_folder}/shared")
+ File.write(changes_file_content, "I am a partial!")
+ end
+
+ context 'when the partial is not part of the changed files' do
+ let(:changes_file_content) { "#{view_base_folder}/shared/not_the_partial" }
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when the partial is part of the changed files' do
+ let(:changes_file_content) { "#{view_base_folder}/shared/_absolute_partial.html.haml" }
+
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe '#reconstruct_partial_filename' do
+ subject { instance.reconstruct_partial_filename(partial_name) }
+
+ context 'when the partial does not contain a path' do
+ let(:partial_name) { 'sidebar' }
+
+ it 'returns the correct filename' do
+ expect(subject).to eq('_sidebar.html.haml')
+ end
+ end
+
+ context 'when the partial contains a path' do
+ let(:partial_name) { 'shared/components/sidebar' }
+
+ it 'returns the correct filename' do
+ expect(subject).to eq('shared/components/_sidebar.html.haml')
+ end
+ end
+ end
+
+ describe '#find_pattern_in_file' do
+ let(:subject) { instance.find_pattern_in_file(file.path, /pattern/) }
+ let(:file) { Tempfile.new('find_pattern_in_file') }
+
+ before do
+ file.write(file_content)
+ file.close
+ end
+
+ context 'when the file contains the pattern' do
+ let(:file_content) do
+ <<~FILE
+ Beginning of file
+
+ pattern
+ pattern
+ pattern
+
+ End of file
+ FILE
+ end
+
+ it 'returns the pattern once' do
+ expect(subject).to match_array(%w[pattern])
+ end
+ end
+
+ context 'when the file does not contain the pattern' do
+ let(:file_content) do
+ <<~FILE
+ Beginning of file
+ End of file
+ FILE
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to match_array([])
+ end
+ end
+ end
+end
diff --git a/tooling/bin/partial_to_views_mappings b/tooling/bin/partial_to_views_mappings
new file mode 100755
index 00000000000..12c994ee556
--- /dev/null
+++ b/tooling/bin/partial_to_views_mappings
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../lib/tooling/mappings/partial_to_views_mappings'
+
+changes_file = ARGV.shift
+output_file = ARGV.shift
+
+Tooling::Mappings::PartialToViewsMappings.new(changes_file, output_file).execute
diff --git a/tooling/lib/tooling/kubernetes_client.rb b/tooling/lib/tooling/kubernetes_client.rb
index 27eb4c8151e..b373f5d6980 100644
--- a/tooling/lib/tooling/kubernetes_client.rb
+++ b/tooling/lib/tooling/kubernetes_client.rb
@@ -6,78 +6,49 @@ require_relative '../../../lib/gitlab/popen' unless defined?(Gitlab::Popen)
module Tooling
class KubernetesClient
- RESOURCE_LIST = 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd'
K8S_ALLOWED_NAMESPACES_REGEX = /^review-(?!apps).+/.freeze
CommandFailedError = Class.new(StandardError)
- attr_reader :namespace
+ def cleanup_pvcs_by_created_at(created_before:)
+ stale_pvcs = pvcs_created_before(created_before: created_before)
- def initialize(namespace:)
- @namespace = namespace
- end
+ # `kubectl` doesn't allow us to filter namespaces with a regexp. We therefore do the filtering in Ruby.
+ review_apps_stale_pvcs = stale_pvcs.select do |stale_pvc_hash|
+ K8S_ALLOWED_NAMESPACES_REGEX.match?(stale_pvc_hash[:namespace])
+ end
+ return if review_apps_stale_pvcs.empty?
- def cleanup_by_release(release_name:, wait: true)
- delete_by_selector(release_name: release_name, wait: wait)
- delete_by_matching_name(release_name: release_name)
+ review_apps_stale_pvcs.each do |pvc_hash|
+ delete_pvc(pvc_hash[:resource_name], pvc_hash[:namespace])
+ end
end
- def cleanup_by_created_at(resource_type:, created_before:, wait: true)
- resource_names = resource_names_created_before(resource_type: resource_type, created_before: created_before)
- return if resource_names.empty?
+ def cleanup_namespaces_by_created_at(created_before:)
+ stale_namespaces = namespaces_created_before(created_before: created_before)
- delete_by_exact_names(resource_type: resource_type, resource_names: resource_names, wait: wait)
- end
-
- def cleanup_review_app_namespaces(created_before:, wait: true)
- namespaces = review_app_namespaces_created_before(created_before: created_before)
- return if namespaces.empty?
+ # `kubectl` doesn't allow us to filter namespaces with a regexp. We therefore do the filtering in Ruby.
+ review_apps_stale_namespaces = stale_namespaces.select { |ns| K8S_ALLOWED_NAMESPACES_REGEX.match?(ns) }
+ return if review_apps_stale_namespaces.empty?
- delete_namespaces_by_exact_names(resource_names: namespaces, wait: wait)
+ delete_namespaces(review_apps_stale_namespaces)
end
- def delete_namespaces_by_exact_names(resource_names:, wait:)
- command = [
- 'delete',
- 'namespace',
- '--now',
- '--ignore-not-found',
- %(--wait=#{wait}),
- resource_names.join(' ')
- ]
+ def delete_pvc(pvc, namespace)
+ return unless K8S_ALLOWED_NAMESPACES_REGEX.match?(namespace)
- run_command(command)
+ run_command("kubectl delete pvc --namespace=#{namespace} --now --ignore-not-found #{pvc}")
end
- private
-
- def delete_by_selector(release_name:, wait:)
- selector = case release_name
- when String
- %(-l release="#{release_name}")
- when Array
- %(-l 'release in (#{release_name.join(', ')})')
- else
- raise ArgumentError, 'release_name must be a string or an array'
- end
-
- command = [
- 'delete',
- RESOURCE_LIST,
- %(--namespace "#{namespace}"),
- '--now',
- '--ignore-not-found',
- %(--wait=#{wait}),
- selector
- ]
+ def delete_namespaces(namespaces)
+ return if namespaces.any? { |ns| !K8S_ALLOWED_NAMESPACES_REGEX.match?(ns) }
- run_command(command)
+ run_command("kubectl delete namespace --now --ignore-not-found #{namespaces.join(' ')}")
end
- def delete_by_exact_names(resource_names:, wait:, resource_type: nil)
+ def delete_namespaces_by_exact_names(resource_names:, wait:)
command = [
'delete',
- resource_type,
- %(--namespace "#{namespace}"),
+ 'namespace',
'--now',
'--ignore-not-found',
%(--wait=#{wait}),
@@ -87,87 +58,44 @@ module Tooling
run_command(command)
end
- def delete_by_matching_name(release_name:)
- resource_names = raw_resource_names
- command = [
- 'delete',
- %(--namespace "#{namespace}"),
- '--ignore-not-found'
- ]
-
- Array(release_name).each do |release|
- resource_names
- .select { |resource_name| resource_name.include?(release) }
- .each { |matching_resource| run_command(command + [matching_resource]) }
+ def pvcs_created_before(created_before:)
+ resource_created_before(resource_type: 'pvc', created_before: created_before) do |item|
+ {
+ resource_name: item.dig('metadata', 'name'),
+ namespace: item.dig('metadata', 'namespace')
+ }
end
end
- def raw_resource_names
- command = [
- 'get',
- RESOURCE_LIST,
- %(--namespace "#{namespace}"),
- '-o name'
- ]
- run_command(command).lines.map(&:strip)
- end
-
- def resource_names_created_before(resource_type:, created_before:)
- command = [
- 'get',
- resource_type,
- %(--namespace "#{namespace}"),
- "--sort-by='{.metadata.creationTimestamp}'",
- '-o json'
- ]
-
- response = run_command(command)
-
- resources_created_before_date(response, created_before)
+ def namespaces_created_before(created_before:)
+ resource_created_before(resource_type: 'namespace', created_before: created_before) do |item|
+ item.dig('metadata', 'name')
+ end
end
- def review_app_namespaces_created_before(created_before:)
- command = [
- 'get',
- 'namespace',
- "--sort-by='{.metadata.creationTimestamp}'",
- '-o json'
- ]
-
- response = run_command(command)
-
- stale_namespaces = resources_created_before_date(response, created_before)
-
- # `kubectl` doesn't allow us to filter namespaces with a regexp. We therefore do the filtering in Ruby.
- stale_namespaces.select { |ns| K8S_ALLOWED_NAMESPACES_REGEX.match?(ns) }
- end
+ def resource_created_before(resource_type:, created_before:)
+ response = run_command("kubectl get #{resource_type} --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json")
- def resources_created_before_date(response, date)
items = JSON.parse(response)['items'] # rubocop:disable Gitlab/Json
-
- items.each_with_object([]) do |item, result|
+ items.filter_map do |item|
item_created_at = Time.parse(item.dig('metadata', 'creationTimestamp'))
- if item_created_at < date
- resource_name = item.dig('metadata', 'name')
- result << resource_name
- end
+ yield item if item_created_at < created_before
end
rescue ::JSON::ParserError => ex
- puts "Ignoring this JSON parsing error: #{ex}\n\nResponse was:\n#{response}" # rubocop:disable Rails/Output
+ puts "Ignoring this JSON parsing error: #{ex}\n\nResponse was:\n#{response}"
[]
end
def run_command(command)
- final_command = ['kubectl', *command.compact].join(' ')
- puts "Running command: `#{final_command}`" # rubocop:disable Rails/Output
+ puts "Running command: `#{command}`"
- result = Gitlab::Popen.popen_with_detail([final_command])
+ result = Gitlab::Popen.popen_with_detail([command])
if result.status.success?
result.stdout.chomp.freeze
else
- raise CommandFailedError, "The `#{final_command}` command failed (status: #{result.status}) with the following error:\n#{result.stderr}"
+ raise CommandFailedError, "The `#{command}` command failed (status: #{result.status}) with the following error:\n#{result.stderr}"
end
end
end
diff --git a/tooling/lib/tooling/mappings/partial_to_views_mappings.rb b/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
new file mode 100644
index 00000000000..1b36894d881
--- /dev/null
+++ b/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require_relative 'base'
+require_relative '../../../../lib/gitlab_edition'
+
+# Returns view files that include the potential rails partials from the changed files passed as input.
+module Tooling
+ module Mappings
+ class PartialToViewsMappings < Base
+ def initialize(changes_file, output_file, view_base_folder: 'app/views')
+ @output_file = output_file
+ @changed_files = File.read(changes_file).split(' ')
+ @view_base_folders = folders_for_available_editions(view_base_folder)
+ end
+
+ def execute
+ views_including_modified_partials = []
+
+ views_globs = view_base_folders.map { |view_base_folder| "#{view_base_folder}/**/*.html.haml" }
+ Dir[*views_globs].each do |view_file|
+ included_partial_names = find_pattern_in_file(view_file, partials_keywords_regexp)
+ next if included_partial_names.empty?
+
+ included_partial_names.each do |included_partial_name|
+ if view_includes_modified_partial?(view_file, included_partial_name)
+ views_including_modified_partials << view_file
+ end
+ end
+ end
+
+ File.write(output_file, views_including_modified_partials.join(' '))
+ end
+
+ def filter_files
+ @_filter_files ||= changed_files.select do |filename|
+ filename.start_with?(*view_base_folders) &&
+ File.basename(filename).start_with?('_') &&
+ File.basename(filename).end_with?('.html.haml') &&
+ File.exist?(filename)
+ end
+ end
+
+ def partials_keywords_regexp
+ partial_keywords = filter_files.map do |partial_filename|
+ extract_partial_keyword(partial_filename)
+ end
+
+ partial_regexps = partial_keywords.map do |keyword|
+ %r{(?:render|render_if_exists)(?: |\()(?:partial: ?)?['"]([\w\-_/]*#{keyword})['"]}
+ end
+
+ Regexp.union(partial_regexps)
+ end
+
+ # e.g. if app/views/clusters/clusters/_sidebar.html.haml was modified, the partial keyword is `sidebar`.
+ def extract_partial_keyword(partial_filename)
+ File.basename(partial_filename).delete_prefix('_').delete_suffix('.html.haml')
+ end
+
+ # Why do we need this method?
+ #
+ # Assume app/views/clusters/clusters/_sidebar.html.haml was modified in the MR.
+ #
+ # Suppose now you find = render 'sidebar' in a view. Is this view including the sidebar partial
+ # that was modified, or another partial called "_sidebar.html.haml" somewhere else?
+ def view_includes_modified_partial?(view_file, included_partial_name)
+ view_file_parent_folder = File.dirname(view_file)
+ included_partial_filename = reconstruct_partial_filename(included_partial_name)
+ included_partial_relative_path = File.join(view_file_parent_folder, included_partial_filename)
+
+ # We do this because in render (or render_if_exists)
+ # apparently looks for partials in other GitLab editions
+ #
+ # Example:
+ #
+ # ee/app/views/events/_epics_filter.html.haml is used in app/views/shared/_event_filter.html.haml
+ # with render_if_exists 'events/epics_filter'
+ included_partial_absolute_paths = view_base_folders.map do |view_base_folder|
+ File.join(view_base_folder, included_partial_filename)
+ end
+
+ filter_files.include?(included_partial_relative_path) ||
+ (filter_files & included_partial_absolute_paths).any?
+ end
+
+ def reconstruct_partial_filename(partial_name)
+ partial_path = partial_name.split('/')[..-2]
+ partial_filename = partial_name.split('/').last
+ full_partial_filename = "_#{partial_filename}.html.haml"
+
+ return full_partial_filename if partial_path.empty?
+
+ File.join(partial_path.join('/'), full_partial_filename)
+ end
+
+ def find_pattern_in_file(file, pattern)
+ File.read(file).scan(pattern).flatten.uniq
+ end
+
+ private
+
+ attr_reader :changed_files, :output_file, :view_base_folders
+ end
+ end
+end