diff options
69 files changed, 931 insertions, 464 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index b73074fe8ec..a710a8ea871 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -529,16 +529,6 @@ Layout/ArgumentAlignment: - 'app/models/integrations/jira.rb' - 'app/models/jira_connect_installation.rb' - 'app/models/lfs_object.rb' - - 'app/models/loose_foreign_keys/deleted_record.rb' - - 'app/models/merge_request.rb' - - 'app/models/merge_request_diff.rb' - - 'app/models/merge_requests_closing_issues.rb' - - 'app/models/ml/candidate_metadata.rb' - - 'app/models/ml/experiment_metadata.rb' - - 'app/models/namespace.rb' - - 'app/models/namespaces/traversal/linear_scopes.rb' - - 'app/models/note.rb' - - 'app/models/note_diff_file.rb' - 'app/models/packages/cleanup/policy.rb' - 'app/models/packages/conan/metadatum.rb' - 'app/models/packages/debian/file_entry.rb' diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue index 5dcff1f6295..fa842f23cc3 100644 --- a/app/assets/javascripts/content_editor/components/content_editor_provider.vue +++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue @@ -4,7 +4,10 @@ export default { // We can't use this.contentEditor due to bug in vue-apollo when // provide is called in beforeCreate // See https://github.com/vuejs/vue-apollo/pull/1153 for details - const { contentEditor } = this.$options.propsData; + + // @vue-compat does not care to normalize propsData fields + const contentEditor = + this.$options.propsData.contentEditor || this.$options.propsData['content-editor']; return { contentEditor, diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue index f95db498c4c..09fa006cb88 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -130,6 +130,13 @@ export default { }, }, methods: { + updateHistoryAndFetchCount(status = null) { + this.$apollo.queries.jobsCount.refetch({ statuses: status }); + + updateHistory({ + url: setUrlParams({ statuses: status }, window.location.href, true), + }); + }, fetchJobsByStatus(scope) { this.infiniteScrollingTriggered = false; @@ -137,6 +144,8 @@ export default { this.scope = scope; + if (!this.scope) this.updateHistoryAndFetchCount(); + this.$apollo.queries.jobs.refetch({ statuses: scope }); }, filterJobsBySearch(filters) { @@ -146,12 +155,8 @@ export default { // all filters have been cleared reset query param // and refetch jobs/count with defaults if (!filters.length) { - updateHistory({ - url: setUrlParams({ statuses: null }, window.location.href, true), - }); - + this.updateHistoryAndFetchCount(); this.$apollo.queries.jobs.refetch({ statuses: null }); - this.$apollo.queries.jobsCount.refetch({ statuses: null }); return; } @@ -170,12 +175,8 @@ export default { } if (filter.type === 'status') { - updateHistory({ - url: setUrlParams({ statuses: filter.value.data }, window.location.href, true), - }); - + this.updateHistoryAndFetchCount(filter.value.data); this.$apollo.queries.jobs.refetch({ statuses: filter.value.data }); - this.$apollo.queries.jobsCount.refetch({ statuses: filter.value.data }); } }); }, diff --git a/app/assets/javascripts/lib/utils/css_utils.js b/app/assets/javascripts/lib/utils/css_utils.js index e4f68dd1b6c..87cc69bad61 100644 --- a/app/assets/javascripts/lib/utils/css_utils.js +++ b/app/assets/javascripts/lib/utils/css_utils.js @@ -23,3 +23,28 @@ export function loadCSSFile(path) { export function getCssVariable(variable) { return getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); } + +/** + * Return the measured width and height of a temporary element with the given + * CSS classes. + * + * Multiple classes can be given by separating them with spaces. + * + * Since this forces a layout calculation, do not call this frequently or in + * loops. + * + * Finally, this assumes the styles for the given classes are loaded. + * + * @param {string} className CSS class(es) to apply to a temporary element and + * measure. + * @returns {{ width: number, height: number }} Measured width and height in + * CSS pixels. + */ +export function getCssClassDimensions(className) { + const el = document.createElement('div'); + el.className = className; + document.body.appendChild(el); + const { width, height } = el.getBoundingClientRect(); + el.remove(); + return { width, height }; +} diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/pages/admin/jobs/components/constants.js index 57220f0727c..8c4ea2cde92 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/constants.js +++ b/app/assets/javascripts/pages/admin/jobs/components/constants.js @@ -15,6 +15,8 @@ export const PRIMARY_ACTION_TEXT = s__('AdminArea|Yes, proceed'); export const CANCEL_JOBS_WARNING = s__( "AdminArea|You're about to cancel all running and pending jobs across this instance. Do you want to proceed?", ); +export const RUNNER_EMPTY_TEXT = __('None'); +export const RUNNER_NO_DESCRIPTION = s__('Runners|No description'); /* Admin Table constants */ export const DEFAULT_FIELDS_ADMIN = [ diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue index 38301ce1d8a..99caf6ed332 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue +++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue @@ -150,6 +150,13 @@ export default { }, }, methods: { + updateHistoryAndFetchCount(status = null) { + this.$apollo.queries.jobsCount.refetch({ statuses: status }); + + updateHistory({ + url: setUrlParams({ statuses: status }, window.location.href, true), + }); + }, fetchJobsByStatus(scope) { this.infiniteScrollingTriggered = false; @@ -157,6 +164,8 @@ export default { this.scope = scope; + if (!this.scope) this.updateHistoryAndFetchCount(); + this.$apollo.queries.jobs.refetch({ statuses: scope }); }, fetchMoreJobs() { @@ -178,12 +187,8 @@ export default { // all filters have been cleared reset query param // and refetch jobs/count with defaults if (!filters.length) { - updateHistory({ - url: setUrlParams({ statuses: null }, window.location.href, true), - }); - + this.updateHistoryAndFetchCount(); this.$apollo.queries.jobs.refetch({ statuses: null }); - this.$apollo.queries.jobsCount.refetch({ statuses: null }); return; } @@ -202,12 +207,8 @@ export default { } if (filter.type === 'status') { - updateHistory({ - url: setUrlParams({ statuses: filter.value.data }, window.location.href, true), - }); - + this.updateHistoryAndFetchCount(filter.value.data); this.$apollo.queries.jobs.refetch({ statuses: filter.value.data }); - this.$apollo.queries.jobsCount.refetch({ statuses: filter.value.data }); } }); }, diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue b/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue new file mode 100644 index 00000000000..33bcee5b34b --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue @@ -0,0 +1,39 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '~/pages/admin/jobs/components/constants'; + +export default { + i18n: { + emptyRunnerText: RUNNER_EMPTY_TEXT, + noRunnerDescription: RUNNER_NO_DESCRIPTION, + }, + components: { + GlLink, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + adminUrl() { + return this.job.runner?.adminUrl; + }, + description() { + return this.job.runner?.description + ? this.job.runner.description + : this.$options.i18n.noRunnerDescription; + }, + }, +}; +</script> + +<template> + <div class="gl-text-truncate"> + <gl-link v-if="adminUrl" :href="adminUrl"> + {{ description }} + </gl-link> + <span v-else data-testid="empty-runner-text"> {{ $options.i18n.emptyRunnerText }}</span> + </div> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue new file mode 100644 index 00000000000..9d2836e9dfa --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue @@ -0,0 +1,122 @@ +<script> +import { getCssClassDimensions } from '~/lib/utils/css_utils'; +import { SUPER_SIDEBAR_PEEK_OPEN_DELAY, SUPER_SIDEBAR_PEEK_CLOSE_DELAY } from '../constants'; + +export const STATE_CLOSED = 'closed'; +export const STATE_WILL_OPEN = 'will-open'; +export const STATE_OPEN = 'open'; +export const STATE_WILL_CLOSE = 'will-close'; + +export default { + name: 'SidebarPeek', + created() { + // Nothing needs to observe these properties, so they are not reactive. + this.state = null; + this.openTimer = null; + this.closeTimer = null; + this.xNearWindowEdge = null; + this.xSidebarEdge = null; + this.xAwayFromSidebar = null; + }, + mounted() { + this.xNearWindowEdge = getCssClassDimensions('gl-w-3').width; + this.xSidebarEdge = getCssClassDimensions('super-sidebar').width; + this.xAwayFromSidebar = 2 * this.xSidebarEdge; + document.addEventListener('mousemove', this.onMouseMove); + document.documentElement.addEventListener('mouseleave', this.onDocumentLeave); + this.changeState(STATE_CLOSED); + }, + beforeDestroy() { + document.removeEventListener('mousemove', this.onMouseMove); + document.documentElement.removeEventListener('mouseleave', this.onDocumentLeave); + this.clearTimers(); + }, + methods: { + /** + * Callback for document-wide mousemove events. + * + * Since mousemove events can fire frequently, it's important for this to + * do as little work as possible. + * + * When mousemove events fire within one of the defined regions, this ends + * up being a no-op. Only when the cursor moves from one region to another + * does this do any work: it sets a non-reactive instance property, maybe + * cancels/starts timers, and emits an event. + * + * @params {MouseEvent} event + */ + onMouseMove({ clientX }) { + if (this.state === STATE_CLOSED) { + if (clientX < this.xNearWindowEdge) { + this.willOpen(); + } + } else if (this.state === STATE_WILL_OPEN) { + if (clientX >= this.xNearWindowEdge) { + this.close(); + } + } else if (this.state === STATE_OPEN) { + if (clientX >= this.xAwayFromSidebar) { + this.close(); + } else if (clientX >= this.xSidebarEdge) { + this.willClose(); + } + } else if (this.state === STATE_WILL_CLOSE) { + if (clientX >= this.xAwayFromSidebar) { + this.close(); + } else if (clientX < this.xSidebarEdge) { + this.open(); + } + } + }, + onDocumentLeave() { + if (this.state === STATE_OPEN) { + this.willClose(); + } else if (this.state === STATE_WILL_OPEN) { + this.close(); + } + }, + willClose() { + if (this.changeState(STATE_WILL_CLOSE)) { + this.closeTimer = setTimeout(this.close, SUPER_SIDEBAR_PEEK_CLOSE_DELAY); + } + }, + willOpen() { + if (this.changeState(STATE_WILL_OPEN)) { + this.openTimer = setTimeout(this.open, SUPER_SIDEBAR_PEEK_OPEN_DELAY); + } + }, + open() { + if (this.changeState(STATE_OPEN)) { + this.clearTimers(); + } + }, + close() { + if (this.changeState(STATE_CLOSED)) { + this.clearTimers(); + } + }, + clearTimers() { + clearTimeout(this.closeTimer); + clearTimeout(this.openTimer); + }, + /** + * Switches to the new state, and emits a change event. + * + * If the given state is the current state, do nothing. + * + * @param {string} state The state to transition to. + * @returns {boolean} True if the state changed, false otherwise. + */ + changeState(state) { + if (this.state === state) return false; + + this.state = state; + this.$emit('change', state); + return true; + }, + }, + render() { + return null; + }, +}; +</script> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index 1d4f910482c..ab1a23021c2 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -3,18 +3,14 @@ import { GlButton } from '@gitlab/ui'; import { Mousetrap } from '~/lib/mousetrap'; import { keysFor, TOGGLE_SUPER_SIDEBAR } from '~/behaviors/shortcuts/keybindings'; import { __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { - sidebarState, - SUPER_SIDEBAR_PEEK_OPEN_DELAY, - SUPER_SIDEBAR_PEEK_CLOSE_DELAY, -} from '../constants'; +import { sidebarState } from '../constants'; import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; import UserBar from './user_bar.vue'; import SidebarPortalTarget from './sidebar_portal_target.vue'; import ContextSwitcher from './context_switcher.vue'; import HelpCenter from './help_center.vue'; import SidebarMenu from './sidebar_menu.vue'; +import SidebarPeekBehavior, { STATE_CLOSED, STATE_WILL_OPEN } from './sidebar_peek_behavior.vue'; export default { components: { @@ -23,13 +19,13 @@ export default { ContextSwitcher, HelpCenter, SidebarMenu, + SidebarPeekBehavior, SidebarPortalTarget, TrialStatusWidget: () => import('ee_component/contextual_sidebar/components/trial_status_widget.vue'), TrialStatusPopover: () => import('ee_component/contextual_sidebar/components/trial_status_popover.vue'), }, - mixins: [glFeatureFlagsMixin()], i18n: { skipToMainContent: __('Skip to main content'), }, @@ -41,16 +37,25 @@ export default { }, }, data() { - return sidebarState; + return { + sidebarState, + showPeekHint: false, + }; }, computed: { menuItems() { return this.sidebarData.current_menu_items || []; }, + peekClasses() { + return { + 'super-sidebar-peek-hint': this.showPeekHint, + 'super-sidebar-peek': this.sidebarState.isPeek, + }; + }, }, watch: { - isCollapsed() { - if (this.isCollapsed) { + 'sidebarState.isCollapsed': function isCollapsedWatcher(newIsCollapsed) { + if (newIsCollapsed) { this.$refs['context-switcher'].close(); } }, @@ -68,36 +73,23 @@ export default { collapseSidebar() { toggleSuperSidebarCollapsed(true, false); }, - onHoverAreaMouseEnter() { - this.openPeekTimer = setTimeout(this.openPeek, SUPER_SIDEBAR_PEEK_OPEN_DELAY); - }, - onHoverAreaMouseLeave() { - clearTimeout(this.openPeekTimer); - }, - onSidebarMouseEnter() { - clearTimeout(this.closePeekTimer); - }, - onSidebarMouseLeave() { - this.closePeekTimer = setTimeout(this.closePeek, SUPER_SIDEBAR_PEEK_CLOSE_DELAY); - }, - closePeek() { - if (this.isPeek) { - this.isPeek = false; - this.isCollapsed = true; + onPeekChange(state) { + if (state === STATE_CLOSED) { + this.sidebarState.isPeek = false; + this.sidebarState.isCollapsed = true; + this.showPeekHint = false; + } else if (state === STATE_WILL_OPEN) { + this.sidebarState.isPeek = false; + this.sidebarState.isCollapsed = true; + this.showPeekHint = true; + } else { + this.sidebarState.isPeek = true; + this.sidebarState.isCollapsed = false; + this.showPeekHint = false; } }, - openPeek() { - this.isPeek = true; - this.isCollapsed = false; - - // Cancel and start the timer to close sidebar, in case the user moves - // the cursor fast enough away to not trigger a mouseenter event. - // This is cancelled if the user moves the cursor into the sidebar. - this.onSidebarMouseEnter(); - this.onSidebarMouseLeave(); - }, onContextSwitcherToggled(open) { - this.contextSwitcherOpen = open; + this.sidebarState.contextSwitcherOpen = open; }, }, }; @@ -106,22 +98,14 @@ export default { <template> <div> <div class="super-sidebar-overlay" @click="collapseSidebar"></div> - <div - v-if="!isPeek && glFeatures.superSidebarPeek" - class="super-sidebar-hover-area gl-fixed gl-left-0 gl-top-0 gl-bottom-0 gl-w-3" - data-testid="super-sidebar-hover-area" - @mouseenter="onHoverAreaMouseEnter" - @mouseleave="onHoverAreaMouseLeave" - ></div> + <aside id="super-sidebar" class="super-sidebar" - :class="{ 'super-sidebar-peek': isPeek }" + :class="peekClasses" data-testid="super-sidebar" data-qa-selector="navbar" - :inert="isCollapsed" - @mouseenter="onSidebarMouseEnter" - @mouseleave="onSidebarMouseLeave" + :inert="sidebarState.isCollapsed" > <gl-button class="super-sidebar-skip-to gl-sr-only-focusable gl-absolute gl-left-3 gl-right-3 gl-top-3" @@ -130,7 +114,7 @@ export default { > {{ $options.i18n.skipToMainContent }} </gl-button> - <user-bar :has-collapse-button="!isPeek" :sidebar-data="sidebarData" /> + <user-bar :has-collapse-button="!sidebarState.isPeek" :sidebar-data="sidebarData" /> <div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2"> <trial-status-widget class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! nav-item-link gl-py-3" @@ -140,7 +124,7 @@ export default { <div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"> <div class="gl-flex-grow-1" - :class="{ 'gl-overflow-auto': !contextSwitcherOpen }" + :class="{ 'gl-overflow-auto': !sidebarState.contextSwitcherOpen }" data-testid="nav-container" > <context-switcher @@ -176,5 +160,11 @@ export default { > {{ shortcutLink.title }} </a> + + <!-- + Only mount SidebarPeekBehavior if the sidebar is peekable, to avoid + setting up event listeners unnecessarily. + --> + <sidebar-peek-behavior v-if="sidebarState.isPeekable" @change="onPeekChange" /> </div> </template> diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index 08f01524da9..b8a654124e9 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -5,6 +5,7 @@ import { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlDisclosureDropdownItem, + GlButton, } from '@gitlab/ui'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { s__, __, sprintf } from '~/locale'; @@ -41,6 +42,7 @@ export default { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlDisclosureDropdownItem, + GlButton, NewNavToggle, UserNameGroup, }, @@ -245,7 +247,7 @@ export default { @shown="onShow" > <template #toggle> - <button class="user-bar-item btn-with-notification"> + <gl-button category="tertiary" class="user-bar-item btn-with-notification"> <span class="gl-sr-only">{{ toggleText }}</span> <gl-avatar :size="24" @@ -261,7 +263,7 @@ export default { v-bind="data.pipeline_minutes.notification_dot_attrs" > </span> - </button> + </gl-button> </template> <user-name-group :user="data" /> diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index a99aa64ebe4..4a5d0bf637f 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -16,8 +16,7 @@ export const sidebarState = Vue.observable({ contextSwitcherOpen: false, isCollapsed: false, isPeek: false, - openPeekTimer: null, - closePeekTimer: null, + isPeekable: false, }); export const helpCenterState = Vue.observable({ diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index 773976e582e..63424277ffc 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -67,7 +67,7 @@ export const initSuperSidebar = () => { const { rootPath, sidebar, toggleNewNavEndpoint, forceDesktopExpandedSidebar } = el.dataset; - bindSuperSidebarCollapsedEvents(); + bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar); initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar)); const sidebarData = JSON.parse(sidebar); diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js index 996f88c28ca..1a359533435 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js @@ -21,12 +21,10 @@ export const isDesktopBreakpoint = () => bp.windowWidth() >= breakpoints.xl; export const getCollapsedCookie = () => getCookie(SIDEBAR_COLLAPSED_COOKIE) === 'true'; export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => { - clearTimeout(sidebarState.openPeekTimer); - clearTimeout(sidebarState.closePeekTimer); - findPage().classList.toggle(SIDEBAR_COLLAPSED_CLASS, collapsed); sidebarState.isPeek = false; + sidebarState.isPeekable = Boolean(gon.features?.superSidebarPeek) && collapsed; sidebarState.isCollapsed = collapsed; if (saveCookie && isDesktopBreakpoint()) { @@ -44,7 +42,7 @@ export const initSuperSidebarCollapsedState = (forceDesktopExpandedSidebar = fal toggleSuperSidebarCollapsed(collapsed, false); }; -export const bindSuperSidebarCollapsedEvents = () => { +export const bindSuperSidebarCollapsedEvents = (forceDesktopExpandedSidebar = false) => { let previousWindowWidth = window.innerWidth; const callback = debounce(() => { @@ -52,7 +50,7 @@ export const bindSuperSidebarCollapsedEvents = () => { const widthChanged = previousWindowWidth !== newWindowWidth; if (widthChanged) { - initSuperSidebarCollapsedState(); + initSuperSidebarCollapsedState(forceDesktopExpandedSidebar); } previousWindowWidth = newWindowWidth; }, 100); diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index 1faf57bb79e..e54d3130ea0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -206,7 +206,7 @@ export default { <template> <div> <local-storage-sync - v-model="editingMode" + :value="editingMode" as-string storage-key="gl-markdown-editor-mode" @input="onEditingModeRestored" diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 9da860c838b..d58849e21af 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -7,6 +7,9 @@ } } +$super-sidebar-transition-duration: $gl-transition-duration-medium; +$super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; + @mixin notification-dot($color, $size, $top, $left) { background-color: $color; border: 2px solid $gray-10; // Same as the sidebar's background color. @@ -34,16 +37,15 @@ &.super-sidebar-loading { transform: translate3d(-100%, 0, 0); + transition: none; @include media-breakpoint-up(xl) { transform: translate3d(0, 0, 0); } } - &:not(.super-sidebar-loading) { - @media (prefers-reduced-motion: no-preference) { - transition: transform $gl-transition-duration-medium; - } + @media (prefers-reduced-motion: no-preference) { + transition: transform $super-sidebar-transition-duration; } .user-bar { @@ -207,24 +209,23 @@ display: none; } -.super-sidebar-peek { +.super-sidebar-peek, +.super-sidebar-peek-hint { @include gl-shadow; border-right: 0; +} +.super-sidebar-peek-hint { @media (prefers-reduced-motion: no-preference) { - transition: transform 100ms !important; + transition: transform $super-sidebar-transition-hint-duration ease-out; } } -.super-sidebar-hover-area { - z-index: $super-sidebar-z-index; -} - .page-with-super-sidebar { padding-left: 0; @media (prefers-reduced-motion: no-preference) { - transition: padding-left $gl-transition-duration-medium; + transition: padding-left $super-sidebar-transition-duration; } &:not(.page-with-super-sidebar-collapsed) { @@ -260,6 +261,10 @@ &.super-sidebar-peek { transform: translate3d(0, 0, 0); } + + &.super-sidebar-peek-hint { + transform: translate3d(calc(#{$gl-spacing-scale-3} - 100%), 0, 0); + } } @include media-breakpoint-up(xl) { diff --git a/app/assets/stylesheets/page_bundles/branches.scss b/app/assets/stylesheets/page_bundles/branches.scss index 2aa90529e22..a5b201c7dac 100644 --- a/app/assets/stylesheets/page_bundles/branches.scss +++ b/app/assets/stylesheets/page_bundles/branches.scss @@ -39,3 +39,7 @@ flex: 0 0 auto; white-space: nowrap; } + +.branches-list .branch-item:not(:last-of-type) { + border-bottom: 1px solid $border-color; +} diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb index f28e8f81b40..7f64606e97b 100644 --- a/app/models/loose_foreign_keys/deleted_record.rb +++ b/app/models/loose_foreign_keys/deleted_record.rb @@ -9,23 +9,23 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel self.ignored_columns = %i[partition] partitioned_by :partition, strategy: :sliding_list, - next_partition_if: -> (active_partition) do - oldest_record_in_partition = LooseForeignKeys::DeletedRecord - .select(:id, :created_at) - .for_partition(active_partition.value) - .order(:id) - .limit(1) - .take - - oldest_record_in_partition.present? && - oldest_record_in_partition.created_at < PARTITION_DURATION.ago - end, - detach_partition_if: -> (partition) do - !LooseForeignKeys::DeletedRecord - .for_partition(partition.value) - .status_pending - .exists? - end + next_partition_if: -> (active_partition) do + oldest_record_in_partition = LooseForeignKeys::DeletedRecord + .select(:id, :created_at) + .for_partition(active_partition.value) + .order(:id) + .limit(1) + .take + + oldest_record_in_partition.present? && + oldest_record_in_partition.created_at < PARTITION_DURATION.ago + end, + detach_partition_if: -> (partition) do + !LooseForeignKeys::DeletedRecord + .for_partition(partition.value) + .status_pending + .exists? + end scope :for_table, -> (table) { where(fully_qualified_table_name: table) } scope :for_partition, -> (partition) { where(partition: partition) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a67c6a47f59..b9c3019bc2d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -41,13 +41,13 @@ class MergeRequest < ApplicationRecord belongs_to :merge_user, class_name: "User" has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, - init: ->(mr, scope) do - if mr - mr.target_project&.merge_requests&.maximum(:iid) - elsif scope[:project] - where(target_project: scope[:project]).maximum(:iid) - end - end + init: ->(mr, scope) do + if mr + mr.target_project&.merge_requests&.maximum(:iid) + elsif scope[:project] + where(target_project: scope[:project]).maximum(:iid) + end + end has_many :merge_request_diffs, -> { regular }, inverse_of: :merge_request @@ -350,11 +350,12 @@ class MergeRequest < ApplicationRecord end scope :references_project, -> { references(:target_project) } scope :with_api_entity_associations, -> { - preload_routables - .preload(:assignees, :author, :unresolved_notes, :labels, :milestone, - :timelogs, :latest_merge_request_diff, :reviewers, - target_project: :project_feature, - metrics: [:latest_closed_by, :merged_by]) + preload_routables.preload( + :assignees, :author, :unresolved_notes, :labels, :milestone, + :timelogs, :latest_merge_request_diff, :reviewers, + target_project: :project_feature, + metrics: [:latest_closed_by, :merged_by] + ) } scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) } @@ -397,8 +398,10 @@ class MergeRequest < ApplicationRecord scope :preload_target_project, -> { preload(:target_project) } scope :preload_target_project_with_namespace, -> { preload(target_project: [:namespace]) } scope :preload_routables, -> do - preload(target_project: [:route, { namespace: :route }], - source_project: [:route, { namespace: :route }]) + preload( + target_project: [:route, { namespace: :route }], + source_project: [:route, { namespace: :route }] + ) end scope :preload_author, -> { preload(:author) } scope :preload_approved_by_users, -> { preload(:approved_by_users) } @@ -1019,8 +1022,7 @@ class MergeRequest < ApplicationRecord return true if target_project == source_project return true unless source_project_missing? - errors.add :validate_fork, - 'Source project is not a fork of the target project' + errors.add :validate_fork, 'Source project is not a fork of the target project' end def validate_reviewer_size_length @@ -1187,8 +1189,10 @@ class MergeRequest < ApplicationRecord alias_method :wip_title, :draft_title def mergeable?(skip_ci_check: false, skip_discussions_check: false) - return false unless mergeable_state?(skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check) + return false unless mergeable_state?( + skip_ci_check: skip_ci_check, + skip_discussions_check: skip_discussions_check + ) check_mergeability @@ -1209,10 +1213,12 @@ class MergeRequest < ApplicationRecord end def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) - additional_checks = execute_merge_checks(params: { - skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check - }) + additional_checks = execute_merge_checks( + params: { + skip_ci_check: skip_ci_check, + skip_discussions_check: skip_discussions_check + } + ) additional_checks.success? end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 1395b8ff162..0e699d7a81d 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -622,10 +622,12 @@ class MergeRequestDiff < ApplicationRecord end def diffs_in_batch_collection(batch_page, batch_size, diff_options:) - Gitlab::Diff::FileCollection::MergeRequestDiffBatch.new(self, - batch_page, - batch_size, - diff_options: diff_options) + Gitlab::Diff::FileCollection::MergeRequestDiffBatch.new( + self, + batch_page, + batch_size, + diff_options: diff_options + ) end def encode_in_base64?(diff_text) diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index 5c53cfd8c27..54cb6b7888b 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -17,10 +17,11 @@ class MergeRequestsClosingIssues < ApplicationRecord scope :accessible_by, ->(user) do joins(:merge_request) .joins('INNER JOIN project_features ON merge_requests.target_project_id = project_features.project_id') - .where('project_features.merge_requests_access_level >= :access OR EXISTS(:authorizations)', - access: ProjectFeature::ENABLED, - authorizations: user.authorizations_for_projects(min_access_level: Gitlab::Access::REPORTER, related_project_column: "merge_requests.target_project_id") - ) + .where( + 'project_features.merge_requests_access_level >= :access OR EXISTS(:authorizations)', + access: ProjectFeature::ENABLED, + authorizations: user.authorizations_for_projects(min_access_level: Gitlab::Access::REPORTER, related_project_column: "merge_requests.target_project_id") + ) end class << self diff --git a/app/models/ml/candidate_metadata.rb b/app/models/ml/candidate_metadata.rb index 06b893c211f..1191051b1a3 100644 --- a/app/models/ml/candidate_metadata.rb +++ b/app/models/ml/candidate_metadata.rb @@ -4,9 +4,9 @@ module Ml class CandidateMetadata < ApplicationRecord validates :candidate, presence: true validates :name, - length: { maximum: 250 }, - presence: true, - uniqueness: { scope: :candidate, message: ->(candidate, _) { "'#{candidate.name}' already taken" } } + length: { maximum: 250 }, + presence: true, + uniqueness: { scope: :candidate, message: ->(candidate, _) { "'#{candidate.name}' already taken" } } validates :value, length: { maximum: 5000 }, presence: true belongs_to :candidate, class_name: 'Ml::Candidate' diff --git a/app/models/ml/experiment_metadata.rb b/app/models/ml/experiment_metadata.rb index 93496807e1a..37cb2714268 100644 --- a/app/models/ml/experiment_metadata.rb +++ b/app/models/ml/experiment_metadata.rb @@ -4,9 +4,9 @@ module Ml class ExperimentMetadata < ApplicationRecord validates :experiment, presence: true validates :name, - length: { maximum: 250 }, - presence: true, - uniqueness: { scope: :experiment, message: ->(exp, _) { "'#{exp.name}' already taken" } } + length: { maximum: 250 }, + presence: true, + uniqueness: { scope: :experiment, message: ->(exp, _) { "'#{exp.name}' already taken" } } validates :value, length: { maximum: 5000 }, presence: true belongs_to :experiment, class_name: 'Ml::Experiment' diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 86b5d7ea05f..146ce2aa5b6 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -124,18 +124,18 @@ class Namespace < ApplicationRecord delegate :name, to: :owner, allow_nil: true, prefix: true delegate :avatar_url, to: :owner, allow_nil: true delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=, - to: :namespace_settings, allow_nil: true + to: :namespace_settings, allow_nil: true delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=, - to: :namespace_settings + to: :namespace_settings delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=, - to: :namespace_settings + to: :namespace_settings delegate :allow_runner_registration_token, - :allow_runner_registration_token=, - to: :namespace_settings + :allow_runner_registration_token=, + to: :namespace_settings delegate :maven_package_requests_forwarding, - :pypi_package_requests_forwarding, - :npm_package_requests_forwarding, - to: :package_settings + :pypi_package_requests_forwarding, + :npm_package_requests_forwarding, + to: :package_settings before_save :update_new_emails_created_column, if: -> { emails_disabled_changed? } before_create :sync_share_with_group_lock_with_parent diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb index 843de9bce33..792964a6c7f 100644 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ b/app/models/namespaces/traversal/linear_scopes.rb @@ -27,9 +27,11 @@ module Namespaces def self_and_ancestors(include_self: true, upto: nil, hierarchy_order: nil) return super unless use_traversal_ids_for_ancestor_scopes? - self_and_ancestors_from_inner_join(include_self: include_self, - upto: upto, hierarchy_order: - hierarchy_order) + self_and_ancestors_from_inner_join( + include_self: include_self, + upto: upto, hierarchy_order: + hierarchy_order + ) end def self_and_ancestor_ids(include_self: true) diff --git a/app/models/note.rb b/app/models/note.rb index 597ba767a11..ac2b54629ae 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -171,8 +171,10 @@ class Note < ApplicationRecord scope :with_associations, -> do # FYI noteable cannot be loaded for LegacyDiffNote for commits - includes(:author, :noteable, :updated_by, - project: [:project_members, :namespace, { group: [:group_members] }]) + includes( + :author, :noteable, :updated_by, + project: [:project_members, :namespace, { group: [:group_members] }] + ) end scope :with_metadata, -> { includes(:system_note_metadata) } diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb index 4238de0a2f8..e4936de7b40 100644 --- a/app/models/note_diff_file.rb +++ b/app/models/note_diff_file.rb @@ -19,9 +19,11 @@ class NoteDiffFile < ApplicationRecord def raw_diff_file raw_diff = Gitlab::Git::Diff.new(to_hash) - Gitlab::Diff::File.new(raw_diff, - repository: project.repository, - diff_refs: original_position.diff_refs, - unique_identifier: id) + Gitlab::Diff::File.new( + raw_diff, + repository: project.repository, + diff_refs: original_position.diff_refs, + unique_identifier: id + ) end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 1065ddb59e6..0505a205333 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,4 +1,5 @@ - page_title _('Account') +- @force_desktop_expanded_sidebar = true - if current_user.ldap_user? = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' }, diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml index e2b6008934c..54736153223 100644 --- a/app/views/profiles/active_sessions/index.html.haml +++ b/app/views/profiles/active_sessions/index.html.haml @@ -1,4 +1,5 @@ - page_title _('Active Sessions') +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 6072042001c..44cfbc1f74f 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,4 +1,5 @@ - page_title _('Authentication log') +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml index 8a1814e55c3..264ee040d7d 100644 --- a/app/views/profiles/chat_names/index.html.haml +++ b/app/views/profiles/chat_names/index.html.haml @@ -1,5 +1,6 @@ - page_title _('Chat') - @hide_search_settings = true +- @force_desktop_expanded_sidebar = true .row.gl-mt-5.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 53db00c1638..c16f3c3b12b 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,4 +1,5 @@ - page_title _('Emails') +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml index d018035c5d6..b21a4da16b9 100644 --- a/app/views/profiles/gpg_keys/index.html.haml +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -1,5 +1,6 @@ - page_title _('GPG Keys') - add_page_specific_style 'page_bundles/profile' +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 9f1614d4f49..e7c0cf813b5 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,5 +1,6 @@ - page_title _('SSH Keys') - add_page_specific_style 'page_bundles/profile' +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index c757f774d4e..a632c450eda 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,5 +1,6 @@ - add_page_specific_style 'page_bundles/notifications' - page_title _('Notifications') +- @force_desktop_expanded_sidebar = true %div - if @user.errors.any? diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index b6d12bbefc6..4fdf80c1eb1 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,5 +1,6 @@ - breadcrumb_title _('Edit Password') - page_title _('Password') +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index bc3f63372a3..57c0badd033 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -2,6 +2,7 @@ - page_title s_('AccessTokens|Personal Access Tokens') - type = _('personal access token') - type_plural = _('personal access tokens') +- @force_desktop_expanded_sidebar = true .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 64f627bcb35..6d81866e30e 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -5,6 +5,7 @@ - user_fields = { theme: user_theme_id, gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }.to_json - @themes = Gitlab::Themes::available_themes.to_json - data_attributes = { themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path } +- @force_desktop_expanded_sidebar = true - Gitlab::Themes.each do |theme| = stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index ba17078f4c4..930f4f5c397 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -2,6 +2,7 @@ - page_title s_("Profiles|Edit Profile") - add_page_specific_style 'page_bundles/profile' - gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host +- @force_desktop_expanded_sidebar = true = gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f| .row.js-search-settings-section diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 51c218f40b9..dbc1fe24d96 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,50 +1,51 @@ - merged = local_assigns.fetch(:merged, false) - commit = @repository.commit(branch.dereferenced_target) - merge_project = merge_request_source_project_for_project(@project) -%li{ class: "branch-item gl-display-flex! gl-align-items-center! js-branch-item js-branch-#{branch.name}", data: { name: branch.name, qa_selector: 'branch_container', qa_name: branch.name } } - .branch-info - .gl-display-flex.gl-align-items-center - = sprite_icon('branch', size: 12, css_class: 'gl-flex-shrink-0') - = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3', data: { qa_selector: 'branch_link' } do - = branch.name - = clipboard_button(text: branch.name, title: _("Copy branch name")) - - if branch.name == @repository.root_ref - = gl_badge_tag s_('DefaultBranchLabel|default'), { variant: :info, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } - - elsif merged - = gl_badge_tag s_('Branches|merged'), { variant: :info, size: :sm }, { class: 'gl-ml-2', title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref }, data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } } - - if protected_branch?(@project, branch) - = gl_badge_tag s_('Branches|protected'), { variant: :success, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } - - = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch - - .block-truncated - - if commit - = render 'projects/branches/commit', commit: commit, project: @project - - else - = s_('Branches|Can’t find HEAD commit for this branch') - - - if branch.name != @repository.root_ref - .js-branch-divergence-graph - - .controls.d-none.d-md-block< - - if commit_status - = render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5' - - elsif show_commit_status - .gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5 - %svg.s24 - - - if merge_project && create_mr_button?(from: branch.name, source_project: @project) - = render Pajamas::ButtonComponent.new(href: create_mr_path(from: branch.name, source_project: @project)) do - = _('Merge request') +%li{ class: "branch-item gl-py-3! js-branch-item js-branch-#{branch.name}", data: { name: branch.name, qa_selector: 'branch_container', qa_name: branch.name } } + .branch-item-content.gl-display-flex.gl-align-items-center.gl-px-3.gl-py-2 + .branch-info + .gl-display-flex.gl-align-items-center + = sprite_icon('branch', size: 12, css_class: 'gl-flex-shrink-0') + = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3', data: { qa_selector: 'branch_link' } do + = branch.name + = clipboard_button(text: branch.name, title: _("Copy branch name")) + - if branch.name == @repository.root_ref + = gl_badge_tag s_('DefaultBranchLabel|default'), { variant: :info, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } + - elsif merged + = gl_badge_tag s_('Branches|merged'), { variant: :info, size: :sm }, { class: 'gl-ml-2', title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref }, data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } } + - if protected_branch?(@project, branch) + = gl_badge_tag s_('Branches|protected'), { variant: :success, size: :sm }, { class: 'gl-ml-2', data: { qa_selector: 'badge_content' } } + + = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch + + .block-truncated + - if commit + = render 'projects/branches/commit', commit: commit, project: @project + - else + = s_('Branches|Can’t find HEAD commit for this branch') - if branch.name != @repository.root_ref - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), - class: "gl-button btn btn-default js-onboarding-compare-branches #{'gl-ml-3' unless merge_project}", - method: :post, - title: s_('Branches|Compare') do - = s_('Branches|Compare') - - = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], class: 'gl-vertical-align-top' - - - if can?(current_user, :push_code, @project) - = render 'projects/branches/delete_branch_modal_button', project: @project, branch: branch, merged: merged + .js-branch-divergence-graph + + .controls.d-none.d-md-block< + - if commit_status + = render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5' + - elsif show_commit_status + .gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5 + %svg.s24 + + - if merge_project && create_mr_button?(from: branch.name, source_project: @project) + = render Pajamas::ButtonComponent.new(href: create_mr_path(from: branch.name, source_project: @project)) do + = _('Merge request') + + - if branch.name != @repository.root_ref + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), + class: "gl-button btn btn-default js-onboarding-compare-branches #{'gl-ml-3' unless merge_project}", + method: :post, + title: s_('Branches|Compare') do + = s_('Branches|Compare') + + = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], class: 'gl-vertical-align-top' + + - if can?(current_user, :push_code, @project) + = render 'projects/branches/delete_branch_modal_button', project: @project, branch: branch, merged: merged diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index a1f93d21647..a2c6c93278b 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -7,11 +7,12 @@ - return unless branches.any? -= render Pajamas::CardComponent.new(card_options: {class: 'gl-mb-5'}, body_options: {class: 'gl-py-0'}, footer_options: {class: 'gl-text-center'}) do |c| += render Pajamas::CardComponent.new(card_options: {class: 'gl-mt-5 gl-bg-gray-10'}, header_options: {class: 'gl-px-5 gl-py-4 gl-bg-white'}, body_options: {class: 'gl-px-3 gl-py-0'}, footer_options: {class: 'gl-bg-white'}) do |c| - c.header do - = panel_title + %h3.card-title.h5.gl-line-height-24.gl-m-0 + = panel_title - c.body do - %ul.content-list.all-branches{ data: { qa_selector: 'all_branches_container' } } + %ul.content-list.branches-list.all-branches{ data: { qa_selector: 'all_branches_container' } } - branches.first(overview_max_branches).each do |branch| = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any? - if branches.size > overview_max_branches diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 518292effd8..ebe0372ddaf 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -9,7 +9,7 @@ -# @mode - overview|active|stale|all (default:overview) -# @sort - name_asc|updated_asc|updated_desc -.top-area.gl-border-0 +.top-area = gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-b-0' }) do = gl_tab_link_to s_('Branches|Overview'), project_branches_path(@project), { item_active: @mode == 'overview', title: s_('Branches|Show overview of the branches') } = gl_tab_link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), { title: s_('Branches|Show active branches') } diff --git a/app/views/shared/doorkeeper/applications/_index.html.haml b/app/views/shared/doorkeeper/applications/_index.html.haml index c2a47a88f02..abfe3baf8b4 100644 --- a/app/views/shared/doorkeeper/applications/_index.html.haml +++ b/app/views/shared/doorkeeper/applications/_index.html.haml @@ -1,3 +1,5 @@ +- @force_desktop_expanded_sidebar = true + .row.gl-mt-3.js-search-settings-section .col-lg-4.profile-settings-sidebar %h4.gl-mt-0 diff --git a/config/README.md b/config/README.md index 71f18505a88..c1ce37e3eb5 100644 --- a/config/README.md +++ b/config/README.md @@ -83,9 +83,4 @@ An example configuration file for Redis is in this directory under the name | `db_load_balancing` | `shared_state` | [Database Load Balancing](https://docs.gitlab.com/ee/administration/postgresql/database_load_balancing.html) | If no configuration is found, or no URL is found in the configuration -file, the default URL used is: - -1. `redis://localhost:6380` for `cache`. -1. `redis://localhost:6381` for `queues`. -1. `redis://localhost:6382` for `shared_state`. -1. The URL from the fallback instance for all other instances. +file, the default URL used is `redis://localhost:6379` for all Redis instances. diff --git a/config/feature_flags/development/frozen_outbound_job_token_scopes.yml b/config/feature_flags/development/frozen_outbound_job_token_scopes.yml index a73e3b4d4eb..f8ebc46d7f9 100644 --- a/config/feature_flags/development/frozen_outbound_job_token_scopes.yml +++ b/config/feature_flags/development/frozen_outbound_job_token_scopes.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407401 milestone: '16.0' type: development group: group::pipeline execution -default_enabled: false +default_enabled: true diff --git a/doc/ci/runners/runners_scope.md b/doc/ci/runners/runners_scope.md index d20ef846df7..43204b463b3 100644 --- a/doc/ci/runners/runners_scope.md +++ b/doc/ci/runners/runners_scope.md @@ -95,10 +95,8 @@ To disable shared runners for a group: select **Allow projects and subgroups to override the group setting**. NOTE: -To re-enable the shared runners for a group, turn on the -**Enable shared runners for this group** toggle. -Then, a user with the Owner or Maintainer role must explicitly change this setting -for each project subgroup or project. +If you re-enable the shared runners for a group after you disable them, a user with the +Owner or Maintainer role must manually change this setting for each project subgroup or project. ### How shared runners pick jobs diff --git a/doc/development/documentation/topic_types/tutorial.md b/doc/development/documentation/topic_types/tutorial.md index 91f426147b5..2d57029b786 100644 --- a/doc/development/documentation/topic_types/tutorial.md +++ b/doc/development/documentation/topic_types/tutorial.md @@ -32,6 +32,10 @@ For tutorial Markdown files, you can either: - Save the file in a directory with the product documentation. - Create a subfolder under `doc/tutorials` and name the file `index.md`. +In the left nav, add the tutorial near the relevant feature documentation. + +Add a link to the tutorial on one of the [tutorial pages](../../../tutorials/index.md). + ## Tutorial format Tutorials should be in this format: diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 95f0692c3e6..dd417d038cb 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -278,6 +278,27 @@ to be longer. Some methods for shortening a name that's too long: `index_vulnerability_findings_remediations_on_remediation_id`. - Instead of columns, specify the purpose of the index, such as `index_users_for_unconfirmation_notification`. +### Migration timestamp age + +The timestamp portion of a migration filename determines the order in which migrations +are run. It's important to maintain a rough correlation between: + +1. When a migration is added to the GitLab codebase. +1. The timestamp of the migration itself. + +A new migration's timestamp should *never* be before the previous hard stop. +Migrations are occasionally squashed, and if a migration is added whose timestamp +falls before the previous hard stop, a problem like what happened in +[issue 408304](https://gitlab.com/gitlab-org/gitlab/-/issues/408304) can occur. + +For example, if we are currently developing against GitLab 16.0, the previous +hard stop is 15.11. 15.11 was released on April 23rd, 2023. Therefore, the +minimum acceptable timestamp would be 20230424000000. + +#### Best practice + +While the above should be considered a hard rule, it is a best practice to try to keep migration timestamps to within three weeks of the date it is anticipated that the migration will be merged upstream, regardless of how much time has elapsed since the last hard stop. + ## Heavy operations in a single transaction When using a single-transaction migration, a transaction holds a database connection diff --git a/lib/gitlab/ci/jwt.rb b/lib/gitlab/ci/jwt.rb index 7d8f7c0044b..068e6f5949d 100644 --- a/lib/gitlab/ci/jwt.rb +++ b/lib/gitlab/ci/jwt.rb @@ -31,6 +31,9 @@ module Gitlab attr_reader :build, :ttl + delegate :project, :user, :pipeline, :runner, to: :build + delegate :source_ref, :source_ref_path, to: :pipeline + def reserved_claims now = Time.now.to_i @@ -53,8 +56,8 @@ module Gitlab user_id: user&.id.to_s, user_login: user&.username, user_email: user&.email, - pipeline_id: build.pipeline.id.to_s, - pipeline_source: build.pipeline.source.to_s, + pipeline_id: pipeline.id.to_s, + pipeline_source: pipeline.source.to_s, job_id: build.id.to_s, ref: source_ref, ref_type: ref_type, @@ -91,30 +94,10 @@ module Gitlab public_key.to_jwk[:kid] end - def project - build.project - end - def namespace project.namespace end - def user - build.user - end - - def pipeline - build.pipeline - end - - def source_ref - pipeline.source_ref - end - - def source_ref_path - pipeline.source_ref_path - end - def ref_type ::Ci::BuildRunnerPresenter.new(build).ref_type end @@ -126,10 +109,6 @@ module Gitlab def environment_protected? false # Overridden in EE end - - def runner - build.runner - end end end end diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb index e2b4db7d55c..aff30455d09 100644 --- a/lib/gitlab/ci/jwt_v2.rb +++ b/lib/gitlab/ci/jwt_v2.rb @@ -45,7 +45,7 @@ module Gitlab super.merge( runner_id: runner&.id, runner_environment: runner_environment, - sha: build.pipeline.sha + sha: pipeline.sha ) end diff --git a/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers.rb b/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers.rb index 01ff3dcbfb8..2a9d37452bd 100644 --- a/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers.rb +++ b/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers.rb @@ -22,10 +22,8 @@ module Gitlab log "This process prevents the migration from acquiring the necessary locks" log "Query: `#{wraparound_vacuum[:query]}`" log "Current duration: #{wraparound_vacuum[:duration].inspect}" - log "Process id: #{wraparound_vacuum[:pid]}" - log "You can wait until it completes or if absolutely necessary interrupt it using: " \ - "`select pg_cancel_backend(#{wraparound_vacuum[:pid]});`" - log "Be aware that a new process will kick in immediately, so multiple interruptions " \ + log "You can wait until it completes or if absolutely necessary interrupt it, " \ + "but be aware that a new process will kick in immediately, so multiple interruptions " \ "might be required to time it right with the locks retry mechanism" end @@ -48,10 +46,9 @@ module Gitlab def raw_wraparound_vacuum connection.select_all(<<~SQL.squish) - SELECT pid, state, age(clock_timestamp(), query_start) as duration, query - FROM pg_stat_activity + SELECT age(clock_timestamp(), query_start) as duration, query + FROM postgres_pg_stat_activity_autovacuum() WHERE query ILIKE '%VACUUM%' || #{quoted_table_name} || '%(to prevent wraparound)' - AND backend_type = 'autovacuum worker' LIMIT 1 SQL end diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb index c990655769c..288d5db15c6 100644 --- a/lib/gitlab/redis/wrapper.rb +++ b/lib/gitlab/redis/wrapper.rb @@ -160,35 +160,10 @@ module Gitlab def raw_config_hash config_data = fetch_config - config_hash = - if config_data - config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys - else - { url: '' } - end - - if config_hash[:url].blank? && config_hash[:cluster].blank? - config_hash[:url] = legacy_fallback_urls[self.class.store_name] || legacy_fallback_urls[self.class.config_fallback.store_name] - end - - config_hash - end + return { url: '' } if config_data.nil? + return { url: config_data } if config_data.is_a?(String) - # These URLs were defined for cache, queues, and shared_state in - # code. They are used only when no config file exists at all for a - # given instance. The configuration does not seem particularly - # useful - it uses different ports on localhost - but we cannot - # confidently delete it as we don't know if any instances rely on - # this. - # - # DO NOT ADD new instances here. All new instances should define a - # `.config_fallback`, which will then be used to look up this URL. - def legacy_fallback_urls - { - 'Cache' => 'redis://localhost:6380', - 'Queues' => 'redis://localhost:6381', - 'SharedState' => 'redis://localhost:6382' - } + config_data.deep_symbolize_keys end def fetch_config diff --git a/spec/frontend/__helpers__/assert_props.js b/spec/frontend/__helpers__/assert_props.js index 3e372454bf5..9935719580a 100644 --- a/spec/frontend/__helpers__/assert_props.js +++ b/spec/frontend/__helpers__/assert_props.js @@ -1,14 +1,30 @@ import { mount } from '@vue/test-utils'; import { ErrorWithStack } from 'jest-util'; -export function assertProps(Component, props, extraMountArgs = {}) { - const originalConsoleError = global.console.error; - global.console.error = function error(...args) { - throw new ErrorWithStack( - `Unexpected call of console.error() with:\n\n${args.join(', ')}`, - this.error, - ); +function installConsoleHandler(method) { + const originalHandler = global.console[method]; + + global.console[method] = function throwableHandler(...args) { + if (args[0]?.includes('Invalid prop') || args[0]?.includes('Missing required prop')) { + throw new ErrorWithStack( + `Unexpected call of console.${method}() with:\n\n${args.join(', ')}`, + this[method], + ); + } + + originalHandler.apply(this, args); + }; + + return function restore() { + global.console[method] = originalHandler; }; +} + +export function assertProps(Component, props, extraMountArgs = {}) { + const [restoreError, restoreWarn] = [ + installConsoleHandler('error'), + installConsoleHandler('warn'), + ]; const ComponentWithoutRenderFn = { ...Component, render() { @@ -19,6 +35,7 @@ export function assertProps(Component, props, extraMountArgs = {}) { try { mount(ComponentWithoutRenderFn, { propsData: props, ...extraMountArgs }); } finally { - global.console.error = originalConsoleError; + restoreError(); + restoreWarn(); } } diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js index 30f674f5ba7..0e59e9ab5b6 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -127,6 +127,25 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); }); + it('should refetch jobs count query when the amount jobs and count do not match', async () => { + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + // after applying filter a new count is fetched + findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + + // tab is switched to `finished`, no count + await findTabs().vm.$emit('fetchJobsByStatus', ['FAILED', 'SUCCESS', 'CANCELED']); + + // tab is switched back to `all`, the old filter count has to be overwritten with new count + await findTabs().vm.$emit('fetchJobsByStatus', null); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(2); + }); + describe('when infinite scrolling is triggered', () => { it('does not display a skeleton loader', () => { triggerInfiniteScroll(); @@ -251,6 +270,18 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); }); + it('refetches jobs count query when filtering', async () => { + createComponent(); + + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + }); + it('shows raw text warning when user inputs raw text', async () => { const expectedWarning = { message: s__( @@ -262,11 +293,13 @@ describe('Job table app', () => { createComponent(); jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']); expect(createAlert).toHaveBeenCalledWith(expectedWarning); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); }); it('updates URL query string when filtering jobs by status', async () => { diff --git a/spec/frontend/lib/utils/css_utils_spec.js b/spec/frontend/lib/utils/css_utils_spec.js new file mode 100644 index 00000000000..dcaeb075c93 --- /dev/null +++ b/spec/frontend/lib/utils/css_utils_spec.js @@ -0,0 +1,22 @@ +import { getCssClassDimensions } from '~/lib/utils/css_utils'; + +describe('getCssClassDimensions', () => { + const mockDimensions = { width: 1, height: 2 }; + let actual; + + beforeEach(() => { + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(mockDimensions); + actual = getCssClassDimensions('foo bar'); + }); + + it('returns the measured width and height', () => { + expect(actual).toEqual(mockDimensions); + }); + + it('measures an element with the given classes', () => { + expect(Element.prototype.getBoundingClientRect).toHaveBeenCalledTimes(1); + + const [tempElement] = Element.prototype.getBoundingClientRect.mock.contexts; + expect([...tempElement.classList]).toEqual(['foo', 'bar']); + }); +}); diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js index cc6f1c27142..dad7308ac0a 100644 --- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js +++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js @@ -139,6 +139,25 @@ describe('Job table app', () => { expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); }); + it('should refetch jobs count query when the amount jobs and count do not match', async () => { + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); + + // after applying filter a new count is fetched + findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1); + + // tab is switched to `finished`, no count + await findTabs().vm.$emit('fetchJobsByStatus', ['FAILED', 'SUCCESS', 'CANCELED']); + + // tab is switched back to `all`, the old filter count has to be overwritten with new count + await findTabs().vm.$emit('fetchJobsByStatus', null); + + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(2); + }); + describe('when infinite scrolling is triggered', () => { it('does not display a skeleton loader', () => { triggerInfiniteScroll(); @@ -324,11 +343,13 @@ describe('Job table app', () => { createComponent(); jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); + jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn()); await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']); expect(createAlert).toHaveBeenCalledWith(expectedWarning); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); + expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0); }); it('updates URL query string when filtering jobs by status', async () => { diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js b/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js new file mode 100644 index 00000000000..2f76ad66dd5 --- /dev/null +++ b/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js @@ -0,0 +1,64 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerCell from '~/pages/admin/jobs/components/table/cells/runner_cell.vue'; +import { RUNNER_EMPTY_TEXT } from '~/pages/admin/jobs/components/constants'; +import { allRunnersData } from '../../../../../../ci/runner/mock_data'; + +const mockRunner = allRunnersData.data.runners.nodes[0]; + +const mockJobWithRunner = { + id: 'gid://gitlab/Ci::Build/2264', + runner: mockRunner, +}; + +const mockJobWithoutRunner = { + id: 'gid://gitlab/Ci::Build/2265', +}; + +describe('Runner Cell', () => { + let wrapper; + + const findRunnerLink = () => wrapper.findComponent(GlLink); + const findEmptyRunner = () => wrapper.find('[data-testid="empty-runner-text"]'); + + const createComponent = (props = {}) => { + wrapper = shallowMount(RunnerCell, { + propsData: { + ...props, + }, + }); + }; + + describe('Runner Link', () => { + describe('Job with runner', () => { + beforeEach(() => { + createComponent({ job: mockJobWithRunner }); + }); + + it('shows and links to the runner', () => { + expect(findRunnerLink().exists()).toBe(true); + expect(findRunnerLink().text()).toBe(mockRunner.description); + expect(findRunnerLink().attributes('href')).toBe(mockRunner.adminUrl); + }); + + it('hides the empty runner text', () => { + expect(findEmptyRunner().exists()).toBe(false); + }); + }); + + describe('Job without runner', () => { + beforeEach(() => { + createComponent({ job: mockJobWithoutRunner }); + }); + + it('shows default `empty` text', () => { + expect(findEmptyRunner().exists()).toBe(true); + expect(findEmptyRunner().text()).toBe(RUNNER_EMPTY_TEXT); + }); + + it('hides the runner link', () => { + expect(findRunnerLink().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js new file mode 100644 index 00000000000..047dc9a6599 --- /dev/null +++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js @@ -0,0 +1,207 @@ +import { mount } from '@vue/test-utils'; +import { + SUPER_SIDEBAR_PEEK_OPEN_DELAY, + SUPER_SIDEBAR_PEEK_CLOSE_DELAY, +} from '~/super_sidebar/constants'; +import SidebarPeek, { + STATE_CLOSED, + STATE_WILL_OPEN, + STATE_OPEN, + STATE_WILL_CLOSE, +} from '~/super_sidebar/components/sidebar_peek_behavior.vue'; + +// These are measured at runtime in the browser, but statically defined here +// since Jest does not do layout/styling. +const X_NEAR_WINDOW_EDGE = 5; +const X_SIDEBAR_EDGE = 10; +const X_AWAY_FROM_SIDEBAR = 20; + +jest.mock('~/lib/utils/css_utils', () => ({ + getCssClassDimensions: (className) => { + if (className === 'gl-w-3') { + return { width: X_NEAR_WINDOW_EDGE }; + } + + if (className === 'super-sidebar') { + return { width: X_SIDEBAR_EDGE }; + } + + throw new Error(`No mock for CSS class ${className}`); + }, +})); + +describe('SidebarPeek component', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount(SidebarPeek); + }; + + const moveMouse = (clientX) => { + const event = new MouseEvent('mousemove', { + clientX, + }); + + document.dispatchEvent(event); + }; + + const moveMouseOutOfDocument = () => { + const event = new MouseEvent('mouseleave'); + document.documentElement.dispatchEvent(event); + }; + + const lastNChangeEvents = (n = 1) => wrapper.emitted('change').slice(-n).flat(); + + beforeEach(() => { + createComponent(); + }); + + it('begins in the closed state', () => { + expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED]); + }); + + it('does not emit duplicate events in a region', () => { + moveMouse(0); + moveMouse(1); + moveMouse(2); + + expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED, STATE_WILL_OPEN]); + }); + + it('transitions to will-open when in peek region', () => { + moveMouse(X_NEAR_WINDOW_EDGE); + + expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]); + + moveMouse(X_NEAR_WINDOW_EDGE - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]); + }); + + it('transitions will-open -> open after delay', () => { + moveMouse(0); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]); + + jest.advanceTimersByTime(1); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]); + }); + + it('cancels transition will-open -> open if mouse out of peek region', () => { + moveMouse(0); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1); + + moveMouse(X_NEAR_WINDOW_EDGE); + + jest.runOnlyPendingTimers(); + + expect(lastNChangeEvents(3)).toEqual([STATE_CLOSED, STATE_WILL_OPEN, STATE_CLOSED]); + }); + + it('transitions open -> will-close if mouse out of sidebar region', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]); + + moveMouse(X_SIDEBAR_EDGE); + + expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]); + }); + + it('transitions will-close -> closed after delay', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]); + + jest.advanceTimersByTime(1); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]); + }); + + it('cancels transition will-close -> close if mouse move in sidebar region', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE); + jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]); + + moveMouse(X_SIDEBAR_EDGE - 1); + jest.runOnlyPendingTimers(); + + expect(lastNChangeEvents(3)).toEqual([STATE_OPEN, STATE_WILL_CLOSE, STATE_OPEN]); + }); + + it('immediately transitions open -> closed if mouse moves far away', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_CLOSED]); + }); + + it('immediately transitions will-close -> closed if mouse moves far away', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_AWAY_FROM_SIDEBAR - 1); + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]); + }); + + it('cleans up its mousemove listener before destroy', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + + wrapper.destroy(); + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]); + }); + + it('cleans up its timers before destroy', () => { + moveMouse(0); + + wrapper.destroy(); + jest.runOnlyPendingTimers(); + + expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]); + }); + + it('transitions will-open -> closed if cursor leaves document', () => { + moveMouse(0); + moveMouseOutOfDocument(); + + expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_CLOSED]); + }); + + it('transitions open -> will-close if cursor leaves document', () => { + moveMouse(0); + jest.runOnlyPendingTimers(); + moveMouseOutOfDocument(); + + expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]); + }); + + it('cleans up document mouseleave listener before destroy', () => { + moveMouse(0); + + wrapper.destroy(); + + moveMouseOutOfDocument(); + + expect(lastNChangeEvents(1)).not.toEqual([STATE_CLOSED]); + }); +}); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index c3921e0a939..b76c637caf4 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -4,13 +4,16 @@ 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 SidebarPeekBehavior, { + STATE_CLOSED, + STATE_WILL_OPEN, + STATE_OPEN, + STATE_WILL_CLOSE, +} from '~/super_sidebar/components/sidebar_peek_behavior.vue'; import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue'; import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue'; -import { - SUPER_SIDEBAR_PEEK_OPEN_DELAY, - SUPER_SIDEBAR_PEEK_CLOSE_DELAY, -} from '~/super_sidebar/constants'; +import { sidebarState } from '~/super_sidebar/constants'; import { toggleSuperSidebarCollapsed, isCollapsed, @@ -18,6 +21,8 @@ import { import { stubComponent } from 'helpers/stub_component'; import { sidebarData as mockSidebarData } from '../mock_data'; +const initialSidebarState = { ...sidebarState }; + jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager'); const closeContextSwitcherMock = jest.fn(); @@ -28,16 +33,19 @@ const TrialStatusPopoverStub = { template: `<div data-testid="${trialStatusPopoverStubTestId}" />`, }; +const peekClass = 'super-sidebar-peek'; +const peekHintClass = 'super-sidebar-peek-hint'; + describe('SuperSidebar component', () => { let wrapper; const findSidebar = () => wrapper.findByTestId('super-sidebar'); - const findHoverArea = () => wrapper.findByTestId('super-sidebar-hover-area'); const findUserBar = () => wrapper.findComponent(UserBar); const findContextSwitcher = () => wrapper.findComponent(ContextSwitcher); const findNavContainer = () => wrapper.findByTestId('nav-container'); const findHelpCenter = () => wrapper.findComponent(HelpCenter); const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget); + const findPeekBehavior = () => wrapper.findComponent(SidebarPeekBehavior); const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId); const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId); const findSidebarMenu = () => wrapper.findComponent(SidebarMenu); @@ -45,14 +53,11 @@ describe('SuperSidebar component', () => { const createWrapper = ({ provide = {}, sidebarData = mockSidebarData, - sidebarState = {}, + sidebarState: state = {}, } = {}) => { + Object.assign(sidebarState, state); + wrapper = shallowMountExtended(SuperSidebar, { - data() { - return { - ...sidebarState, - }; - }, provide: { showTrialStatusWidget: false, ...provide, @@ -70,6 +75,10 @@ describe('SuperSidebar component', () => { }); }; + beforeEach(() => { + Object.assign(sidebarState, initialSidebarState); + }); + describe('default', () => { it('adds inert attribute when collapsed', () => { createWrapper({ sidebarState: { isCollapsed: true } }); @@ -154,12 +163,18 @@ describe('SuperSidebar component', () => { expect(findTrialStatusWidget().exists()).toBe(false); expect(findTrialStatusPopover().exists()).toBe(false); }); + + it('does not have peek behavior', () => { + createWrapper(); + + expect(findPeekBehavior().exists()).toBe(false); + }); }); describe('on collapse', () => { beforeEach(() => { createWrapper(); - wrapper.vm.isCollapsed = true; + sidebarState.isCollapsed = true; }); it('closes the context switcher', () => { @@ -167,91 +182,39 @@ describe('SuperSidebar component', () => { }); }); - describe('when peeking on hover', () => { - const peekClass = 'super-sidebar-peek'; - - it('updates inert attribute and peek class', async () => { - createWrapper({ - provide: { glFeatures: { superSidebarPeek: true } }, - sidebarState: { isCollapsed: true }, - }); + describe('peek behavior', () => { + it(`initially makes sidebar inert and peekable (${STATE_CLOSED})`, () => { + createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } }); - findHoverArea().trigger('mouseenter'); - - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1); - await nextTick(); - - // Not quite enough time has elapsed yet for sidebar to open - expect(findSidebar().classes()).not.toContain(peekClass); expect(findSidebar().attributes('inert')).toBe('inert'); + expect(findSidebar().classes()).not.toContain(peekHintClass); + expect(findSidebar().classes()).not.toContain(peekClass); + }); - jest.advanceTimersByTime(1); - await nextTick(); - - // Exactly enough time has elapsed to open - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - // Important: assume the cursor enters the sidebar - findSidebar().trigger('mouseenter'); - - jest.runAllTimers(); - await nextTick(); - - // Sidebar remains peeked open indefinitely without a mouseleave - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - findSidebar().trigger('mouseleave'); - - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); - await nextTick(); - - // Not quite enough time has elapsed yet for sidebar to hide - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); + it(`makes sidebar inert and shows peek hint when peek state is ${STATE_WILL_OPEN}`, async () => { + createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } }); - jest.advanceTimersByTime(1); + findPeekBehavior().vm.$emit('change', STATE_WILL_OPEN); await nextTick(); - // Exactly enough time has elapsed for sidebar to hide - expect(findSidebar().classes()).not.toContain('super-sidebar-peek'); expect(findSidebar().attributes('inert')).toBe('inert'); + expect(findSidebar().classes()).toContain(peekHintClass); + expect(findSidebar().classes()).not.toContain(peekClass); }); - it('eventually closes the sidebar if cursor never enters sidebar', async () => { - createWrapper({ - provide: { glFeatures: { superSidebarPeek: true } }, - sidebarState: { isCollapsed: true }, - }); + it.each([STATE_OPEN, STATE_WILL_CLOSE])( + 'makes sidebar interactive and visible when peek state is %s', + async (state) => { + createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } }); - findHoverArea().trigger('mouseenter'); + findPeekBehavior().vm.$emit('change', state); + await nextTick(); - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY); - await nextTick(); - - // Sidebar is now open - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - // Important: do *not* fire a mouseenter event on the sidebar here. This - // imitates what happens if the cursor moves away from the sidebar before - // it actually appears. - - jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1); - await nextTick(); - - // Not quite enough time has elapsed yet for sidebar to hide - expect(findSidebar().classes()).toContain(peekClass); - expect(findSidebar().attributes('inert')).toBe(undefined); - - jest.advanceTimersByTime(1); - await nextTick(); - - // Exactly enough time has elapsed for sidebar to hide - expect(findSidebar().classes()).not.toContain('super-sidebar-peek'); - expect(findSidebar().attributes('inert')).toBe('inert'); - }); + expect(findSidebar().attributes('inert')).toBe(undefined); + expect(findSidebar().classes()).toContain(peekClass); + expect(findSidebar().classes()).not.toContain(peekHintClass); + }, + ); }); describe('nav container', () => { diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js index 4028d91c82f..909f4249e28 100644 --- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js +++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js @@ -42,24 +42,28 @@ describe('Super Sidebar Collapsed State Manager', () => { describe('toggleSuperSidebarCollapsed', () => { it.each` - collapsed | saveCookie | windowWidth | hasClass - ${true} | ${true} | ${xl} | ${true} - ${true} | ${false} | ${xl} | ${true} - ${true} | ${true} | ${sm} | ${true} - ${true} | ${false} | ${sm} | ${true} - ${false} | ${true} | ${xl} | ${false} - ${false} | ${false} | ${xl} | ${false} - ${false} | ${true} | ${sm} | ${false} - ${false} | ${false} | ${sm} | ${false} + collapsed | saveCookie | windowWidth | hasClass | superSidebarPeek | isPeekable + ${true} | ${true} | ${xl} | ${true} | ${false} | ${false} + ${true} | ${true} | ${xl} | ${true} | ${true} | ${true} + ${true} | ${false} | ${xl} | ${true} | ${false} | ${false} + ${true} | ${true} | ${sm} | ${true} | ${false} | ${false} + ${true} | ${false} | ${sm} | ${true} | ${false} | ${false} + ${false} | ${true} | ${xl} | ${false} | ${false} | ${false} + ${false} | ${true} | ${xl} | ${false} | ${true} | ${false} + ${false} | ${false} | ${xl} | ${false} | ${false} | ${false} + ${false} | ${true} | ${sm} | ${false} | ${false} | ${false} + ${false} | ${false} | ${sm} | ${false} | ${false} | ${false} `( 'when collapsed is $collapsed, saveCookie is $saveCookie, and windowWidth is $windowWidth then page class contains `page-with-super-sidebar-collapsed` is $hasClass', - ({ collapsed, saveCookie, windowWidth, hasClass }) => { + ({ collapsed, saveCookie, windowWidth, hasClass, superSidebarPeek, isPeekable }) => { jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth); + gon.features = { superSidebarPeek }; toggleSuperSidebarCollapsed(collapsed, saveCookie); pageHasCollapsedClass(hasClass); expect(sidebarState.isCollapsed).toBe(collapsed); + expect(sidebarState.isPeekable).toBe(isPeekable); if (saveCookie && windowWidth >= xl) { expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, collapsed, { diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index dec2327db0f..d32e148ef79 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -62,10 +62,19 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }, }); }; + + const ContentEditorStub = stubComponent(ContentEditor); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); const findTextarea = () => wrapper.find('textarea'); const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - const findContentEditor = () => wrapper.findComponent(ContentEditor); + const findContentEditor = () => { + const result = wrapper.findComponent(ContentEditor); + + // In Vue.js 3 there are nuances stubbing component with custom stub on mount + // So we try to search for stub also + return result.exists() ? result : wrapper.findComponent(ContentEditorStub); + }; const enableContentEditor = async () => { findMarkdownField().vm.$emit('enableContentEditor'); @@ -185,7 +194,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it('autosizes the textarea when the value changes', async () => { buildWrapper(); await findTextarea().setValue('Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines'); - + await nextTick(); expect(Autosize.update).toHaveBeenCalled(); }); @@ -276,7 +285,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it(`emits ${EDITING_MODE_MARKDOWN_FIELD} event when enableMarkdownEditor emitted from content editor`, async () => { buildWrapper({ - stubs: { ContentEditor: stubComponent(ContentEditor) }, + stubs: { ContentEditor: ContentEditorStub }, }); await enableContentEditor(); @@ -383,7 +392,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { beforeEach(() => { buildWrapper({ propsData: { autofocus: true }, - stubs: { ContentEditor: stubComponent(ContentEditor) }, + stubs: { ContentEditor: ContentEditorStub }, }); }); diff --git a/spec/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers_spec.rb index eb67e81f677..f7d11184ac7 100644 --- a/spec/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::WraparoundVacuumHelpers, feat context 'with wraparound vacuuum running' do before do - swapout_view_for_table(:pg_stat_activity, connection: migration.connection) + swapout_view_for_table(:pg_stat_activity, connection: migration.connection, schema: 'pg_temp') migration.connection.execute(<<~SQL.squish) INSERT INTO pg_stat_activity ( @@ -44,7 +44,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::WraparoundVacuumHelpers, feat state_change, wait_event_type, wait_event, state, backend_xmin, query, backend_type) VALUES ( - 16401, 'gitlabhq_dblab', 178, '2023-03-30 08:10:50.851322+00', + 16401, current_database(), 178, '2023-03-30 08:10:50.851322+00', '2023-03-30 08:10:50.890485+00', now() - '150 minutes'::interval, '2023-03-30 08:10:50.890485+00', 'IO', 'DataFileRead', 'active','3214790381'::xid, 'autovacuum: VACUUM public.ci_builds (to prevent wraparound)', 'autovacuum worker') @@ -58,8 +58,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers::WraparoundVacuumHelpers, feat it { expect { subject }.to output(/autovacuum: VACUUM public.ci_builds \(to prevent wraparound\)/).to_stdout } it { expect { subject }.to output(/Current duration: 2 hours, 30 minutes/).to_stdout } - it { expect { subject }.to output(/Process id: 178/).to_stdout } - it { expect { subject }.to output(/`select pg_cancel_backend\(178\);`/).to_stdout } context 'when GITLAB_MIGRATIONS_DISABLE_WRAPAROUND_CHECK is set' do before do diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index 82ff8a26199..30770a47b84 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -8,14 +8,6 @@ RSpec.describe Gitlab::Redis::Cache do include_examples "redis_shared_examples" - describe '#raw_config_hash' do - it 'has a legacy default URL' do - expect(subject).to receive(:fetch_config) { false } - - expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380' ) - end - end - describe '.active_support_config' do it 'has a default ttl of 8 hours' do expect(described_class.active_support_config[:expires_in]).to eq(8.hours) diff --git a/spec/lib/gitlab/redis/db_load_balancing_spec.rb b/spec/lib/gitlab/redis/db_load_balancing_spec.rb index d633413ddec..d3d3ced62a9 100644 --- a/spec/lib/gitlab/redis/db_load_balancing_spec.rb +++ b/spec/lib/gitlab/redis/db_load_balancing_spec.rb @@ -41,12 +41,4 @@ RSpec.describe Gitlab::Redis::DbLoadBalancing, feature_category: :scalability do it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_db_load_balancing, :use_primary_store_as_default_for_db_load_balancing end - - describe '#raw_config_hash' do - it 'has a legacy default URL' do - expect(subject).to receive(:fetch_config).and_return(false) - - expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6382') - end - end end diff --git a/spec/lib/gitlab/redis/queues_spec.rb b/spec/lib/gitlab/redis/queues_spec.rb index a0f73a654e7..ec324f86cab 100644 --- a/spec/lib/gitlab/redis/queues_spec.rb +++ b/spec/lib/gitlab/redis/queues_spec.rb @@ -13,14 +13,6 @@ RSpec.describe Gitlab::Redis::Queues do expect(subject).to receive(:fetch_config) { config } end - context 'when the config url is blank' do - let(:config) { nil } - - it 'has a legacy default URL' do - expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6381' ) - end - end - context 'when the config url is present' do let(:config) { { url: 'redis://localhost:1111' } } diff --git a/spec/lib/gitlab/redis/repository_cache_spec.rb b/spec/lib/gitlab/redis/repository_cache_spec.rb index 8cdc4580f9e..bc48ee208c1 100644 --- a/spec/lib/gitlab/redis/repository_cache_spec.rb +++ b/spec/lib/gitlab/redis/repository_cache_spec.rb @@ -5,14 +5,6 @@ require 'spec_helper' RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do include_examples "redis_new_instance_shared_examples", 'repository_cache', Gitlab::Redis::Cache - describe '#raw_config_hash' do - it 'has a legacy default URL' do - expect(subject).to receive(:fetch_config).and_return(false) - - expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380') - end - end - describe '.cache_store' do it 'has a default ttl of 8 hours' do expect(described_class.cache_store.options[:expires_in]).to eq(8.hours) diff --git a/spec/lib/gitlab/redis/shared_state_spec.rb b/spec/lib/gitlab/redis/shared_state_spec.rb index d240abfbf5b..76b60440b2c 100644 --- a/spec/lib/gitlab/redis/shared_state_spec.rb +++ b/spec/lib/gitlab/redis/shared_state_spec.rb @@ -7,12 +7,4 @@ RSpec.describe Gitlab::Redis::SharedState do let(:environment_config_file_name) { "GITLAB_REDIS_SHARED_STATE_CONFIG_FILE" } include_examples "redis_shared_examples" - - describe '#raw_config_hash' do - it 'has a legacy default URL' do - expect(subject).to receive(:fetch_config) { false } - - expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6382' ) - end - end end diff --git a/spec/lib/gitlab/redis/sidekiq_status_spec.rb b/spec/lib/gitlab/redis/sidekiq_status_spec.rb index bbfec13e6c8..cbd2c05ce23 100644 --- a/spec/lib/gitlab/redis/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/redis/sidekiq_status_spec.rb @@ -49,14 +49,6 @@ RSpec.describe Gitlab::Redis::SidekiqStatus do :use_primary_store_as_default_for_sidekiq_status end - describe '#raw_config_hash' do - it 'has a legacy default URL' do - expect(subject).to receive(:fetch_config) { false } - - expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6382') - end - end - describe '#store_name' do it 'returns the name of the SharedState store' do expect(described_class.store_name).to eq('SharedState') diff --git a/spec/support/helpers/database/database_helpers.rb b/spec/support/helpers/database/database_helpers.rb index a7f6b4e5cc2..ff694bcd15b 100644 --- a/spec/support/helpers/database/database_helpers.rb +++ b/spec/support/helpers/database/database_helpers.rb @@ -4,11 +4,13 @@ module Database module DatabaseHelpers # In order to directly work with views using factories, # we can swapout the view for a table of identical structure. - def swapout_view_for_table(view, connection:) + def swapout_view_for_table(view, connection:, schema: nil) + table_name = [schema, "_test_#{view}_copy"].compact.join('.') + connection.execute(<<~SQL.squish) - CREATE TABLE _test_#{view}_copy (LIKE #{view}); + CREATE TABLE #{table_name} (LIKE #{view}); DROP VIEW #{view}; - ALTER TABLE _test_#{view}_copy RENAME TO #{view}; + ALTER TABLE #{table_name} RENAME TO #{view}; SQL end diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb index 34d8ba5c30d..7cd41390bf4 100644 --- a/spec/support/shared_examples/redis/redis_shared_examples.rb +++ b/spec/support/shared_examples/redis/redis_shared_examples.rb @@ -411,12 +411,6 @@ RSpec.shared_examples "redis_shared_examples" do end end - it 'has a value for the legacy default URL' do - allow(subject).to receive(:fetch_config).and_return(nil) - - expect(subject.send(:raw_config_hash)).to include(url: a_string_matching(%r{\Aredis://localhost:638[012]\Z})) - end - context 'when redis.yml exists' do subject { described_class.new('test').send(:fetch_config) } |