From ca5de528358c23e9cfcb0ff0f42c4106310c3811 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 10 Dec 2021 18:14:42 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rails.gitlab-ci.yml | 12 +- .rubocop_todo.yml | 2 +- .rubocop_todo/cop/user_admin.yml | 4 +- .rubocop_todo/gitlab/namespaced_class.yml | 2 - GITALY_SERVER_VERSION | 2 +- Gemfile | 2 +- Gemfile.lock | 4 +- .../code_navigation/components/doc_line.vue | 1 + .../import_groups/components/import_table.vue | 18 +- app/assets/javascripts/jobs/bridge/app.vue | 20 ++ .../jobs/bridge/components/constants.js | 1 + .../jobs/bridge/components/empty_state.vue | 45 +++ .../jobs/bridge/components/sidebar.vue | 98 +++++++ .../jobs/components/job_log_controllers.vue | 2 +- app/assets/javascripts/jobs/index.js | 39 ++- .../javascripts/milestones/milestone.js | 40 +-- .../dependency_proxy/app.vue | 24 +- .../dependency_proxy/constants.js | 6 - .../runner/components/runner_status_badge.vue | 2 + app/assets/javascripts/runner/constants.js | 1 + app/assets/javascripts/tabs/constants.js | 20 ++ app/assets/javascripts/tabs/index.js | 239 ++++++++++++++++ .../stylesheets/page_bundles/import.scss | 35 ++- .../concerns/dependency_proxy/group_access.rb | 4 +- .../groups/dependency_proxies_controller.rb | 19 +- app/controllers/projects/jobs_controller.rb | 4 +- app/controllers/user_callouts_controller.rb | 29 -- app/controllers/users/callouts_controller.rb | 31 +++ .../users/group_callouts_controller.rb | 2 +- app/graphql/mutations/user_callouts/create.rb | 2 +- app/graphql/types/ci/runner_status_enum.rb | 8 +- .../types/user_callout_feature_name_enum.rb | 2 +- app/helpers/application_helper.rb | 4 - app/helpers/ci/jobs_helper.rb | 7 + app/helpers/ci/runners_helper.rb | 2 +- app/helpers/merge_requests_helper.rb | 2 +- app/helpers/tab_helper.rb | 5 +- app/helpers/user_callouts_helper.rb | 98 ------- app/helpers/users/callouts_helper.rb | 76 +++++ app/helpers/users/group_callouts_helper.rb | 32 +++ app/models/ci/runner.rb | 7 +- app/models/concerns/calloutable.rb | 15 - app/models/user.rb | 4 +- app/models/user_callout.rb | 48 ---- app/models/users/callout.rb | 52 ++++ app/models/users/calloutable.rb | 17 ++ app/models/users/group_callout.rb | 2 +- app/presenters/blob_presenter.rb | 40 +-- .../merge_request_widget_entity.rb | 2 +- app/services/ci/retry_build_service.rb | 36 ++- .../outdated_discussion_diff_lines_service.rb | 20 +- ..._service.rb => dismiss_callout_service.rb} | 2 +- .../users/dismiss_group_callout_service.rb | 2 +- .../_security_newsletter_callout.html.haml | 2 +- .../_gcp_signup_offer_banner.html.haml | 2 +- app/views/devise/shared/_tab_single.html.haml | 5 +- .../registry/repositories/index.html.haml | 4 +- app/views/groups/show.html.haml | 2 +- .../_registration_enabled_callout.html.haml | 2 +- .../projects/feature_flags/new.html.haml | 4 +- app/views/projects/jobs/show.html.haml | 5 +- .../registry/repositories/index.html.haml | 4 +- app/views/root/index.html.haml | 4 +- .../shared/_flash_user_callout.html.haml | 2 +- ...tor_auth_recovery_settings_check.html.haml | 2 +- app/views/shared/milestones/_tabs.html.haml | 30 +- .../ci_retry_downstream_pipeline.yml | 8 + .../development/use_cmark_renderer.yml | 2 +- .../use_optimized_group_labels_query.yml | 2 +- .../active_record_database_tasks.rb | 7 + config/initializers/database_config.rb | 8 +- .../initializers/validate_database_config.rb | 4 +- config/routes.rb | 2 +- config/routes/user.rb | 1 + ...-runner-api-status-does-contain-paused.yml | 9 +- ...unner-api-status-renames-not_connected.yml | 13 + ...20211201143042_create_lfs_object_states.rb | 32 +++ db/schema_migrations/20211201143042 | 1 + db/structure.sql | 39 +++ .../geo/replication/datatypes.md | 4 +- .../monitoring/prometheus/gitlab_metrics.md | 17 +- doc/api/geo_nodes.md | 41 ++- doc/api/graphql/reference/index.md | 3 +- doc/update/deprecations.md | 18 +- lib/banzai/filter/footnote_filter.rb | 14 +- .../filter/markdown_engines/common_mark.rb | 8 +- .../filter/markdown_post_escape_filter.rb | 2 +- lib/banzai/filter/plantuml_filter.rb | 2 +- lib/banzai/filter/sanitization_filter.rb | 4 +- lib/banzai/filter/syntax_highlight_filter.rb | 4 +- .../html_pipeline_adapter.rb | 2 +- lib/gitlab/ci/status/bridge/common.rb | 6 +- lib/gitlab/database.rb | 4 + lib/gitlab/database/gitlab_schemas.yml | 1 + lib/gitlab/diff/custom_diff.rb | 58 ++++ lib/gitlab/diff/file.rb | 33 +-- lib/gitlab/diff/highlight.rb | 2 - lib/gitlab/git/blob.rb | 3 +- .../importer/diff_note_importer.rb | 4 + .../github_import/importer/note_importer.rb | 1 + .../github_import/representation/diff_note.rb | 21 +- .../github_import/representation/note.rb | 8 + .../metrics/samplers/database_sampler.rb | 23 ++ .../metrics/subscribers/active_record.rb | 34 ++- lib/gitlab/patch/legacy_database_config.rb | 44 +++ .../groups/menus/packages_registries_menu.rb | 18 +- .../projects/menus/infrastructure_menu.rb | 6 +- lib/tasks/gitlab/db.rake | 17 +- locale/gitlab.pot | 32 ++- .../dependency_proxies_controller_spec.rb | 78 ++---- spec/controllers/root_controller_spec.rb | 4 +- .../callouts_controller_spec.rb} | 10 +- spec/db/schema_spec.rb | 2 +- .../{user_callouts.rb => users/callouts.rb} | 2 +- .../user_uploads_designs_spec.rb | 3 + .../projects/milestones/milestone_spec.rb | 39 ++- .../__snapshots__/popover_spec.js.snap | 4 + spec/frontend/fixtures/tabs.rb | 26 ++ .../components/import_table_spec.js | 19 ++ spec/frontend/jobs/bridge/app_spec.js | 33 +++ .../bridge/components/empty_state_spec.js | 59 ++++ .../jobs/bridge/components/sidebar_spec.js | 76 +++++ spec/frontend/jobs/bridge/mock_data.js | 3 + .../dependency_proxy/app_spec.js | 38 +-- .../components/runner_status_badge_spec.js | 14 + spec/frontend/tabs/index_spec.js | 260 ++++++++++++++++++ .../mutations/user_callouts/create_spec.rb | 6 +- .../user_callout_feature_name_enum_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 14 - spec/helpers/ci/jobs_helper_spec.rb | 25 ++ spec/helpers/ide_helper_spec.rb | 2 +- spec/helpers/tab_helper_spec.rb | 8 +- .../callouts_helper_spec.rb} | 81 +----- .../users/group_callouts_helper_spec.rb | 87 ++++++ .../validate_database_config_spec.rb | 3 + .../lib/banzai/filter/markdown_filter_spec.rb | 8 +- .../lib/banzai/filter/plantuml_filter_spec.rb | 6 +- .../filter/syntax_highlight_filter_spec.rb | 18 +- .../pipeline/plain_markdown_pipeline_spec.rb | 2 +- spec/lib/gitlab/asciidoc_spec.rb | 2 +- .../gitlab/ci/status/bridge/common_spec.rb | 10 +- spec/lib/gitlab/diff/custom_diff_spec.rb | 62 +++++ .../importer/diff_note_importer_spec.rb | 27 +- .../importer/note_importer_spec.rb | 2 + .../metrics/samplers/database_sampler_spec.rb | 145 ++++++++-- .../metrics/subscribers/active_record_spec.rb | 1 + .../patch/legacy_database_config_spec.rb | 3 + .../sidekiq_logging/structured_logger_spec.rb | 6 +- .../menus/packages_registries_menu_spec.rb | 59 ++-- spec/models/ci/runner_spec.rb | 2 +- spec/models/user_spec.rb | 14 +- .../callout_spec.rb} | 4 +- .../{concerns => users}/calloutable_spec.rb | 10 +- spec/presenters/blob_presenter_spec.rb | 10 +- spec/requests/api/graphql/ci/runner_spec.rb | 29 +- spec/requests/api/graphql/ci/runners_spec.rb | 9 + .../mutations/user_callouts/create_spec.rb | 2 +- .../merge_request_widget_entity_spec.rb | 4 +- spec/services/ci/retry_build_service_spec.rb | 16 +- ...pec.rb => dismiss_callout_service_spec.rb} | 6 +- spec/support/helpers/stub_gitlab_calls.rb | 2 +- ...ctive_record_subscriber_shared_examples.rb | 42 +-- spec/tasks/gitlab/db_rake_spec.rb | 30 ++ .../projects/jobs/show.html.haml_spec.rb | 37 ++- 164 files changed, 2443 insertions(+), 846 deletions(-) create mode 100644 app/assets/javascripts/jobs/bridge/app.vue create mode 100644 app/assets/javascripts/jobs/bridge/components/constants.js create mode 100644 app/assets/javascripts/jobs/bridge/components/empty_state.vue create mode 100644 app/assets/javascripts/jobs/bridge/components/sidebar.vue create mode 100644 app/assets/javascripts/tabs/constants.js create mode 100644 app/assets/javascripts/tabs/index.js delete mode 100644 app/controllers/user_callouts_controller.rb create mode 100644 app/controllers/users/callouts_controller.rb delete mode 100644 app/helpers/user_callouts_helper.rb create mode 100644 app/helpers/users/callouts_helper.rb create mode 100644 app/helpers/users/group_callouts_helper.rb delete mode 100644 app/models/concerns/calloutable.rb delete mode 100644 app/models/user_callout.rb create mode 100644 app/models/users/callout.rb create mode 100644 app/models/users/calloutable.rb rename app/services/users/{dismiss_user_callout_service.rb => dismiss_callout_service.rb} (83%) create mode 100644 config/feature_flags/development/ci_retry_downstream_pipeline.yml create mode 100644 config/initializers/active_record_database_tasks.rb create mode 100644 data/deprecations/14-6-runner-api-status-renames-not_connected.yml create mode 100644 db/migrate/20211201143042_create_lfs_object_states.rb create mode 100644 db/schema_migrations/20211201143042 create mode 100644 lib/gitlab/diff/custom_diff.rb rename spec/controllers/{user_callouts_controller_spec.rb => users/callouts_controller_spec.rb} (73%) rename spec/factories/{user_callouts.rb => users/callouts.rb} (71%) create mode 100644 spec/frontend/fixtures/tabs.rb create mode 100644 spec/frontend/jobs/bridge/app_spec.js create mode 100644 spec/frontend/jobs/bridge/components/empty_state_spec.js create mode 100644 spec/frontend/jobs/bridge/components/sidebar_spec.js create mode 100644 spec/frontend/jobs/bridge/mock_data.js create mode 100644 spec/frontend/tabs/index_spec.js create mode 100644 spec/helpers/ci/jobs_helper_spec.rb rename spec/helpers/{user_callouts_helper_spec.rb => users/callouts_helper_spec.rb} (76%) create mode 100644 spec/helpers/users/group_callouts_helper_spec.rb create mode 100644 spec/lib/gitlab/diff/custom_diff_spec.rb rename spec/models/{user_callout_spec.rb => users/callout_spec.rb} (80%) rename spec/models/{concerns => users}/calloutable_spec.rb (57%) rename spec/services/users/{dismiss_user_callout_service_spec.rb => dismiss_callout_service_spec.rb} (63%) 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 { {{ token.value }} +
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 { /> 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 similarity index 83% rename from app/services/users/dismiss_user_callout_service.rb rename to 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: "".html_safe, anchorClose: ''.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).

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).

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 | | ----- | ----------- | | `ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. | -| `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. | +| `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. | +| `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. | | `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. | | `ONLINE` | Runner that contacted this instance within the last 2 hours. | | `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) %(
#{node.content}
) else %(
#{node.content}
) 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| - 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 = [] - ::Gitlab::Database.db_config_names.each do |config_name| - counters << compose_metric_key(metric, role, config_name) - end + metrics.each do |metric| + DB_LOAD_BALANCING_ROLES.each do |role| + counters << compose_metric_key(metric, role) + 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 + subject { get :show, params: { group_id: group.to_param } } + + before do + stub_config(dependency_proxy: { enabled: config_enabled }) + end + + context 'with global config enabled' do + let(:config_enabled) { true } + + context 'with the setting enabled' do + it 'returns 200 and renders the view' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('groups/dependency_proxies/show') + end end - it 'returns 200 and renders the view' do - get :show, params: { group_id: group.to_param } + context 'with the setting disabled' do + before do + dependency_proxy_group_setting.update!(enabled: false) + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template('groups/dependency_proxies/show') + it_behaves_like 'returning response status', :not_found end end - it 'returns 404 when feature is disabled' do - disable_dependency_proxy + context 'with global config disabled' do + let(:config_enabled) { false } - get :show, params: { group_id: group.to_param } - - expect(response).to have_gitlab_http_status(:not_found) + it_behaves_like 'returning response status', :not_found end end - - describe 'PUT #update' do - context 'feature enabled' do - before do - enable_dependency_proxy - end - - it 'redirects back to show page' do - put :update, params: update_params - - expect(response).to have_gitlab_http_status(:found) - end - end - - it 'returns 404 when feature is disabled' do - put :update, params: update_params - - expect(response).to have_gitlab_http_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 similarity index 73% rename from spec/controllers/user_callouts_controller_spec.rb rename to 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 similarity index 71% rename from spec/factories/user_callouts.rb rename to 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`] = ` main() { + +
} + +
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{