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:
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml5
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/config/manifest.js120
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/index.js2
-rw-r--r--app/assets/javascripts/flash.js61
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/terms/components/app.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/constants.js (renamed from app/assets/javascripts/vue_shared/components/group_select/constants.js)1
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue (renamed from app/assets/javascripts/vue_shared/components/group_select/group_select.vue)137
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue132
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/init_group_selects.js (renamed from app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/utils.js (renamed from app/assets/javascripts/vue_shared/components/group_select/utils.js)0
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_body.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue6
-rw-r--r--app/views/clusters/clusters/_namespace.html.haml15
-rw-r--r--app/views/clusters/clusters/_provider_details_form.html.haml82
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml77
-rw-r--r--app/views/groups/runners/index.html.haml2
-rw-r--r--config/application.rb183
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/development/documentation/redirects.md4
-rw-r--r--doc/development/testing_guide/end_to_end/index.md3
-rw-r--r--doc/development/testing_guide/end_to_end/package_and_test_pipeline.md134
-rw-r--r--doc/development/testing_guide/review_apps.md19
-rw-r--r--doc/security/responding_to_security_incidents.md2
-rw-r--r--doc/security/token_overview.md9
-rw-r--r--doc/user/application_security/policies/scan-execution-policies.md5
-rw-r--r--doc/user/infrastructure/iac/terraform_template_recipes.md13
-rw-r--r--doc/user/project/repository/forking_workflow.md73
-rw-r--r--doc/user/project/repository/mirror/pull.md7
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml1
-rw-r--r--lib/gitlab/github_import/importer/pull_request_review_importer.rb3
-rw-r--r--qa/qa/scenario/test/integration/gitaly_cluster.rb13
-rw-r--r--qa/qa/scenario/test/integration/integrations.rb (renamed from qa/qa/scenario/test/instance/integrations.rb)4
-rw-r--r--qa/qa/scenario/test/integration/jira.rb (renamed from qa/qa/scenario/test/instance/jira.rb)4
-rw-r--r--qa/qa/scenario/test/integration/mtls.rb13
-rw-r--r--rubocop/cop/migration/versioned_migration_class.rb24
-rw-r--r--spec/db/migration_spec.rb2
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js3
-rw-r--r--spec/frontend/flash_spec.js61
-rw-r--r--spec/frontend/lazy_loader_spec.js6
-rw-r--r--spec/frontend/terms/components/app_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js253
-rw-r--r--spec/frontend/vue_shared/components/entity_select/group_select_spec.js134
-rw-r--r--spec/frontend/vue_shared/components/entity_select/utils_spec.js (renamed from spec/frontend/vue_shared/components/group_select/utils_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js322
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js3
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb16
-rw-r--r--spec/rubocop/cop/migration/versioned_migration_class_spec.rb12
57 files changed, 1207 insertions, 824 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 48059d9518f..68c3d5a6706 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -1,4 +1,5 @@
# E2E tests pipeline loaded dynamically by script: scripts/generate-e2e-pipeline
+# For adding new tests, refer to: doc/development/testing_guide/end_to_end/package_and_test_pipeline.md
default:
interruptible: true
@@ -440,7 +441,7 @@ ee:jira:
JIRA_ADMIN_PASSWORD: $QA_JIRA_ADMIN_PASSWORD
rules:
- !reference [.rules:test:qa, rules]
- - if: $QA_SUITES =~ /Test::Instance::Jira/
+ - if: $QA_SUITES =~ /Test::Integration::Jira/
- !reference [.rules:test:manual, rules]
ee:integrations:
@@ -485,7 +486,7 @@ ee:mtls:
QA_SCENARIO: Test::Integration::MTLS
rules:
- !reference [.rules:test:qa, rules]
- - if: $QA_SUITES =~ /Test::Integration::MTLS/
+ - if: $QA_SUITES =~ /Test::Integration::Mtls/
- !reference [.rules:test:manual, rules]
ee:mattermost:
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index b55a274d06b..5e3b05e064d 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -3526,7 +3526,6 @@ Layout/LineLength:
- 'rubocop/cop/migration/add_limit_to_text_columns.rb'
- 'rubocop/cop/migration/add_reference.rb'
- 'rubocop/cop/migration/prevent_global_enable_lock_retries_with_disable_ddl_transaction.rb'
- - 'rubocop/cop/migration/versioned_migration_class.rb'
- 'rubocop/cop/migration/with_lock_retries_disallowed_method.rb'
- 'rubocop/cop/qa/selector_usage.rb'
- 'rubocop/cop/rspec/top_level_describe_path.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index b7c05b0dffe..ff519785c2e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-9e1a85895ad712011caf5f5849f8e4efdd2ee515
+84f978eedd2aa130b1598f0c10def895ccf218d2
diff --git a/Gemfile b/Gemfile
index 03a9ec7ed88..61368292997 100644
--- a/Gemfile
+++ b/Gemfile
@@ -29,7 +29,7 @@ gem 'ipaddr', '1.2.2'
# Responders respond_to and respond_with
gem 'responders', '~> 3.0'
-gem 'sprockets', '~> 4.1.1'
+gem 'sprockets', '~> 3.7.0'
gem 'view_component', '~> 2.74.1'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index ada5d80de1c..c75e1530c47 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -565,7 +565,7 @@
{"name":"spring","version":"4.1.0","platform":"ruby","checksum":"f17f080fb0df558d663c897a6229ed3d5cc54819ab51876ea6eef49a67f0a3cb"},
{"name":"spring-commands-rspec","version":"1.0.4","platform":"ruby","checksum":"6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c"},
{"name":"sprite-factory","version":"1.7.1","platform":"ruby","checksum":"5586524a1aec003241f1abc6852b61433e988aba5ee2b55f906387bf49b01ba2"},
-{"name":"sprockets","version":"4.1.1","platform":"ruby","checksum":"68b10b0e574fc2a080e4779d025bf39bc7a20bc8659e32f827cccce9581348e2"},
+{"name":"sprockets","version":"3.7.2","platform":"ruby","checksum":"5ea1d7facd09203c1aa196afd6178208cd25abdbcc2a9978810a2f0754e152a0"},
{"name":"sprockets-rails","version":"3.4.2","platform":"ruby","checksum":"36d6327757ccf7460a00d1d52b2d5ef0019a4670503046a129fa1fb1300931ad"},
{"name":"sqlite3","version":"1.4.2","platform":"ruby","checksum":"e8b8ef3b0f75c18e1a7ee62c5678c827e99389e53fa55eb7a9a5f57459004a52"},
{"name":"ssh_data","version":"1.3.0","platform":"ruby","checksum":"ec7c1e95a3aebeee412147998f4c147b4b05da6ed0aafda6083f9449318eaac0"},
diff --git a/Gemfile.lock b/Gemfile.lock
index a37c27439a4..3b65e2b4061 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1405,7 +1405,7 @@ GEM
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprite-factory (1.7.1)
- sprockets (4.1.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
@@ -1843,7 +1843,7 @@ DEPENDENCIES
spring (~> 4.1.0)
spring-commands-rspec (~> 1.0.4)
sprite-factory (~> 1.7)
- sprockets (~> 4.1.1)
+ sprockets (~> 3.7.0)
ssh_data (~> 1.3)
stackprof (~> 0.2.21)
state_machines-activerecord (~> 0.8.0)
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
deleted file mode 100644
index c8dc05847bb..00000000000
--- a/app/assets/config/manifest.js
+++ /dev/null
@@ -1,120 +0,0 @@
-//= link_tree ../images
-
-//= link application_utilities.css
-//= link application_utilities_dark.css
-//= link application.css
-//= link application_dark.css
-
-//= link_directory ../stylesheets/startup .css
-
-//= link print.css
-//= link mailer.css
-//= link mailer_client_specific.css
-//= link notify.css
-//= link notify_enhanced.css
-
-//= link mailers/highlighted_diff_email.css
-//= link page_bundles/_mixins_and_variables_and_functions.css
-
-//= link page_bundles/admin/application_settings_metrics_and_profiling.css
-//= link page_bundles/admin/geo_nodes.css
-//= link page_bundles/admin/geo_replicable.css
-//= link page_bundles/admin/jobs_index.css
-
-//= link page_bundles/alert_management_details.css
-//= link page_bundles/alert_management_settings.css
-//= link page_bundles/boards.css
-//= link page_bundles/branches.css
-//= link page_bundles/build.css
-//= link page_bundles/ci_status.css
-//= link page_bundles/cluster_agents.css
-//= link page_bundles/clusters.css
-//= link page_bundles/dashboard.css
-//= link page_bundles/dashboard_projects.css
-//= link page_bundles/design_management.css
-//= link page_bundles/editor.css
-//= link page_bundles/environments.css
-//= link page_bundles/error_tracking_details.css
-//= link page_bundles/error_tracking_index.css
-//= link page_bundles/escalation_policies.css
-//= link page_bundles/graph_charts.css
-//= link page_bundles/group.css
-//= link page_bundles/ide.css
-//= link page_bundles/import.css
-//= link page_bundles/incident_management_list.css
-//= link page_bundles/incidents.css
-//= link page_bundles/issuable.css
-//= link page_bundles/issuable_list.css
-//= link page_bundles/issues_list.css
-//= link page_bundles/issues_show.css
-//= link page_bundles/jira_connect.css
-//= link page_bundles/jira_connect_users.css
-//= link page_bundles/learn_gitlab.css
-//= link page_bundles/members.css
-//= link page_bundles/merge_conflicts.css
-//= link page_bundles/merge_requests.css
-//= link page_bundles/milestone.css
-//= link page_bundles/new_namespace.css
-//= link page_bundles/notifications.css
-//= link page_bundles/oncall_schedules.css
-//= link page_bundles/operations.css
-//= link page_bundles/pipeline.css
-//= link page_bundles/pipeline_editor.css
-//= link page_bundles/pipeline_schedules.css
-//= link page_bundles/pipelines.css
-//= link page_bundles/profile.css
-//= link page_bundles/profile_two_factor_auth.css
-//= link page_bundles/profiles/preferences.css
-//= link page_bundles/project.css
-//= link page_bundles/projects_edit.css
-//= link page_bundles/prometheus.css
-//= link page_bundles/releases.css
-//= link page_bundles/reports.css
-//= link page_bundles/runner_details.css
-//= link page_bundles/search.css
-//= link page_bundles/settings.css
-//= link page_bundles/signup.css
-//= link page_bundles/terminal.css
-//= link page_bundles/terms.css
-//= link page_bundles/todos.css
-//= link page_bundles/tree.css
-//= link page_bundles/users.css
-//= link page_bundles/wiki.css
-//= link page_bundles/work_items.css
-//= link page_bundles/xterm.css
-
-//= link lazy_bundles/cropper.css
-//= link lazy_bundles/select2.css
-//= link lazy_bundles/gridstack.css
-
-//= link performance_bar.css
-//= link disable_animations.css
-//= link test_environment.css
-//= link snippets.css
-//= link fonts.css
-
-//= link_tree ../javascripts/locale .js
-//= link gettext/all
-
-//= link emoji_sprites.css
-//= link errors.css
-
-//= link_directory ../stylesheets/themes .css
-
-//= link_directory ../stylesheets/highlight/themes .css
-//= link highlight/diff_custom_colors_addition.css
-//= link highlight/diff_custom_colors_deletion.css
-
-// @gitlab/fonts package
-//= link gitlab-sans/GitLabSans.woff2
-//= link jetbrains-mono/JetBrainsMono.woff2
-
-// @gitlab-svg package
-//= link icons.svg
-//= link icons.json
-//= link_tree ../../../node_modules/@gitlab/svgs/dist/illustrations .svg
-//= link_tree ../../../node_modules/@gitlab/svgs/dist/illustrations .png
-
-//= link xterm/src/xterm.css
-
-//= link snowplow/sp.js
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index 57ceaa24b6e..f61c19b151e 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -64,10 +64,6 @@ export default {
type: String,
required: true,
},
- groupRunnersLimitedCount: {
- type: Number,
- required: true,
- },
},
data() {
return {
diff --git a/app/assets/javascripts/ci/runner/group_runners/index.js b/app/assets/javascripts/ci/runner/group_runners/index.js
index 0e7efd2b8a1..46514d5afe8 100644
--- a/app/assets/javascripts/ci/runner/group_runners/index.js
+++ b/app/assets/javascripts/ci/runner/group_runners/index.js
@@ -20,7 +20,6 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
runnerInstallHelpPage,
groupId,
groupFullPath,
- groupRunnersLimitedCount,
onlineContactTimeoutSecs,
staleTimeoutSecs,
emptyStateSvgPath,
@@ -50,7 +49,6 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
props: {
registrationToken,
groupFullPath,
- groupRunnersLimitedCount: parseInt(groupRunnersLimitedCount, 10),
},
});
},
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 20fb2b1aa94..483f1d2c7a0 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -3,46 +3,11 @@ import Vue from 'vue';
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
-const FLASH_TYPES = {
- ALERT: 'alert',
- NOTICE: 'notice',
- SUCCESS: 'success',
- WARNING: 'warning',
-};
-
-const VARIANT_SUCCESS = 'success';
-const VARIANT_WARNING = 'warning';
-const VARIANT_DANGER = 'danger';
-const VARIANT_INFO = 'info';
-const VARIANT_TIP = 'tip';
-
-const FLASH_CLOSED_EVENT = 'flashClosed';
-
-const hideFlash = (flashEl, fadeTransition = true) => {
- if (fadeTransition) {
- Object.assign(flashEl.style, {
- transition: 'opacity 0.15s',
- opacity: '0',
- });
- }
-
- flashEl.addEventListener(
- 'transitionend',
- () => {
- flashEl.remove();
- window.dispatchEvent(new Event('resize'));
- flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
- if (document.body.classList.contains('flash-shown'))
- document.body.classList.remove('flash-shown');
- },
- {
- once: true,
- passive: true,
- },
- );
-
- if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
-};
+export const VARIANT_SUCCESS = 'success';
+export const VARIANT_WARNING = 'warning';
+export const VARIANT_DANGER = 'danger';
+export const VARIANT_INFO = 'info';
+export const VARIANT_TIP = 'tip';
/**
* Render an alert at the top of the page, or, optionally an
@@ -86,7 +51,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
* @param {boolean} [options.captureError] - Whether to send error to Sentry
* @param {object} [options.error] - Error to be captured in Sentry
*/
-const createAlert = function createAlert({
+export const createAlert = ({
message,
title,
variant = VARIANT_DANGER,
@@ -98,7 +63,7 @@ const createAlert = function createAlert({
onDismiss = null,
captureError = false,
error = null,
-}) {
+}) => {
if (captureError && error) Sentry.captureException(error);
const alertContainer = parent.querySelector(containerSelector);
@@ -170,15 +135,3 @@ const createAlert = function createAlert({
},
});
};
-
-export {
- hideFlash,
- FLASH_TYPES,
- FLASH_CLOSED_EVENT,
- createAlert,
- VARIANT_SUCCESS,
- VARIANT_WARNING,
- VARIANT_DANGER,
- VARIANT_INFO,
- VARIANT_TIP,
-};
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index fb685247bd4..d0ccc8fd599 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -2,7 +2,7 @@ import { GROUP_BADGE } from '~/badges/constants';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import initTransferGroupForm from '~/groups/init_transfer_group_form';
-import { initGroupSelects } from '~/vue_shared/components/group_select/init_group_selects';
+import { initGroupSelects } from '~/vue_shared/components/entity_select/init_group_selects';
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
index eecf32f83df..58b8937d410 100644
--- a/app/assets/javascripts/terms/components/app.vue
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -2,7 +2,6 @@
import { GlButton, GlIntersectionObserver } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
@@ -26,6 +25,9 @@ export default {
data() {
return {
acceptDisabled: true,
+ observer: new MutationObserver(() => {
+ this.setScrollableViewportHeight();
+ }),
};
},
computed: {
@@ -34,23 +36,10 @@ export default {
mounted() {
this.renderGFM();
this.setScrollableViewportHeight();
-
- this.$options.flashElements = [
- ...document.querySelectorAll(
- Object.values(FLASH_TYPES)
- .map((flashType) => `.flash-${flashType}`)
- .join(','),
- ),
- ];
-
- this.$options.flashElements.forEach((flashElement) => {
- flashElement.addEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
- });
+ this.observer.observe(document.body, { childList: true, subtree: true });
},
beforeDestroy() {
- this.$options.flashElements.forEach((flashElement) => {
- flashElement.removeEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
- });
+ this.observer.disconnect();
},
methods: {
renderGFM() {
@@ -70,10 +59,6 @@ export default {
scrollHeight - clientHeight
}px)`;
},
- handleFlashClose(event) {
- this.setScrollableViewportHeight();
- event.target.removeEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
- },
trackTrialAcceptTerms,
},
};
diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
index 06537d682fe..a48bba3be8c 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/constants.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
@@ -1,6 +1,7 @@
import { __ } from '~/locale';
export const TOGGLE_TEXT = __('Search for a group');
+export const HEADER_TEXT = __('Select a group');
export const RESET_LABEL = __('Reset');
export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.');
export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.');
diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index d295052e2ce..42a37eef1eb 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -1,28 +1,15 @@
<script>
import { debounce } from 'lodash';
-import { GlFormGroup, GlAlert, GlCollapsibleListbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import axios from '~/lib/utils/axios_utils';
-import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
-import Api from '~/api';
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
import { __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { groupsPath } from './utils';
-import {
- TOGGLE_TEXT,
- RESET_LABEL,
- FETCH_GROUPS_ERROR,
- FETCH_GROUP_ERROR,
- QUERY_TOO_SHORT_MESSAGE,
-} from './constants';
+import { RESET_LABEL, QUERY_TOO_SHORT_MESSAGE } from './constants';
const MINIMUM_QUERY_LENGTH = 3;
-const GROUPS_PER_PAGE = 20;
export default {
components: {
GlFormGroup,
- GlAlert,
GlCollapsibleListbox,
},
props: {
@@ -48,13 +35,20 @@ export default {
required: false,
default: false,
},
- parentGroupID: {
+ headerText: {
type: String,
- required: false,
- default: null,
+ required: true,
},
- groupsFilter: {
+ defaultToggleText: {
type: String,
+ required: true,
+ },
+ fetchItems: {
+ type: Function,
+ required: true,
+ },
+ fetchInitialSelectionText: {
+ type: Function,
required: false,
default: null,
},
@@ -63,10 +57,10 @@ export default {
return {
pristine: true,
searching: false,
- hasMoreGroups: true,
+ hasMoreItems: true,
infiniteScrollLoading: false,
searchString: '',
- groups: [],
+ items: [],
page: 1,
selectedValue: null,
selectedText: null,
@@ -78,14 +72,14 @@ export default {
set(value) {
this.selectedValue = value;
this.selectedText =
- value === null ? null : this.groups.find((group) => group.value === value).full_name;
+ value === null ? null : this.items.find((item) => item.value === value).text;
},
get() {
return this.selectedValue;
},
},
toggleText() {
- return this.selectedText ?? this.$options.i18n.toggleText;
+ return this.selectedText ?? this.defaultToggleText;
},
resetButtonLabel() {
return this.clearable ? RESET_LABEL : '';
@@ -109,90 +103,64 @@ export default {
search: debounce(function debouncedSearch(searchString) {
this.searchString = searchString;
if (this.isSearchQueryTooShort) {
- this.groups = [];
+ this.items = [];
} else {
- this.fetchGroups();
+ this.fetchEntities();
}
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- async fetchGroups(page = 1) {
+ async fetchEntities(page = 1) {
if (page === 1) {
this.searching = true;
- this.groups = [];
- this.hasMoreGroups = true;
+ this.items = [];
+ this.hasMoreItems = true;
} else {
this.infiniteScrollLoading = true;
}
- try {
- const { data, headers } = await axios.get(
- Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
- {
- params: {
- search: this.searchString,
- per_page: GROUPS_PER_PAGE,
- page,
- },
- },
- );
- const groups = data.length ? data : data.results || [];
-
- this.groups.push(
- ...groups.map((group) => ({
- ...group,
- value: String(group.id),
- })),
- );
+ const { items, totalPages } = await this.fetchItems(this.searchString, page);
- const { totalPages } = parseIntPagination(normalizeHeaders(headers));
- if (page === totalPages) {
- this.hasMoreGroups = false;
- }
+ this.items.push(...items);
- this.page = page;
- this.searching = false;
- this.infiniteScrollLoading = false;
- } catch (error) {
- this.handleError({ message: FETCH_GROUPS_ERROR, error });
+ if (page === totalPages) {
+ this.hasMoreItems = false;
}
+
+ this.page = page;
+ this.searching = false;
+ this.infiniteScrollLoading = false;
},
async fetchInitialSelection() {
if (!this.initialSelection) {
this.pristine = false;
return;
}
- this.searching = true;
- try {
- const group = await Api.group(this.initialSelection);
- this.selectedValue = this.initialSelection;
- this.selectedText = group.full_name;
- this.pristine = false;
- this.searching = false;
- } catch (error) {
- this.handleError({ message: FETCH_GROUP_ERROR, error });
+
+ if (!this.fetchInitialSelectionText) {
+ throw new Error(
+ '`initialSelection` is provided but lacks `fetchInitialSelectionText` to retrieve the corresponding text',
+ );
}
+
+ this.searching = true;
+ const name = await this.fetchInitialSelectionText(this.initialSelection);
+ this.selectedValue = this.initialSelection;
+ this.selectedText = name;
+ this.pristine = false;
+ this.searching = false;
},
onShown() {
- if (!this.searchString && !this.groups.length) {
- this.fetchGroups();
+ if (!this.searchString && !this.items.length) {
+ this.fetchEntities();
}
},
onReset() {
this.selected = null;
},
onBottomReached() {
- this.fetchGroups(this.page + 1);
- },
- handleError({ message, error }) {
- Sentry.captureException(error);
- this.errorMessage = message;
- },
- dismissError() {
- this.errorMessage = '';
+ this.fetchEntities(this.page + 1);
},
},
i18n: {
- toggleText: TOGGLE_TEXT,
- selectGroup: __('Select a group'),
noResultsText: __('No results found.'),
searchQueryTooShort: QUERY_TOO_SHORT_MESSAGE,
},
@@ -201,20 +169,18 @@ export default {
<template>
<gl-form-group :label="label">
- <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
- errorMessage
- }}</gl-alert>
+ <slot name="error"></slot>
<gl-collapsible-listbox
ref="listbox"
v-model="selected"
- :header-text="$options.i18n.selectGroup"
+ :header-text="headerText"
:reset-button-label="resetButtonLabel"
:toggle-text="toggleText"
:loading="searching && pristine"
:searching="searching"
- :items="groups"
+ :items="items"
:no-results-text="noResultsText"
- :infinite-scroll="hasMoreGroups"
+ :infinite-scroll="hasMoreItems"
:infinite-scroll-loading="infiniteScrollLoading"
searchable
@shown="onShown"
@@ -223,10 +189,7 @@ export default {
@bottom-reached="onBottomReached"
>
<template #list-item="{ item }">
- <div class="gl-font-weight-bold">
- {{ item.full_name }}
- </div>
- <div class="gl-text-gray-300">{{ item.full_path }}</div>
+ <slot name="list-item" :item="item"></slot>
</template>
</gl-collapsible-listbox>
<input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
new file mode 100644
index 00000000000..a5fc438e932
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -0,0 +1,132 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import axios from '~/lib/utils/axios_utils';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
+import Api, { DEFAULT_PER_PAGE } from '~/api';
+import { groupsPath } from './utils';
+import { TOGGLE_TEXT, HEADER_TEXT, FETCH_GROUPS_ERROR, FETCH_GROUP_ERROR } from './constants';
+import EntitySelect from './entity_select.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EntitySelect,
+ },
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ initialSelection: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ clearable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ parentGroupID: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupsFilter: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ errorMessage: '',
+ };
+ },
+ methods: {
+ async fetchGroups(searchString = '', page = 1) {
+ let groups = [];
+ let totalPages = 0;
+ try {
+ const { data, headers } = await axios.get(
+ Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
+ {
+ params: {
+ search: searchString,
+ per_page: DEFAULT_PER_PAGE,
+ page,
+ },
+ },
+ );
+ groups = (data.length ? data : data.results || []).map((group) => ({
+ ...group,
+ text: group.full_name,
+ value: String(group.id),
+ }));
+
+ totalPages = parseIntPagination(normalizeHeaders(headers)).totalPages;
+ } catch (error) {
+ this.handleError({ message: FETCH_GROUPS_ERROR, error });
+ }
+ return { items: groups, totalPages };
+ },
+ async fetchGroupName(groupId) {
+ let groupName = '';
+ try {
+ const group = await Api.group(groupId);
+ groupName = group.full_name;
+ } catch (error) {
+ this.handleError({ message: FETCH_GROUP_ERROR, error });
+ }
+ return groupName;
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
+ },
+ i18n: {
+ toggleText: TOGGLE_TEXT,
+ selectGroup: HEADER_TEXT,
+ },
+};
+</script>
+
+<template>
+ <entity-select
+ :label="label"
+ :input-name="inputName"
+ :input-id="inputId"
+ :initial-selection="initialSelection"
+ :clearable="clearable"
+ :header-text="$options.i18n.selectGroup"
+ :default-toggle-text="$options.i18n.toggleText"
+ :fetch-items="fetchGroups"
+ :fetch-initial-selection-text="fetchGroupName"
+ >
+ <template #error>
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
+ </template>
+ <template #list-item="{ item }">
+ <div class="gl-font-weight-bold">
+ {{ item.full_name }}
+ </div>
+ <div class="gl-text-gray-300">{{ item.full_path }}</div>
+ </template>
+ </entity-select>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js b/app/assets/javascripts/vue_shared/components/entity_select/init_group_selects.js
index dbfac8a0339..dbfac8a0339 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/init_group_selects.js
diff --git a/app/assets/javascripts/vue_shared/components/group_select/utils.js b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
index 0a4622269f4..0a4622269f4 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/utils.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 5efa9c94f2b..c8e6e79145b 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -1,8 +1,8 @@
<script>
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
+import NoteBody from './work_item_note_body.vue';
export default {
components: {
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue
index dcee8750f81..9aa01784c9f 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue
@@ -18,6 +18,7 @@ export default {
methods: {
renderGFM() {
renderGFM(this.$refs['note-body']);
+ gl?.lazyLoader?.searchLazyImages();
},
},
safeHtmlConfig: {
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index d58983c013d..9a2cdc1c172 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -47,6 +47,7 @@ export default {
await this.$nextTick();
renderGFM(this.$refs['gfm-content']);
+ gl?.lazyLoader?.searchLazyImages();
if (this.canEdit) {
this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 3e2a141960a..427df3d1925 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -451,7 +451,11 @@ export default {
});
},
openInModal(event, modalWorkItem) {
- if (event) {
+ if (!this.workItemsMvc2Enabled) {
+ return;
+ }
+
+ if (this.event) {
event.preventDefault();
this.updateUrl(modalWorkItem);
diff --git a/app/views/clusters/clusters/_namespace.html.haml b/app/views/clusters/clusters/_namespace.html.haml
index 572f2d6d9a2..34576b6e5af 100644
--- a/app/views/clusters/clusters/_namespace.html.haml
+++ b/app/views/clusters/clusters/_namespace.html.haml
@@ -3,11 +3,12 @@
- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
.js-namespace-prefixed
- = platform_field.text_field :namespace,
- label: s_('ClusterIntegration|Project namespace prefix (optional, unique)'), label_class: 'label-bold',
- help: '%{help_text} %{help_link}'.html_safe % { help_text: managed_namespace_help_text, help_link: managed_namespace_help_link }
+ .form-group
+ = platform_field.label :namespace, s_('ClusterIntegration|Project namespace prefix (optional, unique)'), class: 'label-bold'
+ = platform_field.text_field :namespace, class: 'form-control'
+ %small.form-text.text-muted= '%{help_text} %{help_link}'.html_safe % { help_text: managed_namespace_help_text, help_link: managed_namespace_help_link }
.js-namespace.hidden
- = platform_field.text_field :namespace,
- label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold',
- help: '%{help_text}'.html_safe % { help_text: non_managed_namespace_help_text },
- disabled: true
+ .form-group
+ = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_field.text_field :namespace, class: 'form-control', disabled: true
+ %small.form-text.text-muted= '%{help_text}'.html_safe % { help_text: non_managed_namespace_help_text }
diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml
index 11277a83e3a..59706b6d8c4 100644
--- a/app/views/clusters/clusters/_provider_details_form.html.haml
+++ b/app/views/clusters/clusters/_provider_details_form.html.haml
@@ -1,52 +1,62 @@
-= bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors' },
+= gitlab_ui_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors', role: 'form' },
as: :cluster do |field|
- - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
- = field.text_field :name, class: 'js-select-on-focus cluster-name', required: true,
- title: s_('ClusterIntegration|Cluster name is required.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold',
- input_group_class: 'gl-field-error-anchor', append: copy_name_btn
+ .form-group
+ - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required'
+ .input-group.gl-field-error-anchor
+ = field.text_field :name, class: 'form-control js-select-on-focus cluster-name', required: true,
+ title: s_('ClusterIntegration|Cluster name is required.'),
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ append: copy_name_btn
= field.fields_for :platform_kubernetes, platform do |platform_field|
- - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
- = platform_field.text_field :api_url, class: 'js-select-on-focus', required: true,
- title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- label: s_('ClusterIntegration|API URL'), label_class: 'label-bold',
- input_group_class: 'gl-field-error-anchor', append: copy_api_url
-
- - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
- = platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '10',
- readonly: cluster.read_only_kubernetes_platform_fields?,
- placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
- label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
- input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn
-
- = platform_field.password_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
- readonly: cluster.read_only_kubernetes_platform_fields?, autocomplete: 'new-password',
- label: s_('ClusterIntegration|Enter new Service Token'), label_class: 'label-bold',
- input_group_class: 'gl-field-error-anchor'
-
- = platform_field.form_group :authorization_type do
- = platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'),
- label_class: 'label-bold', inline: true }, 'rbac', 'abac'
+ .form-group
+ - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
+ = platform_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required'
+ .input-group.gl-field-error-anchor
+ = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', required: true,
+ title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ append: copy_api_url
+
+ .form-group
+ - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
+ = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
+ .input-group.gl-field-error-anchor
+ = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', rows: '10',
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
+ append: copy_ca_cert_btn
+
+ .form-group
+ = platform_field.label :token, s_('ClusterIntegration|Enter new Service Token'), class: 'label-bold required'
+ .input-group.gl-field-error-anchor
+ = platform_field.password_field :token, type: 'password', class: 'form-control js-select-on-focus js-cluster-token',
+ readonly: cluster.read_only_kubernetes_platform_fields?, autocomplete: 'new-password'
+
+ .form-group
+ .form-check
+ = platform_field.check_box :authorization_type, { disabled: true, inline: true, class: 'form-check-input' }, 'rbac', 'abac'
+ = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
- = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
- class: 'js-gl-managed',
- label_class: 'label-bold' }
+ .form-check
+ = field.check_box :managed, { class: 'js-gl-managed form-check-input' }
+ = field.label :managed, s_('ClusterIntegration|GitLab-managed cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
= link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
.form-group
- = field.check_box :namespace_per_environment, { label: s_('ClusterIntegration|Namespace per environment'), label_class: 'label-bold' }
+ .form-check
+ = field.check_box :namespace_per_environment, { class: 'form-check-input' }
+ = field.label :namespace_per_environment, s_('ClusterIntegration|Namespace per environment'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
= link_to _('More information'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 557c95f8478..ed169b2bfd1 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -7,48 +7,63 @@
- rbac_help_text = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + ' '
- rbac_help_text << s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
-= bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' },
+= gitlab_ui_form_for @user_cluster, html: { class: 'gl-show-field-errors', role: 'form' },
url: clusterable.create_user_clusters_path, as: :cluster do |field|
- = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
- label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- = field.text_field :environment_scope, required: true, title: s_('ClusterIntegration|Environment scope is required.'),
- label: s_('ClusterIntegration|Environment scope'), label_class: 'label-bold',
- help: s_('ClusterIntegration|Choose which of your environments will use this cluster.')
+ = form_errors(@user_cluster)
+
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required'
+ = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'), class: 'form-control'
+
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold required'
+ = field.text_field :environment_scope, required: true, title: s_('ClusterIntegration|Environment scope is required.'), class: 'form-control'
+ %small.form-text.text-muted
+ = s_('ClusterIntegration|Choose which of your environments will use this cluster.')
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
- = platform_kubernetes_field.url_field :api_url, required: true,
- title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- label: s_('ClusterIntegration|API URL'), label_class: 'label-bold',
- help: '%{help_text} %{help_link}'.html_safe % { help_text: api_url_help_text, help_link: more_info_link }
-
- = platform_kubernetes_field.text_area :ca_cert,
- rows: '10',
- placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
- label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
- help: '%{help_text} %{help_link}'.html_safe % { help_text: ca_cert_help_text, help_link: more_info_link }
-
- = platform_kubernetes_field.text_field :token, required: true,
- title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'),
- autocomplete: 'off', label_class: 'label-bold',
- help: '%{help_text} %{help_link}'.html_safe % { help_text: token_help_text, help_link: more_info_link }
-
- = platform_kubernetes_field.form_group :authorization_type,
- { help: '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link } } do
- = platform_kubernetes_field.check_box :authorization_type,
- { data: { qa_selector: 'rbac_checkbox'}, label: s_('ClusterIntegration|RBAC-enabled cluster'),
- label_class: 'label-bold', inline: true }, 'rbac', 'abac'
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required'
+ = platform_kubernetes_field.url_field :api_url, required: true,
+ title: s_('ClusterIntegration|API URL should be a valid http/https url.'), class: 'form-control'
+ %small.form-text.text-muted
+ = '%{help_text} %{help_link}'.html_safe % { help_text: api_url_help_text, help_link: more_info_link }
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
+ = platform_kubernetes_field.text_area :ca_cert,
+ rows: '10',
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
+ class: 'form-control'
+ %small.form-text.text-muted
+ = '%{help_text} %{help_link}'.html_safe % { help_text: ca_cert_help_text, help_link: more_info_link }
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Service Token'), class: 'label-bold required'
+ = platform_kubernetes_field.text_field :token, required: true, title: s_('ClusterIntegration|Service token is required.'), autocomplete: 'off', class: 'form-control'
+ %small.form-text.text-muted
+ = '%{help_text} %{help_link}'.html_safe % { help_text: token_help_text, help_link: more_info_link }
+
+ .form-group
+ .form-check
+ = platform_kubernetes_field.check_box :authorization_type, { data: { qa_selector: 'rbac_checkbox'}, inline: true, class: 'form-check-input' }, 'rbac', 'abac'
+ = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
+ %small.form-text.text-muted
+ = '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link }
.form-group
- = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
- class: 'js-gl-managed',
- label_class: 'label-bold' }
+ .form-check
+ = field.check_box :managed, { class: 'js-gl-managed form-check-input' }
+ = field.label :managed, s_('ClusterIntegration|GitLab-managed cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
= link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
.form-group
- = field.check_box :namespace_per_environment, { label: s_('ClusterIntegration|Namespace per environment'), label_class: 'label-bold' }
+ .form-check
+ = field.check_box :namespace_per_environment, { class: 'form-check-input' }
+ = field.label :namespace_per_environment, s_('ClusterIntegration|Namespace per environment'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
= link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
index 819e77a8698..7e98f6035a6 100644
--- a/app/views/groups/runners/index.html.haml
+++ b/app/views/groups/runners/index.html.haml
@@ -1,3 +1,3 @@
- page_title s_('Runners|Runners')
-#js-group-runners{ data: group_runners_data_attributes(@group).merge({registration_token: @group_runner_registration_token }) }
+#js-group-runners{ data: group_runners_data_attributes(@group).merge({ registration_token: @group_runner_registration_token }) }
diff --git a/config/application.rb b/config/application.rb
index 49bb7207c24..61dfa59a90d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -240,32 +240,145 @@ module Gitlab
# Support legacy unicode file named img emojis, `1F939.png`
config.assets.paths << TanukiEmoji.images_path
+ config.assets.paths << "#{config.root}/vendor/assets/fonts"
+
+ config.assets.precompile << "application_utilities.css"
+ config.assets.precompile << "application_utilities_dark.css"
+ config.assets.precompile << "application_dark.css"
+
+ config.assets.precompile << "startup/*.css"
+
+ config.assets.precompile << "print.css"
+ config.assets.precompile << "mailer.css"
+ config.assets.precompile << "mailer_client_specific.css"
+ config.assets.precompile << "notify.css"
+ config.assets.precompile << "notify_enhanced.css"
+ config.assets.precompile << "mailers/*.css"
+ config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
+ config.assets.precompile << "page_bundles/admin/application_settings_metrics_and_profiling.css"
+ config.assets.precompile << "page_bundles/admin/geo_nodes.css"
+ config.assets.precompile << "page_bundles/admin/geo_replicable.css"
+ config.assets.precompile << "page_bundles/admin/jobs_index.css"
+ config.assets.precompile << "page_bundles/alert_management_details.css"
+ config.assets.precompile << "page_bundles/alert_management_settings.css"
+ config.assets.precompile << "page_bundles/billings.css"
+ config.assets.precompile << "page_bundles/boards.css"
+ config.assets.precompile << "page_bundles/branches.css"
+ config.assets.precompile << "page_bundles/build.css"
+ config.assets.precompile << "page_bundles/ci_status.css"
+ config.assets.precompile << "page_bundles/cluster_agents.css"
+ config.assets.precompile << "page_bundles/clusters.css"
+ config.assets.precompile << "page_bundles/cycle_analytics.css"
+ config.assets.precompile << "page_bundles/dashboard.css"
+ config.assets.precompile << "page_bundles/dashboard_projects.css"
+ config.assets.precompile << "page_bundles/design_management.css"
+ config.assets.precompile << "page_bundles/dev_ops_reports.css"
+ config.assets.precompile << "page_bundles/editor.css"
+ config.assets.precompile << "page_bundles/environments.css"
+ config.assets.precompile << "page_bundles/epics.css"
+ config.assets.precompile << "page_bundles/error_tracking_details.css"
+ config.assets.precompile << "page_bundles/error_tracking_index.css"
+ config.assets.precompile << "page_bundles/graph_charts.css"
+ config.assets.precompile << "page_bundles/group.css"
+ config.assets.precompile << "page_bundles/ide.css"
+ config.assets.precompile << "page_bundles/import.css"
+ config.assets.precompile << "page_bundles/incident_management_list.css"
+ config.assets.precompile << "page_bundles/incidents.css"
+ config.assets.precompile << "page_bundles/issues_analytics.css"
+ config.assets.precompile << "page_bundles/issuable.css"
+ config.assets.precompile << "page_bundles/issuable_list.css"
+ config.assets.precompile << "page_bundles/issues_list.css"
+ config.assets.precompile << "page_bundles/issues_show.css"
+ config.assets.precompile << "page_bundles/jira_connect.css"
+ config.assets.precompile << "page_bundles/jira_connect_users.css"
+ config.assets.precompile << "page_bundles/learn_gitlab.css"
+ config.assets.precompile << "page_bundles/marketing_popover.css"
+ config.assets.precompile << "page_bundles/members.css"
+ config.assets.precompile << "page_bundles/merge_conflicts.css"
+ config.assets.precompile << "page_bundles/merge_request_analytics.css"
+ config.assets.precompile << "page_bundles/merge_requests.css"
+ config.assets.precompile << "page_bundles/milestone.css"
+ config.assets.precompile << "page_bundles/new_namespace.css"
+ config.assets.precompile << "page_bundles/notifications.css"
+ config.assets.precompile << "page_bundles/oncall_schedules.css"
+ config.assets.precompile << "page_bundles/operations.css"
+ config.assets.precompile << "page_bundles/escalation_policies.css"
+ config.assets.precompile << "page_bundles/pipeline.css"
+ config.assets.precompile << "page_bundles/pipeline_schedules.css"
+ config.assets.precompile << "page_bundles/pipelines.css"
+ config.assets.precompile << "page_bundles/pipeline_editor.css"
+ config.assets.precompile << "page_bundles/productivity_analytics.css"
+ config.assets.precompile << "page_bundles/profile.css"
+ config.assets.precompile << "page_bundles/profile_two_factor_auth.css"
+ config.assets.precompile << "page_bundles/profiles/preferences.css"
+ config.assets.precompile << "page_bundles/project.css"
+ config.assets.precompile << "page_bundles/projects_edit.css"
+ config.assets.precompile << "page_bundles/prometheus.css"
+ config.assets.precompile << "page_bundles/releases.css"
+ config.assets.precompile << "page_bundles/reports.css"
+ config.assets.precompile << "page_bundles/roadmap.css"
+ config.assets.precompile << "page_bundles/requirements.css"
+ config.assets.precompile << "page_bundles/runner_details.css"
+ config.assets.precompile << "page_bundles/search.css"
+ config.assets.precompile << "page_bundles/security_dashboard.css"
+ config.assets.precompile << "page_bundles/security_discover.css"
+ config.assets.precompile << "page_bundles/settings.css"
+ config.assets.precompile << "page_bundles/signup.css"
+ config.assets.precompile << "page_bundles/terminal.css"
+ config.assets.precompile << "page_bundles/terms.css"
+ config.assets.precompile << "page_bundles/todos.css"
+ config.assets.precompile << "page_bundles/tree.css"
+ config.assets.precompile << "page_bundles/users.css"
+ config.assets.precompile << "page_bundles/wiki.css"
+ config.assets.precompile << "page_bundles/work_items.css"
+ config.assets.precompile << "page_bundles/xterm.css"
+ config.assets.precompile << "lazy_bundles/cropper.css"
+ config.assets.precompile << "lazy_bundles/select2.css"
+ config.assets.precompile << "lazy_bundles/gridstack.css"
+ config.assets.precompile << "performance_bar.css"
+ config.assets.precompile << "disable_animations.css"
+ config.assets.precompile << "test_environment.css"
+ config.assets.precompile << "snippets.css"
+ config.assets.precompile << "fonts.css"
+ config.assets.precompile << "locale/**/app.js"
+ config.assets.precompile << "emoji_sprites.css"
+ config.assets.precompile << "errors.css"
+ config.assets.precompile << "jira_connect.js"
+
+ config.assets.precompile << "themes/*.css"
+
+ config.assets.precompile << "highlight/themes/*.css"
+ config.assets.precompile << "highlight/diff_custom_colors_addition.css"
+ config.assets.precompile << "highlight/diff_custom_colors_deletion.css"
+
+ # Import woff2 for fonts
+ config.assets.paths << "#{config.root}/node_modules/@gitlab/fonts/"
+ config.assets.precompile << "gitlab-sans/*.woff2"
+ config.assets.precompile << "jetbrains-mono/*.woff2"
# Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist"
-
- config.assets.paths << "#{config.root}/node_modules/@gitlab/fonts"
-
- # BEGIN Import path for EE/JH specific SCSS entry point
+ config.assets.paths << "#{config.root}/node_modules/@jihulab/svgs/dist" if Gitlab.jh?
+ config.assets.precompile << "illustrations/jh/*.svg" if Gitlab.jh?
+ config.assets.precompile << "icons.svg"
+ config.assets.precompile << "icons.json"
+ config.assets.precompile << "illustrations/*.svg"
+ config.assets.precompile << "illustrations/*.png"
+
+ # Import css for xterm
+ config.assets.paths << "#{config.root}/node_modules/xterm/src/"
+ config.assets.precompile << "xterm.css"
+
+ # Import path for EE specific SCSS entry point
# In CE it will import a noop file, in EE a functioning file
# Order is important, so that the ee file takes precedence:
- if Gitlab.jh?
- config.assets.precompile << "#{config.root}/jh/app/assets/config/jh.js"
- config.assets.paths << "#{config.root}/jh/app/assets"
- config.assets.paths << "#{config.root}/jh/app/assets/stylesheets/_jh"
- end
-
- if Gitlab.ee?
- config.assets.precompile << "#{config.root}/ee/app/assets/config/ee.js"
- config.assets.paths << "#{config.root}/ee/app/assets"
- config.assets.paths << "#{config.root}/ee/app/assets/stylesheets/_ee"
- end
-
+ config.assets.paths << "#{config.root}/jh/app/assets/stylesheets/_jh" if Gitlab.jh?
+ config.assets.paths << "#{config.root}/ee/app/assets/stylesheets/_ee" if Gitlab.ee?
config.assets.paths << "#{config.root}/app/assets/stylesheets/_jh"
config.assets.paths << "#{config.root}/app/assets/stylesheets/_ee"
- # END Import path for EE/JH specific SCSS entry point
config.assets.paths << "#{config.root}/vendor/assets/javascripts/"
+ config.assets.precompile << "snowplow/sp.js"
# This path must come last to avoid confusing sprockets
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/64091#note_194512508
@@ -388,6 +501,42 @@ module Gitlab
config.factory_bot.definition_file_paths << 'jh/spec/factories' if Gitlab.jh?
end
+ # sprocket-rails adds some precompile assets we actually do not need.
+ #
+ # It copies all _non_ js and CSS files from the app/assets/ older.
+ #
+ # In our case this copies for example: Vue, Markdown and Graphql, which we do not need
+ # for production.
+ #
+ # We remove this default behavior and then reimplement it in order to consider ee/ as well
+ # and remove those other files we do not need.
+ #
+ # For reference: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87
+ initializer :correct_precompile_targets, after: :set_default_precompile do |app|
+ app.config.assets.precompile.reject! { |entry| entry == Sprockets::Railtie::LOOSE_APP_ASSETS }
+
+ # if two files in assets are named the same, it'll likely resolve to the normal app/assets version.
+ # See https://gitlab.com/gitlab-jh/gitlab/-/merge_requests/27#note_609101582 for more details
+ asset_roots = []
+
+ if Gitlab.jh?
+ asset_roots << config.root.join("jh/app/assets").to_s
+ end
+
+ asset_roots << config.root.join("app/assets").to_s
+
+ if Gitlab.ee?
+ asset_roots << config.root.join("ee/app/assets").to_s
+ end
+
+ LOOSE_APP_ASSETS = lambda do |logical_path, filename|
+ filename.start_with?(*asset_roots) &&
+ !['.js', '.css', '.md', '.vue', '.graphql', ''].include?(File.extname(logical_path))
+ end
+
+ app.config.assets.precompile << LOOSE_APP_ASSETS
+ end
+
# This empty initializer forces the :let_zeitwerk_take_over initializer to run before we load
# initializers in config/initializers. This is done because autoloading before Zeitwerk takes
# over is deprecated but our initializers do a lot of autoloading.
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 2605106bb66..6e233674f17 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1319,6 +1319,10 @@ GET /groups/:id/hooks/:hook_id
"releases_events": true,
"subgroup_events": true,
"enable_ssl_verification": true,
+ "repository_update_events": false,
+ "alert_status": "executable",
+ "disabled_until": null,
+ "url_variables": [ ],
"created_at": "2012-10-12T17:04:47Z"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 25003a5f06a..5dad1fe7abe 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -2413,6 +2413,10 @@ GET /projects/:id/hooks/:hook_id
"deployment_events": true,
"releases_events": true,
"enable_ssl_verification": true,
+ "repository_update_events": false,
+ "alert_status": "executable",
+ "disabled_until": null,
+ "url_variables": [ ],
"created_at": "2012-10-12T17:04:47Z"
}
```
diff --git a/doc/development/documentation/redirects.md b/doc/development/documentation/redirects.md
index 37c17b1d6ee..4cfe8be713a 100644
--- a/doc/development/documentation/redirects.md
+++ b/doc/development/documentation/redirects.md
@@ -32,9 +32,9 @@ Be sure to assign a technical writer to any merge request that moves, renames, o
Technical Writers can help with any questions and can review your change.
NOTE:
-When you change a page name, the Google Analytics are removed
+When you change the filename of a page, the Google Analytics are removed
from the content audit and the page views start from scratch.
-If you want to change a page name, edit the page first,
+If you want to change the filename, edit the page first,
so you can ensure the new page name is as accurate as possible.
## Types of redirects
diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md
index 8369dcb0740..1481b4364a7 100644
--- a/doc/development/testing_guide/end_to_end/index.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -97,6 +97,9 @@ This problem was discovered in <https://gitlab.com/gitlab-org/gitlab-qa/-/issues
work-around was suggested in <https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4717>.
A feature proposal to segregate access control regarding running pipelines from ability to push/merge was also created at <https://gitlab.com/gitlab-org/gitlab/-/issues/24585>.
+For more technical details on CI/CD setup and documentation on adding new test jobs to `package-and-test` pipeline, see
+[`package_and_test` setup documentation](package_and_test_pipeline.md).
+
#### With merged results pipelines
In a merged results pipeline, the pipeline runs on a new ref that contains the merge result of the source and target branch.
diff --git a/doc/development/testing_guide/end_to_end/package_and_test_pipeline.md b/doc/development/testing_guide/end_to_end/package_and_test_pipeline.md
new file mode 100644
index 00000000000..ac0e55ea2a4
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/package_and_test_pipeline.md
@@ -0,0 +1,134 @@
+---
+stage: none
+group: unassigned
+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
+---
+
+# e2e:package-and-test
+
+The `e2e:package-and-test` child pipeline is the main executor of E2E testing for the GitLab platform. The pipeline definition has several dynamic
+components to reduce the number of tests being executed in merge request pipelines.
+
+## Setup
+
+Pipeline setup consists of:
+
+- The `e2e-test-pipeline-generate` job in the `prepare` stage of the main GitLab pipeline.
+- The `e2e:package-and-test` job in the `qa` stage, which triggers the child pipeline that is responsible for building the `omnibus` package and
+ running E2E tests.
+
+### e2e-test-pipeline-generate
+
+This job consists of two components that implement selective test execution:
+
+- The [`detect_changes`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/tasks/ci.rake) Rake task determines which e2e specs should be executed
+ in a particular merge request pipeline. This task analyzes changes in a particular merge request and determines which specs must be executed.
+ Based on that, a `dry-run` of every [scenario](https://gitlab.com/gitlab-org/gitlab/-/tree/master/qa/qa/scenario/test) executes and determines if a
+ scenario contains any executable tests. Selective test execution uses [these criteria](index.md#selective-test-execution) to determine which specific
+ tests to execute.
+- [`generate-e2e-pipeline`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/generate-e2e-pipeline) is executed, which generates a child
+ pipeline YAML definition file with appropriate environment variables.
+
+### e2e:package-and-test
+
+E2E test execution pipeline consists of several stages which all support execution of E2E tests.
+
+#### .pre
+
+This stage is responsible for the following tasks:
+
+- Fetching `knapsack` reports that support [parallel test execution](index.md#run-tests-in-parallel).
+- Triggering downstream pipeline which builds the [`omnibus-gitlab`](https://gitlab.com/gitlab-org/omnibus-gitlab) Docker image.
+
+#### test
+
+This stage runs e2e tests against different types of GitLab configurations. The number of jobs executed is determined dynamically by
+[`e2e-test-pipeline-generate`](package_and_test_pipeline.md#e2e-test-pipeline-generate) job.
+
+#### report
+
+This stage is responsible for [allure test report](index.md#allure-report) generation.
+
+## Adding new jobs
+
+Selective test execution depends on a set of rules present in every job definition. A typical job contains the following attributes:
+
+```yaml
+ variables:
+ QA_SCENARIO: Test::Integration::MyNewJob
+ rules:
+ - !reference [.rules:test:qa, rules]
+ - if: $QA_SUITES =~ /Test::Integration::MyNewJob/
+ - !reference [.rules:test:manual, rules]
+```
+
+In this example:
+
+- `QA_SCENARIO: Test::Integration::MyNewJob`: name of the scenario class that is passed to the
+ [`gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md) executor.
+- `!reference [.rules:test:qa, rules]`: main rule definition that is matched for pipelines that should execute all tests. For example, when changes to
+ `qa` framework are present.
+- `if: $QA_SUITES =~ /Test::Integration::MyNewJob/`: main rule responsible for selective test execution. `QA_SUITE` is the name of the scenario
+ abstraction located in [`qa framework`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/qa/qa/scenario/test).
+
+ `QA_SUITE` is not the same as `QA_SCENARIO`, which is passed to the `gitlab-qa` executor. For consistency, it usually has the same name. `QA_SUITE`
+ abstraction class usually contains information on what tags to run and optionally some additional setup steps.
+- `!reference [.rules:test:manual, rules]`: final rule that is always matched and sets the job to `manual` so it can still be executed on demand,
+ even if not set to execute by selective test execution.
+
+Considering example above, perform the following steps to create a new job:
+
+1. Create new scenario type `my_new_job.rb` in the [`integration`](https://gitlab.com/gitlab-org/gitlab-qa/-/tree/master/lib/gitlab/qa/scenario/test/integration) directory
+ of the [`gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa) project and release new version so it's generally available.
+1. Create new scenario `my_new_job.rb` in [`integration`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/qa/qa/scenario/test/integration) directory of the
+ [`qa`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/qa) framework. In the most simple case, this scenario would define RSpec tags that should be executed:
+
+```ruby
+ module QA
+ module Scenario
+ module Test
+ module Integration
+ class MyNewJob < Test::Instance::All
+ tags :some_special_tag
+ end
+ end
+ end
+ end
+ end
+```
+
+1. Add new job definition in the [`main.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/package-and-test/main.gitlab-ci.yml) pipeline definition:
+
+```yaml
+ ee:my-new-job:
+ extends: .qa
+ variables:
+ QA_SCENARIO: Test::Integration::MyNewJob
+ rules:
+ - !reference [.rules:test:qa, rules]
+ - if: $QA_SUITES =~ /Test::Integration::MyNewJob/
+ - !reference [.rules:test:manual, rules]
+```
+
+### Parallel jobs
+
+For selective execution to work correctly with job types that require running multiple parallel jobs,
+a job definition typically must be split into parallel and non parallel variants. Splitting is necessary so that when selective execution
+executes only a single spec, multiple unnecessary jobs are not spawned. For example:
+
+```yaml
+ ee:my-new-job:
+ extends: .qa
+ variables:
+ QA_SCENARIO: Test::Integration::MyNewJob
+ rules:
+ - !reference [.rules:test:qa-non-parallel, rules]
+ - if: $QA_SUITES =~ /Test::Integration::MyNewJob/
+ ee:instance-parallel:
+ extends:
+ - .parallel
+ - ee:my-new-job
+ rules:
+ - !reference [.rules:test:qa-parallel, rules]
+ - if: $QA_SUITES =~ /Test::Integration::MyNewJob/
+```
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 5edf18725c0..332f9f8fbe5 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -21,18 +21,23 @@ For any of the following scenarios, the `start-review-app-pipeline` job would be
- for scheduled pipelines
- the MR has the `pipeline:run-review-app` label set
-## QA runs on review apps
+## E2E test runs on review apps
-On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) in the `qa` stage (which comes after the
-`review` stage), the `review-qa-smoke` and `review-qa-reliable` jobs are automatically started. The `review-qa-smoke` runs
-the QA smoke suite and the `review-qa-reliable` executes E2E tests identified as [reliable](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/reliable-tests/).
+On every pipeline in the `qa` stage (which comes after the `review` stage), the `review-qa-smoke` and `review-qa-blocking` jobs are automatically started.
-`review-qa-*` jobs ensure that end-to-end tests for the changes in the merge request pass in a live environment. This shifts the identification of e2e failures from an environment on the path to production to the merge request, to prevent breaking features on GitLab.com or costly GitLab.com deployment blockers. `review-qa-*` failures should be investigated with counterpart SET involvement if needed to help determine the root cause of the error.
+`qa` stage consists of following jobs:
-You can also manually start the `review-qa-all`: it runs the full QA suite.
+- `review-qa-smoke`: small and fast subset of tests to validate core functionality of GitLab.
+- `review-qa-blocking`: subset of tests identified as [reliable](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/reliable-tests/). These tests are
+ considered stable and are not allowed to fail.
+- `review-qa-non-blocking`: rest of the e2e tests that can be triggered manually.
+
+`review-qa-*` jobs ensure that end-to-end tests for the changes in the merge request pass in a live environment. This shifts the identification of e2e failures from an environment
+on the path to production to the merge request to prevent breaking features on GitLab.com or costly GitLab.com deployment blockers. If needed, `review-qa-*` failures should be
+investigated with an SET (software engineer in test) counterpart to help determine the root cause of the error.
After the end-to-end test runs have finished, [Allure reports](https://github.com/allure-framework/allure2) are generated and published by
-the `allure-report-qa-smoke`, `allure-report-qa-reliable`, and `allure-report-qa-all` jobs. A comment with links to the reports are added to the merge request.
+the `e2e-test-report` job. A comment with links to the reports is added to the merge request.
Errors can be found in the `gitlab-review-apps` Sentry project and [filterable by review app URL](https://sentry.gitlab.net/gitlab/gitlab-review-apps/?query=url%3A%22https%3A%2F%2Fgitlab-review-require-ve-u92nn2.gitlab-review.app%2F%22) or [commit SHA](https://sentry.gitlab.net/gitlab/gitlab-review-apps/releases/6095b501da7/all-events/).
diff --git a/doc/security/responding_to_security_incidents.md b/doc/security/responding_to_security_incidents.md
index fb35c389583..5c00c53c5bf 100644
--- a/doc/security/responding_to_security_incidents.md
+++ b/doc/security/responding_to_security_incidents.md
@@ -26,7 +26,7 @@ If you suspect that a user account or bot account has been compromised, consider
- Addition or modification of runners.
- Addition or modification of webhooks or Git hooks.
- Reset any credentials the user might have had access to. For example, users with at least the Maintainer role can view protected
- [CI/CD variables](../ci/variables/index.md) and [runner registration tokens](token_overview.md#runner-registration-tokens).
+ [CI/CD variables](../ci/variables/index.md) and [runner registration tokens](token_overview.md#runner-registration-tokens-deprecated).
- [Reset the user's password](reset_user_password.md).
- Get the user to [enable two factor authentication](../user/profile/account/two_factor_authentication.md) (2FA), and consider [enforcing 2FA at the instance or group level](two_factor_authentication.md)
- After completing an investigation and mitigating impacts, unblock the user.
diff --git a/doc/security/token_overview.md b/doc/security/token_overview.md
index fcb47b4c738..470fdba3aef 100644
--- a/doc/security/token_overview.md
+++ b/doc/security/token_overview.md
@@ -74,7 +74,14 @@ This is useful, for example, for cloning repositories to your Continuous Integra
Project maintainers and owners can add or enable a deploy key for a project repository
-## Runner registration tokens
+## Runner registration tokens (deprecated)
+
+WARNING:
+The ability to pass a runner registration token was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/380872) in GitLab 15.6 and is
+planned for removal in 17.0, along with support for certain configuration arguments. This change is a breaking change. GitLab plans to introduce a new
+[GitLab Runner token architecture](../architecture/blueprints/runner_tokens/index.md), which introduces
+a new method for registering runners and eliminates the
+runner registration token.
Runner registration tokens are used to [register](https://docs.gitlab.com/runner/register/) a [runner](https://docs.gitlab.com/runner/) with GitLab. Group or project owners or instance administrators can obtain them through the GitLab user interface. The registration token is limited to runner registration and has no further scope.
diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md
index a14dbb1c00a..f080d5e4661 100644
--- a/doc/user/application_security/policies/scan-execution-policies.md
+++ b/doc/user/application_security/policies/scan-execution-policies.md
@@ -22,10 +22,9 @@ For details on the similarities and differences between these features, see
[Enforce scan execution](../index.md#enforce-scan-execution).
NOTE:
-Policy jobs are created in the `test` stage of the pipeline. If you modify the default pipeline
+Policy jobs for scans other than DAST scans are created in the `test` stage of the pipeline. If you modify the default pipeline
[`stages`](../../../ci/yaml/index.md#stages),
-you must ensure that the `test` stage exists in the list. Otherwise, the pipeline fails to run and
-an error appears that states `chosen stage does not exist`.
+to remove the `test` stage, jobs will run in the `scan-policies` stage instead. This stage is injected into the CI pipeline at evaluation time if it doesn't exist. If the `build` stage exists, it is injected just after the `build` stage. If the `build` stage does not exist, it is injected at the beginning of the pipeline. DAST scans always run in the `dast` stage. If this stage does not exist, then a `dast` stage is injected at the end of the pipeline.
## Scan execution policy editor
diff --git a/doc/user/infrastructure/iac/terraform_template_recipes.md b/doc/user/infrastructure/iac/terraform_template_recipes.md
index 0d1b56ae979..894c53c0f46 100644
--- a/doc/user/infrastructure/iac/terraform_template_recipes.md
+++ b/doc/user/infrastructure/iac/terraform_template_recipes.md
@@ -182,6 +182,19 @@ deploy:
For an example repository, see the [GitLab Terraform template usage project](https://gitlab.com/gitlab-org/configure/examples/terraform-template-usage).
+## Automatically deploy from the default branch
+
+You can automatically deploy from the default branch by setting the `TF_AUTO_DEPLOY` variable
+to `"true"`. All other values are interpreted as `"false"`.
+
+```yaml
+variables:
+ TF_AUTO_DEPLOY: "true"
+
+include:
+ - template: Terraform.latest.gitlab-ci.yml
+```
+
## Deploy Terraform to multiple environments
You can run pipelines in multiple environments, each with a unique Terraform state.
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index fae6e210a6c..f6c5ef20c82 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -37,19 +37,71 @@ To fork an existing project in GitLab:
GitLab creates your fork, and redirects you to the new fork's page.
-## Repository mirroring
+## Update your fork
-You can use [repository mirroring](mirror/index.md) to keep your fork synced with the original repository. You can also use `git remote add upstream` to achieve the same result.
+To copy the latest changes from the upstream repository into your fork, update it
+from the command line. GitLab Premium and higher tiers can also
+[configure forks as pull mirrors](mirror/pull.md#configure-pull-mirroring)
+of the upstream repository.
-The main difference is that with repository mirroring, your remote fork is automatically kept up-to-date.
+### From the command line
-Without mirroring, to work locally you must use `git pull` to update your local repository
-with the upstream project, then push the changes back to your fork to update it.
+To update your fork from the command line, first ensure that you have configured
+an `upstream` remote repository for your fork:
-WARNING:
-With mirroring, before approving a merge request, you are asked to sync. We recommend you automate it.
+1. Clone your fork locally, if you have not already done so. For more information, see
+ [Clone a repository](../../../gitlab-basics/start-using-git.md#clone-a-repository).
+1. View the remotes configured for your fork with `git remote -v`.
+1. If your fork does not have an `upstream` remote pointing to the original repository,
+ use one of these examples to configure an `upstream` remote:
+
+ ```shell
+ # Use this line to set any repository as your upstream after editing <upstream_url>
+ git remote add upstream <upstream_url>
+
+ # Use this line to set the main GitLab repository as your upstream
+ git remote add upstream https://gitlab.com/gitlab-org/gitlab.git
+ ```
+
+ After ensuring your fork has an `upstream` remote configured, you are ready to update your fork.
+
+1. In your local copy, ensure you have checked out the [default branch](branches/default.md),
+ replacing `main` with the name of your default branch:
+
+ ```shell
+ git checkout main
+ ```
+
+ If Git identifies unstaged changes, commit or stash them before continuing.
+1. Fetch the changes to the upstream repository with `git fetch upstream`.
+1. Pull the changes into your fork, replacing `main` with the name of the branch
+ you are updating:
+
+ ```shell
+ git pull upstream main
+ ```
+
+1. Push the changes to your fork repository on the server (GitLab.com or self-managed).
-Read more about [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/).
+ ```shell
+ git push origin main
+ ```
+
+### With repository mirroring **(PREMIUM)**
+
+A fork can be configured as a mirror of the upstream if all these conditions are met:
+
+1. Your subscription is **(PREMIUM)** or a higher tier.
+1. You create all changes in branches (not `main`).
+1. You do not work on [merge requests for confidential issues](../merge_requests/confidential.md),
+ which requires changes to `main`.
+
+[Repository mirroring](mirror/index.md) keeps your fork synced with the original repository.
+This method updates your fork once per hour, with no manual `git pull` required.
+For instructions, read [Configure pull mirroring](mirror/pull.md#configure-pull-mirroring).
+
+WARNING:
+With mirroring, before approving a merge request, you are asked to sync. You should automate it.
## Merging upstream
@@ -69,3 +121,8 @@ changes are added to the repository and branch you're merging into.
## Removing a fork relationship
You can unlink your fork from its upstream project in the [advanced settings](../settings/index.md#remove-a-fork-relationship).
+
+## Related topics
+
+- GitLab blog post: [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/).
+- GitLab community forum: [Refreshing a fork](https://forum.gitlab.com/t/refreshing-a-fork/).
diff --git a/doc/user/project/repository/mirror/pull.md b/doc/user/project/repository/mirror/pull.md
index 1923d8e2ae7..e2e86bfbde0 100644
--- a/doc/user/project/repository/mirror/pull.md
+++ b/doc/user/project/repository/mirror/pull.md
@@ -65,7 +65,12 @@ Prerequisite:
1. On the left sidebar, select **Settings > Repository**.
1. Expand **Mirroring repositories**.
1. Enter the **Git repository URL**. Include the username
- in the URL, if required: `https://MYUSERNAME@github.com/GROUPNAME/PROJECTNAME.git`
+ in the URL, if required: `https://MYUSERNAME@gitlab.com/GROUPNAME/PROJECTNAME.git`
+
+ NOTE:
+ To mirror the `gitlab` repository, use `git@gitlab.com:gitlab-org/gitlab.git`
+ or `https://gitlab.com/gitlab-org/gitlab.git`.
+
1. In **Mirror direction**, select **Pull**.
1. In **Authentication method**, select your authentication method. To learn more, read
[Authentication methods for mirrors](index.md#authentication-methods-for-mirrors).
diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
index 9c967d48de1..bc23a7c2a95 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -69,6 +69,7 @@ cache:
- gitlab-terraform apply
resource_group: ${TF_STATE_NAME}
rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $TF_AUTO_DEPLOY == "true"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
diff --git a/lib/gitlab/github_import/importer/pull_request_review_importer.rb b/lib/gitlab/github_import/importer/pull_request_review_importer.rb
index de66f310edf..b1e259fe940 100644
--- a/lib/gitlab/github_import/importer/pull_request_review_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_review_importer.rb
@@ -113,6 +113,9 @@ module Gitlab
state: ::MergeRequestReviewer.states['reviewed'],
created_at: submitted_at
)
+ rescue ActiveRecord::RecordNotUnique
+ # multiple reviews from single person could make a SQL concurrency issue here
+ nil
end
# rubocop:disable CodeReuse/ActiveRecord
diff --git a/qa/qa/scenario/test/integration/gitaly_cluster.rb b/qa/qa/scenario/test/integration/gitaly_cluster.rb
new file mode 100644
index 00000000000..66b7485a8dc
--- /dev/null
+++ b/qa/qa/scenario/test/integration/gitaly_cluster.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class GitalyCluster < Test::Instance::All
+ tags :gitaly_cluster
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/instance/integrations.rb b/qa/qa/scenario/test/integration/integrations.rb
index b943b95c51e..0797f5d36c7 100644
--- a/qa/qa/scenario/test/instance/integrations.rb
+++ b/qa/qa/scenario/test/integration/integrations.rb
@@ -3,8 +3,8 @@
module QA
module Scenario
module Test
- module Instance
- class Integrations < All
+ module Integration
+ class Integrations < Test::Instance::All
tags :integrations
end
end
diff --git a/qa/qa/scenario/test/instance/jira.rb b/qa/qa/scenario/test/integration/jira.rb
index 784a71515cb..dda804f0fad 100644
--- a/qa/qa/scenario/test/instance/jira.rb
+++ b/qa/qa/scenario/test/integration/jira.rb
@@ -3,8 +3,8 @@
module QA
module Scenario
module Test
- module Instance
- class Jira < All
+ module Integration
+ class Jira < Test::Instance::All
tags :jira
end
end
diff --git a/qa/qa/scenario/test/integration/mtls.rb b/qa/qa/scenario/test/integration/mtls.rb
new file mode 100644
index 00000000000..6c4a035b0f2
--- /dev/null
+++ b/qa/qa/scenario/test/integration/mtls.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class Mtls < Test::Instance::All
+ tags :mtls
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/versioned_migration_class.rb b/rubocop/cop/migration/versioned_migration_class.rb
index 572ddcd1b12..27f1c0e16a1 100644
--- a/rubocop/cop/migration/versioned_migration_class.rb
+++ b/rubocop/cop/migration/versioned_migration_class.rb
@@ -8,13 +8,18 @@ module RuboCop
class VersionedMigrationClass < RuboCop::Cop::Base
include MigrationHelpers
- ENFORCED_SINCE = 2021_09_02_00_00_00
- CURRENT_DATABASE_MIGRATION_CLASS = 'Gitlab::Database::Migration[2.1]'
+ ENFORCED_SINCE = 2023_01_12_00_00_00
+ DOC_LINK = "https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning"
- MSG_INHERIT = 'Don\'t inherit from ActiveRecord::Migration but use Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.'
- MSG_INCLUDE = 'Don\'t include migration helper modules directly. Inherit from Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.'
+ MSG_INHERIT = "Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. " \
+ "Use Gitlab::Database::Migration[2.1] instead. See #{DOC_LINK}."
+ MSG_INCLUDE = "Don't include migration helper modules directly. " \
+ "Inherit from Gitlab::Database::Migration[2.1] instead. See #{DOC_LINK}."
+
+ GITLAB_MIGRATION_CLASS = 'Gitlab::Database::Migration'
ACTIVERECORD_MIGRATION_CLASS = 'ActiveRecord::Migration'
+ CURRENT_MIGRATION_VERSION = 2.1 # Should be the same value as Gitlab::Database::Migration.current_version
def_node_search :includes_helpers?, <<~PATTERN
(send nil? :include
@@ -25,7 +30,7 @@ module RuboCop
def on_class(node)
return unless relevant_migration?(node)
- return unless activerecord_migration_class?(node)
+ return unless activerecord_migration_class?(node) || old_version_migration_class?(node)
add_offense(node, message: MSG_INHERIT)
end
@@ -51,6 +56,15 @@ module RuboCop
others.find { |node| node.const_type? && node&.const_name != 'Types' }&.const_name
end
+
+ # Returns true for any parent class of format Gitlab::Database::Migration[version] if version < current_version
+ def old_version_migration_class?(class_node)
+ parent_class_node = class_node.parent_class
+ return false unless parent_class_node.send_type? && parent_class_node.arguments.last.float_type?
+ return false unless parent_class_node.children[0].const_name == GITLAB_MIGRATION_CLASS
+
+ parent_class_node.arguments[0].value < CURRENT_MIGRATION_VERSION
+ end
end
end
end
diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb
index a5449c6dccd..b7a4a302290 100644
--- a/spec/db/migration_spec.rb
+++ b/spec/db/migration_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Migrations Validation', feature_category: :database do
let(:all_migration_classes) do
{
2022_12_01_02_15_00.. => Gitlab::Database::Migration[2.1],
- 2022_01_26_21_06_58.. => Gitlab::Database::Migration[2.0],
+ 2022_01_26_21_06_58..2023_01_11_12_45_12 => Gitlab::Database::Migration[2.0],
2021_09_01_15_33_24..2022_04_25_12_06_03 => Gitlab::Database::Migration[1.0],
2021_05_31_05_39_16..2021_09_01_15_33_24 => ActiveRecord::Migration[6.1],
..2021_05_31_05_39_16 => ActiveRecord::Migration[6.0]
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 1e5bb828dbf..3e07523eed6 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -112,7 +112,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
@@ -466,7 +465,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
@@ -482,7 +480,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: null,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 334117e0e3c..17d6cea23df 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,6 +1,6 @@
import * as Sentry from '@sentry/browser';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { hideFlash, FLASH_CLOSED_EVENT, createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
jest.mock('@sentry/browser');
@@ -8,65 +8,6 @@ describe('Flash', () => {
const findTextContent = (containerSelector = '.flash-container') =>
document.querySelector(containerSelector).textContent.replace(/\s+/g, ' ').trim();
- describe('hideFlash', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.className = 'js-testing';
- });
-
- it('sets transition style', () => {
- hideFlash(el);
-
- expect(el.style.transition).toBe('opacity 0.15s');
- });
-
- it('sets opacity style', () => {
- hideFlash(el);
-
- expect(el.style.opacity).toBe('0');
- });
-
- it('does not set styles when fadeTransition is false', () => {
- hideFlash(el, false);
-
- expect(el.style.opacity).toBe('');
- expect(el.style.transition).toHaveLength(0);
- });
-
- it('removes element after transitionend', () => {
- document.body.appendChild(el);
-
- hideFlash(el);
- el.dispatchEvent(new Event('transitionend'));
-
- expect(document.querySelector('.js-testing')).toBeNull();
- });
-
- it('calls event listener callback once', () => {
- jest.spyOn(el, 'remove');
- document.body.appendChild(el);
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.remove.mock.calls.length).toBe(1);
- });
-
- it(`dispatches ${FLASH_CLOSED_EVENT} event after transitionend event`, () => {
- jest.spyOn(el, 'dispatchEvent');
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.dispatchEvent).toHaveBeenCalledWith(new Event(FLASH_CLOSED_EVENT));
- });
- });
-
describe('createAlert', () => {
const mockMessage = 'a message';
let alert;
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index e0b6c7119f9..190114606ec 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -60,7 +60,6 @@ describe('LazyLoader', () => {
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(execImmediately);
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(LazyLoader, 'loadImage');
mockLoadEvent();
});
@@ -106,7 +105,6 @@ describe('LazyLoader', () => {
trigger(img);
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(TEST_PATH);
expect(img.dataset.src).toBeUndefined();
expect(img).toHaveClass('js-lazy-loaded');
@@ -121,7 +119,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(TEST_PATH);
expect(newImg).toHaveClass('js-lazy-loaded');
});
@@ -131,7 +128,6 @@ describe('LazyLoader', () => {
lazyLoader.register();
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
});
@@ -143,7 +139,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
});
@@ -158,7 +153,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(TEST_PATH);
expect(newImg).toHaveClass('js-lazy-loaded');
});
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index ce1c126f868..99f61a31dbd 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -3,7 +3,6 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import TermsApp from '~/terms/components/app.vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
@@ -129,7 +128,6 @@ describe('TermsApp', () => {
beforeEach(() => {
flashEl = document.createElement('div');
- flashEl.classList.add(`flash-${FLASH_TYPES.ALERT}`);
document.body.appendChild(flashEl);
});
@@ -137,7 +135,7 @@ describe('TermsApp', () => {
document.body.innerHTML = '';
});
- it('recalculates height of scrollable viewport', () => {
+ it('recalculates height of scrollable viewport', async () => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
@@ -148,7 +146,8 @@ describe('TermsApp', () => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 700);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
- flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
+ flashEl.remove();
+ await nextTick();
expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 100px);');
});
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
new file mode 100644
index 00000000000..b66ce3544c2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -0,0 +1,253 @@
+import { nextTick } from 'vue';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import { QUERY_TOO_SHORT_MESSAGE } from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('EntitySelect', () => {
+ let wrapper;
+ let fetchItemsMock;
+ let fetchInitialSelectionTextMock;
+
+ // Mocks
+ const itemMock = {
+ text: 'selectedGroup',
+ value: '1',
+ };
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const headerText = 'headerText';
+ const defaultToggleText = 'defaultToggleText';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {}, slots = {} } = {}) => {
+ wrapper = shallowMountExtended(EntitySelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ headerText,
+ defaultToggleText,
+ fetchItems: fetchItemsMock,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ slots,
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+ const search = (searchString) => findListbox().vm.$emit('search', searchString);
+ const selectGroup = async () => {
+ openListbox();
+ await nextTick();
+ findListbox().vm.$emit('select', itemMock.value);
+ return nextTick();
+ };
+
+ beforeEach(() => {
+ fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
+ });
+
+ describe('on mount', () => {
+ it('calls the fetch function when the listbox is opened', async () => {
+ createComponent();
+ openListbox();
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("fetches the initially selected value's name", async () => {
+ fetchInitialSelectionTextMock = jest.fn().mockImplementation(() => itemMock.text);
+ createComponent({
+ props: {
+ fetchInitialSelectionText: fetchInitialSelectionTextMock,
+ initialSelection: itemMock.value,
+ },
+ });
+ await nextTick();
+
+ expect(fetchInitialSelectionTextMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+ });
+
+ it("renders the error slot's content", () => {
+ const selector = 'data-test-id="error-element"';
+ createComponent({
+ slots: {
+ error: `<div ${selector} />`,
+ },
+ });
+
+ expect(wrapper.find(`[${selector}]`).exists()).toBe(true);
+ });
+
+ describe('selection', () => {
+ it('uses the default toggle text while no group is selected', () => {
+ createComponent();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+
+ describe('once a group is selected', () => {
+ it(`uses the selected group's name as the toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+
+ it(`uses the selected group's ID as the listbox' and input value`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().attributes('selected')).toBe(itemMock.value);
+ expect(findInput().attributes('value')).toBe(itemMock.value);
+ });
+
+ it(`on reset, falls back to the default toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+ });
+ });
+
+ describe('search', () => {
+ it('sets `searching` to `true` when first opening the dropdown', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ openListbox();
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('sets `searching` to `true` while searching', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ search('foo');
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('fetches groups matching the search string', async () => {
+ const searchString = 'searchString';
+ createComponent();
+ openListbox();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+
+ fetchItemsMock.mockImplementation(() => ({ items: [], totalPages: 1 }));
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows a notice if the search query is too short', async () => {
+ const searchString = 'a';
+ createComponent();
+ openListbox();
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
+ });
+ });
+
+ describe('pagination', () => {
+ const searchString = 'searchString';
+
+ beforeEach(async () => {
+ let requestCount = 0;
+ fetchItemsMock.mockImplementation((searchQuery, page) => {
+ requestCount += 1;
+ return {
+ items: [
+ {
+ text: `Group [page: ${page} - search: ${searchQuery}]`,
+ value: `id:${requestCount}`,
+ },
+ ],
+ totalPages: 3,
+ };
+ });
+ createComponent();
+ openListbox();
+ findListbox().vm.$emit('bottom-reached');
+ return nextTick();
+ });
+
+ it('fetches the next page when bottom is reached', () => {
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith('', 2);
+ });
+
+ it('fetches the first page when the search query changes', async () => {
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(3);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 1);
+ });
+
+ it('retains the search query when infinite scrolling', async () => {
+ search(searchString);
+ await nextTick();
+ findListbox().vm.$emit('bottom-reached');
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(4);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 2);
+ });
+
+ it('pauses infinite scroll after fetching the last page', async () => {
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+ });
+
+ it('resumes infinite scroll when search query changes', async () => {
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+
+ search(searchString);
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
new file mode 100644
index 00000000000..bcf3aee5a4f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
@@ -0,0 +1,134 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import GroupSelect from '~/vue_shared/components/entity_select/group_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ TOGGLE_TEXT,
+ HEADER_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${TOGGLE_TEXT}
+ ${'headerText'} | ${HEADER_TEXT}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches groups when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ });
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(groupEndpoint).reply(200, groupMock);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it('show an error if fetching the individual group fails', async () => {
+ mock
+ .onGet('/api/undefined/groups.json')
+ .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
+ mock.onGet(groupEndpoint).reply(500);
+ createComponent({ props: { initialSelection: groupMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(500);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/group_select/utils_spec.js b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
index 5188e1aabf1..9aa1baf204e 100644
--- a/spec/frontend/vue_shared/components/group_select/utils_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
@@ -1,6 +1,6 @@
-import { groupsPath } from '~/vue_shared/components/group_select/utils';
+import { groupsPath } from '~/vue_shared/components/entity_select/utils';
-describe('group_select utils', () => {
+describe('entity_select utils', () => {
describe('groupsPath', () => {
it.each`
groupsFilter | parentGroupID | expectedPath
diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
deleted file mode 100644
index 87dd7795b98..00000000000
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ /dev/null
@@ -1,322 +0,0 @@
-import { nextTick } from 'vue';
-import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import axios from '~/lib/utils/axios_utils';
-import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
-import {
- TOGGLE_TEXT,
- RESET_LABEL,
- FETCH_GROUPS_ERROR,
- FETCH_GROUP_ERROR,
- QUERY_TOO_SHORT_MESSAGE,
-} from '~/vue_shared/components/group_select/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-
-describe('GroupSelect', () => {
- let wrapper;
- let mock;
-
- // Mocks
- const groupMock = {
- full_name: 'selectedGroup',
- id: '1',
- };
- const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
-
- // Stubs
- const GlAlert = {
- template: '<div><slot /></div>',
- };
-
- // Props
- const label = 'label';
- const inputName = 'inputName';
- const inputId = 'inputId';
-
- // Finders
- const findFormGroup = () => wrapper.findComponent(GlFormGroup);
- const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- const findInput = () => wrapper.findByTestId('input');
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- // Helpers
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(GroupSelect, {
- propsData: {
- label,
- inputName,
- inputId,
- ...props,
- },
- stubs: {
- GlAlert,
- },
- });
- };
- const openListbox = () => findListbox().vm.$emit('shown');
- const search = (searchString) => findListbox().vm.$emit('search', searchString);
- const createComponentWithGroups = () => {
- mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]);
- createComponent();
- openListbox();
- return waitForPromises();
- };
- const selectGroup = () => {
- findListbox().vm.$emit('select', groupMock.id);
- return nextTick();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('passes the label to GlFormGroup', () => {
- createComponent();
-
- expect(findFormGroup().attributes('label')).toBe(label);
- });
-
- describe('on mount', () => {
- it('fetches groups when the listbox is opened', async () => {
- createComponent();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(0);
-
- openListbox();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- });
-
- describe('with an initial selection', () => {
- it('if the selected group is not part of the fetched list, fetches it individually', async () => {
- mock.onGet(groupEndpoint).reply(200, groupMock);
- createComponent({ props: { initialSelection: groupMock.id } });
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it('show an error if fetching the individual group fails', async () => {
- mock
- .onGet('/api/undefined/groups.json')
- .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
- mock.onGet(groupEndpoint).reply(500);
- createComponent({ props: { initialSelection: groupMock.id } });
-
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
- });
- });
- });
-
- it('shows an error when fetching groups fails', async () => {
- mock.onGet('/api/undefined/groups.json').reply(500);
- createComponent();
- openListbox();
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
- });
-
- describe('selection', () => {
- it('uses the default toggle text while no group is selected', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
-
- describe('once a group is selected', () => {
- it(`uses the selected group's name as the toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it(`uses the selected group's ID as the listbox' and input value`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().attributes('selected')).toBe(groupMock.id);
- expect(findInput().attributes('value')).toBe(groupMock.id);
- });
-
- it(`on reset, falls back to the default toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- findListbox().vm.$emit('reset');
- await nextTick();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
- });
- });
-
- describe('search', () => {
- it('sets `searching` to `true` when first opening the dropdown', async () => {
- createComponent();
-
- expect(findListbox().props('searching')).toBe(false);
-
- openListbox();
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('sets `searching` to `true` while searching', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('searching')).toBe(false);
-
- search('foo');
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('fetches groups matching the search string', async () => {
- const searchString = 'searchString';
- await createComponentWithGroups();
-
- expect(mock.history.get).toHaveLength(1);
-
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({
- page: 1,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('shows a notice if the search query is too short', async () => {
- const searchString = 'a';
- await createComponentWithGroups();
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
- });
- });
-
- describe('pagination', () => {
- const searchString = 'searchString';
-
- beforeEach(async () => {
- let requestCount = 0;
- mock.onGet('/api/undefined/groups.json').reply(({ params }) => {
- requestCount += 1;
- return [
- 200,
- [
- {
- full_name: `Group [page: ${params.page} - search: ${params.search}]`,
- id: requestCount,
- },
- ],
- {
- page: params.page,
- 'x-total-pages': 3,
- },
- ];
- });
- createComponent();
- openListbox();
- findListbox().vm.$emit('bottom-reached');
- return waitForPromises();
- });
-
- it('fetches the next page when bottom is reached', async () => {
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({
- page: 2,
- per_page: 20,
- search: '',
- });
- });
-
- it('fetches the first page when the search query changes', async () => {
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(3);
- expect(mock.history.get[2].params).toStrictEqual({
- page: 1,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('retains the search query when infinite scrolling', async () => {
- search(searchString);
- await waitForPromises();
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(4);
- expect(mock.history.get[3].params).toStrictEqual({
- page: 2,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('pauses infinite scroll after fetching the last page', async () => {
- expect(findListbox().props('infiniteScroll')).toBe(true);
-
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(false);
- });
-
- it('resumes infinite scroll when search query changes', async () => {
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(false);
-
- search(searchString);
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(true);
- });
- });
-
- it.each`
- description | clearable | expectedLabel
- ${'passes'} | ${true} | ${RESET_LABEL}
- ${'does not pass'} | ${false} | ${''}
- `(
- '$description the reset button label to the listbox when clearable is $clearable',
- ({ clearable, expectedLabel }) => {
- createComponent({
- props: {
- clearable,
- },
- });
-
- expect(findListbox().props('resetButtonLabel')).toBe(expectedLabel);
- },
- );
-});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 0ccfc9aca34..b3b64c4fd40 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -700,7 +700,7 @@ describe('WorkItemDetail component', () => {
});
it('opens the modal with the child when `show-modal` is emitted', async () => {
- createComponent({ handler });
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
const event = {
@@ -721,6 +721,7 @@ describe('WorkItemDetail component', () => {
createComponent({
isModal: true,
handler,
+ workItemsMvc2Enabled: true,
});
await waitForPromises();
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
index 2e1a3c496cc..3e62e8f473c 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter,
+ :clean_gitlab_redis_cache, feature_category: :importers do
using RSpec::Parameterized::TableSyntax
let_it_be(:merge_request) { create(:merge_request) }
@@ -39,6 +40,19 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
expect(merge_request.reviewers).to contain_exactly(author)
end
end
+
+ context 'when because of concurrency an attempt of duplication appeared' do
+ before do
+ allow(MergeRequestReviewer)
+ .to receive(:create!).and_raise(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'does not change Merge Request reviewers', :aggregate_failures do
+ expect { subject.execute }.not_to change(MergeRequestReviewer, :count)
+
+ expect(merge_request.reviewers).to contain_exactly(author)
+ end
+ end
end
shared_examples 'imports an approval for the Merge Request' do
diff --git a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
index 506e3146afa..332b02078f4 100644
--- a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
+++ b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/versioned_migration_class'
-RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass do
+RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass, feature_category: :database do
let(:migration) do
<<~SOURCE
class TestMigration < Gitlab::Database::Migration[2.1]
@@ -49,7 +49,15 @@ RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass do
it 'adds an offence if inheriting from ActiveRecord::Migration' do
expect_offense(<<~RUBY)
class MyMigration < ActiveRecord::Migration[6.1]
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration but use Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. Use Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
+ end
+ RUBY
+ end
+
+ it 'adds an offence if inheriting from old version of Gitlab::Database::Migration' do
+ expect_offense(<<~RUBY)
+ class MyMigration < Gitlab::Database::Migration[2.0]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. Use Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
end
RUBY
end