Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bd979acf95
commit
ceb5cdd5c3
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform 100ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
.super-sidebar-hover-area {
|
||||
z-index: $super-sidebar-z-index;
|
||||
.super-sidebar-peek-hint {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform $super-sidebar-transition-hint-duration ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- add_page_specific_style 'page_bundles/notifications'
|
||||
- page_title _('Notifications')
|
||||
- @force_desktop_expanded_sidebar = true
|
||||
|
||||
%div
|
||||
- if @user.errors.any?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' } }
|
||||
%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
|
||||
= 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')
|
||||
.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')
|
||||
.js-branch-divergence-graph
|
||||
|
||||
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], class: 'gl-vertical-align-top'
|
||||
.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 can?(current_user, :push_code, @project)
|
||||
= render 'projects/branches/delete_branch_modal_button', project: @project, branch: branch, merged: merged
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ module Gitlab
|
|||
super.merge(
|
||||
runner_id: runner&.id,
|
||||
runner_environment: runner_environment,
|
||||
sha: build.pipeline.sha
|
||||
sha: pipeline.sha
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
return { url: '' } if config_data.nil?
|
||||
return { url: config_data } if config_data.is_a?(String)
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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';
|
||||
describe('peek behavior', () => {
|
||||
it(`initially makes sidebar inert and peekable (${STATE_CLOSED})`, () => {
|
||||
createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
|
||||
|
||||
it('updates inert attribute and peek class', async () => {
|
||||
createWrapper({
|
||||
provide: { glFeatures: { superSidebarPeek: true } },
|
||||
sidebarState: { isCollapsed: 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().attributes('inert')).toBe('inert');
|
||||
expect(findSidebar().classes()).not.toContain(peekHintClass);
|
||||
expect(findSidebar().classes()).not.toContain(peekClass);
|
||||
expect(findSidebar().attributes('inert')).toBe('inert');
|
||||
|
||||
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);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('eventually closes the sidebar if cursor never enters sidebar', async () => {
|
||||
createWrapper({
|
||||
provide: { glFeatures: { superSidebarPeek: true } },
|
||||
sidebarState: { isCollapsed: true },
|
||||
});
|
||||
it(`makes sidebar inert and shows peek hint when peek state is ${STATE_WILL_OPEN}`, async () => {
|
||||
createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
|
||||
|
||||
findHoverArea().trigger('mouseenter');
|
||||
|
||||
jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY);
|
||||
findPeekBehavior().vm.$emit('change', STATE_WILL_OPEN);
|
||||
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().classes()).toContain(peekHintClass);
|
||||
expect(findSidebar().classes()).not.toContain(peekClass);
|
||||
});
|
||||
|
||||
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 } });
|
||||
|
||||
findPeekBehavior().vm.$emit('change', state);
|
||||
await nextTick();
|
||||
|
||||
expect(findSidebar().attributes('inert')).toBe(undefined);
|
||||
expect(findSidebar().classes()).toContain(peekClass);
|
||||
expect(findSidebar().classes()).not.toContain(peekHintClass);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('nav container', () => {
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' } }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue