summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml10
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_provider.vue5
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue21
-rw-r--r--app/assets/javascripts/lib/utils/css_utils.js25
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/constants.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue21
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue39
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue122
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue90
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js3
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js2
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue2
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss27
-rw-r--r--app/assets/stylesheets/page_bundles/branches.scss4
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb34
-rw-r--r--app/models/merge_request.rb50
-rw-r--r--app/models/merge_request_diff.rb10
-rw-r--r--app/models/merge_requests_closing_issues.rb9
-rw-r--r--app/models/ml/candidate_metadata.rb6
-rw-r--r--app/models/ml/experiment_metadata.rb6
-rw-r--r--app/models/namespace.rb16
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb8
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/note_diff_file.rb10
-rw-r--r--app/views/profiles/accounts/show.html.haml1
-rw-r--r--app/views/profiles/active_sessions/index.html.haml1
-rw-r--r--app/views/profiles/audit_log.html.haml1
-rw-r--r--app/views/profiles/chat_names/index.html.haml1
-rw-r--r--app/views/profiles/emails/index.html.haml1
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml1
-rw-r--r--app/views/profiles/keys/index.html.haml1
-rw-r--r--app/views/profiles/notifications/show.html.haml1
-rw-r--r--app/views/profiles/passwords/edit.html.haml1
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml1
-rw-r--r--app/views/profiles/preferences/show.html.haml1
-rw-r--r--app/views/profiles/show.html.haml1
-rw-r--r--app/views/projects/branches/_branch.html.haml91
-rw-r--r--app/views/projects/branches/_panel.html.haml7
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/shared/doorkeeper/applications/_index.html.haml2
-rw-r--r--config/README.md7
-rw-r--r--config/feature_flags/development/frozen_outbound_job_token_scopes.yml2
-rw-r--r--doc/ci/runners/runners_scope.md6
-rw-r--r--doc/development/documentation/topic_types/tutorial.md4
-rw-r--r--doc/development/migration_style_guide.md21
-rw-r--r--lib/gitlab/ci/jwt.rb31
-rw-r--r--lib/gitlab/ci/jwt_v2.rb2
-rw-r--r--lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers.rb11
-rw-r--r--lib/gitlab/redis/wrapper.rb31
-rw-r--r--spec/frontend/__helpers__/assert_props.js33
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js33
-rw-r--r--spec/frontend/lib/utils/css_utils_spec.js22
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js21
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js64
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js207
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js135
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js17
-rw-r--r--spec/lib/gitlab/database/migration_helpers/wraparound_vacuum_helpers_spec.rb6
-rw-r--r--spec/lib/gitlab/redis/cache_spec.rb8
-rw-r--r--spec/lib/gitlab/redis/db_load_balancing_spec.rb8
-rw-r--r--spec/lib/gitlab/redis/queues_spec.rb8
-rw-r--r--spec/lib/gitlab/redis/repository_cache_spec.rb8
-rw-r--r--spec/lib/gitlab/redis/shared_state_spec.rb8
-rw-r--r--spec/lib/gitlab/redis/sidekiq_status_spec.rb8
-rw-r--r--spec/support/helpers/database/database_helpers.rb8
-rw-r--r--spec/support/shared_examples/redis/redis_shared_examples.rb6
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) }