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