Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-04 21:09:14 +00:00
parent bd979acf95
commit ceb5cdd5c3
69 changed files with 929 additions and 462 deletions

View File

@ -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'

View File

@ -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,

View File

@ -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 });
}
});
},

View File

@ -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 };
}

View File

@ -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 = [

View File

@ -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 });
}
});
},

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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({

View File

@ -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);

View File

@ -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);

View File

@ -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"

View File

@ -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) {

View File

@ -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;
}

View File

@ -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) }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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) }

View File

@ -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

View File

@ -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' },

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,6 @@
- add_page_specific_style 'page_bundles/notifications'
- page_title _('Notifications')
- @force_desktop_expanded_sidebar = true
%div
- if @user.errors.any?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|Cant 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|Cant 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

View File

@ -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

View File

@ -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') }

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -45,7 +45,7 @@ module Gitlab
super.merge(
runner_id: runner&.id,
runner_environment: runner_environment,
sha: build.pipeline.sha
sha: pipeline.sha
)
end

View File

@ -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

View File

@ -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

View File

@ -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();
}
}

View File

@ -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 () => {

View File

@ -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']);
});
});

View File

@ -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 () => {

View File

@ -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);
});
});
});
});

View File

@ -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]);
});
});

View File

@ -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', () => {

View File

@ -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, {

View File

@ -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 },
});
});

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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' } }

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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) }