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/rails.gitlab-ci.yml12
-rw-r--r--.rubocop_todo.yml2
-rw-r--r--.rubocop_todo/cop/user_admin.yml4
-rw-r--r--.rubocop_todo/gitlab/namespaced_class.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/code_navigation/components/doc_line.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue18
-rw-r--r--app/assets/javascripts/jobs/bridge/app.vue20
-rw-r--r--app/assets/javascripts/jobs/bridge/components/constants.js1
-rw-r--r--app/assets/javascripts/jobs/bridge/components/empty_state.vue45
-rw-r--r--app/assets/javascripts/jobs/bridge/components/sidebar.vue98
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/jobs/index.js39
-rw-r--r--app/assets/javascripts/milestones/milestone.js40
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue24
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js6
-rw-r--r--app/assets/javascripts/runner/components/runner_status_badge.vue2
-rw-r--r--app/assets/javascripts/runner/constants.js1
-rw-r--r--app/assets/javascripts/tabs/constants.js20
-rw-r--r--app/assets/javascripts/tabs/index.js239
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss35
-rw-r--r--app/controllers/concerns/dependency_proxy/group_access.rb4
-rw-r--r--app/controllers/groups/dependency_proxies_controller.rb19
-rw-r--r--app/controllers/projects/jobs_controller.rb4
-rw-r--r--app/controllers/user_callouts_controller.rb29
-rw-r--r--app/controllers/users/callouts_controller.rb31
-rw-r--r--app/controllers/users/group_callouts_controller.rb2
-rw-r--r--app/graphql/mutations/user_callouts/create.rb2
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb8
-rw-r--r--app/graphql/types/user_callout_feature_name_enum.rb2
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/ci/jobs_helper.rb7
-rw-r--r--app/helpers/ci/runners_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/tab_helper.rb5
-rw-r--r--app/helpers/user_callouts_helper.rb98
-rw-r--r--app/helpers/users/callouts_helper.rb76
-rw-r--r--app/helpers/users/group_callouts_helper.rb32
-rw-r--r--app/models/ci/runner.rb7
-rw-r--r--app/models/concerns/calloutable.rb15
-rw-r--r--app/models/user.rb4
-rw-r--r--app/models/user_callout.rb48
-rw-r--r--app/models/users/callout.rb52
-rw-r--r--app/models/users/calloutable.rb17
-rw-r--r--app/models/users/group_callout.rb2
-rw-r--r--app/presenters/blob_presenter.rb40
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/services/ci/retry_build_service.rb36
-rw-r--r--app/services/merge_requests/outdated_discussion_diff_lines_service.rb20
-rw-r--r--app/services/users/dismiss_callout_service.rb (renamed from app/services/users/dismiss_user_callout_service.rb)2
-rw-r--r--app/services/users/dismiss_group_callout_service.rb2
-rw-r--r--app/views/admin/dashboard/_security_newsletter_callout.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/devise/shared/_tab_single.html.haml5
-rw-r--r--app/views/groups/registry/repositories/index.html.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/layouts/header/_registration_enabled_callout.html.haml2
-rw-r--r--app/views/projects/feature_flags/new.html.haml4
-rw-r--r--app/views/projects/jobs/show.html.haml5
-rw-r--r--app/views/projects/registry/repositories/index.html.haml4
-rw-r--r--app/views/root/index.html.haml4
-rw-r--r--app/views/shared/_flash_user_callout.html.haml2
-rw-r--r--app/views/shared/_two_factor_auth_recovery_settings_check.html.haml2
-rw-r--r--app/views/shared/milestones/_tabs.html.haml30
-rw-r--r--config/feature_flags/development/ci_retry_downstream_pipeline.yml8
-rw-r--r--config/feature_flags/development/use_cmark_renderer.yml2
-rw-r--r--config/feature_flags/development/use_optimized_group_labels_query.yml2
-rw-r--r--config/initializers/active_record_database_tasks.rb7
-rw-r--r--config/initializers/database_config.rb8
-rw-r--r--config/initializers/validate_database_config.rb4
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/user.rb1
-rw-r--r--data/deprecations/14-5-runner-api-status-does-contain-paused.yml9
-rw-r--r--data/deprecations/14-6-runner-api-status-renames-not_connected.yml13
-rw-r--r--db/migrate/20211201143042_create_lfs_object_states.rb32
-rw-r--r--db/schema_migrations/202112011430421
-rw-r--r--db/structure.sql39
-rw-r--r--doc/administration/geo/replication/datatypes.md4
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md17
-rw-r--r--doc/api/geo_nodes.md41
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/update/deprecations.md18
-rw-r--r--lib/banzai/filter/footnote_filter.rb14
-rw-r--r--lib/banzai/filter/markdown_engines/common_mark.rb8
-rw-r--r--lib/banzai/filter/markdown_post_escape_filter.rb2
-rw-r--r--lib/banzai/filter/plantuml_filter.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb4
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb4
-rw-r--r--lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb2
-rw-r--r--lib/gitlab/ci/status/bridge/common.rb6
-rw-r--r--lib/gitlab/database.rb4
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--lib/gitlab/diff/custom_diff.rb58
-rw-r--r--lib/gitlab/diff/file.rb33
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/git/blob.rb3
-rw-r--r--lib/gitlab/github_import/importer/diff_note_importer.rb4
-rw-r--r--lib/gitlab/github_import/importer/note_importer.rb1
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb21
-rw-r--r--lib/gitlab/github_import/representation/note.rb8
-rw-r--r--lib/gitlab/metrics/samplers/database_sampler.rb23
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb32
-rw-r--r--lib/gitlab/patch/legacy_database_config.rb44
-rw-r--r--lib/sidebars/groups/menus/packages_registries_menu.rb18
-rw-r--r--lib/sidebars/projects/menus/infrastructure_menu.rb6
-rw-r--r--lib/tasks/gitlab/db.rake17
-rw-r--r--locale/gitlab.pot32
-rw-r--r--spec/controllers/groups/dependency_proxies_controller_spec.rb68
-rw-r--r--spec/controllers/root_controller_spec.rb4
-rw-r--r--spec/controllers/users/callouts_controller_spec.rb (renamed from spec/controllers/user_callouts_controller_spec.rb)10
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/factories/users/callouts.rb (renamed from spec/factories/user_callouts.rb)2
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb3
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb39
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap4
-rw-r--r--spec/frontend/fixtures/tabs.rb26
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js19
-rw-r--r--spec/frontend/jobs/bridge/app_spec.js33
-rw-r--r--spec/frontend/jobs/bridge/components/empty_state_spec.js59
-rw-r--r--spec/frontend/jobs/bridge/components/sidebar_spec.js76
-rw-r--r--spec/frontend/jobs/bridge/mock_data.js3
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js38
-rw-r--r--spec/frontend/runner/components/runner_status_badge_spec.js14
-rw-r--r--spec/frontend/tabs/index_spec.js260
-rw-r--r--spec/graphql/mutations/user_callouts/create_spec.rb6
-rw-r--r--spec/graphql/types/user_callout_feature_name_enum_spec.rb2
-rw-r--r--spec/helpers/application_helper_spec.rb14
-rw-r--r--spec/helpers/ci/jobs_helper_spec.rb25
-rw-r--r--spec/helpers/ide_helper_spec.rb2
-rw-r--r--spec/helpers/tab_helper_spec.rb8
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb (renamed from spec/helpers/user_callouts_helper_spec.rb)81
-rw-r--r--spec/helpers/users/group_callouts_helper_spec.rb87
-rw-r--r--spec/initializers/validate_database_config_spec.rb3
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb18
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/bridge/common_spec.rb10
-rw-r--r--spec/lib/gitlab/diff/custom_diff_spec.rb62
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb145
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb1
-rw-r--r--spec/lib/gitlab/patch/legacy_database_config_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb6
-rw-r--r--spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb59
-rw-r--r--spec/models/ci/runner_spec.rb2
-rw-r--r--spec/models/user_spec.rb14
-rw-r--r--spec/models/users/callout_spec.rb (renamed from spec/models/user_callout_spec.rb)4
-rw-r--r--spec/models/users/calloutable_spec.rb (renamed from spec/models/concerns/calloutable_spec.rb)10
-rw-r--r--spec/presenters/blob_presenter_spec.rb10
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb29
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb9
-rw-r--r--spec/requests/api/graphql/mutations/user_callouts/create_spec.rb2
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb4
-rw-r--r--spec/services/ci/retry_build_service_spec.rb16
-rw-r--r--spec/services/users/dismiss_callout_service_spec.rb (renamed from spec/services/users/dismiss_user_callout_service_spec.rb)6
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb2
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb42
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb30
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb37
164 files changed, 2437 insertions, 840 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 27471a123d1..92f8e1ad2b1 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -342,8 +342,8 @@ rspec fast_spec_helper minimal:
db:rollback:
extends: .db-job-base
script:
- - bundle exec rake db:migrate VERSION=20181228175414
- - bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
+ - bundle exec rake db:migrate:main VERSION=20181228175414
+ - bundle exec rake db:migrate:main SKIP_SCHEMA_VERSION_CHECK=true
db:migrate:reset:
extends: .db-job-base
@@ -368,7 +368,7 @@ db:migrate-from-previous-major-version:
- git checkout -f $CI_COMMIT_SHA
- SETUP_DB=false USE_BUNDLE_INSTALL=true bash scripts/prepare_build.sh
script:
- - run_timed_command "bundle exec rake db:migrate"
+ - run_timed_command "bundle exec rake db:migrate:main"
db:check-schema:
extends:
@@ -377,7 +377,7 @@ db:check-schema:
variables:
TAG_TO_CHECKOUT: "v14.4.0"
script:
- - run_timed_command "bundle exec rake db:migrate"
+ - run_timed_command "bundle exec rake db:migrate:main"
- scripts/schema_changed.sh
- scripts/validate_migration_timestamps
@@ -900,8 +900,8 @@ db:rollback geo:
- db:rollback
- .rails:rules:ee-only-migration
script:
- - bundle exec rake geo:db:migrate VERSION=20170627195211
- - bundle exec rake geo:db:migrate
+ - bundle exec rake db:migrate:geo VERSION=20170627195211
+ - bundle exec rake db:migrate:geo
# EE: default refs (MRs, default branch, schedules) jobs #
##################################################
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index f29cb429169..17841377974 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -543,7 +543,7 @@ Rails/LexicallyScopedActionFilter:
Rails/LinkToBlank:
Exclude:
- 'app/helpers/projects_helper.rb'
- - 'ee/app/helpers/ee/user_callouts_helper.rb'
+ - 'ee/app/helpers/ee/users/callouts_helper.rb'
# Offense count: 1
# Cop supports --auto-correct.
diff --git a/.rubocop_todo/cop/user_admin.yml b/.rubocop_todo/cop/user_admin.yml
index 392e194953f..5f0f7213950 100644
--- a/.rubocop_todo/cop/user_admin.yml
+++ b/.rubocop_todo/cop/user_admin.yml
@@ -16,7 +16,7 @@ Cop/UserAdmin:
- app/helpers/nav_helper.rb
- app/helpers/projects_helper.rb
- app/helpers/search_helper.rb
- - app/helpers/user_callouts_helper.rb
+ - app/helpers/users/callouts_helper.rb
- app/helpers/users_helper.rb
- app/helpers/visibility_level_helper.rb
- app/models/concerns/protected_ref_access.rb
@@ -38,7 +38,7 @@ Cop/UserAdmin:
- ee/app/helpers/ee/dashboard_helper.rb
- ee/app/helpers/ee/import_helper.rb
- ee/app/helpers/ee/subscribable_banner_helper.rb
- - ee/app/helpers/ee/user_callouts_helper.rb
+ - ee/app/helpers/ee/users/callouts_helper.rb
- ee/app/helpers/license_monitoring_helper.rb
- ee/app/helpers/push_rules_helper.rb
- ee/app/models/concerns/ee/protected_ref_access.rb
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index 0c007ff8081..898768c2425 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -32,7 +32,6 @@ Gitlab/NamespacedClass:
- app/controllers/sessions_controller.rb
- app/controllers/snippets_controller.rb
- app/controllers/uploads_controller.rb
- - app/controllers/user_callouts_controller.rb
- app/controllers/users_controller.rb
- app/controllers/whats_new_controller.rb
- app/finders/abuse_reports_finder.rb
@@ -351,7 +350,6 @@ Gitlab/NamespacedClass:
- app/models/upload.rb
- app/models/user.rb
- app/models/user_agent_detail.rb
- - app/models/user_callout.rb
- app/models/user_canonical_email.rb
- app/models/user_custom_attribute.rb
- app/models/user_detail.rb
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index b9abc36b3c3..938ba6046e8 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1de88e4247d4b940f843003781cb2bf75582b826
+f9af7fbcbfda556c61dcbb2280cda6c6e210cb77
diff --git a/Gemfile b/Gemfile
index 2c96224d99b..8dd01dc9141 100644
--- a/Gemfile
+++ b/Gemfile
@@ -476,7 +476,7 @@ gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support
group :ed25519 do
gem 'ed25519', '~> 1.2'
- gem 'bcrypt_pbkdf', '~> 1.0'
+ gem 'bcrypt_pbkdf', '~> 1.1'
end
# Spamcheck GRPC protocol definitions
diff --git a/Gemfile.lock b/Gemfile.lock
index 1089aa7e02f..ba93e7ce6df 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -137,7 +137,7 @@ GEM
base32 (0.3.2)
batch-loader (2.0.1)
bcrypt (3.1.16)
- bcrypt_pbkdf (1.0.0)
+ bcrypt_pbkdf (1.1.0)
benchmark (0.1.1)
benchmark-ips (2.3.0)
benchmark-memory (0.1.2)
@@ -1410,7 +1410,7 @@ DEPENDENCIES
base32 (~> 0.3.0)
batch-loader (~> 2.0.1)
bcrypt (~> 3.1, >= 3.1.14)
- bcrypt_pbkdf (~> 1.0)
+ bcrypt_pbkdf (~> 1.1)
benchmark-ips (~> 2.3.0)
benchmark-memory (~> 0.1)
better_errors (~> 2.9.0)
diff --git a/app/assets/javascripts/code_navigation/components/doc_line.vue b/app/assets/javascripts/code_navigation/components/doc_line.vue
index 69d398893d9..4d44c984833 100644
--- a/app/assets/javascripts/code_navigation/components/doc_line.vue
+++ b/app/assets/javascripts/code_navigation/components/doc_line.vue
@@ -18,5 +18,6 @@ export default {
<span v-for="(token, tokenIndex) in tokens" :key="tokenIndex" :class="token.class">{{
token.value
}}</span>
+ <br />
</span>
</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index ec6025c84bb..298771a4d12 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -170,7 +170,7 @@ export default {
},
availableGroupsForImport() {
- return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && g.flags.isInvalid);
+ return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && !g.flags.isInvalid);
},
humanizedTotal() {
@@ -521,13 +521,15 @@ export default {
/>
<template v-else>
<div
- class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-p-4 gl-display-flex gl-align-items-center"
+ class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-px-4 gl-display-flex gl-align-items-center import-table-bar"
>
- <gl-sprintf :message="__('%{count} selected')">
- <template #count>
- {{ selectedGroupsIds.length }}
- </template>
- </gl-sprintf>
+ <span data-test-id="selection-count">
+ <gl-sprintf :message="__('%{count} selected')">
+ <template #count>
+ {{ selectedGroupsIds.length }}
+ </template>
+ </gl-sprintf>
+ </span>
<gl-button
category="primary"
variant="confirm"
@@ -539,7 +541,7 @@ export default {
</div>
<gl-table
ref="table"
- class="gl-w-full"
+ class="gl-w-full import-table"
data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
diff --git a/app/assets/javascripts/jobs/bridge/app.vue b/app/assets/javascripts/jobs/bridge/app.vue
new file mode 100644
index 00000000000..67c22712776
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/app.vue
@@ -0,0 +1,20 @@
+<script>
+import BridgeEmptyState from './components/empty_state.vue';
+import BridgeSidebar from './components/sidebar.vue';
+
+export default {
+ name: 'BridgePageApp',
+ components: {
+ BridgeEmptyState,
+ BridgeSidebar,
+ },
+};
+</script>
+<template>
+ <div>
+ <!-- TODO: get job details and show CI header -->
+ <!-- TODO: add downstream pipeline path -->
+ <bridge-empty-state downstream-pipeline-path="#" />
+ <bridge-sidebar />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/constants.js b/app/assets/javascripts/jobs/bridge/components/constants.js
new file mode 100644
index 00000000000..33310b3157a
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/constants.js
@@ -0,0 +1 @@
+export const SIDEBAR_COLLAPSE_BREAKPOINTS = ['xs', 'sm'];
diff --git a/app/assets/javascripts/jobs/bridge/components/empty_state.vue b/app/assets/javascripts/jobs/bridge/components/empty_state.vue
new file mode 100644
index 00000000000..bd07d863719
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/empty_state.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ name: 'BridgeEmptyState',
+ i18n: {
+ title: __('This job triggers a downstream pipeline'),
+ linkBtnText: __('View downstream pipeline'),
+ },
+ components: {
+ GlButton,
+ },
+ inject: {
+ emptyStateIllustrationPath: {
+ type: String,
+ require: true,
+ },
+ },
+ props: {
+ downstreamPipelinePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
+ <img :src="emptyStateIllustrationPath" />
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <gl-button
+ v-if="downstreamPipelinePath"
+ class="gl-mt-3"
+ category="secondary"
+ variant="confirm"
+ size="medium"
+ :href="downstreamPipelinePath"
+ >
+ {{ $options.i18n.linkBtnText }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/sidebar.vue b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
new file mode 100644
index 00000000000..68b767408f0
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { __ } from '~/locale';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
+import { JOB_SIDEBAR } from '../../constants';
+import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './constants';
+
+export default {
+ styles: {
+ top: '75px',
+ width: '290px',
+ },
+ name: 'BridgeSidebar',
+ i18n: {
+ ...JOB_SIDEBAR,
+ retryButton: __('Retry'),
+ retryTriggerJob: __('Retry the trigger job'),
+ retryDownstreamPipeline: __('Retry the downstream pipeline'),
+ },
+ borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ TooltipOnTruncate,
+ },
+ inject: {
+ buildName: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isSidebarExpanded: true,
+ };
+ },
+ created() {
+ window.addEventListener('resize', this.onResize);
+ },
+ mounted() {
+ this.onResize();
+ },
+ methods: {
+ toggleSidebar() {
+ this.isSidebarExpanded = !this.isSidebarExpanded;
+ },
+ onResize() {
+ const breakpoint = bp.getBreakpointSize();
+ if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) {
+ this.isSidebarExpanded = false;
+ } else if (!this.isSidebarExpanded) {
+ this.isSidebarExpanded = true;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <aside
+ class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden"
+ :style="this.$options.styles"
+ :class="{
+ 'gl-display-none': !isSidebarExpanded,
+ }"
+ >
+ <div class="gl-py-5 gl-display-flex gl-align-items-center">
+ <tooltip-on-truncate :title="buildName" truncate-target="child"
+ ><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate">
+ {{ buildName }}
+ </h4>
+ </tooltip-on-truncate>
+ <!-- TODO: implement retry actions -->
+ <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <gl-dropdown
+ :text="$options.i18n.retryButton"
+ category="primary"
+ variant="confirm"
+ right
+ size="medium"
+ >
+ <gl-dropdown-item>{{ $options.i18n.retryTriggerJob }}</gl-dropdown-item>
+ <gl-dropdown-item>{{ $options.i18n.retryDownstreamPipeline }}</gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ <gl-button
+ :aria-label="$options.i18n.toggleSidebar"
+ data-testid="sidebar-expansion-toggle"
+ category="tertiary"
+ class="gl-md-display-none gl-ml-2"
+ icon="chevron-double-lg-right"
+ @click="toggleSidebar"
+ />
+ </div>
+ <!-- TODO: get job details and show commit block, stage dropdown, jobs list -->
+ </aside>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 6105299e15c..97141a27a5e 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -5,7 +5,7 @@ import { __, s__, sprintf } from '~/locale';
export default {
i18n: {
- eraseLogButtonLabel: s__('Job|Erase job log'),
+ eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
scrollToBottomButtonLabel: s__('Job|Scroll to bottom'),
scrollToTopButtonLabel: s__('Job|Scroll to top'),
showRawButtonLabel: s__('Job|Show complete raw'),
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 1fb6a6f9850..e078a6c2319 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import BridgeApp from './bridge/app.vue';
import JobApp from './components/job_app.vue';
import createStore from './store';
-export default () => {
- const element = document.getElementById('js-job-vue-app');
-
+const initializeJobPage = (element) => {
const store = createStore();
// Let's start initializing the store (i.e. fetching data) right away
@@ -51,3 +52,35 @@ export default () => {
},
});
};
+
+const initializeBridgePage = (el) => {
+ const { buildName, emptyStateIllustrationPath } = el.dataset;
+
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ buildName,
+ emptyStateIllustrationPath,
+ },
+ render(h) {
+ return h(BridgeApp);
+ },
+ });
+};
+
+export default () => {
+ const jobElement = document.getElementById('js-job-page');
+ const bridgeElement = document.getElementById('js-bridge-page');
+
+ if (jobElement) {
+ initializeJobPage(jobElement);
+ } else {
+ initializeBridgePage(bridgeElement);
+ }
+};
diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js
index 2c43bed412e..05102f73f92 100644
--- a/app/assets/javascripts/milestones/milestone.js
+++ b/app/assets/javascripts/milestones/milestone.js
@@ -1,43 +1,43 @@
-import $ from 'jquery';
import createFlash from '~/flash';
+import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
+import { historyPushState } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs';
export default class Milestone {
constructor() {
+ this.tabsEl = document.querySelector('.js-milestone-tabs');
+ this.glTabs = new GlTabsBehavior(this.tabsEl);
+ this.loadedTabs = new WeakSet();
+
this.bindTabsSwitching();
this.loadInitialTab();
}
bindTabsSwitching() {
- return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
- const $target = $(e.target);
-
- window.location.hash = $target.attr('href');
- this.loadTab($target);
+ this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => {
+ const tab = event.target;
+ const { activeTabPanel } = event.detail;
+ historyPushState(tab.getAttribute('href'));
+ this.loadTab(tab, activeTabPanel);
});
}
loadInitialTab() {
- const $target = $(`.js-milestone-tabs a:not(.active)[href="${window.location.hash}"]`);
-
- if ($target.length) {
- $target.tab('show');
- } else {
- this.loadTab($('.js-milestone-tabs a.active'));
- }
+ const tab = this.tabsEl.querySelector(`a[href="${window.location.hash}"]`);
+ this.glTabs.activateTab(tab || this.glTabs.activeTab);
}
- // eslint-disable-next-line class-methods-use-this
- loadTab($target) {
- const endpoint = $target.data('endpoint');
- const tabElId = $target.attr('href');
+ loadTab(tab, tabPanel) {
+ const { endpoint } = tab.dataset;
- if (endpoint && !$target.hasClass('is-loaded')) {
+ if (endpoint && !this.loadedTabs.has(tab)) {
axios
.get(endpoint)
.then(({ data }) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
+ // eslint-disable-next-line no-param-reassign
+ tabPanel.innerHTML = sanitize(data.html);
+ this.loadedTabs.add(tab);
})
.catch(() =>
createFlash({
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index 52c63a1355a..eb112238c11 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -4,7 +4,6 @@ import {
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
- GlLink,
GlSkeletonLoader,
GlSprintf,
} from '@gitlab/ui';
@@ -16,10 +15,7 @@ import {
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/settings/group/constants';
-import {
- GRAPHQL_PAGE_SIZE,
- ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
-} from '~/packages_and_registries/dependency_proxy/constants';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
@@ -29,7 +25,6 @@ export default {
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
- GlLink,
GlSkeletonLoader,
GlSprintf,
ClipboardButton,
@@ -41,9 +36,6 @@ export default {
proxyNotAvailableText: s__(
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
),
- proxyDisabledText: s__(
- 'DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}.',
- ),
proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
@@ -52,7 +44,6 @@ export default {
},
links: {
DEPENDENCY_PROXY_DOCS_PATH,
- ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
},
data() {
return {
@@ -79,9 +70,7 @@ export default {
},
];
},
- dependencyProxyEnabled() {
- return this.group?.dependencyProxySetting?.enabled;
- },
+
queryVariables() {
return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
},
@@ -131,7 +120,7 @@ export default {
<gl-skeleton-loader v-else-if="$apollo.queries.group.loading" />
- <div v-else-if="dependencyProxyEnabled" data-testid="main-area">
+ <div v-else data-testid="main-area">
<gl-form-group :label="$options.i18n.proxyImagePrefix">
<gl-form-input-group
readonly
@@ -170,12 +159,5 @@ export default {
:title="$options.i18n.noManifestTitle"
/>
</div>
- <gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
- <gl-sprintf :message="$options.i18n.proxyDisabledText">
- <template #docLink="{ content }">
- <gl-link :href="$options.links.ENABLE_DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
index ab1e3adcb55..3c6ede6fdce 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
@@ -1,7 +1 @@
-import { helpPagePath } from '~/helpers/help_page_helper';
-
export const GRAPHQL_PAGE_SIZE = 20;
-export const ENABLE_DEPENDENCY_PROXY_DOCS_PATH = helpPagePath(
- 'user/packages/dependency_proxy/index',
- { anchor: 'enable-or-disable-the-dependency-proxy-for-a-group' },
-);
diff --git a/app/assets/javascripts/runner/components/runner_status_badge.vue b/app/assets/javascripts/runner/components/runner_status_badge.vue
index 24ca6aacfc9..0823876a187 100644
--- a/app/assets/javascripts/runner/components/runner_status_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_status_badge.vue
@@ -9,6 +9,7 @@ import {
I18N_STALE_RUNNER_DESCRIPTION,
STATUS_ONLINE,
STATUS_NOT_CONNECTED,
+ STATUS_NEVER_CONTACTED,
STATUS_OFFLINE,
STATUS_STALE,
} from '../constants';
@@ -45,6 +46,7 @@ export default {
}),
};
case STATUS_NOT_CONNECTED:
+ case STATUS_NEVER_CONTACTED:
return {
variant: 'muted',
label: s__('Runners|not connected'),
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 68e45fcf8e9..355f3054917 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -61,6 +61,7 @@ export const STATUS_PAUSED = 'PAUSED';
export const STATUS_ONLINE = 'ONLINE';
export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
+export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_STALE = 'STALE';
diff --git a/app/assets/javascripts/tabs/constants.js b/app/assets/javascripts/tabs/constants.js
new file mode 100644
index 00000000000..3b84d7394d4
--- /dev/null
+++ b/app/assets/javascripts/tabs/constants.js
@@ -0,0 +1,20 @@
+export const ACTIVE_TAB_CLASSES = Object.freeze([
+ 'active',
+ 'gl-tab-nav-item-active',
+ 'gl-tab-nav-item-active-indigo',
+]);
+
+export const ACTIVE_PANEL_CLASS = 'active';
+
+export const KEY_CODE_LEFT = 'ArrowLeft';
+export const KEY_CODE_UP = 'ArrowUp';
+export const KEY_CODE_RIGHT = 'ArrowRight';
+export const KEY_CODE_DOWN = 'ArrowDown';
+
+export const ATTR_ARIA_CONTROLS = 'aria-controls';
+export const ATTR_ARIA_LABELLEDBY = 'aria-labelledby';
+export const ATTR_ARIA_SELECTED = 'aria-selected';
+export const ATTR_ROLE = 'role';
+export const ATTR_TABINDEX = 'tabindex';
+
+export const TAB_SHOWN_EVENT = 'gl-tab-shown';
diff --git a/app/assets/javascripts/tabs/index.js b/app/assets/javascripts/tabs/index.js
new file mode 100644
index 00000000000..44937e593e0
--- /dev/null
+++ b/app/assets/javascripts/tabs/index.js
@@ -0,0 +1,239 @@
+import { uniqueId } from 'lodash';
+import {
+ ACTIVE_TAB_CLASSES,
+ ATTR_ROLE,
+ ATTR_ARIA_CONTROLS,
+ ATTR_TABINDEX,
+ ATTR_ARIA_SELECTED,
+ ATTR_ARIA_LABELLEDBY,
+ ACTIVE_PANEL_CLASS,
+ KEY_CODE_LEFT,
+ KEY_CODE_UP,
+ KEY_CODE_RIGHT,
+ KEY_CODE_DOWN,
+ TAB_SHOWN_EVENT,
+} from './constants';
+
+export { TAB_SHOWN_EVENT };
+
+/**
+ * The `GlTabsBehavior` class adds interactivity to tabs created by the `gl_tabs_nav` and
+ * `gl_tab_link_to` Rails helpers.
+ *
+ * Example using `href` references:
+ *
+ * ```haml
+ * = gl_tabs_nav({ class: 'js-my-tabs' }) do
+ * = gl_tab_link_to '#foo', item_active: true do
+ * = _('Foo')
+ * = gl_tab_link_to '#bar' do
+ * = _('Bar')
+ *
+ * .tab-content
+ * .tab-pane.active#foo
+ * .tab-pane#bar
+ * ```
+ *
+ * ```javascript
+ * import { GlTabsBehavior } from '~/tabs';
+ *
+ * const glTabs = new GlTabsBehavior(document.querySelector('.js-my-tabs'));
+ * ```
+ *
+ * Example using `aria-controls` references:
+ *
+ * ```haml
+ * = gl_tabs_nav({ class: 'js-my-tabs' }) do
+ * = gl_tab_link_to '#', item_active: true, 'aria-controls': 'foo' do
+ * = _('Foo')
+ * = gl_tab_link_to '#', 'aria-controls': 'bar' do
+ * = _('Bar')
+ *
+ * .tab-content
+ * .tab-pane.active#foo
+ * .tab-pane#bar
+ * ```
+ *
+ * ```javascript
+ * import { GlTabsBehavior } from '~/tabs';
+ *
+ * const glTabs = new GlTabsBehavior(document.querySelector('.js-my-tabs'));
+ * ```
+ *
+ * `GlTabsBehavior` can be used to replace Bootstrap tab implementations that cannot
+ * easily be rewritten in Vue.
+ *
+ * NOTE: Do *not* use `GlTabsBehavior` with markup generated by other means, as it may not
+ * work correctly.
+ *
+ * Tab panels must exist somewhere in the page for the tabs to control. Tab panels
+ * must:
+ * - be immediate children of a `.tab-content` element
+ * - have the `tab-pane` class
+ * - if the panel is active, have the `active` class
+ * - have a unique `id` attribute
+ *
+ * In order to associate tabs with panels, the tabs must reference their panel's
+ * `id` by having one of the following attributes:
+ * - `href`, e.g., `href="#the-panel-id"` (note the leading `#` in the value)
+ * - `aria-controls`, e.g., `aria-controls="the-panel-id"` (no leading `#`)
+ *
+ * Exactly one tab/panel must be active in the original markup.
+ *
+ * Call the `destroy` method on an instance to remove event listeners that were
+ * added during construction. Other DOM mutations (like ARIA attributes) are
+ * _not_ reverted.
+ */
+export class GlTabsBehavior {
+ /**
+ * Create a GlTabsBehavior instance.
+ *
+ * @param {HTMLElement} el The element created by the Rails `gl_tabs_nav` helper.
+ */
+ constructor(el) {
+ if (!el) {
+ throw new Error('Cannot instantiate GlTabsBehavior without an element');
+ }
+
+ this.destroyFns = [];
+ this.tabList = el;
+ this.tabs = this.getTabs();
+ this.activeTab = null;
+
+ this.setAccessibilityAttrs();
+ this.bindEvents();
+ }
+
+ setAccessibilityAttrs() {
+ this.tabList.setAttribute(ATTR_ROLE, 'tablist');
+ this.tabs.forEach((tab) => {
+ if (!tab.hasAttribute('id')) {
+ tab.setAttribute('id', uniqueId('gl_tab_nav__tab_'));
+ }
+
+ if (!this.activeTab && tab.classList.contains(ACTIVE_TAB_CLASSES[0])) {
+ this.activeTab = tab;
+ tab.setAttribute(ATTR_ARIA_SELECTED, 'true');
+ tab.removeAttribute(ATTR_TABINDEX);
+ } else {
+ tab.setAttribute(ATTR_ARIA_SELECTED, 'false');
+ tab.setAttribute(ATTR_TABINDEX, '-1');
+ }
+
+ tab.setAttribute(ATTR_ROLE, 'tab');
+ tab.closest('.nav-item').setAttribute(ATTR_ROLE, 'presentation');
+
+ const tabPanel = this.getPanelForTab(tab);
+ if (!tab.hasAttribute(ATTR_ARIA_CONTROLS)) {
+ tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id);
+ }
+
+ tabPanel.setAttribute(ATTR_ROLE, 'tabpanel');
+ tabPanel.setAttribute(ATTR_ARIA_LABELLEDBY, tab.id);
+ });
+ }
+
+ bindEvents() {
+ this.tabs.forEach((tab) => {
+ this.bindEvent(tab, 'click', (event) => {
+ event.preventDefault();
+
+ if (tab !== this.activeTab) {
+ this.activateTab(tab);
+ }
+ });
+
+ this.bindEvent(tab, 'keydown', (event) => {
+ const { code } = event;
+ if (code === KEY_CODE_UP || code === KEY_CODE_LEFT) {
+ event.preventDefault();
+ this.activatePreviousTab();
+ } else if (code === KEY_CODE_DOWN || code === KEY_CODE_RIGHT) {
+ event.preventDefault();
+ this.activateNextTab();
+ }
+ });
+ });
+ }
+
+ bindEvent(el, ...args) {
+ el.addEventListener(...args);
+
+ this.destroyFns.push(() => {
+ el.removeEventListener(...args);
+ });
+ }
+
+ activatePreviousTab() {
+ const currentTabIndex = this.tabs.indexOf(this.activeTab);
+
+ if (currentTabIndex <= 0) return;
+
+ const previousTab = this.tabs[currentTabIndex - 1];
+ this.activateTab(previousTab);
+ previousTab.focus();
+ }
+
+ activateNextTab() {
+ const currentTabIndex = this.tabs.indexOf(this.activeTab);
+
+ if (currentTabIndex >= this.tabs.length - 1) return;
+
+ const nextTab = this.tabs[currentTabIndex + 1];
+ this.activateTab(nextTab);
+ nextTab.focus();
+ }
+
+ getTabs() {
+ return Array.from(this.tabList.querySelectorAll('.gl-tab-nav-item'));
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getPanelForTab(tab) {
+ const ariaControls = tab.getAttribute(ATTR_ARIA_CONTROLS);
+
+ if (ariaControls) {
+ return document.querySelector(`#${ariaControls}`);
+ }
+
+ return document.querySelector(tab.getAttribute('href'));
+ }
+
+ activateTab(tabToActivate) {
+ // Deactivate active tab first
+ this.activeTab.setAttribute(ATTR_ARIA_SELECTED, 'false');
+ this.activeTab.setAttribute(ATTR_TABINDEX, '-1');
+ this.activeTab.classList.remove(...ACTIVE_TAB_CLASSES);
+
+ const activePanel = this.getPanelForTab(this.activeTab);
+ activePanel.classList.remove(ACTIVE_PANEL_CLASS);
+
+ // Now activate the given tab/panel
+ tabToActivate.setAttribute(ATTR_ARIA_SELECTED, 'true');
+ tabToActivate.removeAttribute(ATTR_TABINDEX);
+ tabToActivate.classList.add(...ACTIVE_TAB_CLASSES);
+
+ const tabPanel = this.getPanelForTab(tabToActivate);
+ tabPanel.classList.add(ACTIVE_PANEL_CLASS);
+
+ this.activeTab = tabToActivate;
+
+ this.dispatchTabShown(tabToActivate, tabPanel);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ dispatchTabShown(tab, activeTabPanel) {
+ const event = new CustomEvent(TAB_SHOWN_EVENT, {
+ bubbles: true,
+ detail: {
+ activeTabPanel,
+ },
+ });
+
+ tab.dispatchEvent(event);
+ }
+
+ destroy() {
+ this.destroyFns.forEach((destroy) => destroy());
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss
index c74b5460e1a..79468ce62ce 100644
--- a/app/assets/stylesheets/page_bundles/import.scss
+++ b/app/assets/stylesheets/page_bundles/import.scss
@@ -1,12 +1,5 @@
@import 'mixins_and_variables_and_functions';
-// Fixing double scrollbar issue
-// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1156 and
-// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54837
-.import-entities-namespace-dropdown.show.dropdown .dropdown-menu {
- max-height: initial;
-}
-
.import-jobs-to-col {
width: 39%;
}
@@ -38,3 +31,31 @@
box-shadow: inset 0 0 0 1px var(--gray-200, $gray-200);
}
}
+
+$import-bar-height: $gl-spacing-scale-11;
+
+.import-table-bar {
+ @include gl-sticky;
+ height: $import-bar-height;
+ top: $header-height;
+ z-index: 3;
+
+ html.with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+}
+
+.import-table {
+ border-collapse: separate;
+
+ thead {
+ @include gl-sticky;
+ background-color: var(--gray-10, $gray-10);
+ top: calc(#{$header-height} + #{$import-bar-height});
+ z-index: 3;
+
+ html.with-performance-bar & {
+ top: calc(#{$header-height + $performance-bar-height} + #{$import-bar-height});
+ }
+ }
+}
diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb
index 07aca72b22f..44611641529 100644
--- a/app/controllers/concerns/dependency_proxy/group_access.rb
+++ b/app/controllers/concerns/dependency_proxy/group_access.rb
@@ -5,13 +5,13 @@ module DependencyProxy
extend ActiveSupport::Concern
included do
- before_action :verify_dependency_proxy_enabled!
+ before_action :verify_dependency_proxy_available!
before_action :authorize_read_dependency_proxy!
end
private
- def verify_dependency_proxy_enabled!
+ def verify_dependency_proxy_available!
render_404 unless group&.dependency_proxy_feature_available?
end
diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb
index b037aa52939..2e120de435e 100644
--- a/app/controllers/groups/dependency_proxies_controller.rb
+++ b/app/controllers/groups/dependency_proxies_controller.rb
@@ -5,30 +5,19 @@ module Groups
include ::DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update
- before_action :dependency_proxy
+ before_action :verify_dependency_proxy_enabled!
feature_category :package_registry
- def show
- @blobs_count = group.dependency_proxy_blobs.count
- @blobs_total_size = group.dependency_proxy_blobs.total_size
- end
-
- def update
- dependency_proxy.update(dependency_proxy_params)
-
- redirect_to group_dependency_proxy_path(group)
- end
-
private
def dependency_proxy
@dependency_proxy ||=
- group.dependency_proxy_setting || group.create_dependency_proxy_setting
+ group.dependency_proxy_setting || group.create_dependency_proxy_setting!
end
- def dependency_proxy_params
- params.require(:dependency_proxy_group_setting).permit(:enabled)
+ def verify_dependency_proxy_enabled!
+ render_404 unless dependency_proxy.enabled?
end
end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 81b8da9cba3..32a192192bd 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,8 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
include ContinueParams
- before_action :find_job_as_build, except: [:index, :play]
- before_action :find_job_as_processable, only: [:play]
+ before_action :find_job_as_build, except: [:index, :play, :show]
+ before_action :find_job_as_processable, only: [:play, :show]
before_action :authorize_read_build_trace!, only: [:trace, :raw]
before_action :authorize_read_build!
before_action :authorize_update_build!,
diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb
deleted file mode 100644
index f52a09adf5a..00000000000
--- a/app/controllers/user_callouts_controller.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-class UserCalloutsController < ApplicationController
- feature_category :navigation
-
- def create
- if callout.persisted?
- respond_to do |format|
- format.json { head :ok }
- end
- else
- respond_to do |format|
- format.json { head :bad_request }
- end
- end
- end
-
- private
-
- def callout
- Users::DismissUserCalloutService.new(
- container: nil, current_user: current_user, params: { feature_name: feature_name }
- ).execute
- end
-
- def feature_name
- params.require(:feature_name)
- end
-end
diff --git a/app/controllers/users/callouts_controller.rb b/app/controllers/users/callouts_controller.rb
new file mode 100644
index 00000000000..fe308d9dd1e
--- /dev/null
+++ b/app/controllers/users/callouts_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Users
+ class CalloutsController < ApplicationController
+ feature_category :navigation
+
+ def create
+ if callout.persisted?
+ respond_to do |format|
+ format.json { head :ok }
+ end
+ else
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
+ end
+ end
+
+ private
+
+ def callout
+ Users::DismissCalloutService.new(
+ container: nil, current_user: current_user, params: { feature_name: feature_name }
+ ).execute
+ end
+
+ def feature_name
+ params.require(:feature_name)
+ end
+ end
+end
diff --git a/app/controllers/users/group_callouts_controller.rb b/app/controllers/users/group_callouts_controller.rb
index cc27452e6a3..abca12ccea7 100644
--- a/app/controllers/users/group_callouts_controller.rb
+++ b/app/controllers/users/group_callouts_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
- class GroupCalloutsController < UserCalloutsController
+ class GroupCalloutsController < Users::CalloutsController
private
def callout
diff --git a/app/graphql/mutations/user_callouts/create.rb b/app/graphql/mutations/user_callouts/create.rb
index ff6e5cd28dd..1be99ea0ecd 100644
--- a/app/graphql/mutations/user_callouts/create.rb
+++ b/app/graphql/mutations/user_callouts/create.rb
@@ -15,7 +15,7 @@ module Mutations
description: 'User callout dismissed.'
def resolve(feature_name:)
- callout = Users::DismissUserCalloutService.new(
+ callout = Users::DismissCalloutService.new(
container: nil, current_user: current_user, params: { feature_name: feature_name }
).execute
errors = errors_on_object(callout)
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
index 14eae1cdce5..dd056191ceb 100644
--- a/app/graphql/types/ci/runner_status_enum.rb
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -25,13 +25,17 @@ module Types
value: :offline
value 'STALE',
- description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0",
+ description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0.",
value: :stale
value 'NOT_CONNECTED',
description: 'Runner that has never contacted this instance.',
- deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact', milestone: '14.6' },
+ deprecated: { reason: "Use NEVER_CONTACTED instead. NEVER_CONTACTED will have a slightly different scope starting in 15.0, with STALE being returned instead after #{::Ci::Runner::STALE_TIMEOUT.inspect} of no contact", milestone: '14.6' },
value: :not_connected
+
+ value 'NEVER_CONTACTED',
+ description: 'Runner that has never contacted this instance. Set legacyMode to null to utilize this value. Will replace NOT_CONNECTED starting in 15.0.',
+ value: :never_contacted
end
end
end
diff --git a/app/graphql/types/user_callout_feature_name_enum.rb b/app/graphql/types/user_callout_feature_name_enum.rb
index 410ca5e1c95..bcb49a709ed 100644
--- a/app/graphql/types/user_callout_feature_name_enum.rb
+++ b/app/graphql/types/user_callout_feature_name_enum.rb
@@ -5,7 +5,7 @@ module Types
graphql_name 'UserCalloutFeatureNameEnum'
description 'Name of the feature that the callout is for.'
- ::UserCallout.feature_names.keys.each do |feature_name|
+ ::Users::Callout.feature_names.keys.each do |feature_name|
value feature_name.upcase, value: feature_name, description: "Callout feature name for #{feature_name}."
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 58f933a7fe0..02a87979f40 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -206,10 +206,6 @@ module ApplicationHelper
'https://' + promo_host
end
- def contact_sales_url
- promo_url + '/sales'
- end
-
def support_url
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index d02fe3f20b0..c7f40decae8 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -19,6 +19,13 @@ module Ci
}
end
+ def bridge_data(build)
+ {
+ "build_name" => build.name,
+ "empty-state-illustration-path" => image_path('illustrations/job-trigger-md.svg')
+ }
+ end
+
def job_counts
{
"all" => limited_counter_with_delimiter(@all_builds),
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 17057505173..8f219656b71 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -23,7 +23,7 @@ module Ci
icon = 'status-paused'
span_class = 'gl-text-gray-600'
end
- when :not_connected
+ when :not_connected, :never_contacted
title = s_("Runners|New runner, has not connected yet")
icon = 'warning-solid'
when :offline
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index d5d692f2d6e..abb7128470f 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -182,7 +182,7 @@ module MergeRequestsHelper
project_path: project_path(merge_request.project),
changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'),
is_fluid_layout: fluid_layout.to_s,
- dismiss_endpoint: user_callouts_path,
+ dismiss_endpoint: callouts_path,
show_suggest_popover: show_suggest_popover?.to_s,
show_whitespace_default: @show_whitespace_default.to_s,
file_by_file_default: @file_by_file_default.to_s,
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 3d51ba30c62..2efc3f27dc7 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -14,8 +14,7 @@ module TabHelper
gl_tabs_classes = %w[nav gl-tabs-nav]
html_options = html_options.merge(
- class: [*html_options[:class], gl_tabs_classes].join(' '),
- role: 'tablist'
+ class: [*html_options[:class], gl_tabs_classes].join(' ')
)
content = capture(&block) if block_given?
@@ -54,7 +53,7 @@ module TabHelper
extra_tab_classes = html_options.delete(:tab_class)
tab_class = %w[nav-item].push(*extra_tab_classes)
- content_tag(:li, class: tab_class, role: 'presentation') do
+ content_tag(:li, class: tab_class) do
if block_given?
link_to(options, html_options, &block)
else
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
deleted file mode 100644
index d8e69145c40..00000000000
--- a/app/helpers/user_callouts_helper.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-module UserCalloutsHelper
- GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
- GCP_SIGNUP_OFFER = 'gcp_signup_offer'
- SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
- TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
- CUSTOMIZE_HOMEPAGE = 'customize_homepage'
- FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
- REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
- UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
- INVITE_MEMBERS_BANNER = 'invite_members_banner'
- SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
-
- def show_gke_cluster_integration_callout?(project)
- active_nav_link?(controller: sidebar_operations_paths) &&
- can?(current_user, :create_cluster, project) &&
- !user_dismissed?(GKE_CLUSTER_INTEGRATION)
- end
-
- def show_gcp_signup_offer?
- !user_dismissed?(GCP_SIGNUP_OFFER)
- end
-
- def render_flash_user_callout(flash_type, message, feature_name)
- render 'shared/flash_user_callout', flash_type: flash_type, message: message, feature_name: feature_name
- end
-
- def render_dashboard_ultimate_trial(user)
- end
-
- def render_two_factor_auth_recovery_settings_check
- end
-
- def show_suggest_popover?
- !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
- end
-
- def show_customize_homepage_banner?
- current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
- end
-
- def show_feature_flags_new_version?
- !user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
- end
-
- def show_unfinished_tag_cleanup_callout?
- !user_dismissed?(UNFINISHED_TAG_CLEANUP_CALLOUT)
- end
-
- def show_registration_enabled_user_callout?
- !Gitlab.com? &&
- current_user&.admin? &&
- signup_enabled? &&
- !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
- end
-
- def dismiss_two_factor_auth_recovery_settings_check
- end
-
- def show_invite_banner?(group)
- Ability.allowed?(current_user, :admin_group, group) &&
- !just_created? &&
- !user_dismissed_for_group(INVITE_MEMBERS_BANNER, group) &&
- !multiple_members?(group)
- end
-
- def show_security_newsletter_user_callout?
- current_user&.admin? &&
- !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
- end
-
- private
-
- def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
- return false unless current_user
-
- current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
- end
-
- def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil)
- return false unless current_user
-
- current_user.dismissed_callout_for_group?(feature_name: feature_name,
- group: group,
- ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
- end
-
- def just_created?
- flash[:notice]&.include?('successfully created')
- end
-
- def multiple_members?(group)
- group.member_count > 1 || group.members_with_parents.count > 1
- end
-end
-
-UserCalloutsHelper.prepend_mod
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
new file mode 100644
index 00000000000..5ed17357e9b
--- /dev/null
+++ b/app/helpers/users/callouts_helper.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Users
+ module CalloutsHelper
+ GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'
+ SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
+ TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
+ CUSTOMIZE_HOMEPAGE = 'customize_homepage'
+ FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
+ REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
+ UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
+ SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
+
+ def show_gke_cluster_integration_callout?(project)
+ active_nav_link?(controller: sidebar_operations_paths) &&
+ can?(current_user, :create_cluster, project) &&
+ !user_dismissed?(GKE_CLUSTER_INTEGRATION)
+ end
+
+ def show_gcp_signup_offer?
+ !user_dismissed?(GCP_SIGNUP_OFFER)
+ end
+
+ def render_flash_user_callout(flash_type, message, feature_name)
+ render 'shared/flash_user_callout', flash_type: flash_type, message: message, feature_name: feature_name
+ end
+
+ def render_dashboard_ultimate_trial(user)
+ end
+
+ def render_two_factor_auth_recovery_settings_check
+ end
+
+ def show_suggest_popover?
+ !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
+ end
+
+ def show_customize_homepage_banner?
+ current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
+ end
+
+ def show_feature_flags_new_version?
+ !user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
+ end
+
+ def show_unfinished_tag_cleanup_callout?
+ !user_dismissed?(UNFINISHED_TAG_CLEANUP_CALLOUT)
+ end
+
+ def show_registration_enabled_user_callout?
+ !Gitlab.com? &&
+ current_user&.admin? &&
+ signup_enabled? &&
+ !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
+ end
+
+ def dismiss_two_factor_auth_recovery_settings_check
+ end
+
+ def show_security_newsletter_user_callout?
+ current_user&.admin? &&
+ !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
+ end
+
+ private
+
+ def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
+ return false unless current_user
+
+ current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
+ end
+ end
+end
+
+Users::CalloutsHelper.prepend_mod
diff --git a/app/helpers/users/group_callouts_helper.rb b/app/helpers/users/group_callouts_helper.rb
new file mode 100644
index 00000000000..b66c7f9f821
--- /dev/null
+++ b/app/helpers/users/group_callouts_helper.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Users
+ module GroupCalloutsHelper
+ INVITE_MEMBERS_BANNER = 'invite_members_banner'
+
+ def show_invite_banner?(group)
+ Ability.allowed?(current_user, :admin_group, group) &&
+ !just_created? &&
+ !user_dismissed_for_group(INVITE_MEMBERS_BANNER, group) &&
+ !multiple_members?(group)
+ end
+
+ private
+
+ def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil)
+ return false unless current_user
+
+ current_user.dismissed_callout_for_group?(feature_name: feature_name,
+ group: group,
+ ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
+ end
+
+ def just_created?
+ flash[:notice]&.include?('successfully created')
+ end
+
+ def multiple_members?(group)
+ group.member_count > 1 || group.members_with_parents.count > 1
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 3ede3ef3347..a441d362b74 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -44,7 +44,7 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
- AVAILABLE_STATUSES = %w[active paused online offline not_connected stale].freeze
+ AVAILABLE_STATUSES = %w[active paused online offline not_connected never_contacted stale].freeze # TODO: Remove in %15.0: active, paused, not_connected. Relevant issues: https://gitlab.com/gitlab-org/gitlab/-/issues/347303, https://gitlab.com/gitlab-org/gitlab/-/issues/347305, https://gitlab.com/gitlab-org/gitlab/-/issues/344648
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
@@ -66,7 +66,8 @@ module Ci
scope :recent, -> { where('ci_runners.created_at >= :date OR ci_runners.contacted_at >= :date', date: stale_deadline) }
scope :stale, -> { where('ci_runners.created_at < :date AND (ci_runners.contacted_at IS NULL OR ci_runners.contacted_at < :date)', date: stale_deadline) }
scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
- scope :not_connected, -> { where(contacted_at: nil) }
+ scope :not_connected, -> { where(contacted_at: nil) } # TODO: Remove in 15.0
+ scope :never_contacted, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) }
@@ -284,7 +285,7 @@ module Ci
return deprecated_rest_status if legacy_mode == '14.5'
return :stale if stale?
- return :not_connected unless contacted_at
+ return :never_contacted unless contacted_at
online? ? :online : :offline
end
diff --git a/app/models/concerns/calloutable.rb b/app/models/concerns/calloutable.rb
deleted file mode 100644
index 8b9cfae6a32..00000000000
--- a/app/models/concerns/calloutable.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Calloutable
- extend ActiveSupport::Concern
-
- included do
- belongs_to :user
-
- validates :user, presence: true
- end
-
- def dismissed_after?(dismissed_after)
- dismissed_at > dismissed_after
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index f8579add392..98d2ceb6dbe 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -204,7 +204,7 @@ class User < ApplicationRecord
has_many :bulk_imports
has_many :custom_attributes, class_name: 'UserCustomAttribute'
- has_many :callouts, class_name: 'UserCallout'
+ has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -1947,7 +1947,7 @@ class User < ApplicationRecord
end
def find_or_initialize_callout(feature_name)
- callouts.find_or_initialize_by(feature_name: ::UserCallout.feature_names[feature_name])
+ callouts.find_or_initialize_by(feature_name: ::Users::Callout.feature_names[feature_name])
end
def find_or_initialize_group_callout(feature_name, group_id)
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
deleted file mode 100644
index 5956c82384e..00000000000
--- a/app/models/user_callout.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-class UserCallout < ApplicationRecord
- include Calloutable
-
- enum feature_name: {
- gke_cluster_integration: 1,
- gcp_signup_offer: 2,
- cluster_security_warning: 3,
- ultimate_trial: 4, # EE-only
- geo_enable_hashed_storage: 5, # EE-only
- geo_migrate_hashed_storage: 6, # EE-only
- canary_deployment: 7, # EE-only
- gold_trial_billings: 8, # EE-only
- suggest_popover_dismissed: 9,
- tabs_position_highlight: 10,
- threat_monitoring_info: 11, # EE-only
- two_factor_auth_recovery_settings_check: 12, # EE-only
- web_ide_alert_dismissed: 16, # no longer in use
- active_user_count_threshold: 18, # EE-only
- buy_pipeline_minutes_notification_dot: 19, # EE-only
- personal_access_token_expiry: 21, # EE-only
- suggest_pipeline: 22,
- customize_homepage: 23,
- feature_flags_new_version: 24,
- registration_enabled_callout: 25,
- new_user_signups_cap_reached: 26, # EE-only
- unfinished_tag_cleanup_callout: 27,
- eoa_bronze_plan_banner: 28, # EE-only
- pipeline_needs_banner: 29,
- pipeline_needs_hover_tip: 30,
- web_ide_ci_environments_guidance: 31,
- security_configuration_upgrade_banner: 32,
- cloud_licensing_subscription_activation_banner: 33, # EE-only
- trial_status_reminder_d14: 34, # EE-only
- trial_status_reminder_d3: 35, # EE-only
- security_configuration_devops_alert: 36, # EE-only
- profile_personal_access_token_expiry: 37, # EE-only
- terraform_notification_dismissed: 38,
- security_newsletter_callout: 39,
- verification_reminder: 40 # EE-only
- }
-
- validates :feature_name,
- presence: true,
- uniqueness: { scope: :user_id },
- inclusion: { in: UserCallout.feature_names.keys }
-end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
new file mode 100644
index 00000000000..9a729072051
--- /dev/null
+++ b/app/models/users/callout.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Users
+ class Callout < ApplicationRecord
+ include Users::Calloutable
+
+ self.table_name = 'user_callouts'
+
+ enum feature_name: {
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3,
+ ultimate_trial: 4, # EE-only
+ geo_enable_hashed_storage: 5, # EE-only
+ geo_migrate_hashed_storage: 6, # EE-only
+ canary_deployment: 7, # EE-only
+ gold_trial_billings: 8, # EE-only
+ suggest_popover_dismissed: 9,
+ tabs_position_highlight: 10,
+ threat_monitoring_info: 11, # EE-only
+ two_factor_auth_recovery_settings_check: 12, # EE-only
+ web_ide_alert_dismissed: 16, # no longer in use
+ active_user_count_threshold: 18, # EE-only
+ buy_pipeline_minutes_notification_dot: 19, # EE-only
+ personal_access_token_expiry: 21, # EE-only
+ suggest_pipeline: 22,
+ customize_homepage: 23,
+ feature_flags_new_version: 24,
+ registration_enabled_callout: 25,
+ new_user_signups_cap_reached: 26, # EE-only
+ unfinished_tag_cleanup_callout: 27,
+ eoa_bronze_plan_banner: 28, # EE-only
+ pipeline_needs_banner: 29,
+ pipeline_needs_hover_tip: 30,
+ web_ide_ci_environments_guidance: 31,
+ security_configuration_upgrade_banner: 32,
+ cloud_licensing_subscription_activation_banner: 33, # EE-only
+ trial_status_reminder_d14: 34, # EE-only
+ trial_status_reminder_d3: 35, # EE-only
+ security_configuration_devops_alert: 36, # EE-only
+ profile_personal_access_token_expiry: 37, # EE-only
+ terraform_notification_dismissed: 38,
+ security_newsletter_callout: 39,
+ verification_reminder: 40 # EE-only
+ }
+
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: :user_id },
+ inclusion: { in: Users::Callout.feature_names.keys }
+ end
+end
diff --git a/app/models/users/calloutable.rb b/app/models/users/calloutable.rb
new file mode 100644
index 00000000000..280a819e4d5
--- /dev/null
+++ b/app/models/users/calloutable.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Users
+ module Calloutable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :user
+
+ validates :user, presence: true
+ end
+
+ def dismissed_after?(dismissed_after)
+ dismissed_at > dismissed_after
+ end
+ end
+end
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 540d1a1d242..da9b95fd718 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -2,7 +2,7 @@
module Users
class GroupCallout < ApplicationRecord
- include Calloutable
+ include Users::Calloutable
self.table_name = 'user_group_callouts'
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index b310f8fff15..3bd92ebc942 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -15,19 +15,8 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
Gitlab::Highlight.highlight(
blob.path,
- limited_blob_data(to: to),
- language: language,
- plain: plain
- )
- end
-
- def highlight_transformed(plain: nil)
- load_all_blob_data
-
- Gitlab::Highlight.highlight(
- blob.path,
- transformed_blob_data,
- language: transformed_blob_language,
+ blob_data(to),
+ language: blob_language,
plain: plain
)
end
@@ -38,6 +27,14 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
highlight(plain: false)
end
+ def blob_data(to)
+ @_blob_data ||= Gitlab::Diff::CustomDiff.transformed_blob_data(blob) || limited_blob_data(to: to)
+ end
+
+ def blob_language
+ @_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || language
+ end
+
def raw_plain_data
blob.data unless blob.binary?
end
@@ -134,23 +131,6 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
def language
blob.language_from_gitattributes
end
-
- def transformed_blob_language
- @transformed_blob_language ||= blob.path.ends_with?('.ipynb') ? 'md' : language
- end
-
- def transformed_blob_data
- @transformed_blob ||= if blob.path.ends_with?('.ipynb') && blob.transformed_for_diff
- IpynbDiff.transform(blob.data,
- raise_errors: true,
- options: { include_metadata: false, cell_decorator: :percent })
- end
-
- @transformed_blob ||= blob.data
- rescue IpynbDiff::InvalidNotebookError => e
- Gitlab::ErrorTracking.log_exception(e)
- blob.data
- end
end
BlobPresenter.prepend_mod_with('BlobPresenter')
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index bd60d60c8db..b9c71e6d97b 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -73,7 +73,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :user_callouts_path do |_merge_request|
- user_callouts_path
+ callouts_path
end
expose :suggest_pipeline_feature_id do |_merge_request|
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index be21ed5b73d..89fe4ff9f60 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -2,6 +2,8 @@
module Ci
class RetryBuildService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def self.clone_accessors
%i[pipeline project ref tag options name
allow_failure stage stage_id stage_idx trigger_request
@@ -45,6 +47,11 @@ module Ci
job.save!
end
end
+
+ if create_deployment_in_separate_transaction?
+ clone_deployment!(new_build, build)
+ end
+
build.reset # refresh the data to get new values of `retried` and `processed`.
new_build
@@ -63,7 +70,9 @@ module Ci
def clone_build(build)
project.builds.new(build_attributes(build)).tap do |new_build|
- new_build.assign_attributes(deployment_attributes_for(new_build, build))
+ unless create_deployment_in_separate_transaction?
+ new_build.assign_attributes(deployment_attributes_for(new_build, build))
+ end
end
end
@@ -72,6 +81,11 @@ module Ci
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end
+ if create_deployment_in_separate_transaction? && build.persisted_environment.present?
+ attributes[:metadata_attributes] ||= {}
+ attributes[:metadata_attributes][:expanded_environment_name] = build.expanded_environment_name
+ end
+
attributes[:user] = current_user
attributes
end
@@ -80,6 +94,26 @@ module Ci
::Gitlab::Ci::Pipeline::Seed::Build
.deployment_attributes_for(new_build, old_build.persisted_environment)
end
+
+ def clone_deployment!(new_build, old_build)
+ return unless old_build.deployment.present?
+
+ # We should clone the previous deployment attributes instead of initializing
+ # new object with `Seed::Deployment`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/347206
+ deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
+ .new(new_build, old_build.persisted_environment).to_resource
+
+ return unless deployment
+
+ new_build.create_deployment!(deployment.attributes)
+ end
+
+ def create_deployment_in_separate_transaction?
+ strong_memoize(:create_deployment_in_separate_transaction) do
+ ::Feature.enabled?(:create_deployment_in_separate_transaction, project, default_enabled: :yaml)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
index ad65a9afa6b..a3d94e888df 100644
--- a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
+++ b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
@@ -15,12 +15,22 @@ module MergeRequests
def execute
line_position = position.line_range["end"] || position.line_range["start"]
- diff_line_index = diff_lines.find_index do |l|
- if line_position["new_line"]
- l.new_line == line_position["new_line"]
- elsif line_position["old_line"]
- l.old_line == line_position["old_line"]
+ found_line = false
+ diff_line_index = -1
+ diff_lines.each_with_index do |l, i|
+ if found_line
+ if !l.type
+ break
+ elsif l.type == 'new'
+ diff_line_index = i
+ break
+ end
+ else
+ # Find the old line
+ found_line = l.old_line == line_position["new_line"]
end
+
+ diff_line_index = i
end
initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max
last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min
diff --git a/app/services/users/dismiss_user_callout_service.rb b/app/services/users/dismiss_callout_service.rb
index 96f3f3acb57..4324e6232c2 100644
--- a/app/services/users/dismiss_user_callout_service.rb
+++ b/app/services/users/dismiss_callout_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
- class DismissUserCalloutService < BaseContainerService
+ class DismissCalloutService < BaseContainerService
def execute
callout.tap do |record|
record.update(dismissed_at: Time.current) if record.valid?
diff --git a/app/services/users/dismiss_group_callout_service.rb b/app/services/users/dismiss_group_callout_service.rb
index 8afee6a8187..f482142b911 100644
--- a/app/services/users/dismiss_group_callout_service.rb
+++ b/app/services/users/dismiss_group_callout_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
- class DismissGroupCalloutService < DismissUserCalloutService
+ class DismissGroupCalloutService < DismissCalloutService
private
def callout
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
index ece0f7ca4d9..3aba91e8765 100644
--- a/app/views/admin/dashboard/_security_newsletter_callout.html.haml
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -5,7 +5,7 @@
variant: :tip,
alert_class: 'js-security-newsletter-callout',
is_contained: true,
- alert_data: { feature_id: UserCalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' },
close_button_data: { testid: 'close-security-newsletter-callout' } do
.gl-alert-body
= s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 81f4be9fce5..9d249931a34 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,5 +1,5 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
-.gcp-signup-offer.gl-alert.gl-alert-info.gl-my-3{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
+.gcp-signup-offer.gl-alert.gl-alert-info.gl-my-3{ role: 'alert', data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path } }
.gl-alert-container
%button.js-close.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index 5683b4207b4..1b5a932a09a 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,3 +1,2 @@
-%ul.nav-links.new-session-tabs.single-tab.nav-tabs.nav
- %li.nav-item
- %a.nav-link.active= tab_title
+= gl_tabs_nav({ class: 'new-session-tabs gl-border-0' }) do
+ = gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1' }
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index 2901c8fa46b..f6d05959d2e 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -18,6 +18,6 @@
"gid_prefix": container_repository_gid_prefix,
connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s,
- user_callouts_path: user_callouts_path,
- user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
+ user_callouts_path: callouts_path,
+ user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s } }
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index ed3f2b0c6db..bb409190dd8 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -15,7 +15,7 @@
track_label: 'invite_members_banner',
invite_members_path: group_group_members_path(@group),
callouts_path: group_callouts_path,
- callouts_feature_id: UserCalloutsHelper::INVITE_MEMBERS_BANNER,
+ callouts_feature_id: Users::GroupCalloutsHelper::INVITE_MEMBERS_BANNER,
group_id: @group.id } }
= render 'groups/invite_members_modal', group: @group
diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml
index 25a7f7ba9d7..90f3ac61614 100644
--- a/app/views/layouts/header/_registration_enabled_callout.html.haml
+++ b/app/views/layouts/header/_registration_enabled_callout.html.haml
@@ -4,7 +4,7 @@
title: _('Open registration is enabled on your instance.'),
variant: :warning,
alert_class: 'js-registration-enabled-callout',
- alert_data: { feature_id: UserCalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: user_callouts_path },
+ alert_data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: callouts_path },
close_button_data: { testid: 'close-registration-enabled-callout' } do
.gl-alert-body
= html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\" class=\"gl-link\">".html_safe, anchorClose: '</a>'.html_safe }
diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml
index 097475d2928..9fef9864475 100644
--- a/app/views/projects/feature_flags/new.html.haml
+++ b/app/views/projects/feature_flags/new.html.haml
@@ -6,8 +6,8 @@
#js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json),
feature_flags_path: project_feature_flags_path(@project),
environments_endpoint: search_project_environments_path(@project, format: :json),
- user_callouts_path: user_callouts_path,
- user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
+ user_callouts_path: callouts_path,
+ user_callout_id: Users::CalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scope-environments-with-specs'),
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 44336b95e0f..7af825b2819 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -7,4 +7,7 @@
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
-#js-job-vue-app{ data: jobs_data }
+- if @build.is_a? ::Ci::Build
+ #js-job-page{ data: jobs_data }
+- else
+ #js-bridge-page{ data: bridge_data(@build) }
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index cfdbf3410b1..03927cd3bfa 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -22,6 +22,6 @@
"cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project),
connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s,
- user_callouts_path: user_callouts_path,
- user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
+ user_callouts_path: callouts_path,
+ user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s, } }
diff --git a/app/views/root/index.html.haml b/app/views/root/index.html.haml
index 97dd8e133f5..4b1ac213d68 100644
--- a/app/views/root/index.html.haml
+++ b/app/views/root/index.html.haml
@@ -3,8 +3,8 @@
.gl-display-none.gl-md-display-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
.js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
- callouts_path: user_callouts_path,
- callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE,
+ callouts_path: callouts_path,
+ callouts_feature_id: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE,
track_label: 'home_page' } }
= render template: 'dashboard/projects/index'
diff --git a/app/views/shared/_flash_user_callout.html.haml b/app/views/shared/_flash_user_callout.html.haml
index d8032ac521d..7b2d59407b4 100644
--- a/app/views/shared/_flash_user_callout.html.haml
+++ b/app/views/shared/_flash_user_callout.html.haml
@@ -1,4 +1,4 @@
-- callout_data = { uid: "callout_feature_#{feature_name}_dismissed", feature_id: feature_name, dismiss_endpoint: user_callouts_path }
+- callout_data = { uid: "callout_feature_#{feature_name}_dismissed", feature_id: feature_name, dismiss_endpoint: callouts_path }
- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil)
.flash-container.flash-container-page.user-callout{ data: callout_data }
diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
index d4764d1a5d9..e7239661313 100644
--- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
+++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
@@ -1,7 +1,7 @@
= render 'shared/global_alert',
variant: :warning,
alert_class: 'js-recovery-settings-callout',
- alert_data: { feature_id: UserCalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, dismiss_endpoint: callouts_path, defer_links: 'true' },
close_button_data: { testid: 'close-account-recovery-regular-check-callout' } do
.gl-alert-body
= s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.')
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 3524a1b17ea..8c49977fe82 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -3,24 +3,20 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
- %li.nav-item
- = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do
- = _('Issues')
- %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
+ = gl_tabs_nav({ class: %w[scrolling-tabs js-milestone-tabs] }) do
+ = gl_tab_link_to '#tab-issues', item_active: true, data: { endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do
+ = _('Issues')
+ = gl_tab_counter_badge milestone.issues_visible_to_user(current_user).size
- if milestone.merge_requests_enabled?
- %li.nav-item
- = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests', show_project_name: show_project_name) } do
- = _('Merge requests')
- %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
- %li.nav-item
- = link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do
- = _('Participants')
- %span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
- %li.nav-item
- = link_to '#tab-labels', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'labels') } do
- = _('Labels')
- %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
+ = gl_tab_link_to '#tab-merge-requests', data: { endpoint: milestone_tab_path(milestone, 'merge_requests', show_project_name: show_project_name) } do
+ = _('Merge requests')
+ = gl_tab_counter_badge milestone.merge_requests_visible_to_user(current_user).size
+ = gl_tab_link_to '#tab-participants', data: { endpoint: milestone_tab_path(milestone, 'participants') } do
+ = _('Participants')
+ = gl_tab_counter_badge milestone.issue_participants_visible_by_user(current_user).count
+ = gl_tab_link_to '#tab-labels', data: { endpoint: milestone_tab_path(milestone, 'labels') } do
+ = _('Labels')
+ = gl_tab_counter_badge milestone.issue_labels_visible_by_user(current_user).count
.tab-content.milestone-content
.tab-pane.active#tab-issues
diff --git a/config/feature_flags/development/ci_retry_downstream_pipeline.yml b/config/feature_flags/development/ci_retry_downstream_pipeline.yml
new file mode 100644
index 00000000000..0eac0330188
--- /dev/null
+++ b/config/feature_flags/development/ci_retry_downstream_pipeline.yml
@@ -0,0 +1,8 @@
+---
+name: ci_retry_downstream_pipeline
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76115
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347424
+milestone: '14.16'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/use_cmark_renderer.yml b/config/feature_flags/development/use_cmark_renderer.yml
index b47031a6924..5e4ea534590 100644
--- a/config/feature_flags/development/use_cmark_renderer.yml
+++ b/config/feature_flags/development/use_cmark_renderer.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345744
milestone: '14.6'
type: development
group: group::project management
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/use_optimized_group_labels_query.yml b/config/feature_flags/development/use_optimized_group_labels_query.yml
index 37e2525d03e..82cecb5f337 100644
--- a/config/feature_flags/development/use_optimized_group_labels_query.yml
+++ b/config/feature_flags/development/use_optimized_group_labels_query.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344957
milestone: '14.5'
type: development
group: group::workspace
-default_enabled: false
+default_enabled: true
diff --git a/config/initializers/active_record_database_tasks.rb b/config/initializers/active_record_database_tasks.rb
new file mode 100644
index 00000000000..f06174262a9
--- /dev/null
+++ b/config/initializers/active_record_database_tasks.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+return unless Gitlab.ee?
+
+ActiveSupport.on_load(:active_record) do
+ ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(Gitlab::Patch::GeoDatabaseTasks)
+end
diff --git a/config/initializers/database_config.rb b/config/initializers/database_config.rb
index 1eb9d12812a..a3172fae027 100644
--- a/config/initializers/database_config.rb
+++ b/config/initializers/database_config.rb
@@ -8,11 +8,11 @@ Gitlab.ee do
config.geo_database = config_for(:database_geo)
end
end
-end
-Gitlab.ee do
if Gitlab::Runtime.sidekiq? && Gitlab::Geo.geo_database_configured?
- Rails.configuration.geo_database['pool'] = Gitlab::Database.default_pool_size
- Geo::TrackingBase.establish_connection(Rails.configuration.geo_database)
+ # The Geo::TrackingBase model does not yet use connects_to. So,
+ # this will not properly support geo: from config/databse.yml
+ # file yet. This is ACK of the current state and will be fixed.
+ Geo::TrackingBase.establish_connection(Gitlab::Database.geo_db_config_with_default_pool_size)
end
end
diff --git a/config/initializers/validate_database_config.rb b/config/initializers/validate_database_config.rb
index a651db8b783..d5e73cdc1ee 100644
--- a/config/initializers/validate_database_config.rb
+++ b/config/initializers/validate_database_config.rb
@@ -16,11 +16,11 @@ if configurations = ActiveRecord::Base.configurations.configurations
"The `main:` database needs to be defined as a first configuration item instead of `#{configurations.first.name}`."
end
- rejected_config_names = configurations.map(&:name).to_set - Gitlab::Database::DATABASE_NAMES
+ rejected_config_names = configurations.map(&:name).to_set - Gitlab::Database.all_database_names
if rejected_config_names.any?
raise "ERROR: This installation of GitLab uses unsupported database names " \
"in 'config/database.yml': #{rejected_config_names.to_a.join(", ")}. The only supported ones are " \
- "#{Gitlab::Database::DATABASE_NAMES.join(", ")}."
+ "#{Gitlab::Database.all_database_names.join(", ")}."
end
replicas_config_names = configurations.select(&:replica?).map(&:name)
diff --git a/config/routes.rb b/config/routes.rb
index 94d36961b32..6aa5e0a6869 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -145,7 +145,7 @@ Rails.application.routes.draw do
get 'acme-challenge/' => 'acme_challenges#show'
# UserCallouts
- resources :user_callouts, only: [:create]
+ resources :user_callouts, controller: 'users/callouts', only: [:create] # remove after 14.6 2021-12-22 to handle mixed deployments
scope :ide, as: :ide, format: false do
get '/', to: 'ide#index'
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 01de59c3357..64dc56e18ec 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -61,6 +61,7 @@ scope '-/users', module: :users do
post :decline, on: :member
end
+ resources :callouts, only: [:create]
resources :group_callouts, only: [:create]
end
diff --git a/data/deprecations/14-5-runner-api-status-does-contain-paused.yml b/data/deprecations/14-5-runner-api-status-does-contain-paused.yml
index 8c7cde8a121..846e8824565 100644
--- a/data/deprecations/14-5-runner-api-status-does-contain-paused.yml
+++ b/data/deprecations/14-5-runner-api-status-does-contain-paused.yml
@@ -2,14 +2,13 @@
announcement_milestone: "14.5" # The milestone when this feature was first announced as deprecated.
removal_milestone: "15.0" # the milestone when this feature is planned to be removed
body: | # Do not modify this line, instead modify the lines below.
- Runner REST API will not return `paused` as a status in GitLab 15.0.
+ The GitLab Runner REST and GraphQL API endpoints will not return `paused` or `active` as a status in GitLab 15.0.
- Paused runners' status will only relate to runner contact status, such as:
- `online`, `offline`, or `not_connected`. Status `paused` will not appear when the runner is
- not active.
+ A runner's status will only relate to runner contact status, such as:
+ `online`, `offline`, or `not_connected`. Status `paused` or `active` will no longer appear.
When checking if a runner is `paused`, API users are advised to check the boolean attribute
- `active` to be `false` instead.
+ `active` to be `false` instead. When checking if a runner is `active`, check if `active` is `true`.
stage: Verify
tiers: [Core, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344648
diff --git a/data/deprecations/14-6-runner-api-status-renames-not_connected.yml b/data/deprecations/14-6-runner-api-status-renames-not_connected.yml
new file mode 100644
index 00000000000..ac79698cd50
--- /dev/null
+++ b/data/deprecations/14-6-runner-api-status-renames-not_connected.yml
@@ -0,0 +1,13 @@
+- name: "Deprecation of Runner status `not_connected` API value"
+ announcement_milestone: "14.6" # The milestone when this feature was first announced as deprecated.
+ removal_milestone: "15.0" # the milestone when this feature is planned to be removed
+ body: | # Do not modify this line, instead modify the lines below.
+ The GitLab Runner REST and GraphQL [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints
+ will return `never_contacted` instead of `not_connected` as the status values in 15.0.
+
+ Runners that have never contacted the GitLab instance will also return `stale` if created more than 3 months ago.
+ stage: Verify
+ tiers: [Core, Premium, Ultimate]
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347305
+ documentation_url: https://docs.gitlab.com/ee/api/runners.html
+ announcement_date: "2021-12-22"
diff --git a/db/migrate/20211201143042_create_lfs_object_states.rb b/db/migrate/20211201143042_create_lfs_object_states.rb
new file mode 100644
index 00000000000..91accbcd438
--- /dev/null
+++ b/db/migrate/20211201143042_create_lfs_object_states.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class CreateLfsObjectStates < Gitlab::Database::Migration[1.0]
+ VERIFICATION_STATE_INDEX_NAME = "index_lfs_object_states_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_lfs_object_states_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_lfs_object_states_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_lfs_object_states_needs_verification"
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :lfs_object_states, id: false do |t|
+ t.datetime_with_timezone :verification_started_at
+ t.datetime_with_timezone :verification_retry_at
+ t.datetime_with_timezone :verified_at
+ t.references :lfs_object, primary_key: true, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.integer :verification_retry_count, limit: 2
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+ t.text :verification_failure, limit: 255
+
+ t.index :verification_state, name: VERIFICATION_STATE_INDEX_NAME
+ t.index :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
+ t.index :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
+ t.index :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+
+ def down
+ drop_table :lfs_object_states
+ end
+end
diff --git a/db/schema_migrations/20211201143042 b/db/schema_migrations/20211201143042
new file mode 100644
index 00000000000..a5f0c8be842
--- /dev/null
+++ b/db/schema_migrations/20211201143042
@@ -0,0 +1 @@
+0d27ca1250d10b8915fa4523707044f9a8c2372110537f5639a1811aeb0858b8 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3545280f16f..0b37e27b003 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -15769,6 +15769,27 @@ CREATE SEQUENCE lfs_file_locks_id_seq
ALTER SEQUENCE lfs_file_locks_id_seq OWNED BY lfs_file_locks.id;
+CREATE TABLE lfs_object_states (
+ verification_started_at timestamp with time zone,
+ verification_retry_at timestamp with time zone,
+ verified_at timestamp with time zone,
+ lfs_object_id bigint NOT NULL,
+ verification_state smallint DEFAULT 0 NOT NULL,
+ verification_retry_count smallint,
+ verification_checksum bytea,
+ verification_failure text,
+ CONSTRAINT check_efe45a8ab3 CHECK ((char_length(verification_failure) <= 255))
+);
+
+CREATE SEQUENCE lfs_object_states_lfs_object_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE lfs_object_states_lfs_object_id_seq OWNED BY lfs_object_states.lfs_object_id;
+
CREATE TABLE lfs_objects (
id integer NOT NULL,
oid character varying NOT NULL,
@@ -21795,6 +21816,8 @@ ALTER TABLE ONLY ldap_group_links ALTER COLUMN id SET DEFAULT nextval('ldap_grou
ALTER TABLE ONLY lfs_file_locks ALTER COLUMN id SET DEFAULT nextval('lfs_file_locks_id_seq'::regclass);
+ALTER TABLE ONLY lfs_object_states ALTER COLUMN lfs_object_id SET DEFAULT nextval('lfs_object_states_lfs_object_id_seq'::regclass);
+
ALTER TABLE ONLY lfs_objects ALTER COLUMN id SET DEFAULT nextval('lfs_objects_id_seq'::regclass);
ALTER TABLE ONLY lfs_objects_projects ALTER COLUMN id SET DEFAULT nextval('lfs_objects_projects_id_seq'::regclass);
@@ -23513,6 +23536,9 @@ ALTER TABLE ONLY ldap_group_links
ALTER TABLE ONLY lfs_file_locks
ADD CONSTRAINT lfs_file_locks_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY lfs_object_states
+ ADD CONSTRAINT lfs_object_states_pkey PRIMARY KEY (lfs_object_id);
+
ALTER TABLE ONLY lfs_objects
ADD CONSTRAINT lfs_objects_pkey PRIMARY KEY (id);
@@ -26529,6 +26555,16 @@ CREATE UNIQUE INDEX index_lfs_file_locks_on_project_id_and_path ON lfs_file_lock
CREATE INDEX index_lfs_file_locks_on_user_id ON lfs_file_locks USING btree (user_id);
+CREATE INDEX index_lfs_object_states_failed_verification ON lfs_object_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
+
+CREATE INDEX index_lfs_object_states_needs_verification ON lfs_object_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
+
+CREATE INDEX index_lfs_object_states_on_lfs_object_id ON lfs_object_states USING btree (lfs_object_id);
+
+CREATE INDEX index_lfs_object_states_on_verification_state ON lfs_object_states USING btree (verification_state);
+
+CREATE INDEX index_lfs_object_states_pending_verification ON lfs_object_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
+
CREATE INDEX index_lfs_objects_on_file_store ON lfs_objects USING btree (file_store);
CREATE UNIQUE INDEX index_lfs_objects_on_oid ON lfs_objects USING btree (oid);
@@ -30333,6 +30369,9 @@ ALTER TABLE ONLY description_versions
ALTER TABLE ONLY clusters_kubernetes_namespaces
ADD CONSTRAINT fk_rails_40cc7ccbc3 FOREIGN KEY (cluster_project_id) REFERENCES cluster_projects(id) ON DELETE SET NULL;
+ALTER TABLE ONLY lfs_object_states
+ ADD CONSTRAINT fk_rails_4188448cd5 FOREIGN KEY (lfs_object_id) REFERENCES lfs_objects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY geo_node_namespace_links
ADD CONSTRAINT fk_rails_41ff5fb854 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md
index ca9388a3af3..5f98656482c 100644
--- a/doc/administration/geo/replication/datatypes.md
+++ b/doc/administration/geo/replication/datatypes.md
@@ -37,7 +37,7 @@ verification methods:
| Git | Group wiki repository | Geo with Gitaly | _Not implemented_ |
| Blobs | User uploads _(file system)_ | Geo with API | _Not implemented_ |
| Blobs | User uploads _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
-| Blobs | LFS objects _(file system)_ | Geo with API | _Not implemented_ |
+| Blobs | LFS objects _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | LFS objects _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | CI job artifacts _(file system)_ | Geo with API | _Not implemented_ |
| Blobs | CI job artifacts _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
@@ -190,7 +190,7 @@ successfully, you must replicate their data using some other means.
|[Project wiki repository](../../../user/project/wiki/) | **Yes** (10.2) | **Yes** (10.7) | No | |
|[Group wiki repository](../../../user/project/wiki/group.md) | [**Yes** (13.10)](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | No | No | Behind feature flag `geo_group_wiki_repository_replication`, enabled by default. |
|[Uploads](../../uploads.md) | **Yes** (10.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | No | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. |
-|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8922) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br><br>Behind feature flag `geo_lfs_object_replication`, enabled by default. |
+|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | **Yes**(14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is under development behind the feature flag `geo_lfs_object_verification` introduced in 14.6. |
|[Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|[Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|[CI job artifacts](../../../ci/pipelines/job_artifacts.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. Job logs also verified on transfer. |
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 237b5561e70..c6a1a93af7c 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -187,16 +187,25 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `geo_repositories` | Gauge | 10.2 | Total number of repositories available on primary | `url` |
| `geo_repositories_synced` | Gauge | 10.2 | Number of repositories synced on secondary | `url` |
| `geo_repositories_failed` | Gauge | 10.2 | Number of repositories failed to sync on secondary | `url` |
-| `geo_lfs_objects` | Gauge | 10.2 | Total number of LFS objects available on primary | `url` |
-| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of LFS objects synced on secondary | `url` |
-| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | `url` |
+| `geo_lfs_objects` | Gauge | 10.2 | Number of LFS objects on primary | `url` |
+| `geo_lfs_objects_checksummed` | Gauge | 14.6 | Number of LFS objects checksummed successfully on primary | `url` |
+| `geo_lfs_objects_checksum_failed` | Gauge | 14.6 | Number of LFS objects failed to calculate the checksum on primary | `url` |
+| `geo_lfs_objects_checksum_total` | Gauge | 14.6 | Number of LFS objects tried to checksum on primary | `url` |
+| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of syncable LFS objects synced on secondary | `url` |
+| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of syncable LFS objects failed to sync on secondary | `url` |
+| `geo_lfs_objects_registry` | Gauge | 14.6 | Number of LFS objects in the registry | `url` |
+| `geo_lfs_objects_verified` | Gauge | 14.6 | Number of LFS objects verified on secondary | `url` |
+| `geo_lfs_objects_verification_failed` | Gauge | 14.6 | Number of LFS objects' verifications failed on secondary | `url` |
+| `geo_lfs_objects_verification_total` | Gauge | 14.6 | Number of LFS objects' verifications tried on secondary | `url` |LFS objects failed to sync on secondary | `url` |
+| `geo_attachments` | Gauge | 10.2 | Total number of file attachments available on primary | `url` |
+| `geo_attachments_synced` | Gauge | 10.2 | Number of attachments synced on secondary | `url` |
+| `geo_attachments_failed` | Gauge | 10.2 | Number of attachments failed to sync on secondary | `url` |
| `geo_last_event_id` | Gauge | 10.2 | Database ID of the latest event log entry on the primary | `url` |
| `geo_last_event_timestamp` | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | `url` |
| `geo_cursor_last_event_id` | Gauge | 10.2 | Last database ID of the event log processed by the secondary | `url` |
| `geo_cursor_last_event_timestamp` | Gauge | 10.2 | Last UNIX timestamp of the event log processed by the secondary | `url` |
| `geo_status_failed_total` | Counter | 10.2 | Number of times retrieving the status from the Geo Node failed | `url` |
| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | `url` |
-| `geo_lfs_objects_synced_missing_on_primary` | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | `url` |
| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | `url` |
| `geo_repositories_checksummed` | Gauge | 10.7 | Number of repositories checksummed on primary | `url` |
| `geo_repositories_checksum_failed` | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | `url` |
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index 0758dba6f08..3952a87e698 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -307,11 +307,18 @@ Example response:
"health_status": "Healthy",
"missing_oauth_application": false,
"db_replication_lag_seconds": null,
- "lfs_objects_count": 0,
+ "lfs_objects_count": 5,
+ "lfs_objects_checksum_total_count": 5,
+ "lfs_objects_checksummed_count": 5,
+ "lfs_objects_checksum_failed_count": 0,
"lfs_objects_synced_count": null,
"lfs_objects_failed_count": null,
- "lfs_objects_synced_missing_on_primary_count": 0,
+ "lfs_objects_registry_count": null,
+ "lfs_objects_verification_total_count": null,
+ "lfs_objects_verified_count": null,
+ "lfs_objects_verification_failed_count": null,
"lfs_objects_synced_in_percentage": "0.00%",
+ "lfs_objects_verified_in_percentage": "0.00%",
"job_artifacts_count": 2,
"job_artifacts_synced_count": null,
"job_artifacts_failed_count": null,
@@ -468,11 +475,18 @@ Example response:
"health_status": "Healthy",
"missing_oauth_application": false,
"db_replication_lag_seconds": 0,
- "lfs_objects_count": 0,
- "lfs_objects_synced_count": 0,
- "lfs_objects_failed_count": 0,
- "lfs_objects_synced_missing_on_primary_count": 0,
+ "lfs_objects_count": 5,
+ "lfs_objects_checksum_total_count": 5,
+ "lfs_objects_checksummed_count": 5,
+ "lfs_objects_checksum_failed_count": 0,
+ "lfs_objects_synced_count": null,
+ "lfs_objects_failed_count": null,
+ "lfs_objects_registry_count": null,
+ "lfs_objects_verification_total_count": null,
+ "lfs_objects_verified_count": null,
+ "lfs_objects_verification_failed_count": null,
"lfs_objects_synced_in_percentage": "0.00%",
+ "lfs_objects_verified_in_percentage": "0.00%",
"job_artifacts_count": 2,
"job_artifacts_synced_count": 1,
"job_artifacts_failed_count": 1,
@@ -633,11 +647,18 @@ Example response:
"health_status": "Healthy",
"missing_oauth_application": false,
"db_replication_lag_seconds": 0,
- "lfs_objects_count": 0,
- "lfs_objects_synced_count": 0,
- "lfs_objects_failed_count": 0,
- "lfs_objects_synced_missing_on_primary_count": 0,
+ "lfs_objects_count": 5,
+ "lfs_objects_checksum_total_count": 5,
+ "lfs_objects_checksummed_count": 5,
+ "lfs_objects_checksum_failed_count": 0,
+ "lfs_objects_synced_count": null,
+ "lfs_objects_failed_count": null,
+ "lfs_objects_registry_count": null,
+ "lfs_objects_verification_total_count": null,
+ "lfs_objects_verified_count": null,
+ "lfs_objects_verification_failed_count": null,
"lfs_objects_synced_in_percentage": "0.00%",
+ "lfs_objects_verified_in_percentage": "0.00%",
"job_artifacts_count": 2,
"job_artifacts_synced_count": 1,
"job_artifacts_failed_count": 1,
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e0b93119042..4d869f198c0 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -15975,7 +15975,8 @@ Values for sorting runners.
| Value | Description |
| ----- | ----------- |
| <a id="cirunnerstatusactive"></a>`ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
-| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact. |
+| <a id="cirunnerstatusnever_contacted"></a>`NEVER_CONTACTED` | Runner that has never contacted this instance. Set legacyMode to null to utilize this value. Will replace NOT_CONNECTED starting in 15.0. |
+| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` **{warning-solid}** | **Deprecated** in 14.6. Use NEVER_CONTACTED instead. NEVER_CONTACTED will have a slightly different scope starting in 15.0, with STALE being returned instead after 3 months of no contact. |
| <a id="cirunnerstatusoffline"></a>`OFFLINE` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline. |
| <a id="cirunnerstatusonline"></a>`ONLINE` | Runner that contacted this instance within the last 2 hours. |
| <a id="cirunnerstatuspaused"></a>`PAUSED` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index df25146e836..14fdbeb0307 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -124,6 +124,15 @@ Long term service and support (LTSS) for SUSE Linux Enterprise Server (SLES) 12
Announced: 2021-11-22
+### Deprecation of Runner status `not_connected` API value
+
+The GitLab Runner REST and GraphQL [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints
+will return `never_contacted` instead of `not_connected` as the status values in 15.0.
+
+Runners that have never contacted the GitLab instance will also return `stale` if created more than 3 months ago.
+
+Announced: 2021-12-22
+
### Deprecation of bundler-audit Dependency Scanning tool
As of 14.6 bundler-audit is being deprecated from Dependency Scanning. It will continue to be in our CI/CD template while deprecated. We are removing bundler-audit from Dependency Scanning on May 22, 2022 in 15.0. After this removal Ruby scanning functionality will not be affected as it is still being covered by Gemnasium.
@@ -200,14 +209,13 @@ Announced: 2021-11-22
### REST API Runner will not contain `paused`
-Runner REST API will not return `paused` as a status in GitLab 15.0.
+The GitLab Runner REST and GraphQL API endpoints will not return `paused` or `active` as a status in GitLab 15.0.
-Paused runners' status will only relate to runner contact status, such as:
-`online`, `offline`, or `not_connected`. Status `paused` will not appear when the runner is
-not active.
+A runner's status will only relate to runner contact status, such as:
+`online`, `offline`, or `not_connected`. Status `paused` or `active` will no longer appear.
When checking if a runner is `paused`, API users are advised to check the boolean attribute
-`active` to be `false` instead.
+`active` to be `false` instead. When checking if a runner is `active`, check if `active` is `true`.
Announced: 2021-11-22
diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb
index 586f4d1e622..00a38f02141 100644
--- a/lib/banzai/filter/footnote_filter.rb
+++ b/lib/banzai/filter/footnote_filter.rb
@@ -37,7 +37,7 @@ module Banzai
XPATH_SECTION_OLD = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION_OLD).freeze
def call
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
# Sanitization stripped off the section class - add it back in
return doc unless section_node = doc.at_xpath(XPATH_SECTION)
@@ -52,26 +52,26 @@ module Banzai
rand_suffix = "-#{random_number}"
modified_footnotes = {}
- xpath_footnote = if Feature.enabled?(:use_cmark_renderer)
+ xpath_footnote = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
XPATH_FOOTNOTE
else
Gitlab::Utils::Nokogiri.css_to_xpath('sup > a[id]')
end
doc.xpath(xpath_footnote).each do |link_node|
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
ref_num.gsub!(/[[:punct:]]/, '\\\\\&')
else
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX_OLD)
end
- css = Feature.enabled?(:use_cmark_renderer) ? "section[data-footnotes] li[id=#{fn_id(ref_num)}]" : "li[id=#{fn_id(ref_num)}]"
+ css = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? "section[data-footnotes] li[id=#{fn_id(ref_num)}]" : "li[id=#{fn_id(ref_num)}]"
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath(css)
footnote_node = doc.at_xpath(node_xpath)
if footnote_node || modified_footnotes[ref_num]
- next if Feature.disabled?(:use_cmark_renderer) && !INTEGER_PATTERN.match?(ref_num)
+ next if Feature.disabled?(:use_cmark_renderer, default_enabled: :yaml) && !INTEGER_PATTERN.match?(ref_num)
link_node[:href] += rand_suffix
link_node[:id] += rand_suffix
@@ -103,12 +103,12 @@ module Banzai
end
def fn_id(num)
- prefix = Feature.enabled?(:use_cmark_renderer) ? FOOTNOTE_ID_PREFIX : FOOTNOTE_ID_PREFIX_OLD
+ prefix = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? FOOTNOTE_ID_PREFIX : FOOTNOTE_ID_PREFIX_OLD
"#{prefix}#{num}"
end
def fnref_id(num)
- prefix = Feature.enabled?(:use_cmark_renderer) ? FOOTNOTE_LINK_ID_PREFIX : FOOTNOTE_LINK_ID_PREFIX_OLD
+ prefix = Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? FOOTNOTE_LINK_ID_PREFIX : FOOTNOTE_LINK_ID_PREFIX_OLD
"#{prefix}#{num}"
end
end
diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb
index a25ebedf029..dc94e3c925a 100644
--- a/lib/banzai/filter/markdown_engines/common_mark.rb
+++ b/lib/banzai/filter/markdown_engines/common_mark.rb
@@ -42,11 +42,11 @@ module Banzai
def initialize(context)
@context = context
- @renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options) if Feature.disabled?(:use_cmark_renderer)
+ @renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options) if Feature.disabled?(:use_cmark_renderer, default_enabled: :yaml)
end
def render(text)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
CommonMarker.render_html(text, render_options, extensions)
else
doc = CommonMarker.render_doc(text, PARSE_OPTIONS, extensions)
@@ -58,7 +58,7 @@ module Banzai
private
def extensions
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
EXTENSIONS
else
EXTENSIONS + [
@@ -72,7 +72,7 @@ module Banzai
end
def render_options_no_sourcepos
- Feature.enabled?(:use_cmark_renderer) ? RENDER_OPTIONS_C : RENDER_OPTIONS_RUBY
+ Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) ? RENDER_OPTIONS_C : RENDER_OPTIONS_RUBY
end
def render_options_sourcepos
diff --git a/lib/banzai/filter/markdown_post_escape_filter.rb b/lib/banzai/filter/markdown_post_escape_filter.rb
index ccffe1bfbb1..b979b7573ae 100644
--- a/lib/banzai/filter/markdown_post_escape_filter.rb
+++ b/lib/banzai/filter/markdown_post_escape_filter.rb
@@ -42,7 +42,7 @@ module Banzai
private
def lang_tag
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
Gitlab::Utils::Nokogiri.css_to_xpath('pre')
else
Gitlab::Utils::Nokogiri.css_to_xpath('code')
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index e67cdc7df12..3f160960d23 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -26,7 +26,7 @@ module Banzai
def lang_tag
@lang_tag ||=
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
Gitlab::Utils::Nokogiri.css_to_xpath('pre[lang="plantuml"] > code').freeze
else
Gitlab::Utils::Nokogiri.css_to_xpath('pre > code[lang="plantuml"]').freeze
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 16ca05368ae..d5f45ff7689 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -28,7 +28,7 @@ module Banzai
allowlist[:attributes]['li'] = %w[id]
allowlist[:transformers].push(self.class.remove_non_footnote_ids)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
# Allow section elements with data-footnotes attribute
allowlist[:elements].push('section')
allowlist[:attributes]['section'] = %w(data-footnotes)
@@ -61,7 +61,7 @@ module Banzai
return unless node.name == 'a' || node.name == 'li'
return unless node.has_attribute?('id')
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN
return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN
else
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 66bd86c5bb4..cd9b5fe13ad 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -70,7 +70,7 @@ module Banzai
private
def parse_lang_params(node)
- node = node.parent if Feature.enabled?(:use_cmark_renderer)
+ node = node.parent if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
# Commonmarker's FULL_INFO_STRING render option works with the space delimiter.
# But the current behavior of GitLab's markdown renderer is different - it grabs everything as the single
@@ -92,7 +92,7 @@ module Banzai
language, language_params = language.split(LANG_PARAMS_DELIMITER, 2)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
language_params = [node.attr('data-meta'), language_params].compact.join(' ')
end
diff --git a/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb b/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
index 6dbe6f691f6..3ada3f947ee 100644
--- a/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
+++ b/lib/gitlab/asciidoc/syntax_highlighter/html_pipeline_adapter.rb
@@ -7,7 +7,7 @@ module Gitlab
register_for 'gitlab-html-pipeline'
def format(node, lang, opts)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
%(<pre #{lang ? %[lang="#{lang}"] : ''}><code>#{node.content}</code></pre>)
else
%(<pre><code #{lang ? %[ lang="#{lang}"] : ''}>#{node.content}</code></pre>)
diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb
index d66d4b20bba..eaa87157716 100644
--- a/lib/gitlab/ci/status/bridge/common.rb
+++ b/lib/gitlab/ci/status/bridge/common.rb
@@ -16,7 +16,11 @@ module Gitlab
def details_path
return unless can?(user, :read_pipeline, downstream_pipeline)
- project_pipeline_path(downstream_project, downstream_pipeline)
+ if Feature.enabled?(:ci_retry_downstream_pipeline, subject.project, default_enabled: :yaml)
+ project_job_path(subject.project, subject)
+ else
+ project_pipeline_path(downstream_project, downstream_pipeline)
+ end
end
def has_action?
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 1a464555278..f9c346a272f 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -72,6 +72,10 @@ module Gitlab
}.with_indifferent_access.freeze
end
+ def self.all_database_names
+ DATABASE_NAMES
+ end
+
# We configure the database connection pool size automatically based on the
# configured concurrency. We also add some headroom, to make sure we don't
# run out of connections when more threads besides the 'user-facing' ones
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index ddad56061a4..2469c5dd44b 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -287,6 +287,7 @@ ldap_group_links: :gitlab_main
lfs_file_locks: :gitlab_main
lfs_objects: :gitlab_main
lfs_objects_projects: :gitlab_main
+lfs_object_states: :gitlab_main
licenses: :gitlab_main
lists: :gitlab_main
list_user_preferences: :gitlab_main
diff --git a/lib/gitlab/diff/custom_diff.rb b/lib/gitlab/diff/custom_diff.rb
new file mode 100644
index 00000000000..e1d3cea4306
--- /dev/null
+++ b/lib/gitlab/diff/custom_diff.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+module Gitlab
+ module Diff
+ module CustomDiff
+ class << self
+ def preprocess_before_diff(path, old_blob, new_blob)
+ return unless path.ends_with? '.ipynb'
+
+ transformed_diff(old_blob&.data, new_blob&.data)&.tap do
+ transformed_for_diff(new_blob, old_blob)
+ Gitlab::AppLogger.info({ message: 'IPYNB_DIFF_GENERATED' })
+ end
+ rescue IpynbDiff::InvalidNotebookError => e
+ Gitlab::ErrorTracking.log_exception(e)
+ nil
+ end
+
+ def transformed_diff(before, after)
+ transformed_diff = IpynbDiff.diff(before, after,
+ diff_opts: { context: 5, include_diff_info: true },
+ transform_options: { cell_decorator: :percent },
+ raise_if_invalid_notebook: true)
+ strip_diff_frontmatter(transformed_diff)
+ end
+
+ def transformed_blob_language(blob)
+ 'md' if transformed_for_diff?(blob)
+ end
+
+ def transformed_blob_data(blob)
+ if transformed_for_diff?(blob)
+ IpynbDiff.transform(blob.data,
+ raise_errors: true,
+ options: { include_metadata: false, cell_decorator: :percent })
+ end
+ end
+
+ def strip_diff_frontmatter(diff_content)
+ diff_content.scan(/.*\n/)[2..-1]&.join('') if diff_content.present?
+ end
+
+ def blobs_with_transformed_diffs
+ @blobs_with_transformed_diffs ||= {}
+ end
+
+ def transformed_for_diff?(blob)
+ blobs_with_transformed_diffs[blob]
+ end
+
+ def transformed_for_diff(*blobs)
+ blobs.each do |b|
+ blobs_with_transformed_diffs[b] = true if b
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 83f242ff902..d9860d9fb86 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -44,7 +44,11 @@ module Gitlab
new_blob_lazy
old_blob_lazy
- preprocess_before_diff(diff) if Feature.enabled?(:jupyter_clean_diffs, repository.project, default_enabled: true)
+ diff.diff = Gitlab::Diff::CustomDiff.preprocess_before_diff(diff.new_path, old_blob_lazy, new_blob_lazy) || diff.diff if use_custom_diff?
+ end
+
+ def use_custom_diff?
+ strong_memoize(:_custom_diff_enabled) { Feature.enabled?(:jupyter_clean_diffs, repository.project, default_enabled: true) }
end
def position(position_marker, position_type: :text)
@@ -450,33 +454,6 @@ module Gitlab
find_renderable_viewer_class(classes)
end
- def preprocess_before_diff(diff)
- return unless diff.new_path.ends_with? '.ipynb'
-
- from = old_blob_lazy&.data
- to = new_blob_lazy&.data
-
- transformed_diff = IpynbDiff.diff(from, to,
- diff_opts: { context: 5, include_diff_info: true },
- transform_options: { cell_decorator: :percent },
- raise_if_invalid_notebook: true)
- new_diff = strip_diff_frontmatter(transformed_diff)
-
- if new_diff
- diff.diff = new_diff
- new_blob_lazy.transformed_for_diff = true if new_blob_lazy
- old_blob_lazy.transformed_for_diff = true if old_blob_lazy
- end
-
- Gitlab::AppLogger.info({ message: new_diff ? 'IPYNB_DIFF_GENERATED' : 'IPYNB_DIFF_NIL' })
- rescue IpynbDiff::InvalidNotebookError => e
- Gitlab::ErrorTracking.log_exception(e)
- end
-
- def strip_diff_frontmatter(diff_content)
- diff_content.scan(/.*\n/)[2..-1]&.join('') if diff_content.present?
- end
-
def alternate_viewer_class
return unless viewer.instance_of?(DiffViewer::Renamed)
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 7ee9b862876..47f3324752d 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -153,8 +153,6 @@ module Gitlab
blob.load_all_data!
- return blob.present.highlight_transformed.lines if Feature.enabled?(:jupyter_clean_diffs, @project, default_enabled: true)
-
blob.present.highlight.lines
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index b0d194f309a..f72217dedde 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -24,7 +24,7 @@ module Gitlab
LFS_POINTER_MIN_SIZE = 120.bytes
LFS_POINTER_MAX_SIZE = 200.bytes
- attr_accessor :size, :mode, :id, :commit_id, :loaded_size, :binary, :transformed_for_diff
+ attr_accessor :size, :mode, :id, :commit_id, :loaded_size, :binary
attr_writer :name, :path, :data
def self.gitlab_blob_truncated_true
@@ -127,7 +127,6 @@ module Gitlab
# Retain the actual size before it is encoded
@loaded_size = @data.bytesize if @data
@loaded_all_data = @loaded_size == size
- @transformed_for_diff = false
record_metric_blob_size
record_metric_truncated(truncated?)
diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
index 0aa0896aa57..8a8d23401c1 100644
--- a/lib/gitlab/github_import/importer/diff_note_importer.rb
+++ b/lib/gitlab/github_import/importer/diff_note_importer.rb
@@ -31,6 +31,10 @@ module Gitlab
else
import_with_legacy_diff_note
end
+ rescue ::DiffNote::NoteDiffFileCreationError => e
+ Logger.warn(message: e.message, 'error.class': e.class.name)
+
+ import_with_legacy_diff_note
rescue ActiveRecord::InvalidForeignKey => e
# It's possible the project and the issue have been deleted since
# scheduling this job. In this case we'll just skip creating the note
diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb
index 2cc3a82dd9b..673f56b5753 100644
--- a/lib/gitlab/github_import/importer/note_importer.rb
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -29,6 +29,7 @@ module Gitlab
project_id: project.id,
author_id: author_id,
note: note_body,
+ discussion_id: note.discussion_id,
system: false,
created_at: note.created_at,
updated_at: note.updated_at
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
index fecff0644c2..04f53accfeb 100644
--- a/lib/gitlab/github_import/representation/diff_note.rb
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -4,6 +4,7 @@ module Gitlab
module GithubImport
module Representation
class DiffNote
+ include Gitlab::Utils::StrongMemoize
include ToHash
include ExposeAttribute
@@ -127,15 +128,17 @@ module Gitlab
end
def discussion_id
- if in_reply_to_id.present?
- current_discussion_id
- else
- Discussion.discussion_id(
- Struct
- .new(:noteable_id, :noteable_type)
- .new(merge_request.id, NOTEABLE_TYPE)
- ).tap do |discussion_id|
- cache_discussion_id(discussion_id)
+ strong_memoize(:discussion_id) do
+ if in_reply_to_id.present?
+ current_discussion_id
+ else
+ Discussion.discussion_id(
+ Struct
+ .new(:noteable_id, :noteable_type)
+ .new(merge_request.id, NOTEABLE_TYPE)
+ ).tap do |discussion_id|
+ cache_discussion_id(discussion_id)
+ end
end
end
end
diff --git a/lib/gitlab/github_import/representation/note.rb b/lib/gitlab/github_import/representation/note.rb
index bcdb1a5459b..bbf20b7e9e6 100644
--- a/lib/gitlab/github_import/representation/note.rb
+++ b/lib/gitlab/github_import/representation/note.rb
@@ -63,6 +63,14 @@ module Gitlab
@attributes = attributes
end
+ def discussion_id
+ Discussion.discussion_id(
+ Struct
+ .new(:noteable_id, :noteable_type)
+ .new(noteable_id, noteable_type)
+ )
+ end
+
alias_method :issuable_type, :noteable_type
def github_identifiers
diff --git a/lib/gitlab/metrics/samplers/database_sampler.rb b/lib/gitlab/metrics/samplers/database_sampler.rb
index 44304b5891e..965d85e20e5 100644
--- a/lib/gitlab/metrics/samplers/database_sampler.rb
+++ b/lib/gitlab/metrics/samplers/database_sampler.rb
@@ -38,6 +38,10 @@ module Gitlab
end
def host_stats
+ connection_class_stats + replica_host_stats
+ end
+
+ def connection_class_stats
Gitlab::Database.database_base_models.each_value.with_object([]) do |base_model, stats|
next unless base_model.connected?
@@ -45,6 +49,16 @@ module Gitlab
end
end
+ def replica_host_stats
+ Gitlab::Database::LoadBalancing.each_load_balancer.with_object([]) do |load_balancer, stats|
+ next if load_balancer.primary_only?
+
+ load_balancer.host_list.hosts.each do |host|
+ stats << { labels: labels_for_replica_host(load_balancer, host), stats: host.connection.pool.stat }
+ end
+ end
+ end
+
def labels_for_class(klass)
{
host: klass.connection_db_config.host,
@@ -53,6 +67,15 @@ module Gitlab
db_config_name: klass.connection_db_config.name
}
end
+
+ def labels_for_replica_host(load_balancer, host)
+ {
+ host: host.host,
+ port: host.port,
+ class: load_balancer.configuration.primary_connection_specification_name,
+ db_config_name: Gitlab::Database.db_config_name(host.connection)
+ }
+ end
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index df0582149a9..715dd86d93c 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -5,6 +5,8 @@ module Gitlab
module Subscribers
# Class for tracking the total query duration of a transaction.
class ActiveRecord < ActiveSupport::Subscriber
+ extend Gitlab::Utils::StrongMemoize
+
attach_to :active_record
IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze
@@ -107,7 +109,7 @@ module Gitlab
# Per database metrics
db_config_name = db_config_name(event.payload)
- duration_key = compose_metric_key(:duration_s, db_role, db_config_name)
+ duration_key = compose_metric_key(:duration_s, nil, db_config_name)
::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration
end
@@ -144,7 +146,7 @@ module Gitlab
# when we are also logging the db_role. Otherwise it will be hard to
# tell if the log key is referring to a db_role OR a db_config_name.
if db_role.present? && db_config_name.present?
- log_key = compose_metric_key(counter, db_role, db_config_name)
+ log_key = compose_metric_key(counter, nil, db_config_name)
Gitlab::SafeRequestStore[log_key] = Gitlab::SafeRequestStore[log_key].to_i + 1
end
end
@@ -172,26 +174,34 @@ module Gitlab
end
def self.load_balancing_metric_counter_keys
- load_balancing_metric_keys(DB_LOAD_BALANCING_COUNTERS)
+ strong_memoize(:load_balancing_metric_counter_keys) do
+ load_balancing_metric_keys(DB_LOAD_BALANCING_COUNTERS)
+ end
end
def self.load_balancing_metric_duration_keys
- load_balancing_metric_keys(DB_LOAD_BALANCING_DURATIONS)
+ strong_memoize(:load_balancing_metric_duration_keys) do
+ load_balancing_metric_keys(DB_LOAD_BALANCING_DURATIONS)
+ end
end
def self.load_balancing_metric_keys(metrics)
- [].tap do |counters|
+ counters = []
+
+ metrics.each do |metric|
DB_LOAD_BALANCING_ROLES.each do |role|
- metrics.each do |metric|
- counters << compose_metric_key(metric, role)
- next unless ENV['GITLAB_MULTIPLE_DATABASE_METRICS']
+ counters << compose_metric_key(metric, role)
+ end
- ::Gitlab::Database.db_config_names.each do |config_name|
- counters << compose_metric_key(metric, role, config_name)
- end
+ if ENV['GITLAB_MULTIPLE_DATABASE_METRICS']
+ ::Gitlab::Database.db_config_names.each do |config_name|
+ counters << compose_metric_key(metric, nil, config_name) # main
+ counters << compose_metric_key(metric, nil, config_name + ::Gitlab::Database::LoadBalancing::LoadBalancer::REPLICA_SUFFIX) # main_replica
end
end
end
+
+ counters
end
def compose_metric_key(metric, db_role = nil, db_config_name = nil)
diff --git a/lib/gitlab/patch/legacy_database_config.rb b/lib/gitlab/patch/legacy_database_config.rb
index a7d4fdf7490..6040f737c75 100644
--- a/lib/gitlab/patch/legacy_database_config.rb
+++ b/lib/gitlab/patch/legacy_database_config.rb
@@ -35,6 +35,40 @@ module Gitlab
attr_reader :uses_legacy_database_config
end
+ def load_database_yaml
+ return super unless Gitlab.ee?
+
+ super.deep_merge(load_geo_database_yaml)
+ end
+
+ # This method is taken from Rails to load a database YAML file without
+ # evaluating ERB. This allows us to create the rake tasks for the Geo
+ # tracking database without filling in the configuration values or
+ # loading the environment. To be removed when we start configure Geo
+ # tracking database in database.yml instead of custom database_geo.yml
+ #
+ # https://github.com/rails/rails/blob/v6.1.4/railties/lib/rails/application/configuration.rb#L255
+ def load_geo_database_yaml
+ path = Rails.root.join("config/database_geo.yml")
+ return {} unless File.exist?(path)
+
+ require "rails/application/dummy_erb_compiler"
+
+ yaml = DummyERB.new(Pathname.new(path).read).result
+ config = YAML.load(yaml) || {} # rubocop:disable Security/YAMLLoad
+
+ config.to_h do |env, configs|
+ # This check is taken from Rails where the transformation
+ # of a flat database.yml is done into `primary:`
+ # https://github.com/rails/rails/blob/v6.1.4/activerecord/lib/active_record/database_configurations.rb#L169
+ if configs.is_a?(Hash) && !configs.all? { |_, v| v.is_a?(Hash) }
+ configs = { "geo" => configs }
+ end
+
+ [env, configs]
+ end
+ end
+
def database_configuration
@uses_legacy_database_config = false # rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -48,6 +82,16 @@ module Gitlab
@uses_legacy_database_config = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ if Gitlab.ee? && File.exist?(Rails.root.join("config/database_geo.yml"))
+ migrations_paths = ["ee/db/geo/migrate"]
+ migrations_paths << "ee/db/geo/post_migrate" unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
+
+ configs["geo"] =
+ Rails.application.config_for(:database_geo)
+ .merge(migrations_paths: migrations_paths, schema_migrations_path: "ee/db/geo/schema_migrations")
+ .stringify_keys
+ end
+
[env, configs]
end
end
diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb
index 46fcec9f7b8..60d91c8fd10 100644
--- a/lib/sidebars/groups/menus/packages_registries_menu.rb
+++ b/lib/sidebars/groups/menus/packages_registries_menu.rb
@@ -26,9 +26,7 @@ module Sidebars
private
def packages_registry_menu_item
- unless context.group.packages_feature_enabled?
- return ::Sidebars::NilMenuItem.new(item_id: :packages_registry)
- end
+ return nil_menu_item(:packages_registry) unless context.group.packages_feature_enabled?
::Sidebars::MenuItem.new(
title: _('Package Registry'),
@@ -40,7 +38,7 @@ module Sidebars
def container_registry_menu_item
if !::Gitlab.config.registry.enabled || !can?(context.current_user, :read_container_image, context.group)
- return ::Sidebars::NilMenuItem.new(item_id: :container_registry)
+ return nil_menu_item(:container_registry)
end
::Sidebars::MenuItem.new(
@@ -52,9 +50,11 @@ module Sidebars
end
def dependency_proxy_menu_item
- unless can?(context.current_user, :read_dependency_proxy, context.group)
- return ::Sidebars::NilMenuItem.new(item_id: :dependency_proxy)
- end
+ setting_does_not_exist_or_is_enabled = !context.group.dependency_proxy_setting ||
+ context.group.dependency_proxy_setting.enabled
+
+ return nil_menu_item(:dependency_proxy) unless can?(context.current_user, :read_dependency_proxy, context.group)
+ return nil_menu_item(:dependency_proxy) unless setting_does_not_exist_or_is_enabled
::Sidebars::MenuItem.new(
title: _('Dependency Proxy'),
@@ -63,6 +63,10 @@ module Sidebars
item_id: :dependency_proxy
)
end
+
+ def nil_menu_item(item_id)
+ ::Sidebars::NilMenuItem.new(item_id: item_id)
+ end
end
end
end
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index 3a08aeb9116..1018bdd545b 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -57,9 +57,9 @@ module Sidebars
data: { trigger: 'manual',
container: 'body',
placement: 'right',
- highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
- highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
- dismiss_endpoint: user_callouts_path,
+ highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION,
+ highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION],
+ dismiss_endpoint: callouts_path,
auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 6f4eeb23d3b..71cc1c47a1a 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml
+
namespace :gitlab do
namespace :db do
desc 'GitLab | DB | Manually insert schema migration version'
@@ -83,7 +85,7 @@ namespace :gitlab do
desc 'GitLab | DB | Sets up EE specific database functionality'
if Gitlab.ee?
- task setup_ee: %w[geo:db:drop geo:db:create geo:db:schema:load geo:db:migrate]
+ task setup_ee: %w[db:drop:geo db:create:geo db:schema:load:geo db:migrate:geo]
else
task :setup_ee
end
@@ -116,6 +118,19 @@ namespace :gitlab do
Rake::Task['gitlab:db:clean_structure_sql'].invoke
end
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |name|
+ # Inform Rake that custom tasks should be run every time rake db:structure:dump is run
+ #
+ # Rails 6.1 deprecates db:structure:dump in favor of db:schema:dump
+ Rake::Task["db:structure:dump:#{name}"].enhance do
+ Rake::Task['gitlab:db:clean_structure_sql'].invoke
+ end
+
+ Rake::Task["db:schema:dump:#{name}"].enhance do
+ Rake::Task['gitlab:db:clean_structure_sql'].invoke
+ end
+ end
+
desc 'Create missing dynamic database partitions'
task create_dynamic_partitions: :environment do
Gitlab::Database::Partitioning.sync_partitions
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 30b4c4ddf29..14ebf429067 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5421,9 +5421,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "BillingPlan|Contact sales"
-msgstr ""
-
msgid "BillingPlan|Upgrade"
msgstr ""
@@ -11087,6 +11084,9 @@ msgstr ""
msgid "Date"
msgstr ""
+msgid "Date merged"
+msgstr ""
+
msgid "Date picker"
msgstr ""
@@ -11570,9 +11570,6 @@ msgstr ""
msgid "DependencyProxy|Storage settings"
msgstr ""
-msgid "DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}."
-msgstr ""
-
msgid "DependencyProxy|There are no images in the cache"
msgstr ""
@@ -20187,7 +20184,7 @@ msgstr ""
msgid "Job|Download"
msgstr ""
-msgid "Job|Erase job log"
+msgid "Job|Erase job log and artifacts"
msgstr ""
msgid "Job|Job artifacts"
@@ -29916,6 +29913,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Retrieving the compliance report failed. Please refresh the page and try again."
+msgstr ""
+
msgid "Retry"
msgstr ""
@@ -29925,6 +29925,12 @@ msgstr ""
msgid "Retry migration"
msgstr ""
+msgid "Retry the downstream pipeline"
+msgstr ""
+
+msgid "Retry the trigger job"
+msgstr ""
+
msgid "Retry this job"
msgstr ""
@@ -34764,6 +34770,9 @@ msgstr ""
msgid "The compliance report captures merged changes that violate compliance best practices."
msgstr ""
+msgid "The compliance report shows the merge request violations merged in protected environments."
+msgstr ""
+
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
@@ -35894,6 +35903,9 @@ msgstr ""
msgid "This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes."
msgstr ""
+msgid "This job triggers a downstream pipeline"
+msgstr ""
+
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
msgstr ""
@@ -38599,6 +38611,9 @@ msgstr ""
msgid "View documentation"
msgstr ""
+msgid "View downstream pipeline"
+msgstr ""
+
msgid "View eligible approvers"
msgstr ""
@@ -38726,6 +38741,9 @@ msgstr ""
msgid "Viewing commit"
msgstr ""
+msgid "Violation"
+msgstr ""
+
msgid "Visibility"
msgstr ""
diff --git a/spec/controllers/groups/dependency_proxies_controller_spec.rb b/spec/controllers/groups/dependency_proxies_controller_spec.rb
index 35bd7d47aed..67847936a80 100644
--- a/spec/controllers/groups/dependency_proxies_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxies_controller_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe Groups::DependencyProxiesController do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:dependency_proxy_group_setting) { create(:dependency_proxy_group_setting, group: group) }
+ let_it_be(:user) { create(:user) }
before do
group.add_owner(user)
@@ -12,62 +13,37 @@ RSpec.describe Groups::DependencyProxiesController do
end
describe 'GET #show' do
- context 'feature enabled' do
- before do
- enable_dependency_proxy
- end
-
- it 'returns 200 and renders the view' do
- get :show, params: { group_id: group.to_param }
+ subject { get :show, params: { group_id: group.to_param } }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template('groups/dependency_proxies/show')
- end
+ before do
+ stub_config(dependency_proxy: { enabled: config_enabled })
end
- it 'returns 404 when feature is disabled' do
- disable_dependency_proxy
+ context 'with global config enabled' do
+ let(:config_enabled) { true }
- get :show, params: { group_id: group.to_param }
+ context 'with the setting enabled' do
+ it 'returns 200 and renders the view' do
+ subject
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe 'PUT #update' do
- context 'feature enabled' do
- before do
- enable_dependency_proxy
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('groups/dependency_proxies/show')
+ end
end
- it 'redirects back to show page' do
- put :update, params: update_params
+ context 'with the setting disabled' do
+ before do
+ dependency_proxy_group_setting.update!(enabled: false)
+ end
- expect(response).to have_gitlab_http_status(:found)
+ it_behaves_like 'returning response status', :not_found
end
end
- it 'returns 404 when feature is disabled' do
- put :update, params: update_params
+ context 'with global config disabled' do
+ let(:config_enabled) { false }
- expect(response).to have_gitlab_http_status(:not_found)
+ it_behaves_like 'returning response status', :not_found
end
-
- def update_params
- {
- group_id: group.to_param,
- dependency_proxy_group_setting: { enabled: true }
- }
- end
- end
-
- def enable_dependency_proxy
- stub_config(dependency_proxy: { enabled: true })
-
- group.create_dependency_proxy_setting!(enabled: true)
- end
-
- def disable_dependency_proxy
- group.create_dependency_proxy_setting!(enabled: false)
end
end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index dbf1b3baf25..38f8d267a2c 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -142,8 +142,8 @@ RSpec.describe RootController do
context 'without customize homepage banner' do
before do
- Users::DismissUserCalloutService.new(
- container: nil, current_user: user, params: { feature_name: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE }
+ Users::DismissCalloutService.new(
+ container: nil, current_user: user, params: { feature_name: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE }
).execute
end
diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/users/callouts_controller_spec.rb
index 3bb8d78a6b0..13dc565b4ad 100644
--- a/spec/controllers/user_callouts_controller_spec.rb
+++ b/spec/controllers/users/callouts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserCalloutsController do
+RSpec.describe Users::CalloutsController do
let_it_be(:user) { create(:user) }
before do
@@ -15,11 +15,11 @@ RSpec.describe UserCalloutsController do
subject { post :create, params: params, format: :json }
context 'with valid feature name' do
- let(:feature_name) { UserCallout.feature_names.each_key.first }
+ let(:feature_name) { Users::Callout.feature_names.each_key.first }
context 'when callout entry does not exist' do
it 'creates a callout entry with dismissed state' do
- expect { subject }.to change { UserCallout.count }.by(1)
+ expect { subject }.to change { Users::Callout.count }.by(1)
end
it 'returns success' do
@@ -30,10 +30,10 @@ RSpec.describe UserCalloutsController do
end
context 'when callout entry already exists' do
- let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.each_key.first, user: user) }
+ let!(:callout) { create(:callout, feature_name: Users::Callout.feature_names.each_key.first, user: user) }
it 'returns success', :aggregate_failures do
- expect { subject }.not_to change { UserCallout.count }
+ expect { subject }.not_to change { Users::Callout.count }
expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index a8b28b32bd7..94957020bcf 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -169,7 +169,7 @@ RSpec.describe 'Database schema' do
'PrometheusMetric' => %w[group],
'ResourceLabelEvent' => %w[action],
'User' => %w[layout dashboard project_view],
- 'UserCallout' => %w[feature_name],
+ 'Users::Callout' => %w[feature_name],
'PrometheusAlert' => %w[operator]
}.freeze
diff --git a/spec/factories/user_callouts.rb b/spec/factories/users/callouts.rb
index cedc6efd8d7..d9f142fee6f 100644
--- a/spec/factories/user_callouts.rb
+++ b/spec/factories/users/callouts.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :user_callout do
+ factory :callout, class: 'Users::Callout' do
feature_name { :gke_cluster_integration }
user
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index e885c0c4413..211576a93f3 100644
--- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -10,6 +10,9 @@ RSpec.describe 'User uploads new design', :js do
let(:issue) { create(:issue, project: project) }
before do
+ # Cause of raising query limiting threshold https://gitlab.com/gitlab-org/gitlab/-/issues/347334
+ stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 102)
+
sign_in(user)
enable_design_management(feature_enabled)
visit project_issue_path(project, issue)
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index 9ffb1746f3e..6bd139c0ebe 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -2,10 +2,11 @@
require 'spec_helper'
-RSpec.describe 'Project milestone' do
+RSpec.describe 'Project milestone', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project) }
+ let(:active_tab_selector) { '[role="tab"][aria-selected="true"]' }
def toggle_sidebar
find('.milestone-sidebar .gutter-toggle').click
@@ -31,8 +32,9 @@ RSpec.describe 'Project milestone' do
it 'shows issues tab' do
within('#content-body') do
expect(page).to have_link 'Issues', href: '#tab-issues'
- expect(page).to have_selector '.nav-links li a.active', count: 1
- expect(find('.nav-links li a.active')).to have_content 'Issues'
+ expect(page).to have_selector active_tab_selector, count: 1
+ expect(find(active_tab_selector)).to have_content 'Issues'
+ expect(page).to have_text('Unstarted Issues')
end
end
@@ -49,6 +51,35 @@ RSpec.describe 'Project milestone' do
end
end
+ context 'when clicking on other tabs' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:tab_text, :href, :panel_content) do
+ 'Merge requests' | '#tab-merge-requests' | 'Work in progress'
+ 'Participants' | '#tab-participants' | nil
+ 'Labels' | '#tab-labels' | nil
+ end
+
+ with_them do
+ before do
+ visit project_milestone_path(project, milestone)
+ click_link(tab_text, href: href)
+ end
+
+ it 'shows the merge requests tab and panel' do
+ within('#content-body') do
+ expect(find(active_tab_selector)).to have_content tab_text
+ expect(find(href)).to be_visible
+ expect(page).to have_text(panel_content) if panel_content
+ end
+ end
+
+ it 'sets the location hash' do
+ expect(current_url).to end_with(href)
+ end
+ end
+ end
+
context 'when project has disabled issues' do
before do
create(:issue, project: project, milestone: milestone)
@@ -59,7 +90,7 @@ RSpec.describe 'Project milestone' do
it 'does not show any issues under the issues tab' do
within('#content-body') do
- expect(find('.nav-links li a.active')).to have_content 'Issues'
+ expect(find(active_tab_selector)).to have_content 'Issues'
expect(page).not_to have_selector '.issuable-row'
end
end
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index 118d8ceceb9..97d9be110c8 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -42,6 +42,8 @@ exports[`Code navigation popover component renders popover 1`] = `
<span>
main() {
</span>
+
+ <br />
</span>
<span
class="line"
@@ -50,6 +52,8 @@ exports[`Code navigation popover component renders popover 1`] = `
<span>
}
</span>
+
+ <br />
</span>
</pre>
</div>
diff --git a/spec/frontend/fixtures/tabs.rb b/spec/frontend/fixtures/tabs.rb
new file mode 100644
index 00000000000..697ff1c7c20
--- /dev/null
+++ b/spec/frontend/fixtures/tabs.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'GlTabsBehavior', '(JavaScript fixtures)', type: :helper do
+ include JavaScriptFixturesHelpers
+ include TabHelper
+
+ let(:response) { @tabs }
+
+ it 'tabs/tabs.html' do
+ tabs = gl_tabs_nav({ data: { testid: 'tabs' } }) do
+ gl_tab_link_to('Foo', '#foo', item_active: true, data: { testid: 'foo-tab' }) +
+ gl_tab_link_to('Bar', '#bar', item_active: false, data: { testid: 'bar-tab' }) +
+ gl_tab_link_to('Qux', '#qux', item_active: false, data: { testid: 'qux-tab' })
+ end
+
+ panels = content_tag(:div, class: 'tab-content') do
+ content_tag(:div, 'Foo', { id: 'foo', class: 'tab-pane active', data: { testid: 'foo-panel' } }) +
+ content_tag(:div, 'Bar', { id: 'bar', class: 'tab-pane', data: { testid: 'bar-panel' } }) +
+ content_tag(:div, 'Qux', { id: 'qux', class: 'tab-pane', data: { testid: 'qux-panel' } })
+ end
+
+ @tabs = tabs + panels
+ end
+end
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 6e3df21e30a..c0ca3dd4109 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -40,6 +40,10 @@ describe('import table', () => {
wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
const findPaginationDropdown = () => wrapper.find('[aria-label="Page size"]');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
+ const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
+
+ const triggerSelectAllCheckbox = () =>
+ wrapper.find('thead input[type=checkbox]').trigger('click');
const selectRow = (idx) =>
wrapper.findAll('tbody td input[type=checkbox]').at(idx).trigger('click');
@@ -313,6 +317,21 @@ describe('import table', () => {
});
describe('bulk operations', () => {
+ it('import all button correctly selects/deselects all groups', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: FAKE_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ }),
+ });
+ await waitForPromises();
+ expect(findSelectionCount().text()).toMatchInterpolatedText('0 selected');
+ await triggerSelectAllCheckbox();
+ expect(findSelectionCount().text()).toMatchInterpolatedText('2 selected');
+ await triggerSelectAllCheckbox();
+ expect(findSelectionCount().text()).toMatchInterpolatedText('0 selected');
+ });
+
it('import selected button is disabled when no groups selected', async () => {
createComponent({
bulkImportSourceGroups: () => ({
diff --git a/spec/frontend/jobs/bridge/app_spec.js b/spec/frontend/jobs/bridge/app_spec.js
new file mode 100644
index 00000000000..0e232ab240d
--- /dev/null
+++ b/spec/frontend/jobs/bridge/app_spec.js
@@ -0,0 +1,33 @@
+import { shallowMount } from '@vue/test-utils';
+import BridgeApp from '~/jobs/bridge/app.vue';
+import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue';
+import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
+
+describe('Bridge Show Page', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(BridgeApp, {});
+ };
+
+ const findEmptyState = () => wrapper.findComponent(BridgeEmptyState);
+ const findSidebar = () => wrapper.findComponent(BridgeSidebar);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('renders sidebar', () => {
+ expect(findSidebar().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/bridge/components/empty_state_spec.js b/spec/frontend/jobs/bridge/components/empty_state_spec.js
new file mode 100644
index 00000000000..83642450118
--- /dev/null
+++ b/spec/frontend/jobs/bridge/components/empty_state_spec.js
@@ -0,0 +1,59 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue';
+import { MOCK_EMPTY_ILLUSTRATION_PATH, MOCK_PATH_TO_DOWNSTREAM } from '../mock_data';
+
+describe('Bridge Empty State', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(BridgeEmptyState, {
+ provide: {
+ emptyStateIllustrationPath: MOCK_EMPTY_ILLUSTRATION_PATH,
+ },
+ propsData: {
+ downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM,
+ ...props,
+ },
+ });
+ };
+
+ const findSvg = () => wrapper.find('img');
+ const findTitle = () => wrapper.find('h1');
+ const findLinkBtn = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders illustration', () => {
+ expect(findSvg().exists()).toBe(true);
+ });
+
+ it('renders title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
+ });
+
+ it('renders CTA button', () => {
+ expect(findLinkBtn().exists()).toBe(true);
+ expect(findLinkBtn().text()).toBe(wrapper.vm.$options.i18n.linkBtnText);
+ expect(findLinkBtn().attributes('href')).toBe(MOCK_PATH_TO_DOWNSTREAM);
+ });
+ });
+
+ describe('without downstream pipeline', () => {
+ beforeEach(() => {
+ createComponent({ downstreamPipelinePath: undefined });
+ });
+
+ it('does not render CTA button', () => {
+ expect(findLinkBtn().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/bridge/components/sidebar_spec.js b/spec/frontend/jobs/bridge/components/sidebar_spec.js
new file mode 100644
index 00000000000..ba4018753af
--- /dev/null
+++ b/spec/frontend/jobs/bridge/components/sidebar_spec.js
@@ -0,0 +1,76 @@
+import { GlButton, GlDropdown } from '@gitlab/ui';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
+import { BUILD_NAME } from '../mock_data';
+
+describe('Bridge Sidebar', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(BridgeSidebar, {
+ provide: {
+ buildName: BUILD_NAME,
+ },
+ });
+ };
+
+ const findSidebar = () => wrapper.find('aside');
+ const findRetryDropdown = () => wrapper.find(GlDropdown);
+ const findToggle = () => wrapper.find(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders retry dropdown', () => {
+ expect(findRetryDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('sidebar expansion', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('toggles expansion on button click', async () => {
+ expect(findSidebar().classes()).not.toContain('gl-display-none');
+
+ findToggle().vm.$emit('click');
+ await nextTick();
+
+ expect(findSidebar().classes()).toContain('gl-display-none');
+ });
+
+ describe('on resize', () => {
+ it.each`
+ breakpoint | isSidebarExpanded
+ ${'xs'} | ${false}
+ ${'sm'} | ${false}
+ ${'md'} | ${true}
+ ${'lg'} | ${true}
+ ${'xl'} | ${true}
+ `(
+ 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"',
+ async ({ breakpoint, isSidebarExpanded }) => {
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
+
+ window.dispatchEvent(new Event('resize'));
+ await nextTick();
+
+ if (isSidebarExpanded) {
+ expect(findSidebar().classes()).not.toContain('gl-display-none');
+ } else {
+ expect(findSidebar().classes()).toContain('gl-display-none');
+ }
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/jobs/bridge/mock_data.js b/spec/frontend/jobs/bridge/mock_data.js
new file mode 100644
index 00000000000..146d1a062ac
--- /dev/null
+++ b/spec/frontend/jobs/bridge/mock_data.js
@@ -0,0 +1,3 @@
+export const MOCK_EMPTY_ILLUSTRATION_PATH = '/path/to/svg';
+export const MOCK_PATH_TO_DOWNSTREAM = '/path/to/downstream/pipeline';
+export const BUILD_NAME = 'Child Pipeline Trigger';
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index fdc72e10f9a..44a7186904d 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -3,7 +3,6 @@ import {
GlFormGroup,
GlSkeletonLoader,
GlSprintf,
- GlLink,
GlEmptyState,
} from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
@@ -12,10 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
-import {
- GRAPHQL_PAGE_SIZE,
- ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
-} from '~/packages_and_registries/dependency_proxy/constants';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -58,8 +54,6 @@ describe('DependencyProxyApp', () => {
}
const findProxyNotAvailableAlert = () => wrapper.findByTestId('proxy-not-available');
- const findProxyDisabledAlert = () => wrapper.findByTestId('proxy-disabled');
- const findDisabledAlertLink = () => findProxyDisabledAlert().findComponent(GlLink);
const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
@@ -224,36 +218,6 @@ describe('DependencyProxyApp', () => {
});
});
});
-
- describe('when the dependency proxy is disabled', () => {
- beforeEach(() => {
- resolver = jest
- .fn()
- .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } }));
- createComponent();
- return waitForPromises();
- });
-
- it('does not show the main area', () => {
- expect(findMainArea().exists()).toBe(false);
- });
-
- it('does not show the loader', () => {
- expect(findSkeletonLoader().exists()).toBe(false);
- });
-
- it('shows a proxy disabled alert', () => {
- expect(findProxyDisabledAlert().text()).toMatchInterpolatedText(
- DependencyProxyApp.i18n.proxyDisabledText,
- );
- });
-
- it('disabled alert has a link to the docs', () => {
- expect(findDisabledAlertLink().attributes()).toMatchObject({
- href: ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
- });
- });
- });
});
});
});
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js
index e8ad4ae46bd..a19515d6ed2 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/runner/components/runner_status_badge_spec.js
@@ -7,6 +7,7 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NOT_CONNECTED,
+ STATUS_NEVER_CONTACTED,
} from '~/runner/constants';
describe('RunnerTypeBadge', () => {
@@ -62,6 +63,19 @@ describe('RunnerTypeBadge', () => {
expect(getTooltip().value).toMatch('This runner has never connected');
});
+ it('renders never contacted state as not connected, for backwards compatibility', () => {
+ createComponent({
+ runner: {
+ contactedAt: null,
+ status: STATUS_NEVER_CONTACTED,
+ },
+ });
+
+ expect(wrapper.text()).toBe('not connected');
+ expect(findBadge().props('variant')).toBe('muted');
+ expect(getTooltip().value).toMatch('This runner has never connected');
+ });
+
it('renders offline state', () => {
createComponent({
runner: {
diff --git a/spec/frontend/tabs/index_spec.js b/spec/frontend/tabs/index_spec.js
new file mode 100644
index 00000000000..98617b404ff
--- /dev/null
+++ b/spec/frontend/tabs/index_spec.js
@@ -0,0 +1,260 @@
+import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs';
+import { ACTIVE_PANEL_CLASS, ACTIVE_TAB_CLASSES } from '~/tabs/constants';
+import { getFixture, setHTMLFixture } from 'helpers/fixtures';
+
+const tabsFixture = getFixture('tabs/tabs.html');
+
+describe('GlTabsBehavior', () => {
+ let glTabs;
+ let tabShownEventSpy;
+
+ const findByTestId = (testId) => document.querySelector(`[data-testid="${testId}"]`);
+ const findTab = (name) => findByTestId(`${name}-tab`);
+ const findPanel = (name) => findByTestId(`${name}-panel`);
+
+ const getAttributes = (element) =>
+ Array.from(element.attributes).reduce((acc, attr) => {
+ acc[attr.name] = attr.value;
+ return acc;
+ }, {});
+
+ const expectActiveTabAndPanel = (name) => {
+ const tab = findTab(name);
+ const panel = findPanel(name);
+
+ expect(glTabs.activeTab).toBe(tab);
+
+ expect(getAttributes(tab)).toMatchObject({
+ 'aria-controls': panel.id,
+ 'aria-selected': 'true',
+ role: 'tab',
+ id: expect.any(String),
+ });
+
+ ACTIVE_TAB_CLASSES.forEach((klass) => {
+ expect(tab.classList.contains(klass)).toBe(true);
+ });
+
+ expect(getAttributes(panel)).toMatchObject({
+ 'aria-labelledby': tab.id,
+ role: 'tabpanel',
+ });
+
+ expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(true);
+ };
+
+ const expectInactiveTabAndPanel = (name) => {
+ const tab = findTab(name);
+ const panel = findPanel(name);
+
+ expect(glTabs.activeTab).not.toBe(tab);
+
+ expect(getAttributes(tab)).toMatchObject({
+ 'aria-controls': panel.id,
+ 'aria-selected': 'false',
+ role: 'tab',
+ tabindex: '-1',
+ id: expect.any(String),
+ });
+
+ ACTIVE_TAB_CLASSES.forEach((klass) => {
+ expect(tab.classList.contains(klass)).toBe(false);
+ });
+
+ expect(getAttributes(panel)).toMatchObject({
+ 'aria-labelledby': tab.id,
+ role: 'tabpanel',
+ });
+
+ expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(false);
+ };
+
+ const expectGlTabShownEvent = (name) => {
+ expect(tabShownEventSpy).toHaveBeenCalledTimes(1);
+
+ const [event] = tabShownEventSpy.mock.calls[0];
+ expect(event.target).toBe(findTab(name));
+
+ expect(event.detail).toEqual({
+ activeTabPanel: findPanel(name),
+ });
+ };
+
+ const triggerKeyDown = (code, element) => {
+ const event = new KeyboardEvent('keydown', { code });
+
+ element.dispatchEvent(event);
+ };
+
+ it('throws when instantiated without an element', () => {
+ expect(() => new GlTabsBehavior()).toThrow('Cannot instantiate');
+ });
+
+ describe('when given an element', () => {
+ afterEach(() => {
+ glTabs.destroy();
+ });
+
+ beforeEach(() => {
+ setHTMLFixture(tabsFixture);
+
+ const tabsEl = findByTestId('tabs');
+ tabShownEventSpy = jest.fn();
+ tabsEl.addEventListener(TAB_SHOWN_EVENT, tabShownEventSpy);
+
+ glTabs = new GlTabsBehavior(tabsEl);
+ });
+
+ it('instantiates', () => {
+ expect(glTabs).toEqual(expect.any(GlTabsBehavior));
+ });
+
+ it('sets the active tab', () => {
+ expectActiveTabAndPanel('foo');
+ });
+
+ it(`does not fire an initial ${TAB_SHOWN_EVENT} event`, () => {
+ expect(tabShownEventSpy).not.toHaveBeenCalled();
+ });
+
+ describe('clicking on an inactive tab', () => {
+ beforeEach(() => {
+ findTab('bar').click();
+ });
+
+ it('changes the active tab', () => {
+ expectActiveTabAndPanel('bar');
+ });
+
+ it('deactivates the previously active tab', () => {
+ expectInactiveTabAndPanel('foo');
+ });
+
+ it(`dispatches a ${TAB_SHOWN_EVENT} event`, () => {
+ expectGlTabShownEvent('bar');
+ });
+ });
+
+ describe('clicking on the active tab', () => {
+ beforeEach(() => {
+ findTab('foo').click();
+ });
+
+ it('does nothing', () => {
+ expectActiveTabAndPanel('foo');
+ expect(tabShownEventSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('keyboard navigation', () => {
+ it.each(['ArrowRight', 'ArrowDown'])('pressing %s moves to next tab', (code) => {
+ expectActiveTabAndPanel('foo');
+
+ triggerKeyDown(code, glTabs.activeTab);
+
+ expectActiveTabAndPanel('bar');
+ expectInactiveTabAndPanel('foo');
+ expectGlTabShownEvent('bar');
+ tabShownEventSpy.mockClear();
+
+ triggerKeyDown(code, glTabs.activeTab);
+
+ expectActiveTabAndPanel('qux');
+ expectInactiveTabAndPanel('bar');
+ expectGlTabShownEvent('qux');
+ tabShownEventSpy.mockClear();
+
+ // We're now on the last tab, so the active tab should not change
+ triggerKeyDown(code, glTabs.activeTab);
+
+ expectActiveTabAndPanel('qux');
+ expect(tabShownEventSpy).not.toHaveBeenCalled();
+ });
+
+ it.each(['ArrowLeft', 'ArrowUp'])('pressing %s moves to previous tab', (code) => {
+ // First, make the last tab active
+ findTab('qux').click();
+ tabShownEventSpy.mockClear();
+
+ // Now start moving backwards
+ expectActiveTabAndPanel('qux');
+
+ triggerKeyDown(code, glTabs.activeTab);
+
+ expectActiveTabAndPanel('bar');
+ expectInactiveTabAndPanel('qux');
+ expectGlTabShownEvent('bar');
+ tabShownEventSpy.mockClear();
+
+ triggerKeyDown(code, glTabs.activeTab);
+
+ expectActiveTabAndPanel('foo');
+ expectInactiveTabAndPanel('bar');
+ expectGlTabShownEvent('foo');
+ tabShownEventSpy.mockClear();
+
+ // We're now on the first tab, so the active tab should not change
+ triggerKeyDown(code, glTabs.activeTab);
+
+ expectActiveTabAndPanel('foo');
+ expect(tabShownEventSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('destroying', () => {
+ beforeEach(() => {
+ glTabs.destroy();
+ });
+
+ it('removes interactivity', () => {
+ const inactiveTab = findTab('bar');
+
+ // clicks do nothing
+ inactiveTab.click();
+ expectActiveTabAndPanel('foo');
+ expect(tabShownEventSpy).not.toHaveBeenCalled();
+
+ // keydown events do nothing
+ triggerKeyDown('ArrowDown', inactiveTab);
+ expectActiveTabAndPanel('foo');
+ expect(tabShownEventSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('activateTab method', () => {
+ it.each`
+ tabState | name
+ ${'active'} | ${'foo'}
+ ${'inactive'} | ${'bar'}
+ `('can programmatically activate an $tabState tab', ({ name }) => {
+ glTabs.activateTab(findTab(name));
+ expectActiveTabAndPanel(name);
+ expectGlTabShownEvent(name, 'foo');
+ });
+ });
+ });
+
+ describe('using aria-controls instead of href to link tabs to panels', () => {
+ beforeEach(() => {
+ setHTMLFixture(tabsFixture);
+
+ const tabsEl = findByTestId('tabs');
+ ['foo', 'bar', 'qux'].forEach((name) => {
+ const tab = findTab(name);
+ const panel = findPanel(name);
+
+ tab.setAttribute('href', '#');
+ tab.setAttribute('aria-controls', panel.id);
+ });
+
+ glTabs = new GlTabsBehavior(tabsEl);
+ });
+
+ it('connects the panels to their tabs correctly', () => {
+ findTab('bar').click();
+
+ expectActiveTabAndPanel('bar');
+ expectInactiveTabAndPanel('foo');
+ });
+ });
+});
diff --git a/spec/graphql/mutations/user_callouts/create_spec.rb b/spec/graphql/mutations/user_callouts/create_spec.rb
index 93f227d8b82..eac39bdd1b0 100644
--- a/spec/graphql/mutations/user_callouts/create_spec.rb
+++ b/spec/graphql/mutations/user_callouts/create_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Mutations::UserCallouts::Create do
let(:feature_name) { 'not_supported' }
it 'does not create a user callout' do
- expect { resolve }.not_to change(UserCallout, :count).from(0)
+ expect { resolve }.not_to change(Users::Callout, :count).from(0)
end
it 'returns error about feature name not being supported' do
@@ -22,10 +22,10 @@ RSpec.describe Mutations::UserCallouts::Create do
end
context 'when feature name is supported' do
- let(:feature_name) { UserCallout.feature_names.each_key.first.to_s }
+ let(:feature_name) { Users::Callout.feature_names.each_key.first.to_s }
it 'creates a user callout' do
- expect { resolve }.to change(UserCallout, :count).from(0).to(1)
+ expect { resolve }.to change(Users::Callout, :count).from(0).to(1)
end
it 'sets dismissed_at for the user callout' do
diff --git a/spec/graphql/types/user_callout_feature_name_enum_spec.rb b/spec/graphql/types/user_callout_feature_name_enum_spec.rb
index 28755e1301b..5dfcfc21708 100644
--- a/spec/graphql/types/user_callout_feature_name_enum_spec.rb
+++ b/spec/graphql/types/user_callout_feature_name_enum_spec.rb
@@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['UserCalloutFeatureNameEnum'] do
specify { expect(described_class.graphql_name).to eq('UserCalloutFeatureNameEnum') }
it 'exposes all the existing user callout feature names' do
- expect(described_class.values.keys).to match_array(::UserCallout.feature_names.keys.map(&:upcase))
+ expect(described_class.values.keys).to match_array(::Users::Callout.feature_names.keys.map(&:upcase))
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7e3f665a99c..7390b9b3f58 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -192,20 +192,6 @@ RSpec.describe ApplicationHelper do
end
end
- describe '#contact_sales_url' do
- subject { helper.contact_sales_url }
-
- it 'returns the url' do
- is_expected.to eq("https://#{helper.promo_host}/sales")
- end
-
- it 'changes if promo_url changes' do
- allow(helper).to receive(:promo_url).and_return('https://somewhere.else')
-
- is_expected.to eq('https://somewhere.else/sales')
- end
- end
-
describe '#support_url' do
context 'when alternate support url is specified' do
let(:alternate_url) { 'http://company.example.com/getting-help' }
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
new file mode 100644
index 00000000000..e5ef362e91b
--- /dev/null
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobsHelper do
+ describe 'jobs data' do
+ let(:project) { create(:project, :repository) }
+ let(:bridge) { create(:ci_bridge, status: :pending) }
+
+ subject(:bridge_data) { helper.bridge_data(bridge) }
+
+ before do
+ allow(helper)
+ .to receive(:image_path)
+ .and_return('/path/to/illustration')
+ end
+
+ it 'returns bridge data' do
+ expect(bridge_data).to eq({
+ "build_name" => bridge.name,
+ "empty-state-illustration-path" => '/path/to/illustration'
+ })
+ end
+ end
+end
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index 503ad3ad66d..a06c9ec6699 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe IdeHelper do
context 'and the callout has been dismissed' do
it 'disables environment guidance' do
- callout = create(:user_callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
+ callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
callout.update!(dismissed_at: Time.now - 1.week)
allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
index 5b91cb77f79..f338eddedfd 100644
--- a/spec/helpers/tab_helper_spec.rb
+++ b/spec/helpers/tab_helper_spec.rb
@@ -7,17 +7,13 @@ RSpec.describe TabHelper do
describe 'gl_tabs_nav' do
it 'creates a tabs navigation' do
- expect(helper.gl_tabs_nav).to match(%r{<ul class=".*" role="tablist"><\/ul>})
+ expect(helper.gl_tabs_nav).to match(%r{<ul class="nav gl-tabs-nav"><\/ul>})
end
it 'captures block output' do
expect(helper.gl_tabs_nav { "block content" }).to match(/block content/)
end
- it 'adds styles classes' do
- expect(helper.gl_tabs_nav).to match(/class="nav gl-tabs-nav"/)
- end
-
it 'adds custom class' do
expect(helper.gl_tabs_nav(class: 'my-class' )).to match(/class=".*my-class.*"/)
end
@@ -29,7 +25,7 @@ RSpec.describe TabHelper do
end
it 'creates a tab' do
- expect(helper.gl_tab_link_to('Link', '/url')).to eq('<li class="nav-item" role="presentation"><a class="nav-link gl-tab-nav-item" href="/url">Link</a></li>')
+ expect(helper.gl_tab_link_to('Link', '/url')).to eq('<li class="nav-item"><a class="nav-link gl-tab-nav-item" href="/url">Link</a></li>')
end
it 'creates a tab with block output' do
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index 7abc67e29a4..ba4d8797a24 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe UserCalloutsHelper do
+RSpec.describe Users::CalloutsHelper do
let_it_be(:user, refind: true) { create(:user) }
before do
@@ -115,7 +115,7 @@ RSpec.describe UserCalloutsHelper do
context 'when the feature flags new version has been dismissed' do
before do
- create(:user_callout, user: user, feature_name: described_class::FEATURE_FLAGS_NEW_VERSION)
+ create(:callout, user: user, feature_name: described_class::FEATURE_FLAGS_NEW_VERSION)
end
it { is_expected.to be_falsy }
@@ -203,83 +203,6 @@ RSpec.describe UserCalloutsHelper do
end
end
- describe '.show_invite_banner?' do
- let_it_be(:group) { create(:group) }
-
- subject { helper.show_invite_banner?(group) }
-
- context 'when user has the admin ability for the group' do
- before do
- group.add_owner(user)
- end
-
- context 'when the invite_members_banner has not been dismissed' do
- it { is_expected.to eq(true) }
-
- context 'when the group was just created' do
- before do
- flash[:notice] = "Group #{group.name} was successfully created"
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'with concerning multiple members' do
- let_it_be(:user_2) { create(:user) }
-
- context 'on current group' do
- before do
- group.add_guest(user_2)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'on current group that is a subgroup' do
- let_it_be(:subgroup) { create(:group, parent: group) }
-
- subject { helper.show_invite_banner?(subgroup) }
-
- context 'with only one user on parent and this group' do
- it { is_expected.to eq(true) }
- end
-
- context 'when another user is on this group' do
- before do
- subgroup.add_guest(user_2)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when another user is on the parent group' do
- before do
- group.add_guest(user_2)
- end
-
- it { is_expected.to eq(false) }
- end
- end
- end
- end
-
- context 'when the invite_members_banner has been dismissed' do
- before do
- create(:group_callout,
- user: user,
- group: group,
- feature_name: described_class::INVITE_MEMBERS_BANNER)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when user does not have admin ability for the group' do
- it { is_expected.to eq(false) }
- end
- end
-
describe '.show_security_newsletter_user_callout?' do
let_it_be(:admin) { create(:user, :admin) }
diff --git a/spec/helpers/users/group_callouts_helper_spec.rb b/spec/helpers/users/group_callouts_helper_spec.rb
new file mode 100644
index 00000000000..da67c4921b3
--- /dev/null
+++ b/spec/helpers/users/group_callouts_helper_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Users::GroupCalloutsHelper do
+ let_it_be(:user, refind: true) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ describe '.show_invite_banner?' do
+ subject { helper.show_invite_banner?(group) }
+
+ context 'when user has the admin ability for the group' do
+ before do
+ group.add_owner(user)
+ end
+
+ context 'when the invite_members_banner has not been dismissed' do
+ it { is_expected.to eq(true) }
+
+ context 'when the group was just created' do
+ before do
+ flash[:notice] = "Group #{group.name} was successfully created"
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with concerning multiple members' do
+ let_it_be(:user_2) { create(:user) }
+
+ context 'on current group' do
+ before do
+ group.add_guest(user_2)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'on current group that is a subgroup' do
+ let_it_be(:subgroup) { create(:group, parent: group) }
+
+ subject { helper.show_invite_banner?(subgroup) }
+
+ context 'with only one user on parent and this group' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when another user is on this group' do
+ before do
+ subgroup.add_guest(user_2)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when another user is on the parent group' do
+ before do
+ group.add_guest(user_2)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+ end
+
+ context 'when the invite_members_banner has been dismissed' do
+ before do
+ create(:group_callout,
+ user: user,
+ group: group,
+ feature_name: described_class::INVITE_MEMBERS_BANNER)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when user does not have admin ability for the group' do
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/initializers/validate_database_config_spec.rb b/spec/initializers/validate_database_config_spec.rb
index 99e4a4b36ee..209d9691350 100644
--- a/spec/initializers/validate_database_config_spec.rb
+++ b/spec/initializers/validate_database_config_spec.rb
@@ -14,6 +14,9 @@ RSpec.describe 'validate database config' do
end
before do
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(Rails.root.join("config/database_geo.yml")).and_return(false)
+
# The `AS::ConfigurationFile` calls `read` in `def initialize`
# thus we cannot use `expect_next_instance_of`
# rubocop:disable RSpec/AnyInstanceOf
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index a310de5c015..1c9b894e885 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
it 'adds language to lang attribute when specified' do
result = filter("```html\nsome code\n```", no_sourcepos: true)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to start_with('<pre lang="html"><code>')
else
expect(result).to start_with('<pre><code lang="html">')
@@ -49,7 +49,7 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
it 'works with utf8 chars in language' do
result = filter("```æ—¥\nsome code\n```", no_sourcepos: true)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to start_with('<pre lang="æ—¥"><code>')
else
expect(result).to start_with('<pre><code lang="æ—¥">')
@@ -59,7 +59,7 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
it 'works with additional language parameters' do
result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to start_with('<pre lang="ruby:red" data-meta="gem foo"><code>')
else
expect(result).to start_with('<pre><code lang="ruby:red gem foo">')
@@ -102,7 +102,7 @@ RSpec.describe Banzai::Filter::MarkdownFilter do
expect(result).to include('<td>foot <sup')
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result).to include('<section class="footnotes" data-footnotes>')
else
expect(result).to include('<section class="footnotes">')
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index d1a3b5689a8..e1e02c09fbe 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
it 'replaces plantuml pre tag with img tag' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
- input = if Feature.enabled?(:use_cmark_renderer)
+ input = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
'<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
else
'<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
@@ -24,7 +24,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
it 'does not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false)
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
else
@@ -40,7 +40,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter do
it 'does not replace plantuml pre tag with img tag if url is invalid' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
- input = if Feature.enabled?(:use_cmark_renderer)
+ input = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
'<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
else
'<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index dfe022b51d2..62e93cb1653 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context "when a valid language is specified" do
it "highlights as that language" do
- result = if Feature.enabled?(:use_cmark_renderer)
+ result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter('<pre lang="ruby"><code>def fun end</code></pre>')
else
filter('<pre><code lang="ruby">def fun end</code></pre>')
@@ -54,7 +54,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context "when an invalid language is specified" do
it "highlights as plaintext" do
- result = if Feature.enabled?(:use_cmark_renderer)
+ result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
else
filter('<pre><code lang="gnuplot">This is a test</code></pre>')
@@ -73,7 +73,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
%w(math mermaid plantuml suggestion).each do |lang|
context "when #{lang} is specified" do
it "highlights as plaintext but with the correct language attribute and class" do
- result = if Feature.enabled?(:use_cmark_renderer)
+ result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
else
filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
@@ -89,7 +89,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
let(:lang_params) { 'foo-bar-kux' }
let(:xss_lang) do
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
"#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
else
"#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
@@ -97,7 +97,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end
it "includes data-lang-params tag with extra information" do
- result = if Feature.enabled?(:use_cmark_renderer)
+ result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
else
filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
@@ -108,7 +108,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
include_examples "XSS prevention", lang
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
include_examples "XSS prevention",
"#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
else
@@ -131,7 +131,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context 'when delimiter is space' do
it 'delimits on the first appearance' do
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
expect(result.to_html).to eq(expected_result)
@@ -147,7 +147,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it 'delimits on the first appearance' do
result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
expect(result.to_html).to eq(expected_result)
else
expect(result.to_html).to eq(%{<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">This is a test</span></code></pre>})
@@ -173,7 +173,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end
it "highlights as plaintext" do
- result = if Feature.enabled?(:use_cmark_renderer)
+ result = if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
filter('<pre lang="ruby"><code>This is a test</code></pre>')
else
filter('<pre><code lang="ruby">This is a test</code></pre>')
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
index 394fcc06eba..c8cd9d4fcac 100644
--- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
it 'renders correct html' do
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>))
else
correct_html_included(markdown, %Q(<code lang="foo@bar">foo\n</code>))
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index ac29bb22865..87874c73e75 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -96,7 +96,7 @@ module Gitlab
it "does not convert dangerous fenced code with inline script into HTML" do
input = '```mypre"><script>alert(3)</script>'
output =
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
"<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n</div>\n</div>"
else
"<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
index 37524afc83d..30e6ad234a0 100644
--- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
@@ -29,7 +29,15 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do
end
it { expect(subject).to have_details }
- it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" }
+ it { expect(subject.details_path).to include "jobs/#{bridge.id}" }
+
+ context 'with ci_retry_downstream_pipeline ff disabled' do
+ before do
+ stub_feature_flags(ci_retry_downstream_pipeline: false)
+ end
+
+ it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" }
+ end
end
context 'when user does not have access to read downstream pipeline' do
diff --git a/spec/lib/gitlab/diff/custom_diff_spec.rb b/spec/lib/gitlab/diff/custom_diff_spec.rb
new file mode 100644
index 00000000000..246508d2e1e
--- /dev/null
+++ b/spec/lib/gitlab/diff/custom_diff_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Diff::CustomDiff do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:ipynb_blob) { repository.blob_at('f6b7a707', 'files/ipython/markdown-table.ipynb') }
+ let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') }
+
+ describe '#preprocess_before_diff' do
+ context 'for ipynb files' do
+ it 'transforms the diff' do
+ expect(described_class.preprocess_before_diff(ipynb_blob.path, nil, ipynb_blob)).not_to include('cells')
+ end
+
+ it 'adds the blob to the list of transformed blobs' do
+ described_class.preprocess_before_diff(ipynb_blob.path, nil, ipynb_blob)
+
+ expect(described_class.transformed_for_diff?(ipynb_blob)).to be_truthy
+ end
+ end
+
+ context 'for other files' do
+ it 'returns nil' do
+ expect(described_class.preprocess_before_diff(blob.path, nil, blob)).to be_nil
+ end
+
+ it 'does not add the blob to the list of transformed blobs' do
+ described_class.preprocess_before_diff(blob.path, nil, blob)
+
+ expect(described_class.transformed_for_diff?(blob)).to be_falsey
+ end
+ end
+ end
+
+ describe '#transformed_blob_data' do
+ it 'transforms blob data if file was processed' do
+ described_class.preprocess_before_diff(ipynb_blob.path, nil, ipynb_blob)
+
+ expect(described_class.transformed_blob_data(ipynb_blob)).not_to include('cells')
+ end
+
+ it 'does not transform blob data if file was not processed' do
+ expect(described_class.transformed_blob_data(ipynb_blob)).to be_nil
+ end
+ end
+
+ describe '#transformed_blob_language' do
+ it 'is md when file was preprocessed' do
+ described_class.preprocess_before_diff(ipynb_blob.path, nil, ipynb_blob)
+
+ expect(described_class.transformed_blob_language(ipynb_blob)).to eq('md')
+ end
+
+ it 'is nil for a .ipynb blob that was not preprocessed' do
+ expect(described_class.transformed_blob_language(ipynb_blob)).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
index 0448ada6bca..a0e78186caa 100644
--- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -173,9 +173,11 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
EOB
end
- it 'imports the note as diff note' do
+ before do
stub_user_finder(user.id, true)
+ end
+ it 'imports the note as diff note' do
expect { subject.execute }
.to change(DiffNote, :count)
.by(1)
@@ -212,6 +214,29 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
```
NOTE
end
+
+ context 'when the note diff file creation fails' do
+ it 'falls back to the LegacyDiffNote' do
+ exception = ::DiffNote::NoteDiffFileCreationError.new('Failed to create diff note file')
+
+ expect_next_instance_of(::Import::Github::Notes::CreateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_raise(exception)
+ end
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:warn)
+ .with(
+ message: 'Failed to create diff note file',
+ 'error.class': 'DiffNote::NoteDiffFileCreationError'
+ )
+
+ expect { subject.execute }
+ .to change(LegacyDiffNote, :count)
+ .and not_change(DiffNote, :count)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index 96d8acbd3de..165f543525d 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -52,6 +52,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
project_id: project.id,
author_id: user.id,
note: 'This is my note',
+ discussion_id: match(/\A[0-9a-f]{40}\z/),
system: false,
created_at: created_at,
updated_at: updated_at
@@ -82,6 +83,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
project_id: project.id,
author_id: project.creator_id,
note: "*Created by: alice*\n\nThis is my note",
+ discussion_id: match(/\A[0-9a-f]{40}\z/),
system: false,
created_at: created_at,
updated_at: updated_at
diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
index 6127a52b14f..e8f8947c9e8 100644
--- a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
it_behaves_like 'metrics sampler', 'DATABASE_SAMPLER'
describe '#sample' do
- let(:active_record_labels) do
+ let(:main_labels) do
{
class: 'ActiveRecord::Base',
host: ApplicationRecord.database.config['host'],
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
}
end
- let(:ci_application_record_labels) do
+ let(:ci_labels) do
{
class: 'Ci::ApplicationRecord',
host: Ci::ApplicationRecord.database.config['host'],
@@ -26,6 +26,24 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
}
end
+ let(:main_replica_labels) do
+ {
+ class: 'ActiveRecord::Base',
+ host: 'main-replica-host',
+ port: 2345,
+ db_config_name: 'main_replica'
+ }
+ end
+
+ let(:ci_replica_labels) do
+ {
+ class: 'Ci::ApplicationRecord',
+ host: 'ci-replica-host',
+ port: 3456,
+ db_config_name: 'ci_replica'
+ }
+ end
+
before do
described_class::METRIC_DESCRIPTIONS.each_key do |metric|
allow(subject.metrics[metric]).to receive(:set)
@@ -35,35 +53,124 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
.and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord })
end
- context 'when the database is connected', :add_ci_connection do
- it 'samples connection pool statistics' do
- expect(subject.metrics[:size]).to receive(:set).with(active_record_labels, a_value >= 1)
- expect(subject.metrics[:connections]).to receive(:set).with(active_record_labels, a_value >= 1)
- expect(subject.metrics[:busy]).to receive(:set).with(active_record_labels, a_value >= 1)
- expect(subject.metrics[:dead]).to receive(:set).with(active_record_labels, a_value >= 0)
- expect(subject.metrics[:waiting]).to receive(:set).with(active_record_labels, a_value >= 0)
-
- expect(subject.metrics[:size]).to receive(:set).with(ci_application_record_labels, a_value >= 1)
- expect(subject.metrics[:connections]).to receive(:set).with(ci_application_record_labels, a_value >= 1)
- expect(subject.metrics[:busy]).to receive(:set).with(ci_application_record_labels, a_value >= 1)
- expect(subject.metrics[:dead]).to receive(:set).with(ci_application_record_labels, a_value >= 0)
- expect(subject.metrics[:waiting]).to receive(:set).with(ci_application_record_labels, a_value >= 0)
+ context 'when all base models are connected', :add_ci_connection do
+ it 'samples connection pool statistics for all primaries' do
+ expect_metrics_with_labels(main_labels)
+ expect_metrics_with_labels(ci_labels)
subject.sample
end
+
+ context 'when replica hosts are configured' do
+ let(:main_load_balancer) { ActiveRecord::Base.load_balancer } # rubocop:disable Database/MultipleDatabases
+ let(:main_replica_host) { main_load_balancer.host }
+
+ let(:ci_load_balancer) { double(:load_balancer, host_list: ci_host_list, configuration: configuration) }
+ let(:configuration) { double(:configuration, primary_connection_specification_name: 'Ci::ApplicationRecord') }
+ let(:ci_host_list) { double(:host_list, hosts: [ci_replica_host]) }
+ let(:ci_replica_host) { double(:host, connection: ci_connection) }
+ let(:ci_connection) { double(:connection, pool: Ci::ApplicationRecord.connection_pool) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:each_load_balancer)
+ .and_return([main_load_balancer, ci_load_balancer].to_enum)
+
+ allow(main_load_balancer).to receive(:primary_only?).and_return(false)
+ allow(ci_load_balancer).to receive(:primary_only?).and_return(false)
+
+ allow(main_replica_host).to receive(:host).and_return('main-replica-host')
+ allow(ci_replica_host).to receive(:host).and_return('ci-replica-host')
+
+ allow(main_replica_host).to receive(:port).and_return(2345)
+ allow(ci_replica_host).to receive(:port).and_return(3456)
+
+ allow(Gitlab::Database).to receive(:db_config_name)
+ .with(main_replica_host.connection)
+ .and_return('main_replica')
+
+ allow(Gitlab::Database).to receive(:db_config_name)
+ .with(ci_replica_host.connection)
+ .and_return('ci_replica')
+ end
+
+ it 'samples connection pool statistics for primaries and replicas' do
+ expect_metrics_with_labels(main_labels)
+ expect_metrics_with_labels(ci_labels)
+ expect_metrics_with_labels(main_replica_labels)
+ expect_metrics_with_labels(ci_replica_labels)
+
+ subject.sample
+ end
+ end
end
- context 'when a database is not connected', :add_ci_connection do
+ context 'when a base model is not connected', :add_ci_connection do
before do
allow(Ci::ApplicationRecord).to receive(:connected?).and_return(false)
end
- it 'records no samples for that database' do
- expect(subject.metrics[:size]).to receive(:set).with(active_record_labels, anything)
- expect(subject.metrics[:size]).not_to receive(:set).with(ci_application_record_labels, anything)
+ it 'records no samples for that primary' do
+ expect_metrics_with_labels(main_labels)
+ expect_no_metrics_with_labels(ci_labels)
subject.sample
end
+
+ context 'when the base model has replica connections' do
+ let(:main_load_balancer) { ActiveRecord::Base.load_balancer } # rubocop:disable Database/MultipleDatabases
+ let(:main_replica_host) { main_load_balancer.host }
+
+ let(:ci_load_balancer) { double(:load_balancer, host_list: ci_host_list, configuration: configuration) }
+ let(:configuration) { double(:configuration, primary_connection_specification_name: 'Ci::ApplicationRecord') }
+ let(:ci_host_list) { double(:host_list, hosts: [ci_replica_host]) }
+ let(:ci_replica_host) { double(:host, connection: ci_connection) }
+ let(:ci_connection) { double(:connection, pool: Ci::ApplicationRecord.connection_pool) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:each_load_balancer)
+ .and_return([main_load_balancer, ci_load_balancer].to_enum)
+
+ allow(main_load_balancer).to receive(:primary_only?).and_return(false)
+ allow(ci_load_balancer).to receive(:primary_only?).and_return(false)
+
+ allow(main_replica_host).to receive(:host).and_return('main-replica-host')
+ allow(ci_replica_host).to receive(:host).and_return('ci-replica-host')
+
+ allow(main_replica_host).to receive(:port).and_return(2345)
+ allow(ci_replica_host).to receive(:port).and_return(3456)
+
+ allow(Gitlab::Database).to receive(:db_config_name)
+ .with(main_replica_host.connection)
+ .and_return('main_replica')
+
+ allow(Gitlab::Database).to receive(:db_config_name)
+ .with(ci_replica_host.connection)
+ .and_return('ci_replica')
+ end
+
+ it 'still records the replica metrics' do
+ expect_metrics_with_labels(main_labels)
+ expect_metrics_with_labels(main_replica_labels)
+ expect_no_metrics_with_labels(ci_labels)
+ expect_metrics_with_labels(ci_replica_labels)
+
+ subject.sample
+ end
+ end
+ end
+
+ def expect_metrics_with_labels(labels)
+ expect(subject.metrics[:size]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:connections]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:busy]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:dead]).to receive(:set).with(labels, a_value >= 0)
+ expect(subject.metrics[:waiting]).to receive(:set).with(labels, a_value >= 0)
+ end
+
+ def expect_no_metrics_with_labels(labels)
+ described_class::METRIC_DESCRIPTIONS.each_key do |metric|
+ expect(subject.metrics[metric]).not_to receive(:set).with(labels, anything)
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index a8e4f039da4..389b0ef1044 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -198,6 +198,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
context 'query using a connection to a replica' do
before do
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(:replica)
+ allow(connection).to receive_message_chain(:pool, :db_config, :name).and_return(db_config_name)
end
it 'queries connection db role' do
diff --git a/spec/lib/gitlab/patch/legacy_database_config_spec.rb b/spec/lib/gitlab/patch/legacy_database_config_spec.rb
index e6c0bdbf360..b87e16f31ae 100644
--- a/spec/lib/gitlab/patch/legacy_database_config_spec.rb
+++ b/spec/lib/gitlab/patch/legacy_database_config_spec.rb
@@ -11,6 +11,9 @@ RSpec.describe Gitlab::Patch::LegacyDatabaseConfig do
let(:configuration) { Rails::Application::Configuration.new(Rails.root) }
before do
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(Rails.root.join("config/database_geo.yml")).and_return(false)
+
# The `AS::ConfigurationFile` calls `read` in `def initialize`
# thus we cannot use `expect_next_instance_of`
# rubocop:disable RSpec/AnyInstanceOf
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index d801b84775b..210b9162be0 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -272,12 +272,12 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expected_end_payload.merge(
'db_duration_s' => a_value >= 0.1,
'db_count' => a_value >= 1,
- "db_replica_#{db_config_name}_count" => 0,
+ "db_#{db_config_name}_replica_count" => 0,
'db_replica_duration_s' => a_value >= 0,
'db_primary_count' => a_value >= 1,
- "db_primary_#{db_config_name}_count" => a_value >= 1,
+ "db_#{db_config_name}_count" => a_value >= 1,
'db_primary_duration_s' => a_value > 0,
- "db_primary_#{db_config_name}_duration_s" => a_value > 0
+ "db_#{db_config_name}_duration_s" => a_value > 0
)
end
diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
index e954d7a44ba..bc1fa3e88ff 100644
--- a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
let_it_be(:owner) { create(:user) }
- let_it_be(:group) do
+ let_it_be_with_reload(:group) do
build(:group, :private).tap do |g|
g.add_owner(owner)
end
@@ -70,6 +70,18 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
describe 'Menu items' do
subject { find_menu(menu, item_id) }
+ shared_examples 'the menu entry is available' do
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+ end
+
+ shared_examples 'the menu entry is not available' do
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
+ end
+
describe 'Packages Registry' do
let(:item_id) { :packages_registry }
@@ -81,17 +93,13 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
context 'when config package setting is disabled' do
let(:packages_enabled) { false }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
- end
+ it_behaves_like 'the menu entry is not available'
end
context 'when config package setting is enabled' do
let(:packages_enabled) { true }
- it 'the menu item is added to list of menu items' do
- is_expected.not_to be_nil
- end
+ it_behaves_like 'the menu entry is available'
end
end
end
@@ -107,24 +115,18 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
context 'when config registry setting is disabled' do
let(:container_enabled) { false }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
- end
+ it_behaves_like 'the menu entry is not available'
end
context 'when config registry setting is enabled' do
let(:container_enabled) { true }
- it 'the menu item is added to list of menu items' do
- is_expected.not_to be_nil
- end
+ it_behaves_like 'the menu entry is available'
context 'when user cannot read container images' do
let(:user) { nil }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
- end
+ it_behaves_like 'the menu entry is not available'
end
end
end
@@ -141,17 +143,28 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
context 'when config dependency_proxy is enabled' do
let(:dependency_enabled) { true }
- it 'the menu item is added to list of menu items' do
- is_expected.not_to be_nil
+ it_behaves_like 'the menu entry is available'
+
+ context 'when the group settings exist' do
+ let_it_be(:dependency_proxy_group_setting) { create(:dependency_proxy_group_setting, group: group) }
+
+ it_behaves_like 'the menu entry is available'
+
+ context 'when the proxy is disabled at the group level' do
+ before do
+ dependency_proxy_group_setting.enabled = false
+ dependency_proxy_group_setting.save!
+ end
+
+ it_behaves_like 'the menu entry is not available'
+ end
end
end
context 'when config dependency_proxy is not enabled' do
let(:dependency_enabled) { false }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
- end
+ it_behaves_like 'the menu entry is not available'
end
end
@@ -159,9 +172,7 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
let(:user) { nil }
let(:dependency_enabled) { true }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
- end
+ it_behaves_like 'the menu entry is not available'
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 20d8016fae2..af810572106 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -753,7 +753,7 @@ RSpec.describe Ci::Runner do
runner.created_at = 1.day.ago
end
- it { is_expected.to eq(:not_connected) }
+ it { is_expected.to eq(:never_contacted) }
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d10f1405a7b..3f9c3bc6858 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -124,7 +124,7 @@ RSpec.describe User do
it { is_expected.to have_many(:created_custom_emoji).inverse_of(:creator) }
it { is_expected.to have_many(:in_product_marketing_emails) }
it { is_expected.to have_many(:timelogs) }
- it { is_expected.to have_many(:callouts).class_name('UserCallout') }
+ it { is_expected.to have_many(:callouts).class_name('Users::Callout') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') }
describe '#user_detail' do
@@ -5589,7 +5589,7 @@ RSpec.describe User do
describe '#dismissed_callout?' do
let_it_be(:user, refind: true) { create(:user) }
- let_it_be(:feature_name) { UserCallout.feature_names.each_key.first }
+ let_it_be(:feature_name) { Users::Callout.feature_names.each_key.first }
context 'when no callout dismissal record exists' do
it 'returns false when no ignore_dismissal_earlier_than provided' do
@@ -5599,7 +5599,7 @@ RSpec.describe User do
context 'when dismissed callout exists' do
before_all do
- create(:user_callout, user: user, feature_name: feature_name, dismissed_at: 4.months.ago)
+ create(:callout, user: user, feature_name: feature_name, dismissed_at: 4.months.ago)
end
it 'returns true when no ignore_dismissal_earlier_than provided' do
@@ -5618,12 +5618,12 @@ RSpec.describe User do
describe '#find_or_initialize_callout' do
let_it_be(:user, refind: true) { create(:user) }
- let_it_be(:feature_name) { UserCallout.feature_names.each_key.first }
+ let_it_be(:feature_name) { Users::Callout.feature_names.each_key.first }
subject(:find_or_initialize_callout) { user.find_or_initialize_callout(feature_name) }
context 'when callout exists' do
- let!(:callout) { create(:user_callout, user: user, feature_name: feature_name) }
+ let!(:callout) { create(:callout, user: user, feature_name: feature_name) }
it 'returns existing callout' do
expect(find_or_initialize_callout).to eq(callout)
@@ -5633,7 +5633,7 @@ RSpec.describe User do
context 'when callout does not exist' do
context 'when feature name is valid' do
it 'initializes a new callout' do
- expect(find_or_initialize_callout).to be_a_new(UserCallout)
+ expect(find_or_initialize_callout).to be_a_new(Users::Callout)
end
it 'is valid' do
@@ -5645,7 +5645,7 @@ RSpec.describe User do
let(:feature_name) { 'notvalid' }
it 'initializes a new callout' do
- expect(find_or_initialize_callout).to be_a_new(UserCallout)
+ expect(find_or_initialize_callout).to be_a_new(Users::Callout)
end
it 'is not valid' do
diff --git a/spec/models/user_callout_spec.rb b/spec/models/users/callout_spec.rb
index 5b36c8450ea..293f0279e79 100644
--- a/spec/models/user_callout_spec.rb
+++ b/spec/models/users/callout_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe UserCallout do
- let_it_be(:callout) { create(:user_callout) }
+RSpec.describe Users::Callout do
+ let_it_be(:callout) { create(:callout) }
it_behaves_like 'having unique enum values'
diff --git a/spec/models/concerns/calloutable_spec.rb b/spec/models/users/calloutable_spec.rb
index d847413de88..01603d8bbd6 100644
--- a/spec/models/concerns/calloutable_spec.rb
+++ b/spec/models/users/calloutable_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe Calloutable do
- subject { build(:user_callout) }
+RSpec.describe Users::Calloutable do
+ subject { build(:callout) }
describe "Associations" do
it { is_expected.to belong_to(:user) }
@@ -14,9 +14,9 @@ RSpec.describe Calloutable do
end
describe '#dismissed_after?' do
- let(:some_feature_name) { UserCallout.feature_names.keys.second }
- let(:callout_dismissed_month_ago) { create(:user_callout, feature_name: some_feature_name, dismissed_at: 1.month.ago )}
- let(:callout_dismissed_day_ago) { create(:user_callout, feature_name: some_feature_name, dismissed_at: 1.day.ago )}
+ let(:some_feature_name) { Users::Callout.feature_names.keys.second }
+ let(:callout_dismissed_month_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.month.ago )}
+ let(:callout_dismissed_day_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.day.ago )}
it 'returns whether a callout dismissed after specified date' do
expect(callout_dismissed_month_ago.dismissed_after?(15.days.ago)).to eq(false)
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 6dbcb5cace7..8c0347b3c8d 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -159,27 +159,25 @@ RSpec.describe BlobPresenter do
presenter.highlight
end
end
- end
- describe '#highlight_transformed' do
context 'when blob is ipynb' do
let(:blob) { repository.blob_at('f6b7a707', 'files/ipython/markdown-table.ipynb') }
let(:git_blob) { blob.__getobj__ }
before do
- allow(git_blob).to receive(:transformed_for_diff).and_return(true)
+ allow(Gitlab::Diff::CustomDiff).to receive(:transformed_for_diff?).and_return(true)
end
it 'uses md as the transformed language' do
expect(Gitlab::Highlight).to receive(:highlight).with('files/ipython/markdown-table.ipynb', anything, plain: nil, language: 'md')
- presenter.highlight_transformed
+ presenter.highlight
end
it 'transforms the blob' do
expect(Gitlab::Highlight).to receive(:highlight).with('files/ipython/markdown-table.ipynb', include("%%"), plain: nil, language: 'md')
- presenter.highlight_transformed
+ presenter.highlight
end
end
@@ -197,7 +195,7 @@ RSpec.describe BlobPresenter do
it 'does not transform the file' do
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
- presenter.highlight_transformed
+ presenter.highlight
end
end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 66601c0e810..98d3a3b1c51 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -223,20 +223,22 @@ RSpec.describe 'Query.runner(id)' do
describe 'for runner with status' do
let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
+ let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
+
+ let(:status_fragment) do
+ %(
+ status
+ legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
+ newStatus: status(legacyMode: null)
+ )
+ end
let(:query) do
%(
query {
- staleRunner: runner(id: "#{stale_runner.to_global_id}") {
- status
- legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
- newStatus: status(legacyMode: null)
- }
- pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") {
- status
- legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
- newStatus: status(legacyMode: null)
- }
+ staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} }
+ pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} }
+ neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} }
}
)
end
@@ -257,6 +259,13 @@ RSpec.describe 'Query.runner(id)' do
'legacyStatusWithExplicitVersion' => 'PAUSED',
'newStatus' => 'OFFLINE'
)
+
+ never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner)
+ expect(never_contacted_instance_runner_data).to match a_hash_including(
+ 'status' => 'NOT_CONNECTED',
+ 'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED',
+ 'newStatus' => 'NEVER_CONTACTED'
+ )
end
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 51a07e60e15..267dd1b5e6f 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -62,6 +62,15 @@ RSpec.describe 'Query.runners' do
it_behaves_like 'a working graphql query returning expected runner'
end
+
+ context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do
+ let(:runner_type) { 'PROJECT_TYPE' }
+ let(:status) { 'NEVER_CONTACTED' }
+
+ let!(:expected_runner) { project_runner }
+
+ it_behaves_like 'a working graphql query returning expected runner'
+ end
end
describe 'pagination' do
diff --git a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb
index 716983f01d2..28a46583d2a 100644
--- a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Create a user callout' do
let_it_be(:current_user) { create(:user) }
- let(:feature_name) { ::UserCallout.feature_names.each_key.first }
+ let(:feature_name) { ::Users::Callout.feature_names.each_key.first }
let(:input) do
{
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 3e0c61a26c0..1712df6266c 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -252,7 +252,7 @@ RSpec.describe MergeRequestWidgetEntity do
subject { described_class.new(resource, request: request).as_json }
it 'provides a valid path value for user callout path' do
- expect(subject[:user_callouts_path]).to eq '/-/user_callouts'
+ expect(subject[:user_callouts_path]).to eq '/-/users/callouts'
end
it 'provides a valid value for suggest pipeline feature id' do
@@ -362,7 +362,7 @@ RSpec.describe MergeRequestWidgetEntity do
context 'when suggest pipeline has been dismissed' do
before do
- create(:user_callout, user: user, feature_name: described_class::SUGGEST_PIPELINE)
+ create(:callout, user: user, feature_name: described_class::SUGGEST_PIPELINE)
end
it 'is true' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index e3e2f5b59da..5d56084faa8 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -317,7 +317,7 @@ RSpec.describe Ci::RetryBuildService do
expect(build).to be_processed
end
- context 'when build with deployment is retried' do
+ shared_examples_for 'when build with deployment is retried' do
let!(:build) do
create(:ci_build, :with_deployment, :deploy_to_production,
pipeline: pipeline, stage_id: stage.id, project: project)
@@ -336,7 +336,7 @@ RSpec.describe Ci::RetryBuildService do
end
end
- context 'when build with dynamic environment is retried' do
+ shared_examples_for 'when build with dynamic environment is retried' do
let_it_be(:other_developer) { create(:user).tap { |u| project.add_developer(other_developer) } }
let(:environment_name) { 'review/$CI_COMMIT_REF_SLUG-$GITLAB_USER_ID' }
@@ -363,6 +363,18 @@ RSpec.describe Ci::RetryBuildService do
end
end
+ it_behaves_like 'when build with deployment is retried'
+ it_behaves_like 'when build with dynamic environment is retried'
+
+ context 'when create_deployment_in_separate_transaction feature flag is disabled' do
+ before do
+ stub_feature_flags(create_deployment_in_separate_transaction: false)
+ end
+
+ it_behaves_like 'when build with deployment is retried'
+ it_behaves_like 'when build with dynamic environment is retried'
+ end
+
context 'when build has needs' do
before do
create(:ci_build_need, build: build, name: 'build1')
diff --git a/spec/services/users/dismiss_user_callout_service_spec.rb b/spec/services/users/dismiss_callout_service_spec.rb
index 6bf9961eb74..6ba9f180444 100644
--- a/spec/services/users/dismiss_user_callout_service_spec.rb
+++ b/spec/services/users/dismiss_callout_service_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe Users::DismissUserCalloutService do
+RSpec.describe Users::DismissCalloutService do
describe '#execute' do
let_it_be(:user) { create(:user) }
let(:params) { { feature_name: feature_name } }
- let(:feature_name) { UserCallout.feature_names.each_key.first }
+ let(:feature_name) { Users::Callout.feature_names.each_key.first }
subject(:execute) do
described_class.new(
@@ -15,6 +15,6 @@ RSpec.describe Users::DismissUserCalloutService do
).execute
end
- it_behaves_like 'dismissing user callout', UserCallout
+ it_behaves_like 'dismissing user callout', Users::Callout
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index ef3c39c83c2..ae031f58bd4 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -93,7 +93,7 @@ module StubGitlabCalls
def stub_commonmark_sourcepos_disabled
render_options =
- if Feature.enabled?(:use_cmark_renderer)
+ if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml)
Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C
else
Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
index c06083ba952..6e8c340582a 100644
--- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -1,7 +1,11 @@
# frozen_string_literal: true
RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
- let(:db_config_name) { ::Gitlab::Database.db_config_names.first }
+ let(:db_config_name) do
+ db_config_name = ::Gitlab::Database.db_config_names.first
+ db_config_name += "_replica" if db_role == :secondary
+ db_config_name
+ end
let(:expected_payload_defaults) do
result = {}
@@ -39,15 +43,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_primary_cached_count: record_cached_query ? 1 : 0,
- "db_primary_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
+ "db_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
db_primary_count: record_query ? 1 : 0,
- "db_primary_#{db_config_name}_count": record_query ? 1 : 0,
+ "db_#{db_config_name}_count": record_query ? 1 : 0,
db_primary_duration_s: record_query ? 0.002 : 0.0,
- "db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0,
+ "db_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0,
db_primary_wal_count: record_wal_query ? 1 : 0,
- "db_primary_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
+ "db_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
- "db_primary_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
+ "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
})
elsif db_role == :replica
transform_hash(expected_payload_defaults, {
@@ -55,15 +59,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_replica_cached_count: record_cached_query ? 1 : 0,
- "db_replica_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
+ "db_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
db_replica_count: record_query ? 1 : 0,
- "db_replica_#{db_config_name}_count": record_query ? 1 : 0,
+ "db_#{db_config_name}_count": record_query ? 1 : 0,
db_replica_duration_s: record_query ? 0.002 : 0.0,
- "db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0,
+ "db_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0,
db_replica_wal_count: record_wal_query ? 1 : 0,
- "db_replica_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
+ "db_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
- "db_replica_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
+ "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
})
else
transform_hash(expected_payload_defaults, {
@@ -71,15 +75,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_primary_cached_count: 0,
- "db_primary_#{db_config_name}_cached_count": 0,
+ "db_#{db_config_name}_cached_count": 0,
db_primary_count: 0,
- "db_primary_#{db_config_name}_count": 0,
+ "db_#{db_config_name}_count": 0,
db_primary_duration_s: 0.0,
- "db_primary_#{db_config_name}_duration_s": 0.0,
+ "db_#{db_config_name}_duration_s": 0.0,
db_primary_wal_count: 0,
- "db_primary_#{db_config_name}_wal_count": 0,
+ "db_#{db_config_name}_wal_count": 0,
db_primary_wal_cached_count: 0,
- "db_primary_#{db_config_name}_wal_cached_count": 0
+ "db_#{db_config_name}_wal_cached_count": 0
})
end
@@ -105,7 +109,11 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
end
RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role|
- let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.retrieve_connection) }
+ let(:db_config_name) do
+ db_config_name = ::Gitlab::Database.db_config_names.first
+ db_config_name += "_replica" if db_role == :secondary
+ db_config_name
+ end
it 'increments only db counters' do
if record_query
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 17e6af2aedb..48b879fea26 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -138,6 +138,10 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
stub_file_read(structure_file, content: input)
allow(File).to receive(:open).with(structure_file.to_s, any_args).and_yield(output)
end
+
+ if Gitlab.ee?
+ allow(File).to receive(:open).with(Rails.root.join(Gitlab::Database::GEO_DATABASE_DIR, 'structure.sql').to_s, any_args).and_yield(output)
+ end
end
after do
@@ -328,6 +332,32 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end
end
+ context 'with multiple databases', :reestablished_active_record_base do
+ before do
+ allow(ActiveRecord::Tasks::DatabaseTasks).to receive(:setup_initial_database_yaml).and_return([:main, :geo])
+ end
+
+ describe 'db:structure:dump' do
+ it 'invokes gitlab:db:clean_structure_sql' do
+ skip unless Gitlab.ee?
+
+ expect(Rake::Task['gitlab:db:clean_structure_sql']).to receive(:invoke).twice.and_return(true)
+
+ expect { run_rake_task('db:structure:dump:main') }.not_to raise_error
+ end
+ end
+
+ describe 'db:schema:dump' do
+ it 'invokes gitlab:db:clean_structure_sql' do
+ skip unless Gitlab.ee?
+
+ expect(Rake::Task['gitlab:db:clean_structure_sql']).to receive(:invoke).once.and_return(true)
+
+ expect { run_rake_task('db:schema:dump:main') }.not_to raise_error
+ end
+ end
+ end
+
def run_rake_task(task_name, arguments = '')
Rake::Task[task_name].reenable
Rake.application.invoke_task("#{task_name}#{arguments}")
diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index 83a00135629..8242d20a9e7 100644
--- a/spec/views/projects/jobs/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -13,26 +13,47 @@ RSpec.describe 'projects/jobs/show' do
end
before do
- assign(:build, build.present)
assign(:project, project)
assign(:builds, builds)
allow(view).to receive(:can?).and_return(true)
end
- context 'when job is running' do
- let(:build) { create(:ci_build, :trace_live, :running, pipeline: pipeline) }
-
+ context 'when showing a CI build' do
before do
+ assign(:build, build.present)
render
end
- it 'does not show retry button' do
- expect(rendered).not_to have_link('Retry')
+ it 'shows job vue app' do
+ expect(rendered).to have_css('#js-job-page')
+ expect(rendered).not_to have_css('#js-bridge-page')
+ end
+
+ context 'when job is running' do
+ let(:build) { create(:ci_build, :trace_live, :running, pipeline: pipeline) }
+
+ it 'does not show retry button' do
+ expect(rendered).not_to have_link('Retry')
+ end
+
+ it 'does not show New issue button' do
+ expect(rendered).not_to have_link('New issue')
+ end
+ end
+ end
+
+ context 'when showing a bridge job' do
+ let(:bridge) { create(:ci_bridge, status: :pending) }
+
+ before do
+ assign(:build, bridge)
+ render
end
- it 'does not show New issue button' do
- expect(rendered).not_to have_link('New issue')
+ it 'shows bridge vue app' do
+ expect(rendered).to have_css('#js-bridge-page')
+ expect(rendered).not_to have_css('#js-job-page')
end
end
end