diff options
63 files changed, 1032 insertions, 682 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index ba623ef4cbe..0ee810340ff 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -229,14 +229,6 @@ - *node-modules-cache # We don't push this cache as it's already rebuilt by `update-assets-compile-*-cache` - *storybook-node-modules-cache-push -.use-pg11: - services: - - name: postgres:11.6 - command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - - name: redis:5.0-alpine - variables: - POSTGRES_HOST_AUTH_METHOD: trust - PG_VERSION: "11" .use-pg12: services: @@ -256,21 +248,6 @@ POSTGRES_HOST_AUTH_METHOD: trust PG_VERSION: "13" -.use-pg11-es7-ee: - services: - - name: postgres:11.6 - command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - - name: redis:5.0-alpine - - name: elasticsearch:7.17.6 - command: ["elasticsearch", "-E", "discovery.type=single-node", "-E", "xpack.security.enabled=false"] - - name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.0 - alias: zoekt-ci-image - variables: - POSTGRES_HOST_AUTH_METHOD: trust - PG_VERSION: "11" - ZOEKT_INDEX_BASE_URL: http://zoekt-ci-image:6060 - ZOEKT_SEARCH_BASE_URL: http://zoekt-ci-image:6070 - .use-pg12-es7-ee: services: - name: postgres:12 diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 23f38fddb80..671325101e2 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -191,16 +191,6 @@ rspec system pg12 praefect: - .praefect-with-db - .rails:rules:praefect-with-db -# Dedicated job to test DB library code against PG11. -# Note that these are already tested against PG12 in the `rspec unit pg12` / `rspec-ee unit pg12` jobs. -rspec db-library-code pg11: - extends: - - .rspec-base-pg11 - - .rails:rules:ee-and-foss-db-library-code - script: - - !reference [.base-script, script] - - rspec_db_library_code - rspec fast_spec_helper: extends: - .rspec-base-pg12 @@ -616,39 +606,6 @@ rspec-ee system pg12 single-db: ########################################## # EE/FOSS: default branch nightly scheduled jobs # -# PG11 -rspec migration pg11: - extends: - - .rspec-base-pg11 - - .rspec-base-migration - - .rails:rules:rspec-on-pg11 - - .rspec-migration-parallel - -rspec background_migration pg11: - extends: - - .rspec-base-pg11 - - .rspec-base-migration - - .rails:rules:rspec-on-pg11 - - .rspec-background-migration-parallel - -rspec unit pg11: - extends: - - .rspec-base-pg11 - - .rails:rules:rspec-on-pg11 - - .rspec-unit-parallel - -rspec integration pg11: - extends: - - .rspec-base-pg11 - - .rails:rules:rspec-on-pg11 - - .rspec-integration-parallel - -rspec system pg11: - extends: - - .rspec-base-pg11 - - .rails:rules:rspec-on-pg11 - - .rspec-system-parallel - # PG13 rspec migration pg13: extends: @@ -687,39 +644,6 @@ rspec system pg13: ##################################### # EE: default branch nightly scheduled jobs # -# PG11 -rspec-ee migration pg11: - extends: - - .rspec-ee-base-pg11 - - .rspec-base-migration - - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only - - .rspec-ee-migration-parallel - -rspec-ee background_migration pg11: - extends: - - .rspec-ee-base-pg11 - - .rspec-base-migration - - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only - - .rspec-ee-background-migration-parallel - -rspec-ee unit pg11: - extends: - - .rspec-ee-base-pg11 - - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only - - .rspec-ee-unit-parallel - -rspec-ee integration pg11: - extends: - - .rspec-ee-base-pg11 - - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only - - .rspec-ee-integration-parallel - -rspec-ee system pg11: - extends: - - .rspec-ee-base-pg11 - - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only - - .rspec-ee-system-parallel - # PG12 rspec-ee unit pg12 opensearch1: extends: diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml index 4943f7c2e28..ba0781a845f 100644 --- a/.gitlab/ci/rails/shared.gitlab-ci.yml +++ b/.gitlab/ci/rails/shared.gitlab-ci.yml @@ -92,11 +92,6 @@ include: - !reference [.base-script, script] - rspec_paralellized_job "--tag ~quarantine --tag ~zoekt" -.rspec-base-pg11: - extends: - - .rspec-base - - .use-pg11 - .rspec-base-pg12: extends: - .rspec-base @@ -119,11 +114,6 @@ include: - .rspec-base - .use-pg13 -.rspec-ee-base-pg11: - extends: - - .rspec-base - - .use-pg11-es7-ee - .rspec-ee-base-pg12: extends: - .rspec-base diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index d1e29084a5a..ee85ca82673 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -85,9 +85,6 @@ .if-merge-request-labels-run-review-app: &if-merge-request-labels-run-review-app if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-review-app/' -.if-merge-request-labels-run-on-pg11: &if-merge-request-labels-run-on-pg11 - if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-on-pg11/' - .if-merge-request-labels-skip-undercoverage: &if-merge-request-labels-skip-undercoverage if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:skip-undercoverage/' @@ -1605,7 +1602,6 @@ - <<: *if-default-refs changes: *db-library-patterns - <<: *if-merge-request-labels-run-all-rspec - - <<: *if-merge-request-labels-run-on-pg11 .rails:rules:ee-mr-and-default-branch-only: rules: @@ -1695,11 +1691,6 @@ - <<: *if-merge-request changes: *backend-patterns -.rails:rules:rspec-on-pg11: - rules: - - <<: *if-merge-request-labels-run-on-pg11 - - !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules] - .rails:rules:default-branch-schedule-nightly--code-backstage-default-rules: rules: - <<: *if-default-branch-schedule-nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index bba2aeb77cc..b82e5de350f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 15.9.1 (2023-02-23) + +### Fixed (2 changes) + +- [Fix Broadcast messages not showing in admin console](gitlab-org/gitlab@f50dfdfe43231b4bb52378eaaa515ee76c918d03) ([merge request](gitlab-org/gitlab!112831)) +- [Fix dependency check in license approval policies](gitlab-org/gitlab@ff5a77036fdb74c4b410fbb954428dbf8736ffd8) ([merge request](gitlab-org/gitlab!112831)) **GitLab Enterprise Edition** + ## 15.9.0 (2023-02-21) ### Added (223 changes) diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 838debf1ceb..6bbad88715f 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -82,7 +82,7 @@ export default { eventHub.$on('skip-beforeunload', this.handleSkipBeforeUnload); if (this.themeName) - document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`); + document.querySelector('.navbar-gitlab')?.classList.add(`theme-${this.themeName}`); }, destroyed() { eventHub.$off('skip-beforeunload', this.handleSkipBeforeUnload); diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue index df37cd1f585..a92153c0509 100644 --- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue @@ -71,14 +71,14 @@ export default { required: true, }, }, - clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0'], + clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0 gl-m-0! gl-ml-3!'], inputTypes: { key: 'key', value: 'value', }, i18n: { cancel: s__('CiVariables|Cancel'), - clearInputs: s__('CiVariables|Clear inputs'), + removeInputs: s__('CiVariables|Remove inputs'), formHelpText: s__( 'CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default.', ), @@ -209,7 +209,7 @@ export default { <div v-for="(variable, index) in variables" :key="variable.id" - class="gl-display-flex gl-align-items-center gl-mb-4" + class="gl-display-flex gl-align-items-center gl-mb-5" data-testid="ci-variable-row" > <gl-form-input-group class="gl-mr-4 gl-flex-grow-1"> @@ -244,12 +244,11 @@ export default { <gl-button v-if="canRemove(index)" v-gl-tooltip - :aria-label="$options.i18n.clearInputs" - :title="$options.i18n.clearInputs" + :aria-label="$options.i18n.removeInputs" + :title="$options.i18n.removeInputs" :class="$options.clearBtnSharedClasses" category="tertiary" - variant="danger" - icon="clear" + icon="remove" data-testid="delete-variable-btn" @click="deleteVariable(variable.id)" /> @@ -260,8 +259,7 @@ export default { :class="$options.clearBtnSharedClasses" data-testid="delete-variable-btn-placeholder" category="tertiary" - variant="danger" - icon="clear" + icon="remove" /> </div> diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index ebc118c0d87..83ae7142cf2 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Shortcuts from '~/behaviors/shortcuts/shortcuts'; import { insertText } from '~/lib/utils/common_utils'; +import { ENTER_KEY } from '~/lib/utils/keys'; import axios from '~/lib/utils/axios_utils'; const LINK_TAG_PATTERN = '[{text}](url)'; @@ -520,7 +521,7 @@ function continueOlText(listLineMatch, nextLineMatch) { function handleContinueList(e, textArea) { if (!gon.markdown_automatic_lists) return; - if (!(e.key === 'Enter')) return; + if (!(e.key === ENTER_KEY)) return; if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; if (textArea.selectionStart !== textArea.selectionEnd) return; @@ -577,6 +578,25 @@ function handleContinueList(e, textArea) { } } +/** + * Adds a Markdown hard break when `Shift+Enter` is pressed + * + * @param {Object} e - the event + * @param {Object} textArea - the targeted text area + */ +function handleHardBreak(e, textArea) { + if (!(e.key === ENTER_KEY)) return; + if (!e.shiftKey) return; + if (e.altKey || e.ctrlKey || e.metaKey) return; + + // prevent unintended line breaks inserted using Japanese IME on MacOS + if (compositioningNoteText) return; + + e.preventDefault(); + + insertText(textArea, '\\\n'); +} + export function keypressNoteText(e) { const textArea = this; @@ -584,6 +604,7 @@ export function keypressNoteText(e) { handleContinueList(e, textArea); handleSurroundSelectedText(e, textArea); + handleHardBreak(e, textArea); } export function compositionStartNoteText() { diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 667fd89af55..3a293956089 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -20,7 +20,9 @@ import { initReportAbuse } from '~/projects/report_abuse'; const hasPerfBar = document.querySelector('.with-performance-bar'); const performanceHeight = hasPerfBar ? 35 : 0; -initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); +initDiffStatsDropdown( + (document.querySelector('.navbar-gitlab')?.offsetHeight ?? 0) + performanceHeight, +); new ZenMode(); new ShortcutsNavigation(); diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue index fa96eee5f92..35e65698cd2 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue @@ -70,7 +70,7 @@ export default { return this.approvalDetails.length; }, detailsPath() { - return `${this.branchRulesPath}?branch=${this.name}`; + return `${this.branchRulesPath}?branch=${encodeURIComponent(this.name)}`; }, statusChecksText() { return sprintf(this.$options.i18n.statusChecks, { diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 2f25c2fd4b0..b8f659ac56f 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -99,7 +99,7 @@ export default { > <gl-icon name="users" /> <gl-loading-icon v-if="loading" size="sm" /> - <span v-else data-testid="collapsed-count" class="gl-pt-2 gl-px-3 gl-font-sm"> + <span v-else class="gl-pt-2 gl-px-3 gl-font-sm"> {{ participantCount }} </span> </div> @@ -133,7 +133,6 @@ export default { <gl-button variant="link" button-text-classes="gl-text-secondary" - data-testid="more-participants" @click="toggleMoreParticipants" >{{ toggleLabel }}</gl-button > diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_portal.vue b/app/assets/javascripts/super_sidebar/components/sidebar_portal.vue new file mode 100644 index 00000000000..2a805c86a3b --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/sidebar_portal.vue @@ -0,0 +1,30 @@ +<script> +import { MountingPortal } from 'portal-vue'; +import { SIDEBAR_PORTAL_ID, portalState } from '../constants'; + +/** + * Use this component to render content into the sidebar. + * + * Arbitrary content is allowed, but nav items should be added using a Ruby + * Sidebars::Panel subclass instead. + * + * Only one instance of this component on a given page is supported. This is to + * avoid ordering issues and cluttering the sidebar. + */ +export default { + components: { + MountingPortal, + }, + data() { + // This is shared state, by design. Do not mutate this state here. + return portalState; + }, + mountSelector: `#${SIDEBAR_PORTAL_ID}`, +}; +</script> + +<template> + <mounting-portal v-if="ready" :mount-to="$options.mountSelector" append> + <slot></slot> + </mounting-portal> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_portal_target.vue b/app/assets/javascripts/super_sidebar/components/sidebar_portal_target.vue new file mode 100644 index 00000000000..1154a4357e0 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/sidebar_portal_target.vue @@ -0,0 +1,17 @@ +<script> +import { SIDEBAR_PORTAL_ID, portalState } from '../constants'; + +export default { + mounted() { + portalState.ready = true; + }, + beforeDestroy() { + portalState.ready = false; + }, + mountId: SIDEBAR_PORTAL_ID, +}; +</script> + +<template> + <div v-once :id="$options.mountId"></div> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index b7a70825b62..2da71c029fd 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -1,6 +1,7 @@ <script> import { GlCollapse } from '@gitlab/ui'; import UserBar from './user_bar.vue'; +import SidebarPortalTarget from './sidebar_portal_target.vue'; import ContextSwitcherToggle from './context_switcher_toggle.vue'; import ContextSwitcher from './context_switcher.vue'; import HelpCenter from './help_center.vue'; @@ -14,6 +15,7 @@ export default { ContextSwitcher, HelpCenter, SidebarMenu, + SidebarPortalTarget, }, props: { sidebarData: { @@ -53,6 +55,7 @@ export default { </gl-collapse> <gl-collapse :visible="!contextSwitcherOpened"> <sidebar-menu :items="menuItems" /> + <sidebar-portal-target /> </gl-collapse> </div> <div class="gl-p-3"> diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js new file mode 100644 index 00000000000..a1046062cec --- /dev/null +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -0,0 +1,11 @@ +// Note: all constants defined here are considered internal implementation +// details for the sidebar. They should not be imported by anything outside of +// the super_sidebar directory. + +import Vue from 'vue'; + +export const SIDEBAR_PORTAL_ID = 'sidebar-portal-mount'; + +export const portalState = Vue.observable({ + ready: false, +}); diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index cb3293dd1c3..c15bc8d9895 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -133,6 +133,7 @@ .page-with-super-sidebar { padding-left: 0; + transition: padding-left $gl-transition-duration-medium; @include media-breakpoint-up(xl) { padding-left: $contextual-sidebar-width; diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index c8a84988859..a53601445ec 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -1082,6 +1082,9 @@ $tabs-holder-z-index: 250; .merge-request-sticky-header { z-index: 204; box-shadow: 0 1px 2px $issue-boards-card-shadow; +} + +.page-with-contextual-sidebar .merge-request-sticky-header { --width: calc(100% - #{$contextual-sidebar-width}); @include media-breakpoint-down(lg) { @@ -1093,6 +1096,18 @@ $tabs-holder-z-index: 250; --width: calc(100% - #{$contextual-sidebar-collapsed-width}); } +.page-with-super-sidebar .merge-request-sticky-header { + @include media-breakpoint-up(xl) { + --width: calc(100% - #{$super-sidebar-width}); + } +} + +.page-with-super-sidebar-collapsed .merge-request-sticky-header { + @include media-breakpoint-up(xl) { + --width: 100%; + } +} + .merge-request-notification-toggle { .gl-toggle { @include gl-ml-auto; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 75c81b74ba7..d7f95e88290 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -272,20 +272,30 @@ ul.related-merge-requests > li gl-emoji { @include media-breakpoint-up(md) { // collapsed left sidebar + collapsed right sidebar - .issue-sticky-header { + .page-with-contextual-sidebar .issue-sticky-header { left: $contextual-sidebar-collapsed-width; --width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-collapsed-width}); } // collapsed left sidebar + expanded right sidebar - .right-sidebar-expanded .issue-sticky-header { + .page-with-contextual-sidebar.right-sidebar-expanded .issue-sticky-header { --width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-width}); } + + // collapsed super sidebar + collapsed right sidebar + .page-with-super-sidebar .issue-sticky-header { + --width: calc(100% - #{$gutter-collapsed-width}); + } + + // collapsed super sidebar + expanded right sidebar + .page-with-super-sidebar.right-sidebar-expanded .issue-sticky-header { + --width: calc(100% - #{$gutter-width}); + } } @include media-breakpoint-up(xl) { // expanded left sidebar + collapsed right sidebar - .issue-sticky-header { + .page-with-contextual-sidebar .issue-sticky-header { left: $contextual-sidebar-width; --width: calc(100% - #{$contextual-sidebar-width} - #{$gutter-collapsed-width}); } @@ -297,14 +307,38 @@ ul.related-merge-requests > li gl-emoji { } // expanded left sidebar + expanded right sidebar - .right-sidebar-expanded .issue-sticky-header { + .page-with-contextual-sidebar.right-sidebar-expanded .issue-sticky-header { --width: calc(100% - #{$contextual-sidebar-width} - #{$gutter-width}); } // collapsed left sidebar + expanded right sidebar - .right-sidebar-expanded.page-with-icon-sidebar .issue-sticky-header { + .page-with-contextual-sidebar.right-sidebar-expanded.page-with-icon-sidebar .issue-sticky-header { --width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-width}); } + + // expanded super sidebar + collapsed right sidebar + .page-with-super-sidebar .issue-sticky-header { + left: $super-sidebar-width; + --width: calc(100% - #{$super-sidebar-width} - #{$gutter-collapsed-width}); + } + + // collapsed super sidebar + collapsed right sidebar + .page-with-super-sidebar-collapsed .issue-sticky-header { + left: 0; + --width: calc(100% - #{$gutter-collapsed-width}); + } + + // expanded super sidebar + expanded right sidebar + .page-with-super-sidebar.right-sidebar-expanded .issue-sticky-header { + left: $super-sidebar-width; + --width: calc(100% - #{$super-sidebar-width} - #{$gutter-width}); + } + + // collapsed super sidebar + expanded right sidebar + .page-with-super-sidebar-collapsed.right-sidebar-expanded .issue-sticky-header { + left: 0; + --width: calc(100% - #{$gutter-width}); + } } .issuable-header-slide-enter-active, diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb index 300c1d6d779..3dc1780d6fe 100644 --- a/app/controllers/concerns/sorting_preference.rb +++ b/app/controllers/concerns/sorting_preference.rb @@ -90,6 +90,10 @@ module SortingPreference return false unless sort_order return can_sort_by_issue_weight?(action_name == 'issues') if sort_order.include?('weight') + if sort_order.include?('merged_at') + return can_sort_by_merged_date?(controller_name == 'merge_requests' || action_name == 'merge_requests') + end + true end end diff --git a/app/controllers/jira_connect/public_keys_controller.rb b/app/controllers/jira_connect/public_keys_controller.rb index 4505ab16926..8cb932c087f 100644 --- a/app/controllers/jira_connect/public_keys_controller.rb +++ b/app/controllers/jira_connect/public_keys_controller.rb @@ -22,8 +22,6 @@ module JiraConnect end def public_key_storage_enabled? - return true if Gitlab.config.jira_connect.enable_public_keys_storage - Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled? end end diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 50e3c3cc5fe..28b30ae051c 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -12,7 +12,7 @@ module JiraConnectHelper users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in gitlab_user_path: current_user ? user_path(current_user) : nil, oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data(installation).to_json : nil, - public_key_storage_enabled: Gitlab.config.jira_connect.enable_public_keys_storage || Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled? + public_key_storage_enabled: Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled? } end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 0f328410abc..ad1aa3ad734 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -11,10 +11,15 @@ module NavHelper def page_with_sidebar_class class_name = page_gutter_class - class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar - class_name << 'page-with-super-sidebar' if show_super_sidebar? && @left_sidebar - class_name << 'page-with-super-sidebar-collapsed' if show_super_sidebar? && collapsed_super_sidebar? && @left_sidebar - class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar && !show_super_sidebar? + + if show_super_sidebar? + class_name << 'page-with-super-sidebar' if defined?(@left_sidebar) && @left_sidebar + class_name << 'page-with-super-sidebar-collapsed' if collapsed_super_sidebar? && @left_sidebar + else + class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar + end + class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar class_name diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 4a9596a1347..9038d972f65 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -227,7 +227,7 @@ module SortingHelper options.concat([due_date_option]) if viewing_issues options.concat([popularity_option, label_priority_option]) - options.concat([merged_option]) if viewing_merge_requests + options.concat([merged_option]) if can_sort_by_merged_date?(viewing_merge_requests) options.concat([relative_position_option]) if viewing_issues options.concat([title_option]) @@ -237,6 +237,10 @@ module SortingHelper false end + def can_sort_by_merged_date?(viewing_merge_requests) + viewing_merge_requests && %w[all merged].include?(params[:state]) + end + def due_date_option { value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) } end diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 5a3e94afc63..87e8bd69994 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -58,15 +58,15 @@ = c.body do = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer' - .form-group - = label_tag :pin_code, _('Enter verification code'), class: "label-bold" - = text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' } - if current_password_required? .form-group = label_tag :current_password, _('Current password'), class: 'label-bold' = password_field_tag :current_password, nil, autocomplete: 'current-password', required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' } %p.form-text.text-muted = _('Your current password is required to register a two-factor authenticator app.') + .form-group + = label_tag :pin_code, _('Enter verification code'), class: "label-bold" + = text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' } .gl-mt-3 = submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' } diff --git a/app/views/projects/settings/integrations/_form.html.haml b/app/views/projects/settings/integrations/_form.html.haml index 9d74f99bb19..97d90976f18 100644 --- a/app/views/projects/settings/integrations/_form.html.haml +++ b/app/views/projects/settings/integrations/_form.html.haml @@ -11,6 +11,9 @@ = c.body do = s_('ExternalIssueIntegration|Only one issue tracker integration can be active at a time. Please disable the active tracker first and try again.') +- if integration.to_param === 'slack' + = render 'shared/integrations/slack_notifications_deprecation_alert' + %h2.gl-mb-4 = integration.title - if integration.operating? diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 5a9811c0e91..c683250bde3 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -495,7 +495,6 @@ production: &base ## To switch to a Jira connect development environment jira_connect: # atlassian_js_url: 'http://localhost:9292/atlassian.js' - # enable_public_keys_storage: true # enforce_jira_base_url_https: false # additional_iframe_ancestors: ['localhost:*'] diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b05fa6c1d8d..bcd051e8ace 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -449,8 +449,6 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled Settings['jira_connect'] ||= Settingslogic.new({}) Settings.jira_connect['atlassian_js_url'] ||= 'https://connect-cdn.atl-paas.net/all.js' -Settings.jira_connect['enable_public_keys_storage'] ||= false -Settings.jira_connect['enable_public_keys_storage'] = true if Gitlab.com? Settings.jira_connect['enforce_jira_base_url_https'] = true if Settings.jira_connect['enforce_jira_base_url_https'].nil? Settings.jira_connect['additional_iframe_ancestors'] ||= [] @@ -830,7 +828,7 @@ Gitlab.ee do Settings.cron_jobs['abandoned_trial_emails']['cron'] ||= "0 1 * * *" Settings.cron_jobs['abandoned_trial_emails']['job_class'] = 'Emails::AbandonedTrialEmailsCronWorker' Settings.cron_jobs['package_metadata_sync_worker'] ||= Settingslogic.new({}) - Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 1 * * *" + Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 * * * *" Settings.cron_jobs['package_metadata_sync_worker']['job_class'] = 'PackageMetadata::SyncWorker' Gitlab.com do Settings.cron_jobs['free_user_cap_backfill_notification_jobs_worker'] ||= Settingslogic.new({}) diff --git a/db/docs/ci_cost_settings.yml b/db/docs/ci_cost_settings.yml new file mode 100644 index 00000000000..3c5fc00cee0 --- /dev/null +++ b/db/docs/ci_cost_settings.yml @@ -0,0 +1,10 @@ +--- +table_name: ci_cost_settings +classes: +- Ci::Minutes::CostSetting +feature_categories: +- continuous_integration +description: A set of cost factors per runner which are applied to ci job duration based on project type. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111977 +milestone: '15.10' +gitlab_schema: gitlab_ci diff --git a/db/migrate/20230216171309_create_ci_runner_cost_settings.rb b/db/migrate/20230216171309_create_ci_runner_cost_settings.rb new file mode 100644 index 00000000000..5bc624c635a --- /dev/null +++ b/db/migrate/20230216171309_create_ci_runner_cost_settings.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateCiRunnerCostSettings < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + create_table :ci_cost_settings, id: false do |t| + t.timestamps_with_timezone null: false + t.references :runner, null: false, primary_key: true, index: false, + foreign_key: { to_table: :ci_runners, on_delete: :cascade }, + type: :bigint, default: nil + t.float :standard_factor, null: false, default: 1.00 + t.float :os_contribution_factor, null: false, default: 0.008 + t.float :os_plan_factor, null: false, default: 0.5 + end + end +end diff --git a/db/migrate/20230221110256_create_initial_partition_for_ci_runner_machine_builds.rb b/db/migrate/20230221110256_create_initial_partition_for_ci_runner_machine_builds.rb new file mode 100644 index 00000000000..9aa7049dde1 --- /dev/null +++ b/db/migrate/20230221110256_create_initial_partition_for_ci_runner_machine_builds.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class CreateInitialPartitionForCiRunnerMachineBuilds < Gitlab::Database::Migration[2.1] + PARTITION_NAME = 'gitlab_partitions_dynamic.ci_runner_machine_builds_100' + TABLE_NAME = 'p_ci_runner_machine_builds' + FIRST_PARTITION = 100 + BUILDS_TABLE = 'ci_builds' + + disable_ddl_transaction! + + def up + with_lock_retries(**lock_args) do + connection.execute(<<~SQL) + LOCK TABLE #{BUILDS_TABLE} IN SHARE UPDATE EXCLUSIVE MODE; + LOCK TABLE ONLY #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE; + SQL + + connection.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS #{PARTITION_NAME} + PARTITION OF #{TABLE_NAME} + FOR VALUES IN (#{FIRST_PARTITION}); + SQL + end + end + + def down + # no-op + # + # The migration should not remove the partition table since it might + # have been created by 20230215074223_add_ci_runner_machine_builds_partitioned_table.rb. + # In that case, the rollback would result in a different state. + end + + private + + def lock_args + { + raise_on_exhaustion: true, + timing_configuration: lock_timing_configuration + } + end + + def lock_timing_configuration + iterations = Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION + aggressive_iterations = Array.new(5) { [10.seconds, 1.minute] } + + iterations + aggressive_iterations + end +end diff --git a/db/schema_migrations/20230216171309 b/db/schema_migrations/20230216171309 new file mode 100644 index 00000000000..1e25ca0d3b1 --- /dev/null +++ b/db/schema_migrations/20230216171309 @@ -0,0 +1 @@ +ee00d6aba8a310c236dd16749228a42589657d060bbf1785c4358bf886fd59cc
\ No newline at end of file diff --git a/db/schema_migrations/20230221110256 b/db/schema_migrations/20230221110256 new file mode 100644 index 00000000000..2448c317588 --- /dev/null +++ b/db/schema_migrations/20230221110256 @@ -0,0 +1 @@ +661fdc00029ab9bae8b4da6a8d92f172db89087aecc13f3ad65b2b3e8ad501d3
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 335ce18eb3b..f593891995a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -543,6 +543,13 @@ CREATE TABLE batched_background_migration_job_transition_logs ( ) PARTITION BY RANGE (created_at); +CREATE TABLE p_ci_runner_machine_builds ( + partition_id bigint NOT NULL, + build_id bigint NOT NULL, + runner_machine_id bigint NOT NULL +) +PARTITION BY LIST (partition_id); + CREATE TABLE incident_management_pending_alert_escalations ( id bigint NOT NULL, rule_id bigint NOT NULL, @@ -13060,6 +13067,15 @@ CREATE SEQUENCE ci_builds_runner_session_id_seq ALTER SEQUENCE ci_builds_runner_session_id_seq OWNED BY ci_builds_runner_session.id; +CREATE TABLE ci_cost_settings ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + runner_id bigint NOT NULL, + standard_factor double precision DEFAULT 1.0 NOT NULL, + os_contribution_factor double precision DEFAULT 0.008 NOT NULL, + os_plan_factor double precision DEFAULT 0.5 NOT NULL +); + CREATE TABLE ci_daily_build_group_report_results ( id bigint NOT NULL, date date NOT NULL, @@ -19018,13 +19034,6 @@ CREATE SEQUENCE operations_user_lists_id_seq ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id; -CREATE TABLE p_ci_runner_machine_builds ( - partition_id bigint NOT NULL, - build_id bigint NOT NULL, - runner_machine_id bigint NOT NULL -) -PARTITION BY LIST (partition_id); - CREATE TABLE packages_build_infos ( id bigint NOT NULL, package_id integer NOT NULL, @@ -26219,6 +26228,9 @@ ALTER TABLE ONLY ci_builds ALTER TABLE ONLY ci_builds_runner_session ADD CONSTRAINT ci_builds_runner_session_pkey PRIMARY KEY (id); +ALTER TABLE ONLY ci_cost_settings + ADD CONSTRAINT ci_cost_settings_pkey PRIMARY KEY (runner_id); + ALTER TABLE ONLY ci_daily_build_group_report_results ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id); @@ -35618,6 +35630,9 @@ ALTER TABLE ONLY geo_hashed_storage_migrated_events ALTER TABLE ONLY plan_limits ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE; +ALTER TABLE ONLY ci_cost_settings + ADD CONSTRAINT fk_rails_6a70651f75 FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE; + ALTER TABLE ONLY operations_feature_flags_issues ADD CONSTRAINT fk_rails_6a8856ca4f FOREIGN KEY (feature_flag_id) REFERENCES operations_feature_flags(id) ON DELETE CASCADE; diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index d9cfbdf124e..95b8a49f408 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -113,6 +113,8 @@ To disable the inbound job token scope allowlist: 1. Toggle **Allow access to this project with a CI_JOB_TOKEN** to disabled. Enabled by default in new projects. +You can also disable the allowlist [with the API](../../api/graphql/reference/index.md#mutationprojectcicdsettingsupdate). + ### Add a project to the inbound job token scope allowlist You can add projects to the inbound allowlist for a project. Projects added to the allowlist @@ -133,6 +135,8 @@ To add a project: 1. Under **Allow CI job tokens from the following projects to access this project**, add projects to the allowlist. +You can also add a target project to the allowlist [with the API](../../api/graphql/reference/index.md#mutationcijobtokenscopeaddproject). + ### Limit your project's job token access > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328553) in GitLab 14.1. [Deployed behind the `:ci_scoped_job_token` feature flag](../../user/feature_flags.md), disabled by default. diff --git a/doc/development/application_slis/rails_request.md b/doc/development/application_slis/rails_request.md index fa22b5f6aca..b3ee326aa87 100644 --- a/doc/development/application_slis/rails_request.md +++ b/doc/development/application_slis/rails_request.md @@ -207,6 +207,14 @@ class Boards::ListsController < ApplicationController end ``` +A custom RSpec matcher is available to check endpoint's request urgency in the controller specs: + +```ruby +specify do + expect(get(:index, params: request_params)).to have_request_urgency(:medium) +end +``` + ### Grape endpoints To specify the urgency for an entire API class: @@ -240,6 +248,15 @@ get 'client/features', urgency: :low do end ``` +A custom RSpec matcher is also compatible with grape endpoints' specs: + +```ruby + +specify do + expect(get(api('/avatar'), params: { email: 'public@example.com' })).to have_request_urgency(:medium) +end +``` + WARNING: We can't specify the urgency at the namespace level. The directive is ignored when doing so. diff --git a/doc/development/navigation_sidebar.md b/doc/development/navigation_sidebar.md new file mode 100644 index 00000000000..495f30a796c --- /dev/null +++ b/doc/development/navigation_sidebar.md @@ -0,0 +1,38 @@ +--- +stage: Manage +group: Foundations +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Navigation sidebar + +Follow these guidelines when contributing additions or changes to the +[redesigned](https://gitlab.com/groups/gitlab-org/-/epics/9044) navigation +sidebar. + +These guidelines reflect the current state of the navigation sidebar. However, +the sidebar is a work in progress, and so is this documentation. + +## Enable the new navigation sidebar + +To enable the new navigation sidebar: + +- Enable the `super_sidebar_nav` feature flag. +- Select your avatar, then turn on the **New navigation** toggle. + +## Adding page-specific Vue content + +Pages can render arbitrary content into the sidebar using the `SidebarPortal` +component. Content passed to its default slot is rendered below that +page's navigation items in the sidebar. + +NOTE: +Only one instance of this component on a given page is supported. This is to +avoid ordering issues and cluttering the sidebar. + +NOTE: +Arbitrary content is allowed, but nav items should be implemented by +subclassing `::Sidebars::Panel`. + +NOTE: +Do not use the `SidebarPortalTarget` component. It is internal to the sidebar. diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md index 240d98a855f..e27b332d746 100644 --- a/doc/development/pipelines/index.md +++ b/doc/development/pipelines/index.md @@ -484,19 +484,17 @@ This should let us: Our test suite runs against PG12 as GitLab.com runs on PG12 and [Omnibus defaults to PG12 for new installs and upgrades](../../administration/package_information/postgresql_versions.md). -We do run our test suite against PG11 and PG13 on nightly scheduled pipelines. - -We also run our test suite against PG11 upon specific database library changes in MRs and `main` pipelines (with the `rspec db-library-code pg11` job). +We do run our test suite against PG13 on nightly scheduled pipelines. #### Current versions testing -| Where? | PostgreSQL version | Ruby version | -|------------------------------------------------------------------------------------------------|-------------------------------------------------|--------------| -| Merge requests | 12 (default version), 11 for DB library changes | 3.0 (default version) | -| `master` branch commits | 12 (default version), 11 for DB library changes | 3.0 (default version) | -| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 12 (default version), 11 for DB library changes | 3.0 (default version) | -| `maintenance` scheduled pipelines for the `ruby2` branch (every odd-numbered hour), see below. | 12 (default version), 11 for DB library changes | 2.7 | -| `nightly` scheduled pipelines for the `master` branch | 12 (default version), 11, 13 | 3.0 (default version) | +| Where? | PostgreSQL version | Ruby version | +|------------------------------------------------------------------------------------------------|--------------------------|-----------------------| +| Merge requests | 12 (default version) | 3.0 (default version) | +| `master` branch commits | 12 (default version) | 3.0 (default version) | +| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 12 (default version) | 3.0 (default version) | +| `maintenance` scheduled pipelines for the `ruby2` branch (every odd-numbered hour), see below. | 12 (default version) | 2.7 | +| `nightly` scheduled pipelines for the `master` branch | 12 (default version), 13 | 3.0 (default version) | There are 2 pipeline schedules used for testing Ruby 2.7. One is triggering a pipeline in `ruby2-sync` branch, which updates the `ruby2` branch with latest @@ -518,7 +516,6 @@ We follow the [PostgreSQL versions shipped with Omnibus GitLab](../../administra | PostgreSQL version | 14.1 (July 2021) | 14.2 (August 2021) | 14.3 (September 2021) | 14.4 (October 2021) | 14.5 (November 2021) | 14.6 (December 2021) | | -------------------| ---------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | | PG12 | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | -| PG11 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | | PG13 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | ### Redis versions testing diff --git a/doc/development/pipelines/internals.md b/doc/development/pipelines/internals.md index 9ff4e5a35ec..bd96f2f2872 100644 --- a/doc/development/pipelines/internals.md +++ b/doc/development/pipelines/internals.md @@ -136,8 +136,6 @@ that are scoped to a single [configuration keyword](../../ci/yaml/index.md#job-k | `.qa-cache` | Allows a job to use a default `cache` definition suitable for QA tasks. | | `.yarn-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that do a `yarn install`. | | `.assets-compile-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that compile assets. | -| `.use-pg11` | Allows a job to run the `postgres` 11 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). | -| `.use-pg11-ee` | Same as `.use-pg11` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). | | `.use-pg12` | Allows a job to use the `postgres` 12 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). | | `.use-pg12-ee` | Same as `.use-pg12` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). | | `.use-pg13` | Allows a job to use the `postgres` 13 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). | diff --git a/doc/user/packages/yarn_repository/index.md b/doc/user/packages/yarn_repository/index.md index 7e2f45019cd..e756a912928 100644 --- a/doc/user/packages/yarn_repository/index.md +++ b/doc/user/packages/yarn_repository/index.md @@ -6,220 +6,312 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Publish packages with Yarn -Publish npm packages in your project's Package Registry using Yarn. Then install the -packages whenever you need to use them as a dependency. +You can publish packages with [Yarn 1 (Classic)](https://classic.yarnpkg.com) and [Yarn 2+](https://yarnpkg.com). -Learn how to build a [yarn](../workflows/build_packages.md#yarn) package. +To find the Yarn version used in the deployment container, run `yarn --version` in the `script` block of the CI +script job block that is responsible for calling `yarn publish`**`. The Yarn version is shown in the pipeline output. -You can get started with Yarn 2 by following the [Yarn documentation](https://yarnpkg.com/getting-started/install/). +Learn how to build a [Yarn](../workflows/build_packages.md#yarn) package. + +You can use the Yarn documentation to get started with +[Yarn Classic](https://classic.yarnpkg.com/en/docs/getting-started) and +[Yarn 2+](https://yarnpkg.com/getting-started/). ## Publish to GitLab Package Registry +You can use Yarn to publish to the GitLab Package Registry. + ### Authentication to the Package Registry You need a token to publish a package. Different tokens are available depending on what you're trying to achieve. For more information, review the [guidance on tokens](../../../user/packages/package_registry/index.md#authenticate-with-the-registry). -- If your organization uses two-factor authentication (2FA), you must use a personal access token with the scope set to `api`. -- If you publish a package via CI/CD pipelines, you must use a CI job token. +- If your organization uses two-factor authentication (2FA), you must use a + personal access token with the scope set to `api`. +- If you publish a package via CI/CD pipelines, you can use a CI job token in + private runners or you can register a variable for shared runners. -Create a token and save it to use later in the process. +### Publish configuration -### Naming convention +To publish, set the following configuration in `.yarnrc.yml`. This file should be +located in the root directory of your package project source where `package.json` is found. -Depending on how you install the package, you may need to adhere to the naming convention. +```yaml +npmScopes: + <my-org>: + npmPublishRegistry: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/' + npmAlwaysAuth: true + npmAuthToken: '<your_token>' +``` -You can use one of two API endpoints to install packages: +In this configuration: -- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace. -- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group. +- Replace `<my-org>` with your organization scope, exclude the `@` symbol. +- Replace `<your_domain>` with your domain name. +- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page. +- Replace `<your_token>` with a deployment token, group access token, project access token, or personal access token. -If you plan to install a package through the [project level](#install-from-the-project-level), you do not have to -adhere to the naming convention. +Scoped registry does not work in Yarn Classic in `package.json` file, based on +this [issue](https://github.com/yarnpkg/yarn/pull/7829). +Therefore, under `publishConfig` there should be `registry` and not `@scope:registry` for Yarn Classic. +You can publish using your command line or a CI/CD pipeline to the GitLab Package Registry. -If you plan to install a package through the [instance level](#install-from-the-instance-level), then you must name -your package with a [scope](https://docs.npmjs.com/misc/scope/). Scoped packages begin with a `@` and have the -`@owner/package-name` format. You can set up the scope for your package in the `.yarnrc.yml` file and by using the -`publishConfig` option in the `package.json`. +### Publishing via the command line - Manual Publish -- The value used for the `@scope` is the root of the project that hosts the packages and not the root - of the project with the package's source code. The scope should be lowercase. -- The package name can be anything you want +```shell +# Yarn 1 (Classic) +yarn publish -| Project URL | Package Registry in | Scope | Full package name | -| ------------------------------------------------------- | ------------------- | --------- | ---------------------- | -| `https://gitlab.com/my-org/engineering-group/analytics` | Analytics | `@my-org` | `@my-org/package-name` | +# Yarn 2+ +yarn npm publish +``` -### Configuring `.yarnrc.yml` to publish from the project level +Your package should now publish to the Package Registry. -To publish with the project-level npm endpoint, set the following configuration in -`.yarnrc.yml`: +### Publishing via a CI/CD pipeline - Automated Publish -```yaml -npmScopes: - foo: - npmRegistryServer: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/' - npmPublishRegistry: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/' +You can use pipeline variables when you use this method. -npmRegistries: - //gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/: - npmAlwaysAuth: true - npmAuthToken: '<your_token>' -``` +You can use **Shared Runners** *(Default)* or **Private Runners** (Advanced). -In this configuration: +#### Shared runners -- Replace `<your_domain>` with your domain name. -- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page. -- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token. +Third party images such as `node:latest` or `node:current` do not have direct access +to the `CI_JOB_TOKEN` when operating in a shared runner. You must configure an +authentication token or use a private runner. + +To create a authentication token: -### Configuring `.yarnrc.yml` to publish from the instance level +1. On the top bar, select **Main menu**, and: + - For a project, select **Projects** and find your project. + - For a group, select **Groups** and find your group. +1. On the left sidebar, select **Settings > Repository > Deploy Tokens**. +1. Create a deployment token with `read_package_registry` and `write_package_registry` scopes and copy the generated token. +1. On the left sidebar, select **Settings > CI/CD > Variables**. +1. Select `Add variable` and use the following settings: -For the instance-level npm endpoint, use this Yarn 2 configuration in `.yarnrc.yml`: +| Field | Value | +|--------------------|------------------------------| +| key | `NPM_AUTH_TOKEN` | +| value | `<DEPLOY-TOKEN-FROM-STEP-3>` | +| type | Variable | +| Protected variable | `CHECKED` | +| Mask variable | `CHECKED` | +| Expand variable | `CHECKED` | + +To use any **Protected variable**: + + 1. Go to the repository that contains the Yarn package source code. + 1. On the left sidebar, select **Settings > Repository**. + - If you are building from branches with tags, select **Protected Tags** and add `v*` (wildcard) for semantic versioning. + - If you are building from branches without tags, select **Protected Branches**. + +Then add the `NPM_AUTH_TOKEN` created above, to the `.yarnrc.yml` configuration +in your package project root directory where `package.json` is found: ```yaml npmScopes: - <scope>: - npmRegistryServer: 'https://<your_domain>/api/v4/packages/npm/' - -npmRegistries: - //gitlab.example.com/api/v4/packages/npm/: + esp-code: + npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/" npmAlwaysAuth: true - npmAuthToken: '<your_token>' + npmAuthToken: "${NPM_AUTH_TOKEN}" ``` -In this configuration: +#### Private runners -- Replace `<your_domain>` with your domain name. -- Your scope is `<scope>`, without `@`. -- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token. +Add the `CI_JOB_TOKEN` to the `.yarnrc.yml` configuration in your package project +root directory where `package.json` is found: -### Publishing a package via the command line +```yaml +npmScopes: + esp-code: + npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/" + npmAlwaysAuth: true + npmAuthToken: "${CI_JOB_TOKEN}" +``` -Publish a package: +To publish the package using CI/CD pipeline, In the GitLab project that houses +your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example to trigger +only on any tag push: -```shell -npm publish -``` +```yaml +# Yarn 1 +image: node:lts -Your package should now publish to the Package Registry. +stages: + - deploy -### Publishing via a CI/CD pipeline +rules: +- if: $CI_COMMIT_TAG -In the GitLab project that houses your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example: +deploy: + stage: deploy + script: + - yarn publish +``` ```yaml -image: node:latest +# Yarn 2+ +image: node:lts stages: - deploy +rules: + - if: $CI_COMMIT_TAG + deploy: stage: deploy + before_script: + - corepack enable + - yarn set version stable script: - - npm publish + - yarn npm publish ``` Your package should now publish to the Package Registry when the pipeline runs. ## Install a package -If multiple packages have the same name and version, the most recently-published package is retrieved when you install a package. +NOTE: +If multiple packages have the same name and version, the most recently-published +package is retrieved when you install a package. -You can install a package from a GitLab project or instance: +You can use one of two API endpoints to install packages: -- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace. -- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group. +- **Instance-level**: Best used when working with many packages in an organization scope. -### Install from the instance level +- If you plan to install a package through the [instance level](#install-from-the-instance-level), + then you must name your package with a [scope](https://docs.npmjs.com/misc/scope/). + Scoped packages begin with a `@` and have the `@owner/package-name` format. You can set up + the scope for your package in the `.yarnrc.yml` file and by using the `publishConfig` + option in the `package.json`. -WARNING: -You must use packages published with the scoped [naming convention](#naming-convention) when you install a package from the instance level. +- The value used for the `@scope` is the organization root (top-level project) `...com/my-org` + *(@my-org)* that hosts the packages, not the root of the project with the package's source code. +- The scope is always lowercase. +- The package name can be anything you want `@my-org/any-name`. -1. Authenticate to the Package Registry +- **Project-level**: For when you have a one-off package. - If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private. +If you plan to install a package through the [project level](#install-from-the-project-level), +you do not have to adhere to the naming convention. - ```shell - npm config set -- //your_domain_name/api/v4/packages/npm/:_authToken=your_token - ``` +| Project URL | Package Registry | Organization Scope | Full package name | +|-------------------------------------------------------------------|----------------------|--------------------|-----------------------------| +| `https://gitlab.com/<my-org>/<group-name>/<package-name-example>` | Package Name Example | `@my-org` | `@my-org/package-name` | +| `https://gitlab.com/<example-org>/<group-name>/<project-name>` | Project Name | `@example-org` | `@example-org/project-name` | - - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. - - Replace `your_token` with a deploy token, group access token, project access token, or personal access token. +You can install from the instance level or from the project level. -1. Set the registry +The configurations for `.yarnrc.yml` can be added per package consuming project +root where `package.json` is located, or you can use a global +configuration located in your system user home directory. - ```shell - npm config set @scope:registry https://your_domain_name.com/api/v4/packages/npm/ - ``` +### Install from the instance level - - Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from. - - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. - - Replace `your_token` with a deploy token, group access token, project access token, or personal access token. +Use these steps for global configuration in the `.yarnrc.yml` file: -1. Install the package +1. [Configure organization scope](#configure-organization-scope). +1. [Set the registry](#set-the-registry). - ```shell - yarn add @scope/my-package - ``` +#### Configure organization scope + +```yaml +npmScopes: + <my-org>: + npmRegistryServer: "https://<your_domain_name>/api/v4/packages/npm" +``` + +- Replace `<my-org>` with the root level group of the project you're installing to the package from excluding the `@` symbol. +- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`. + +#### Set the registry + +Skip this step if your package is public not private. + +```yaml + npmRegistries: + //<your_domain_name>/api/v4/packages/npm: + npmAlwaysAuth: true + npmAuthToken: "<your_token>" +``` + +- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`. +- Replace `<your_token>` with a deployment token (recommended), group access token, project access token, or personal access token. ### Install from the project level -1. Authenticate to the Package Registry +Use these steps for each project in the `.yarnrc.yml` file: + +1. [Configure project scope](#configure-project-scope). +1. [Set the registry](#set-the-registry-project-level). - If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private. +#### Configure project scope - ```shell - npm config set -- //your_domain_name/api/v4/projects/your_project_id/packages/npm/:_authToken=your_token - ``` + ```yaml + npmScopes: + <my-org>: + npmRegistryServer: "https://<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm" +``` - - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. - - Replace `your_project_id` is your project ID, found on the project's home page. - - Replace `your_token` with a deploy token, group access token, project access token, or personal access token. +- Replace `<my-org>` with the root level group of the project you're installing to the package from excluding the `@` symbol. +- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`. +- Replace `<your_project_id>` with your project ID, found on the project's home page. -1. Set the registry +#### Set the registry (project level) - ```shell - npm config set @scope:registry=https://your_domain_name/api/v4/projects/your_project_id/packages/npm/ - ``` +Skip this step if your package is public not private. - - Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from. - - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. - - Replace `your_project_id` is your project ID, found on the project's home page. +```yaml +npmRegistries: + //<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm: + npmAlwaysAuth: true + npmAuthToken: "<your_token>" +``` -1. Install the package +- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`. +- Replace `<your_token>` with a deployment token (recommended), group access token, project access token, or personal access token. +- Replace `<your_project_id>` with your project ID, found on the project's home page. - ```shell - yarn add @scope/my-package - ``` +### Install the package -## Helpful hints +For Yarn 2+, use `yarn add` either in the command line or in the CI/CD pipelines to install your packages: -For full helpful hints information, refer to the [npm documentation](../npm_registry/index.md#helpful-hints). +```shell +yarn add @scope/my-package +``` -### Supported CLI commands +#### For Yarn Classic -The GitLab npm repository supports the following commands for the npm CLI (`npm`) and yarn CLI -(`yarn`): +The Yarn Classic setup, requires both `.npmrc` and `.yarnrc` files as +[mentioned in issue](https://github.com/yarnpkg/yarn/issues/4451#issuecomment-753670295): -- `npm install`: Install npm packages. -- `npm publish`: Publish an npm package to the registry. -- `npm dist-tag add`: Add a dist-tag to an npm package. -- `npm dist-tag ls`: List dist-tags for a package. -- `npm dist-tag rm`: Delete a dist-tag. -- `npm ci`: Install npm packages directly from your `package-lock.json` file. -- `npm view`: Show package metadata. -- `yarn add`: Install an npm package. -- `yarn update`: Update your dependencies. +- Place credentials in the `.npmrc` file. +- Place the scoped registry in the `.yarnrc` file. -## Troubleshooting +```shell +# .npmrc +//<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm/:_authToken="<your_token>" + +# .yarnrc +"@scope:registry" "https://<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm/" +``` + +Then you can use `yarn add` to install your packages. + +## Related topics -For full troubleshooting information, refer to the [npm documentation](../npm_registry/index.md#troubleshooting). +- For full helpful hints information, see the + [npm documentation](../npm_registry/index.md#helpful-hints). +- For Yarn 1 to Yarn 2+ migration information see the + [Yarn Migration Guide](https://yarnpkg.com/getting-started/migration). + +## Troubleshooting ### Error running Yarn with the Package Registry for the npm registry -If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get -an error message like: +If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get an error message like: ```shell yarn install v1.15.2 @@ -233,14 +325,7 @@ info If you think this is a bug, please open a bug report with the information p info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command ``` -In this case, try adding this to your `.npmrc` file (and replace `<your_token>` -with your personal access token or deploy token): - -```plaintext -//gitlab.example.com/api/v4/projects/:_authToken=<your_token> -``` - -You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically: +In this case, the following commands creates a file called `.yarnrc` in the current directory. Make sure to be in either your user home directory for global configuration or your project root for per-project configuration: ```shell yarn config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>" diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md index 9c2925ec647..dde4abc7a3c 100644 --- a/doc/user/shortcuts.md +++ b/doc/user/shortcuts.md @@ -39,9 +39,9 @@ These shortcuts are available in most areas of GitLab: | <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your [Merge requests](project/merge_requests/index.md) page. | | <kbd>Shift</kbd> + <kbd>r</kbd> | Go to your Review requests page. | | <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. | -| <kbd>p</kbd>, then <kbd>b</kbd> | Show or hide the Performance Bar. | +| <kbd>p</kbd>, then <kbd>b</kbd> | Show or hide the Performance Bar. | | <kbd>Escape</kbd> | Hide tooltips or popovers. | -| <kbd>g</kbd>, then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). | +| <kbd>g</kbd>, then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). | | <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). | Additionally, the following shortcuts are available when editing text in text @@ -55,9 +55,10 @@ descriptions): | <kbd>Command</kbd> + <kbd>b</kbd> | <kbd>Control</kbd> + <kbd>b</kbd> | Bold the selected text (surround it with `**`). | | <kbd>Command</kbd> + <kbd>i</kbd> | <kbd>Control</kbd> + <kbd>i</kbd> | Italicize the selected text (surround it with `_`). | | <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | Strike through the selected text (surround it with `~~`). | -| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). | -| <kbd>Command</kbd> + <kbd>]</kbd> | <kbd>Control</kbd> + <kbd>]</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. | -| <kbd>Command</kbd> + <kbd>[</kbd> | <kbd>Control</kbd> + <kbd>[</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. | +| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). | +| <kbd>Command</kbd> + <kbd>]</kbd> | <kbd>Control</kbd> + <kbd>]</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. | +| <kbd>Command</kbd> + <kbd>[</kbd> | <kbd>Control</kbd> + <kbd>[</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. | +| <kbd>Shift</kbd> + <kbd>Enter</kbd> | <kbd>Shift</kbd> + <kbd>Enter</kbd> | Add a [line break](markdown.md#newlines). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21916) in GitLab 15.10. | The shortcuts for editing in text fields are always enabled, even if other keyboard shortcuts are disabled. @@ -112,7 +113,7 @@ These shortcuts are available when viewing [merge requests](project/merge_reques | macOS shortcut | Windows shortcut | Description | |-----------------------------------|---------------------|-------------| | <kbd>]</kbd> or <kbd>j</kbd> | | Move to next file. | -| <kbd>[</kbd> or <kbd>k</kbd> | | Move to previous file. | +| <kbd>[</kbd> or <kbd>k</kbd> | | Move to previous file. | | <kbd>Command</kbd> + <kbd>p</kbd> | <kbd>Control</kbd> + <kbd>p</kbd> | Search for, and then jump to a file for review. | | <kbd>n</kbd> | | Move to next unresolved discussion. | | <kbd>p</kbd> | | Move to previous unresolved discussion. | @@ -277,7 +278,7 @@ These shortcuts are available when editing a file with the | <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>h</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>h</kbd> | Highlight | | <kbd>Command</kbd> + <kbd>,</kbd> | <kbd>Control</kbd> + <kbd>,</kbd> | Subscript | | <kbd>Command</kbd> + <kbd>.</kbd> | <kbd>Control</kbd> + <kbd>.</kbd> | Superscript | -| <kbd>Tab</kbd> | <kbd>Tab</kbd> | Indent list | +| <kbd>Tab</kbd> | <kbd>Tab</kbd> | Indent list | | <kbd>Shift</kbd> + <kbd>Tab</kbd> | <kbd>Shift</kbd> + <kbd>Tab</kbd> | Outdent list | #### Text selection diff --git a/lib/gitlab/database/gitlab_schema.rb b/lib/gitlab/database/gitlab_schema.rb index dbb55c0afd1..926a4aeedf1 100644 --- a/lib/gitlab/database/gitlab_schema.rb +++ b/lib/gitlab/database/gitlab_schema.rb @@ -45,6 +45,11 @@ module Gitlab return gitlab_schema end + # Partitions that belong to the CI domain + if table_name.start_with?('ci_') && gitlab_schema = views_and_tables_to_schema["p_#{table_name}"] + return gitlab_schema + end + # All tables from `information_schema.` are marked as `internal` return :gitlab_internal if schema_name == 'information_schema' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8659f21df42..aa7bd1c5af2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8869,9 +8869,6 @@ msgstr "" msgid "CiVariables|Cannot use Masked Variable with current value" msgstr "" -msgid "CiVariables|Clear inputs" -msgstr "" - msgid "CiVariables|Environments" msgstr "" @@ -8899,6 +8896,9 @@ msgstr "" msgid "CiVariables|Protected" msgstr "" +msgid "CiVariables|Remove inputs" +msgstr "" + msgid "CiVariables|Remove variable" msgstr "" diff --git a/spec/controllers/concerns/sorting_preference_spec.rb b/spec/controllers/concerns/sorting_preference_spec.rb index 82a920215ca..6880d83142d 100644 --- a/spec/controllers/concerns/sorting_preference_spec.rb +++ b/spec/controllers/concerns/sorting_preference_spec.rb @@ -26,11 +26,14 @@ RSpec.describe SortingPreference do describe '#set_sort_order' do let(:group) { build(:group) } + let(:controller_name) { 'issues' } + let(:action_name) { 'issues' } let(:issue_weights_available) { true } before do allow(controller).to receive(:default_sort_order).and_return('updated_desc') - allow(controller).to receive(:action_name).and_return('issues') + allow(controller).to receive(:controller_name).and_return(controller_name) + allow(controller).to receive(:action_name).and_return(action_name) allow(controller).to receive(:can_sort_by_issue_weight?).and_return(issue_weights_available) user.user_preference.update!(issues_sort: sorting_field) end @@ -62,6 +65,42 @@ RSpec.describe SortingPreference do end end end + + context 'when user preference contains merged date sorting' do + let(:sorting_field) { 'merged_at_desc' } + let(:can_sort_by_merged_date?) { false } + + before do + allow(controller) + .to receive(:can_sort_by_merged_date?) + .with(can_sort_by_merged_date?) + .and_return(can_sort_by_merged_date?) + end + + it 'sets default sort order' do + is_expected.to eq('updated_desc') + end + + shared_examples 'user can sort by merged date' do + it 'sets sort order from user_preference' do + is_expected.to eq('merged_at_desc') + end + end + + context 'when controller_name is merge_requests' do + let(:controller_name) { 'merge_requests' } + let(:can_sort_by_merged_date?) { true } + + it_behaves_like 'user can sort by merged date' + end + + context 'when action_name is merge_requests' do + let(:action_name) { 'merge_requests' } + let(:can_sort_by_merged_date?) { true } + + it_behaves_like 'user can sort by merged date' + end + end end describe '#set_sort_order_from_user_preference' do diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 1f913e10f43..96006974c13 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -37,6 +37,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : project.add_developer(user) end + specify { expect(get(:index, params: request_params)).to have_request_urgency(:medium) } + it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do last_fetched_at = Time.zone.at(3.hours.ago.to_i) # remove nanoseconds @@ -244,6 +246,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : sign_in(user) end + specify { expect(create!).to have_request_urgency(:low) } + describe 'making the creation request' do before do create! @@ -732,19 +736,21 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : end describe 'PUT update' do - context "should update the note with a valid issue" do - let(:request_params) do - { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :json, - note: { - note: "New comment" - } + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" } - end + } + end + + specify { expect(put(:update, params: request_params)).to have_request_urgency(:low) } + context "should update the note with a valid issue" do before do sign_in(note.author) project.add_developer(note.author) @@ -790,6 +796,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : } end + specify { expect(delete(:destroy, params: request_params)).to have_request_urgency(:low) } + context 'user is the author of a note' do before do sign_in(note.author) @@ -831,6 +839,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : let(:emoji_name) { 'thumbsup' } + it { is_expected.to have_request_urgency(:low) } + it "toggles the award emoji" do expect do subject @@ -866,6 +876,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : sign_in user end + specify { expect(post(:resolve, params: request_params)).to have_request_urgency(:low) } + context "when the user is not authorized to resolve the note" do it "returns status 404" do post :resolve, params: request_params @@ -929,6 +941,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : note.resolve!(user) end + specify { expect(delete(:unresolve, params: request_params)).to have_request_urgency(:low) } + context "when the user is not authorized to resolve the note" do it "returns status 404" do delete :unresolve, params: request_params @@ -998,6 +1012,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: : expect(json_response.count).to eq(1) expect(json_response.first).to include({ "line_text" => "Test" }) end + + specify { expect(get(:outdated_line_change, params: request_params)).to have_request_urgency(:low) } end # Convert a time to an integer number of microseconds diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb index a9672569a4a..a3208ca6d37 100644 --- a/spec/features/commit_spec.rb +++ b/spec/features/commit_spec.rb @@ -6,7 +6,7 @@ RSpec.describe 'Commit', feature_category: :source_code_management do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - describe "single commit view" do + shared_examples "single commit view" do let(:commit) do project.repository.commits(nil, limit: 100).find do |commit| commit.diffs.size > 1 @@ -69,4 +69,15 @@ RSpec.describe 'Commit', feature_category: :source_code_management do end end end + + it_behaves_like "single commit view" + + context "when super sidebar is enabled" do + before do + user.update!(use_new_navigation: true) + stub_feature_flags(super_sidebar_nav: true) + end + + it_behaves_like "single commit view" + end end diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 3171ae89fe6..371c40b40a5 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -23,7 +23,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : milestone: create(:milestone, project: project, due_date: '2013-12-11'), created_at: 1.minute.ago, updated_at: 1.minute.ago) - @fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.seconds.ago) + @fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 20.seconds.ago) @markdown = create(:merge_request, title: 'markdown', @@ -33,7 +33,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : reviewers: [user, user2, user3, user4], milestone: create(:milestone, project: project, due_date: '2013-12-12'), created_at: 2.minutes.ago, - updated_at: 2.minutes.ago) + updated_at: 2.minutes.ago, + state: 'merged') @markdown.metrics.update!(merged_at: 10.minutes.ago, latest_closed_at: 10.seconds.ago) @merge_test = create(:merge_request, @@ -49,7 +50,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : source_project: project, source_branch: 'feautre', created_at: 2.minutes.ago, - updated_at: 1.minute.ago) + updated_at: 1.minute.ago, + state: 'merged') @feature.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.minutes.ago) end @@ -79,10 +81,9 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : expect(page).to have_current_path(project_merge_requests_path(project), ignore_query: true) expect(page).to have_content 'merge-test' - expect(page).to have_content 'feature' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' - expect(count_merge_requests).to eq(2) + expect(count_merge_requests).to eq(1) end it 'filters on a specific assignee' do @@ -90,8 +91,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : expect(page).not_to have_content 'merge-test' expect(page).to have_content 'fix' - expect(page).to have_content 'markdown' - expect(count_merge_requests).to eq(2) + expect(count_merge_requests).to eq(1) end it 'sorts by newest' do @@ -99,35 +99,35 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : expect(first_merge_request).to include('fix') expect(last_merge_request).to include('merge-test') - expect(count_merge_requests).to eq(4) + expect(count_merge_requests).to eq(2) end it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) expect(first_merge_request).to include('merge-test') - expect(count_merge_requests).to eq(4) + expect(count_merge_requests).to eq(2) end it 'sorts by milestone due date' do visit_merge_requests(project, sort: sort_value_milestone) expect(first_merge_request).to include('fix') - expect(count_merge_requests).to eq(4) + expect(count_merge_requests).to eq(2) end - it 'sorts by merged at' do + it 'ignores sorting by merged at' do visit_merge_requests(project, sort: sort_value_merged_date) - expect(first_merge_request).to include('markdown') - expect(count_merge_requests).to eq(4) + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(2) end it 'sorts by closed at' do visit_merge_requests(project, sort: sort_value_closed_date) - expect(first_merge_request).to include('feature') - expect(count_merge_requests).to eq(4) + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(2) end it 'filters on one label and sorts by milestone due date' do @@ -141,6 +141,15 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: : expect(count_merge_requests).to eq(1) end + context 'when viewing merged merge requests' do + it 'sorts by merged at' do + visit_merge_requests(project, state: 'merged', sort: sort_value_merged_date) + + expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(2) + end + end + context 'while filtering on two labels' do let(:label) { create(:label, project: project) } let(:label2) { create(:label, project: project) } diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js index eac53c5f761..232d6177862 100644 --- a/spec/frontend/issuable/components/issue_milestone_spec.js +++ b/spec/frontend/issuable/components/issue_milestone_spec.js @@ -1,160 +1,61 @@ -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltip } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; - import { mockMilestone } from 'jest/boards/mock_data'; import IssueMilestone from '~/issuable/components/issue_milestone.vue'; -const createComponent = (milestone = mockMilestone) => { - const Component = Vue.extend(IssueMilestone); - - return shallowMount(Component, { - propsData: { - milestone, - }, - }); -}; - -describe('IssueMilestoneComponent', () => { +describe('IssueMilestone component', () => { let wrapper; - let vm; - beforeEach(async () => { - wrapper = createComponent(); + const findTooltip = () => wrapper.findComponent(GlTooltip); - ({ vm } = wrapper); + const createComponent = (milestone = mockMilestone) => + shallowMount(IssueMilestone, { propsData: { milestone } }); - await nextTick(); + beforeEach(() => { + wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); + it('renders milestone icon', () => { + expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock'); }); - describe('computed', () => { - describe('isMilestoneStarted', () => { - it('should return `false` when milestoneStart prop is not defined', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.isMilestoneStarted).toBe(false); - }); - - it('should return `true` when milestone start date is past current date', async () => { - await wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '1990-07-22' }, - }); - await nextTick(); + it('renders milestone title', () => { + expect(wrapper.find('.milestone-title').text()).toBe(mockMilestone.title); + }); - expect(wrapper.vm.isMilestoneStarted).toBe(true); - }); + describe('tooltip', () => { + it('renders `Milestone`', () => { + expect(findTooltip().text()).toContain('Milestone'); }); - describe('isMilestonePastDue', () => { - it('should return `false` when milestoneDue prop is not defined', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.isMilestonePastDue).toBe(false); - }); - - it('should return `true` when milestone due is past current date', () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '1990-07-22' }, - }); - - expect(wrapper.vm.isMilestonePastDue).toBe(true); - }); + it('renders milestone title', () => { + expect(findTooltip().text()).toContain(mockMilestone.title); }); - describe('milestoneDatesAbsolute', () => { - it('returns string containing absolute milestone due date', () => { - expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)'); - }); + describe('humanized dates', () => { + it('renders `Expired` when there is a due date in the past', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' }); - it('returns string containing absolute milestone start date when due date is not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)'); + expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)'); }); - it('returns empty string when both milestone start and due dates are not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '', due_date: '' }, - }); - await nextTick(); + it('renders `remaining` when there is a due date in the future', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' }); - expect(wrapper.vm.milestoneDatesAbsolute).toBe(''); + expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)'); }); - }); - describe('milestoneDatesHuman', () => { - it('returns string containing milestone due date when date is yet to be due', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` }, - }); - await nextTick(); + it('renders `Started` when there is a start date in the past', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' }); - expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining'); + expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)'); }); - it('returns string containing milestone start date when date has already started and due date is not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' }, - }); - await nextTick(); + it('renders `Starts` when there is a start date in the future', () => { + wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' }); - expect(wrapper.vm.milestoneDatesHuman).toContain('Started'); + expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)'); }); - - it('returns string containing milestone start date when date is yet to start and due date is not present', async () => { - wrapper.setProps({ - milestone: { - ...mockMilestone, - start_date: `${new Date().getFullYear() + 10}-01-01`, - due_date: '', - }, - }); - await nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toContain('Starts'); - }); - - it('returns empty string when milestone start and due dates are not present', async () => { - wrapper.setProps({ - milestone: { ...mockMilestone, start_date: '', due_date: '' }, - }); - await nextTick(); - - expect(wrapper.vm.milestoneDatesHuman).toBe(''); - }); - }); - }); - - describe('template', () => { - it('renders component root element with class `issue-milestone-details`', () => { - expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true); - }); - - it('renders milestone icon', () => { - expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock'); - }); - - it('renders milestone title', () => { - expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title); - }); - - it('renders milestone tooltip', () => { - expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain( - mockMilestone.title, - ); }); }); }); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 2180ea7e6c2..7ca9715430d 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -10,6 +10,7 @@ import { } from '~/lib/utils/text_markdown'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import '~/lib/utils/jquery_at_who'; +import { ENTER_KEY } from '~/lib/utils/keys'; import axios from '~/lib/utils/axios_utils'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; @@ -208,7 +209,7 @@ describe('init markdown', () => { let enterEvent; beforeEach(() => { - enterEvent = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true }); + enterEvent = new KeyboardEvent('keydown', { key: ENTER_KEY, cancelable: true }); textArea.addEventListener('keydown', keypressNoteText); textArea.addEventListener('compositionstart', compositionStartNoteText); textArea.addEventListener('compositionend', compositionEndNoteText); @@ -492,6 +493,53 @@ describe('init markdown', () => { }); }); + describe('adding a hard break using Shift+Enter', () => { + let enterEvent; + + beforeEach(() => { + enterEvent = new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true }); + textArea.addEventListener('keydown', keypressNoteText); + textArea.addEventListener('compositionstart', compositionStartNoteText); + textArea.addEventListener('compositionend', compositionEndNoteText); + }); + + it.each` + selectionStart | selectionEnd | expected | expectedSelectionStart + ${0} | ${0} | ${'\\\n0123456789'} | ${2} + ${3} | ${3} | ${'012\\\n3456789'} | ${5} + ${3} | ${6} | ${'012\\\n6789'} | ${5} + `( + 'adds a hard break', + ({ selectionStart, selectionEnd, expected, expectedSelectionStart }) => { + const text = '0123456789'; + textArea.value = text; + textArea.setSelectionRange(selectionStart, selectionEnd); + + textArea.dispatchEvent(enterEvent); + + expect(textArea.value).toEqual(expected); + expect(textArea.selectionStart).toEqual(expectedSelectionStart); + expect(textArea.selectionEnd).toEqual(expectedSelectionStart); + }, + ); + + it.each` + keyEvent + ${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: false })} + ${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, metaKey: true })} + ${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, altKey: true })} + ${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, ctrlKey: true })} + `('does not add when shift is pressed with other keys', ({ keyEvent }) => { + const text = '0123456789'; + textArea.value = text; + textArea.setSelectionRange(0, 0); + + textArea.dispatchEvent(keyEvent); + + expect(textArea.value).toEqual(text); + }); + }); + describe('with selection', () => { let text = 'initial selected value'; let selected = 'selected'; diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js index 028c30cb291..b19db73459d 100644 --- a/spec/frontend/profile/account/components/update_username_spec.js +++ b/spec/frontend/profile/account/components/update_username_spec.js @@ -1,6 +1,7 @@ import { GlModal } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'helpers/test_constants'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createAlert } from '~/flash'; @@ -43,6 +44,7 @@ describe('UpdateUsername component', () => { afterEach(() => { wrapper.destroy(); axiosMock.restore(); + Vue.config.errorHandler = null; }); const findElements = () => { @@ -58,6 +60,13 @@ describe('UpdateUsername component', () => { }; }; + const clickModalWithErrorResponse = () => { + Vue.config.errorHandler = jest.fn(); // silence thrown error + const { modal } = findElements(); + modal.vm.$emit('primary'); + return waitForPromises(); + }; + it('has a disabled button if the username was not changed', async () => { const { openModalBtn } = findElements(); @@ -98,14 +107,15 @@ describe('UpdateUsername component', () => { axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]); jest.spyOn(axios, 'put'); - await wrapper.vm.onConfirm(); - await nextTick(); + const { modal } = findElements(); + modal.vm.$emit('primary'); + await waitForPromises(); expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } }); }); it('sets the username after a successful update', async () => { - const { input, openModalBtn } = findElements(); + const { input, openModalBtn, modal } = findElements(); axiosMock.onPut(actionUrl).replyOnce(() => { expect(input.attributes('disabled')).toBe('disabled'); @@ -115,8 +125,8 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_OK, { message: 'Username changed' }]; }); - await wrapper.vm.onConfirm(); - await nextTick(); + modal.vm.$emit('primary'); + await waitForPromises(); expect(input.attributes('disabled')).toBe(undefined); expect(openModalBtn.props('disabled')).toBe(true); @@ -134,7 +144,8 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }]; }); - await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + await clickModalWithErrorResponse(); + expect(input.attributes('disabled')).toBe(undefined); expect(openModalBtn.props('disabled')).toBe(false); expect(openModalBtn.props('loading')).toBe(false); @@ -145,7 +156,7 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }]; }); - await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + await clickModalWithErrorResponse(); expect(createAlert).toHaveBeenCalledWith({ message: 'Invalid username', @@ -157,7 +168,7 @@ describe('UpdateUsername component', () => { return [HTTP_STATUS_BAD_REQUEST]; }); - await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + await clickModalWithErrorResponse(); expect(createAlert).toHaveBeenCalledWith({ message: 'An error occurred while updating your username, please try again.', diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js index 8d0fd390e35..8bea84f4429 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js @@ -71,8 +71,10 @@ describe('Branch rule', () => { }); it('renders a detail button with the correct href', () => { + const encodedBranchName = encodeURIComponent(branchRulePropsMock.name); + expect(findDetailsButton().attributes('href')).toBe( - `${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`, + `${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`, ); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js index de7f6c8b88d..d169397241d 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js +++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js @@ -74,7 +74,7 @@ export const branchRuleProvideMock = { }; export const branchRulePropsMock = { - name: 'main', + name: 'branch-with-$speci@l-#-chars', isDefault: true, matchingBranchesCount: 1, branchProtection: { diff --git a/spec/frontend/sidebar/components/participants/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js index f7a626a189c..72d83ebeca4 100644 --- a/spec/frontend/sidebar/components/participants/participants_spec.js +++ b/spec/frontend/sidebar/components/participants/participants_spec.js @@ -1,203 +1,114 @@ -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import Participants from '~/sidebar/components/participants/participants.vue'; -const PARTICIPANT = { - id: 1, - state: 'active', - username: 'marcene', - name: 'Allie Will', - web_url: 'foo.com', - avatar_url: 'gravatar.com/avatar/xxx', -}; - -const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }]; - -describe('Participants', () => { +describe('Participants component', () => { let wrapper; - const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]'); - const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]'); + const participant = { + id: 1, + state: 'active', + username: 'marcene', + name: 'Allie Will', + web_url: 'foo.com', + avatar_url: 'gravatar.com/avatar/xxx', + }; - const mountComponent = (propsData) => - shallowMount(Participants, { - propsData, - }); + const participants = [participant, { ...participant, id: 2 }, { ...participant, id: 3 }]; - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findMoreParticipantsButton = () => wrapper.findComponent(GlButton); + const findCollapsedIcon = () => wrapper.find('.sidebar-collapsed-icon'); + const findParticipantsAuthor = () => wrapper.findAll('.participants-author'); + + const mountComponent = (propsData) => shallowMount(Participants, { propsData }); describe('collapsed sidebar state', () => { it('shows loading spinner when loading', () => { - wrapper = mountComponent({ - loading: true, - }); + wrapper = mountComponent({ loading: true }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('does not show loading spinner not loading', () => { - wrapper = mountComponent({ - loading: false, - }); + it('does not show loading spinner when not loading', () => { + wrapper = mountComponent({ loading: false }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(findLoadingIcon().exists()).toBe(false); }); it('shows participant count when given', () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - }); + wrapper = mountComponent({ participants }); - expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`); + expect(findCollapsedIcon().text()).toBe(participants.length.toString()); }); it('shows full participant count when there are hidden participants', () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 1, - }); + wrapper = mountComponent({ participants, numberOfLessParticipants: 1 }); - expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`); + expect(findCollapsedIcon().text()).toBe(participants.length.toString()); }); }); describe('expanded sidebar state', () => { it('shows loading spinner when loading', () => { - wrapper = mountComponent({ - loading: true, - }); + wrapper = mountComponent({ loading: true }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => { + it('when only showing visible participants, shows an avatar only for each participant under the limit', () => { const numberOfLessParticipants = 2; - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: false, - }); - - await nextTick(); - expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants); + wrapper = mountComponent({ participants, numberOfLessParticipants }); + + expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants); }); it('when only showing all participants, each has an avatar', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: true, - }); - - await nextTick(); - expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length); + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); + + await findMoreParticipantsButton().vm.$emit('click'); + + expect(findParticipantsAuthor()).toHaveLength(participants.length); }); it('does not have more participants link when they can all be shown', () => { const numberOfLessParticipants = 100; - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - - expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants); - expect(getMoreParticipantsButton().exists()).toBe(false); - }); + wrapper = mountComponent({ participants, numberOfLessParticipants }); - it('when too many participants, has more participants link to show more', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: false, - }); - - await nextTick(); - expect(getMoreParticipantsButton().text()).toBe('+ 1 more'); + expect(participants.length).toBeLessThan(numberOfLessParticipants); + expect(findMoreParticipantsButton().exists()).toBe(false); }); - it('when too many participants and already showing them, has more participants link to show less', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isShowingMoreParticipants: true, - }); - - await nextTick(); - expect(getMoreParticipantsButton().text()).toBe('- show less'); - }); + it('when too many participants, has more participants link to show more', () => { + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); - it('clicking more participants link emits event', () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); + expect(findMoreParticipantsButton().text()).toBe('+ 1 more'); + }); - expect(wrapper.vm.isShowingMoreParticipants).toBe(false); + it('when too many participants and already showing them, has more participants link to show less', async () => { + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); - getMoreParticipantsButton().vm.$emit('click'); + await findMoreParticipantsButton().vm.$emit('click'); - expect(wrapper.vm.isShowingMoreParticipants).toBe(true); + expect(findMoreParticipantsButton().text()).toBe('- show less'); }); - it('clicking on participants icon emits `toggleSidebar` event', async () => { - wrapper = mountComponent({ - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - - const spy = jest.spyOn(wrapper.vm, '$emit'); + it('clicking on participants icon emits `toggleSidebar` event', () => { + wrapper = mountComponent({ participants, numberOfLessParticipants: 2 }); - wrapper.find('.sidebar-collapsed-icon').trigger('click'); + findCollapsedIcon().trigger('click'); - await nextTick(); - expect(spy).toHaveBeenCalledWith('toggleSidebar'); - spy.mockRestore(); + expect(wrapper.emitted('toggleSidebar')).toEqual([[]]); }); }); describe('when not showing participants label', () => { beforeEach(() => { - wrapper = mountComponent({ - participants: PARTICIPANT_LIST, - showParticipantLabel: false, - }); + wrapper = mountComponent({ participants, showParticipantLabel: false }); }); it('does not show sidebar collapsed icon', () => { - expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false); + expect(findCollapsedIcon().exists()).toBe(false); }); it('does not show participants label title', () => { diff --git a/spec/frontend/super_sidebar/components/sidebar_portal_spec.js b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js new file mode 100644 index 00000000000..3ef1cb7e692 --- /dev/null +++ b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js @@ -0,0 +1,68 @@ +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue'; +import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; + +describe('SidebarPortal', () => { + let targetWrapper; + + const Target = { + components: { SidebarPortalTarget }, + props: ['show'], + template: '<sidebar-portal-target v-if="show" />', + }; + + const Source = { + components: { SidebarPortal }, + template: '<sidebar-portal><br data-testid="test"></sidebar-portal>', + }; + + const mountSource = () => { + mount(Source); + }; + + const mountTarget = ({ show = true } = {}) => { + targetWrapper = mount(Target, { + propsData: { show }, + attachTo: document.body, + }); + }; + + const findTestContent = () => targetWrapper.find('[data-testid="test"]'); + + it('renders content into the target', async () => { + mountTarget(); + await nextTick(); + + mountSource(); + await nextTick(); + + expect(findTestContent().exists()).toBe(true); + }); + + it('waits for target to be available before rendering', async () => { + mountSource(); + await nextTick(); + + mountTarget(); + await nextTick(); + + expect(findTestContent().exists()).toBe(true); + }); + + it('supports conditional rendering of target', async () => { + mountTarget({ show: false }); + await nextTick(); + + mountSource(); + await nextTick(); + + expect(findTestContent().exists()).toBe(false); + + await targetWrapper.setProps({ show: true }); + expect(findTestContent().exists()).toBe(true); + + await targetWrapper.setProps({ show: false }); + expect(findTestContent().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index 45fc30c08f0..57c84bc87a6 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue'; import HelpCenter from '~/super_sidebar/components/help_center.vue'; import UserBar from '~/super_sidebar/components/user_bar.vue'; +import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; import { sidebarData } from '../mock_data'; describe('SuperSidebar component', () => { @@ -9,6 +10,7 @@ describe('SuperSidebar component', () => { const findUserBar = () => wrapper.findComponent(UserBar); const findHelpCenter = () => wrapper.findComponent(HelpCenter); + const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget); const createWrapper = (props = {}) => { wrapper = shallowMountExtended(SuperSidebar, { @@ -31,5 +33,9 @@ describe('SuperSidebar component', () => { it('renders HelpCenter with sidebarData', () => { expect(findHelpCenter().props('sidebarData')).toBe(sidebarData); }); + + it('renders SidebarPortalTarget', () => { + expect(findSidebarPortalTarget().exists()).toBe(true); + }); }); }); diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb index 31aeff85c70..4f56bb7467f 100644 --- a/spec/helpers/jira_connect_helper_spec.rb +++ b/spec/helpers/jira_connect_helper_spec.rb @@ -9,8 +9,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do let(:user) { create(:user) } let(:client_id) { '123' } - let(:enable_public_keys_storage_config) { false } - let(:enable_public_keys_storage_setting) { false } + let(:enable_public_keys_storage) { false } before do stub_application_setting(jira_connect_application_key: client_id) @@ -22,9 +21,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do before do allow(view).to receive(:current_user).and_return(nil) allow(Gitlab.config.gitlab).to receive(:url).and_return('http://test.host') - allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage) - .and_return(enable_public_keys_storage_config) - stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage_setting) + stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage) end it 'includes Jira Connect app attributes' do @@ -108,16 +105,8 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do expect(subject[:public_key_storage_enabled]).to eq(false) end - context 'when public_key_storage is enabled via config' do - let(:enable_public_keys_storage_config) { true } - - it 'assignes public_key_storage_enabled to true' do - expect(subject[:public_key_storage_enabled]).to eq(true) - end - end - - context 'when public_key_storage is enabled via setting' do - let(:enable_public_keys_storage_setting) { true } + context 'when public_key_storage is enabled' do + let(:enable_public_keys_storage) { true } it 'assignes public_key_storage_enabled to true' do expect(subject[:public_key_storage_enabled]).to eq(true) diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb index d561b08efac..d625b46e286 100644 --- a/spec/helpers/sorting_helper_spec.rb +++ b/spec/helpers/sorting_helper_spec.rb @@ -10,6 +10,60 @@ RSpec.describe SortingHelper do allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: { label_name: option })) end + describe '#issuable_sort_options' do + let(:viewing_issues) { false } + let(:viewing_merge_requests) { false } + let(:params) { {} } + + subject(:options) { helper.issuable_sort_options(viewing_issues, viewing_merge_requests) } + + before do + allow(helper).to receive(:params).and_return(params) + end + + shared_examples 'with merged date option' do + it 'adds merged date option' do + expect(options).to include( + a_hash_including( + value: 'merged_at', + text: 'Merged date' + ) + ) + end + end + + shared_examples 'without merged date option' do + it 'does not set merged date option' do + expect(options).not_to include( + a_hash_including( + value: 'merged_at', + text: 'Merged date' + ) + ) + end + end + + it_behaves_like 'without merged date option' + + context 'when viewing_merge_requests is true' do + let(:viewing_merge_requests) { true } + + it_behaves_like 'without merged date option' + + context 'when state param is all' do + let(:params) { { state: 'all' } } + + it_behaves_like 'with merged date option' + end + + context 'when state param is merged' do + let(:params) { { state: 'merged' } } + + it_behaves_like 'with merged date option' + end + end + end + describe '#admin_users_sort_options' do it 'returns correct link attributes in array' do options = admin_users_sort_options(filter: 'filter', search_query: 'search') diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb index 28a087d5401..b187b29c270 100644 --- a/spec/lib/gitlab/database/gitlab_schema_spec.rb +++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb @@ -16,19 +16,21 @@ RSpec.shared_examples 'validate schema data' do |tables_and_views| end end -RSpec.describe Gitlab::Database::GitlabSchema do +RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do shared_examples 'maps table name to table schema' do using RSpec::Parameterized::TableSyntax where(:name, :classification) do - 'ci_builds' | :gitlab_ci - 'my_schema.ci_builds' | :gitlab_ci - 'information_schema.columns' | :gitlab_internal - 'audit_events_part_5fc467ac26' | :gitlab_main - '_test_gitlab_main_table' | :gitlab_main - '_test_gitlab_ci_table' | :gitlab_ci - '_test_my_table' | :gitlab_shared - 'pg_attribute' | :gitlab_internal + 'ci_builds' | :gitlab_ci + 'my_schema.ci_builds' | :gitlab_ci + 'my_schema.ci_runner_machine_builds_100' | :gitlab_ci + 'my_schema._test_gitlab_main_table' | :gitlab_main + 'information_schema.columns' | :gitlab_internal + 'audit_events_part_5fc467ac26' | :gitlab_main + '_test_gitlab_main_table' | :gitlab_main + '_test_gitlab_ci_table' | :gitlab_ci + '_test_my_table' | :gitlab_shared + 'pg_attribute' | :gitlab_internal end with_them do diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb index c7e4eaf8f46..ac54c307108 100644 --- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb +++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb @@ -238,23 +238,20 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do expect(pending_drop.drop_after).to eq(Time.current + described_class::RETAIN_DETACHED_PARTITIONS_FOR) end - # Postgres 11 does not support foreign keys to partitioned tables - if ApplicationRecord.database.version.to_f >= 12 - context 'when the model is the target of a foreign key' do - before do - connection.execute(<<~SQL) + context 'when the model is the target of a foreign key' do + before do + connection.execute(<<~SQL) create unique index idx_for_fk ON #{partitioned_table_name}(created_at); create table _test_gitlab_main_referencing_table ( id bigserial primary key not null, referencing_created_at timestamptz references #{partitioned_table_name}(created_at) ); - SQL - end + SQL + end - it 'does not detach partitions with a referenced foreign key' do - expect { subject }.not_to change { find_partitions(my_model.table_name).size } - end + it 'does not detach partitions with a referenced foreign key' do + expect { subject }.not_to change { find_partitions(my_model.table_name).size } end end end diff --git a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb index 75f94bf2654..c128c56c708 100644 --- a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb +++ b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb @@ -203,10 +203,8 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ end end - context 'when supporting foreign keys to inherited tables in postgres 12' do + context 'when supporting foreign keys to inherited tables' do before do - skip('not supported before postgres 12') if ApplicationRecord.database.version.to_f < 12 - ApplicationRecord.connection.execute(<<~SQL) create table #{schema_table_name('parent')} ( id bigserial primary key not null diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb index fcef5b6ca78..0a77b6e228e 100644 --- a/spec/requests/api/avatar_spec.rb +++ b/spec/requests/api/avatar_spec.rb @@ -19,6 +19,7 @@ RSpec.describe API::Avatar, feature_category: :user_profile do expect(response).to have_gitlab_http_status(:ok) expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}") + is_expected.to have_request_urgency(:medium) end end diff --git a/spec/requests/jira_connect/public_keys_controller_spec.rb b/spec/requests/jira_connect/public_keys_controller_spec.rb index 7f0262eaf65..62a81d43e65 100644 --- a/spec/requests/jira_connect/public_keys_controller_spec.rb +++ b/spec/requests/jira_connect/public_keys_controller_spec.rb @@ -5,11 +5,10 @@ require 'spec_helper' RSpec.describe JiraConnect::PublicKeysController, feature_category: :integrations do describe 'GET /-/jira_connect/public_keys/:uuid' do let(:uuid) { non_existing_record_id } - let(:public_key_storage_enabled_config) { true } + let(:public_key_storage_enabled) { true } before do - allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage) - .and_return(public_key_storage_enabled_config) + stub_application_setting(jira_connect_public_key_storage_enabled: public_key_storage_enabled) end it 'renders 404' do @@ -30,26 +29,14 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration expect(response.body).to eq(public_key.key) end - context 'when public key storage config disabled' do - let(:public_key_storage_enabled_config) { false } + context 'when public key storage setting disabled' do + let(:public_key_storage_enabled) { false } it 'renders 404' do get jira_connect_public_key_path(id: uuid) expect(response).to have_gitlab_http_status(:not_found) end - - context 'when public key storage setting is enabled' do - before do - stub_application_setting(jira_connect_public_key_storage_enabled: true) - end - - it 'renders 404' do - get jira_connect_public_key_path(id: uuid) - - expect(response).to have_gitlab_http_status(:ok) - end - end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3f0900886eb..f76ba3528e9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -178,6 +178,8 @@ RSpec.configure do |config| config.include RenderedHelpers config.include RSpec::Benchmark::Matchers, type: :benchmark config.include DetailedErrorHelpers + config.include RequestUrgencyMatcher, type: :controller + config.include RequestUrgencyMatcher, type: :request config.include_context 'when rendered has no HTML escapes', type: :view diff --git a/spec/support/matchers/request_urgency_matcher.rb b/spec/support/matchers/request_urgency_matcher.rb new file mode 100644 index 00000000000..d3c5093719e --- /dev/null +++ b/spec/support/matchers/request_urgency_matcher.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module RequestUrgencyMatcher + RSpec::Matchers.define :have_request_urgency do |request_urgency| + match do |_actual| + if controller_instance = request.env["action_controller.instance"] + controller_instance.urgency.name == request_urgency + elsif endpoint = request.env['api.endpoint'] + urgency = endpoint.options[:for].try(:urgency_for_app, endpoint) + urgency.name == request_urgency + else + raise 'neither a controller nor a request spec' + end + end + + failure_message do |_actual| + if controller_instance = request.env["action_controller.instance"] + "request urgency #{controller_instance.urgency.name} is set, \ + but expected to be #{request_urgency}".squish + elsif endpoint = request.env['api.endpoint'] + urgency = endpoint.options[:for].try(:urgency_for_app, endpoint) + "request urgency #{urgency.name} is set, \ + but expected to be #{request_urgency}".squish + end + end + end +end |