Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
115190b6cd
commit
48f93eadd0
|
@ -19,7 +19,6 @@ Database/MultipleDatabases:
|
|||
- lib/gitlab/database/migrations/observers/query_log.rb
|
||||
- lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
|
||||
- lib/gitlab/database.rb
|
||||
- lib/gitlab/database/with_lock_retries.rb
|
||||
- lib/gitlab/gitlab_import/importer.rb
|
||||
- lib/gitlab/health_checks/db_check.rb
|
||||
- lib/gitlab/import_export/base/relation_factory.rb
|
||||
|
|
|
@ -1 +1 @@
|
|||
4b380e760d37508a2dc3c8e6a8fe1cfaae846916
|
||||
b7f0c0462a8f689c8ee9e654f0875157b238158b
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
|
||||
import { GlBadge, GlButton, GlCollapse, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
|
||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
@ -10,6 +11,8 @@ export default {
|
|||
ClipboardButton,
|
||||
DeploymentStatusBadge,
|
||||
GlBadge,
|
||||
GlButton,
|
||||
GlCollapse,
|
||||
GlIcon,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
|
@ -27,6 +30,9 @@ export default {
|
|||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { visible: false };
|
||||
},
|
||||
computed: {
|
||||
status() {
|
||||
return this.deployment?.status;
|
||||
|
@ -40,43 +46,103 @@ export default {
|
|||
createdAt() {
|
||||
return this.deployment?.createdAt;
|
||||
},
|
||||
isMobile() {
|
||||
return !GlBreakpointInstance.isDesktop();
|
||||
},
|
||||
detailsButton() {
|
||||
return this.visible
|
||||
? { text: this.$options.i18n.hideDetails, icon: 'expand-up' }
|
||||
: { text: this.$options.i18n.showDetails, icon: 'expand-down' };
|
||||
},
|
||||
detailsButtonClasses() {
|
||||
return this.isMobile ? 'gl-sr-only' : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
latestBadge: s__('Deployment|Latest Deployed'),
|
||||
deploymentId: s__('Deployment|Deployment ID'),
|
||||
copyButton: __('Copy commit SHA'),
|
||||
commitSha: __('Commit SHA'),
|
||||
showDetails: __('Show details'),
|
||||
hideDetails: __('Hide details'),
|
||||
},
|
||||
headerClasses: [
|
||||
'gl-display-flex',
|
||||
'gl-align-items-flex-start',
|
||||
'gl-md-align-items-center',
|
||||
'gl-justify-content-space-between',
|
||||
'gl-pr-6',
|
||||
],
|
||||
headerDetailsClasses: [
|
||||
'gl-display-flex',
|
||||
'gl-flex-direction-column',
|
||||
'gl-md-flex-direction-row',
|
||||
'gl-align-items-flex-start',
|
||||
'gl-md-align-items-center',
|
||||
'gl-font-sm',
|
||||
'gl-text-gray-700',
|
||||
],
|
||||
deploymentStatusClasses: [
|
||||
'gl-display-flex',
|
||||
'gl-gap-x-3',
|
||||
'gl-mr-0',
|
||||
'gl-md-mr-5',
|
||||
'gl-mb-3',
|
||||
'gl-md-mb-0',
|
||||
],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center gl-gap-x-3 gl-font-sm gl-text-gray-700">
|
||||
<deployment-status-badge v-if="status" :status="status" />
|
||||
<gl-badge v-if="latest" variant="info">{{ $options.i18n.latestBadge }}</gl-badge>
|
||||
<div
|
||||
v-if="iid"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.deploymentId"
|
||||
:aria-label="$options.i18n.deploymentId"
|
||||
>
|
||||
<gl-icon ref="deployment-iid-icon" name="deployments" /> #{{ iid }}
|
||||
</div>
|
||||
<div
|
||||
v-if="shortSha"
|
||||
data-testid="deployment-commit-sha"
|
||||
class="gl-font-monospace gl-display-flex gl-align-items-center"
|
||||
>
|
||||
<gl-icon ref="deployment-commit-icon" name="commit" class="gl-mr-2" />
|
||||
<span v-gl-tooltip :title="$options.i18n.commitSha">{{ shortSha }}</span>
|
||||
<clipboard-button
|
||||
:text="shortSha"
|
||||
<div>
|
||||
<div :class="$options.headerClasses">
|
||||
<div :class="$options.headerDetailsClasses">
|
||||
<div :class="$options.deploymentStatusClasses">
|
||||
<deployment-status-badge v-if="status" :status="status" />
|
||||
<gl-badge v-if="latest" variant="info">{{ $options.i18n.latestBadge }}</gl-badge>
|
||||
</div>
|
||||
<div class="gl-display-flex gl-align-items-center gl-gap-x-5">
|
||||
<div
|
||||
v-if="iid"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.deploymentId"
|
||||
:aria-label="$options.i18n.deploymentId"
|
||||
>
|
||||
<gl-icon ref="deployment-iid-icon" name="deployments" /> #{{ iid }}
|
||||
</div>
|
||||
<div
|
||||
v-if="shortSha"
|
||||
data-testid="deployment-commit-sha"
|
||||
class="gl-font-monospace gl-display-flex gl-align-items-center"
|
||||
>
|
||||
<gl-icon ref="deployment-commit-icon" name="commit" class="gl-mr-2" />
|
||||
<span v-gl-tooltip :title="$options.i18n.commitSha">{{ shortSha }}</span>
|
||||
<clipboard-button
|
||||
:text="shortSha"
|
||||
category="tertiary"
|
||||
:title="$options.i18n.copyButton"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<time-ago-tooltip v-if="createdAt" :time="createdAt">
|
||||
<template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template>
|
||||
</time-ago-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<gl-button
|
||||
ref="details-toggle"
|
||||
category="tertiary"
|
||||
:title="$options.i18n.copyButton"
|
||||
size="small"
|
||||
/>
|
||||
<time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-ml-5!">
|
||||
<template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template>
|
||||
</time-ago-tooltip>
|
||||
:icon="detailsButton.icon"
|
||||
:button-text-classes="detailsButtonClasses"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
{{ detailsButton.text }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-collapse :visible="visible" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { createAlert } from '~/flash';
|
|||
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import RunnerEditButton from '../components/runner_edit_button.vue';
|
||||
import RunnerPauseButton from '../components/runner_pause_button.vue';
|
||||
import RunnerHeader from '../components/runner_header.vue';
|
||||
import RunnerDetails from '../components/runner_details.vue';
|
||||
import { I18N_FETCH_ERROR } from '../constants';
|
||||
|
@ -14,6 +15,7 @@ export default {
|
|||
name: 'AdminRunnerShowApp',
|
||||
components: {
|
||||
RunnerEditButton,
|
||||
RunnerPauseButton,
|
||||
RunnerHeader,
|
||||
RunnerDetails,
|
||||
},
|
||||
|
@ -66,6 +68,7 @@ export default {
|
|||
<runner-header v-if="runner" :runner="runner">
|
||||
<template #actions>
|
||||
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
|
||||
<runner-pause-button v-if="canUpdate" :runner="runner" />
|
||||
</template>
|
||||
</runner-header>
|
||||
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<script>
|
||||
import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { createAlert } from '~/flash';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import RunnerEditButton from '../runner_edit_button.vue';
|
||||
import RunnerPauseButton from '../runner_pause_button.vue';
|
||||
import RunnerDeleteModal from '../runner_delete_modal.vue';
|
||||
|
||||
const I18N_PAUSE = __('Pause');
|
||||
const I18N_RESUME = __('Resume');
|
||||
const I18N_DELETE = s__('Runners|Delete runner');
|
||||
const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
|
||||
|
||||
|
@ -20,6 +18,7 @@ export default {
|
|||
GlButton,
|
||||
GlButtonGroup,
|
||||
RunnerEditButton,
|
||||
RunnerPauseButton,
|
||||
RunnerDeleteModal,
|
||||
},
|
||||
directives: {
|
||||
|
@ -39,20 +38,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isActive() {
|
||||
return this.runner.active;
|
||||
},
|
||||
toggleActiveIcon() {
|
||||
return this.isActive ? 'pause' : 'play';
|
||||
},
|
||||
toggleActiveTitle() {
|
||||
if (this.updating) {
|
||||
// Prevent a "sticky" tooltip: If this button is disabled,
|
||||
// mouseout listeners don't run leaving the tooltip stuck
|
||||
return '';
|
||||
}
|
||||
return this.isActive ? I18N_PAUSE : I18N_RESUME;
|
||||
},
|
||||
deleteTitle() {
|
||||
if (this.deleting) {
|
||||
// Prevent a "sticky" tooltip: If this button is disabled,
|
||||
|
@ -78,35 +63,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
async onToggleActive() {
|
||||
this.updating = true;
|
||||
try {
|
||||
const toggledActive = !this.runner.active;
|
||||
|
||||
const {
|
||||
data: {
|
||||
runnerUpdate: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: runnerActionsUpdateMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.runner.id,
|
||||
active: toggledActive,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (errors && errors.length) {
|
||||
throw new Error(errors.join(' '));
|
||||
}
|
||||
} catch (e) {
|
||||
this.onError(e);
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
},
|
||||
|
||||
async onDelete() {
|
||||
// Deleting stays "true" until this row is removed,
|
||||
// should only change back if the operation fails.
|
||||
|
@ -162,15 +118,7 @@ export default {
|
|||
See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
|
||||
-->
|
||||
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
v-gl-tooltip.hover.viewport="toggleActiveTitle"
|
||||
:aria-label="toggleActiveTitle"
|
||||
:icon="toggleActiveIcon"
|
||||
:loading="updating"
|
||||
data-testid="toggle-active-runner"
|
||||
@click="onToggleActive"
|
||||
/>
|
||||
<runner-pause-button v-if="canUpdate" :runner="runner" :compact="true" />
|
||||
<gl-button
|
||||
v-if="canDelete"
|
||||
v-gl-tooltip.hover.viewport="deleteTitle"
|
||||
|
|
|
@ -63,6 +63,6 @@ export default {
|
|||
<strong>{{ heading }}</strong>
|
||||
</template>
|
||||
</div>
|
||||
<div class="gl-ml-auto"><slot name="actions"></slot></div>
|
||||
<div class="gl-ml-auto gl-flex-shrink-0"><slot name="actions"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import runnerToggleActiveMutation from '~/runner/graphql/runner_toggle_active.mutation.graphql';
|
||||
import { createAlert } from '~/flash';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { I18N_PAUSE, I18N_RESUME } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'RunnerPauseButton',
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updating: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isActive() {
|
||||
return this.runner.active;
|
||||
},
|
||||
icon() {
|
||||
return this.isActive ? 'pause' : 'play';
|
||||
},
|
||||
label() {
|
||||
return this.isActive ? I18N_PAUSE : I18N_RESUME;
|
||||
},
|
||||
buttonContent() {
|
||||
if (this.compact) {
|
||||
return null;
|
||||
}
|
||||
return this.label;
|
||||
},
|
||||
ariaLabel() {
|
||||
if (this.compact) {
|
||||
return this.label;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
tooltip() {
|
||||
// Only show tooltip when compact.
|
||||
// Also prevent a "sticky" tooltip: If this button is
|
||||
// disabled, mouseout listeners don't run leaving the tooltip stuck
|
||||
if (this.compact && !this.updating) {
|
||||
return this.label;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onToggle() {
|
||||
this.updating = true;
|
||||
try {
|
||||
const input = {
|
||||
id: this.runner.id,
|
||||
active: !this.isActive,
|
||||
};
|
||||
|
||||
const {
|
||||
data: {
|
||||
runnerUpdate: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: runnerToggleActiveMutation,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
|
||||
if (errors && errors.length) {
|
||||
throw new Error(errors.join(' '));
|
||||
}
|
||||
} catch (e) {
|
||||
this.onError(e);
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
const { message } = error;
|
||||
createAlert({ message });
|
||||
|
||||
this.reportToSentry(error);
|
||||
},
|
||||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button
|
||||
v-gl-tooltip.hover.viewport="tooltip"
|
||||
v-bind="$attrs"
|
||||
:aria-label="ariaLabel"
|
||||
:icon="icon"
|
||||
:loading="updating"
|
||||
@click="onToggle"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<!--
|
||||
Use <template v-if> to ensure a square button is shown when compact: true.
|
||||
Sending empty content will still show a distorted/rectangular button.
|
||||
-->
|
||||
<template v-if="buttonContent">{{ buttonContent }}</template>
|
||||
</gl-button>
|
||||
</template>
|
|
@ -1,4 +1,4 @@
|
|||
import { s__ } from '~/locale';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
export const RUNNER_PAGE_SIZE = 20;
|
||||
export const RUNNER_JOB_COUNT_LIMIT = 1000;
|
||||
|
@ -28,6 +28,10 @@ export const I18N_STALE_RUNNER_DESCRIPTION = s__(
|
|||
'Runners|No contact from this runner in over 3 months',
|
||||
);
|
||||
|
||||
// Active flag
|
||||
export const I18N_PAUSE = __('Pause');
|
||||
export const I18N_RESUME = __('Resume');
|
||||
|
||||
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
|
||||
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
#import "~/runner/graphql/runner_node.fragment.graphql"
|
||||
|
||||
# Mutation for updates within the runners list via action
|
||||
# buttons (play, pause, ...), loads attributes shown in the
|
||||
# runner list.
|
||||
|
||||
mutation runnerActionsUpdate($input: RunnerUpdateInput!) {
|
||||
runnerUpdate(input: $input) {
|
||||
runner {
|
||||
...RunnerNode
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# Mutation executed for the pause/resume button in the
|
||||
# runner list and details views.
|
||||
|
||||
mutation runnerToggleActive($input: RunnerUpdateInput!) {
|
||||
runnerUpdate(input: $input) {
|
||||
runner {
|
||||
id
|
||||
active
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -50,7 +50,7 @@ export default {
|
|||
TrainingProviderList,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: ['projectPath'],
|
||||
inject: ['projectFullPath'],
|
||||
props: {
|
||||
augmentedSecurityFeatures: {
|
||||
type: Array,
|
||||
|
@ -107,14 +107,14 @@ export default {
|
|||
shouldShowAutoDevopsEnabledAlert() {
|
||||
return (
|
||||
this.autoDevopsEnabled &&
|
||||
!this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectPath)
|
||||
!this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectFullPath)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dismissAutoDevopsEnabledAlert() {
|
||||
const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
|
||||
dismissedProjects.add(this.projectPath);
|
||||
dismissedProjects.add(this.projectFullPath);
|
||||
this.autoDevopsEnabledAlertDismissedProjects = Array.from(dismissedProjects);
|
||||
},
|
||||
onError(message) {
|
||||
|
|
|
@ -21,10 +21,18 @@ export default {
|
|||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
},
|
||||
inject: ['projectPath'],
|
||||
inject: ['projectFullPath'],
|
||||
apollo: {
|
||||
securityTrainingProviders: {
|
||||
query: securityTrainingProvidersQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectFullPath,
|
||||
};
|
||||
},
|
||||
update({ project }) {
|
||||
return project?.securityTrainingProviders;
|
||||
},
|
||||
error() {
|
||||
this.errorMessage = this.$options.i18n.providerQueryErrorMessage;
|
||||
},
|
||||
|
@ -68,7 +76,7 @@ export default {
|
|||
variables: {
|
||||
input: {
|
||||
enabledProviders: enabledProviderIds,
|
||||
fullPath: this.projectPath,
|
||||
fullPath: this.projectFullPath,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
query Query {
|
||||
securityTrainingProviders @client {
|
||||
name
|
||||
query getSecurityTrainingProviders($fullPath: ID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
description
|
||||
isEnabled
|
||||
url
|
||||
securityTrainingProviders {
|
||||
name
|
||||
id
|
||||
description
|
||||
isEnabled
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export const initSecurityConfiguration = (el) => {
|
|||
});
|
||||
|
||||
const {
|
||||
projectPath,
|
||||
projectFullPath,
|
||||
upgradePath,
|
||||
features,
|
||||
latestPipelinePath,
|
||||
|
@ -38,7 +38,7 @@ export const initSecurityConfiguration = (el) => {
|
|||
el,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
projectPath,
|
||||
projectFullPath,
|
||||
upgradePath,
|
||||
autoDevopsHelpPagePath,
|
||||
autoDevopsPath,
|
||||
|
|
|
@ -128,10 +128,12 @@ export default {
|
|||
api.trackRedisHllUserEvent(this.$options.expandEvent);
|
||||
}
|
||||
}),
|
||||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
toggleCollapsed(e) {
|
||||
if (!e?.target?.closest('.btn:not(.btn-icon),a')) {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
this.triggerRedisTracking();
|
||||
this.triggerRedisTracking();
|
||||
}
|
||||
},
|
||||
initExtensionPolling() {
|
||||
const poll = new Poll({
|
||||
|
@ -207,6 +209,19 @@ export default {
|
|||
this.showFade = true;
|
||||
}
|
||||
},
|
||||
onRowMouseDown() {
|
||||
this.down = Number(new Date());
|
||||
},
|
||||
onRowMouseUp(e) {
|
||||
const up = Number(new Date());
|
||||
|
||||
// To allow for text to be selected we check if the the user is clicking
|
||||
// or selecting, if they are selecting the time difference should be
|
||||
// more than 200ms
|
||||
if (up - this.down < 200) {
|
||||
this.toggleCollapsed(e);
|
||||
}
|
||||
},
|
||||
generateText,
|
||||
},
|
||||
EXTENSION_ICON_CLASS,
|
||||
|
@ -215,7 +230,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<section class="media-section" data-testid="widget-extension">
|
||||
<div class="media gl-p-5">
|
||||
<div class="media gl-p-5 gl-cursor-pointer" @mousedown="onRowMouseDown" @mouseup="onRowMouseUp">
|
||||
<status-icon
|
||||
:name="$options.label || $options.name"
|
||||
:is-loading="isLoadingSummary"
|
||||
|
@ -253,7 +268,7 @@ export default {
|
|||
category="tertiary"
|
||||
data-testid="toggle-button"
|
||||
size="small"
|
||||
@click="toggleCollapsed"
|
||||
@click.self="toggleCollapsed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -25,9 +25,9 @@ export default {
|
|||
n__(
|
||||
'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} change',
|
||||
'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} changes',
|
||||
changesFound,
|
||||
count,
|
||||
),
|
||||
{ changesFound },
|
||||
{ changesFound: count },
|
||||
);
|
||||
},
|
||||
// Status icon to be used next to the summary text
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
components: {
|
||||
GlButton,
|
||||
},
|
||||
inject: ['projectPath'],
|
||||
inject: ['projectFullPath'],
|
||||
props: {
|
||||
feature: {
|
||||
type: Object,
|
||||
|
@ -47,7 +47,7 @@ export default {
|
|||
try {
|
||||
const { mutationSettings } = this;
|
||||
const { data } = await this.$apollo.mutate(
|
||||
mutationSettings.getMutationPayload(this.projectPath),
|
||||
mutationSettings.getMutationPayload(this.projectFullPath),
|
||||
);
|
||||
const { errors, successPath } = data[mutationSettings.mutationId];
|
||||
|
||||
|
|
|
@ -110,8 +110,13 @@ class GroupDescendantsFinder
|
|||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def ancestors_of_groups(base_for_ancestors)
|
||||
group_ids = base_for_ancestors.except(:select, :sort).select(:id)
|
||||
Gitlab::ObjectHierarchy.new(Group.where(id: group_ids))
|
||||
.base_and_ancestors(upto: parent_group.id)
|
||||
groups = Group.where(id: group_ids)
|
||||
|
||||
if Feature.enabled?(:linear_group_descendants_finder_upto, current_user, default_enabled: :yaml)
|
||||
groups.self_and_ancestors(upto: parent_group.id)
|
||||
else
|
||||
Gitlab::ObjectHierarchy.new(groups).base_and_ancestors(upto: parent_group.id)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
- page_title _("Security Configuration")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
|
||||
#js-security-configuration-static{ data: { project_path: @project.full_path, upgrade_path: security_upgrade_path } }
|
||||
#js-security-configuration-static{ data: { project_full_path: @project.full_path, upgrade_path: security_upgrade_path } }
|
||||
|
|
|
@ -32,8 +32,12 @@ module AutoDevops
|
|||
|
||||
def email_receivers_for(pipeline, project)
|
||||
recipients = [pipeline.user&.email]
|
||||
recipients << project.owner.email unless project.group
|
||||
recipients.uniq.compact
|
||||
|
||||
if project.personal?
|
||||
recipients << project.owners.map(&:email)
|
||||
end
|
||||
|
||||
recipients.flatten.uniq.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,8 +24,15 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
|
||||
|
||||
export_job&.finish
|
||||
rescue ActiveRecord::RecordNotFound, Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError => e
|
||||
logger.error("Failed to export project #{project_id}: #{e.message}")
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
log_failure(project_id, e)
|
||||
rescue Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError => e
|
||||
log_failure(project_id, e)
|
||||
export_job&.finish
|
||||
rescue StandardError => e
|
||||
log_failure(project_id, e)
|
||||
export_job&.fail_op
|
||||
raise
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -35,4 +42,8 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
|
||||
Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
|
||||
end
|
||||
|
||||
def log_failure(project_id, ex)
|
||||
logger.error("Failed to export project #{project_id}: #{ex.message}")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349977
|
|||
milestone: '14.7'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: export_reduce_relation_batch_size
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34057
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282245
|
||||
milestone: '13.1'
|
||||
name: linear_group_descendants_finder_upto
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78991
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350972
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::import
|
||||
group: group::authentication and authorization
|
||||
default_enabled: false
|
|
@ -5,7 +5,7 @@ class CreatePackagesHelmFileMetadata < ActiveRecord::Migration[6.0]
|
|||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
def up
|
||||
create_table_with_constraints :packages_helm_file_metadata, id: false do |t|
|
||||
t.timestamps_with_timezone
|
||||
t.references :package_file, primary_key: true, index: false, default: nil, null: false, foreign_key: { to_table: :packages_package_files, on_delete: :cascade }, type: :bigint
|
||||
|
@ -17,4 +17,10 @@ class CreatePackagesHelmFileMetadata < ActiveRecord::Migration[6.0]
|
|||
t.index :channel
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :packages_helm_file_metadata
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class CreateClustersIntegrationElasticstack < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def change
|
||||
def up
|
||||
create_table_with_constraints :clusters_integration_elasticstack, id: false do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.references :cluster, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
|
||||
|
@ -12,4 +12,10 @@ class CreateClustersIntegrationElasticstack < ActiveRecord::Migration[6.0]
|
|||
t.text_limit :chart_version, 10
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :clusters_integration_elasticstack
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class CreateDetachedPartitionsTable < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def change
|
||||
def up
|
||||
create_table_with_constraints :detached_partitions do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.datetime_with_timezone :drop_after, null: false
|
||||
|
@ -14,4 +14,10 @@ class CreateDetachedPartitionsTable < ActiveRecord::Migration[6.1]
|
|||
t.text_limit :table_name, 63
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :detached_partitions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class CreatePostgresAsyncIndexesTable < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def change
|
||||
def up
|
||||
create_table_with_constraints :postgres_async_indexes do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
|
||||
|
@ -18,4 +18,10 @@ class CreatePostgresAsyncIndexesTable < ActiveRecord::Migration[6.1]
|
|||
t.index :name, unique: true
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :postgres_async_indexes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class CreateTopics < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def change
|
||||
def up
|
||||
create_table_with_constraints :topics do |t|
|
||||
t.text :name, null: false
|
||||
t.text_limit :name, 255
|
||||
|
@ -13,4 +13,10 @@ class CreateTopics < ActiveRecord::Migration[6.1]
|
|||
t.timestamps_with_timezone
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
drop_table :topics
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveProjectsCiJobTokenProjectScopeLinksTargetProjectIdFk < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless foreign_key_exists?(:ci_job_token_project_scope_links, :projects, name: "fk_rails_6904b38465")
|
||||
|
||||
with_lock_retries do
|
||||
execute('LOCK projects, ci_job_token_project_scope_links IN ACCESS EXCLUSIVE MODE') if transaction_open?
|
||||
|
||||
remove_foreign_key_if_exists(:ci_job_token_project_scope_links, :projects, name: "fk_rails_6904b38465")
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:ci_job_token_project_scope_links, :projects, name: "fk_rails_6904b38465", column: :target_project_id, target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveProjectsCiBuildsProjectIdFk < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return if Gitlab.com? # unsafe migration, skip on GitLab.com due to https://gitlab.com/groups/gitlab-org/-/epics/7249#note_819625526
|
||||
return unless foreign_key_exists?(:ci_builds, :projects, name: "fk_befce0568a")
|
||||
|
||||
with_lock_retries do
|
||||
execute('LOCK projects, ci_builds IN ACCESS EXCLUSIVE MODE') if transaction_open?
|
||||
|
||||
remove_foreign_key_if_exists(:ci_builds, :projects, name: "fk_befce0568a")
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:ci_builds, :projects, name: "fk_befce0568a", column: :project_id, target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
7731772dfac065a60c1626707913ddf6ff632bb69dd5ed6534e8d29e4e03c573
|
|
@ -0,0 +1 @@
|
|||
fd7940bb6f077c91d7f9928f574443ba4bf33bb90cb702c0a2ecad14398ab1cc
|
|
@ -29753,9 +29753,6 @@ ALTER TABLE ONLY ci_sources_pipelines
|
|||
ALTER TABLE ONLY packages_maven_metadata
|
||||
ADD CONSTRAINT fk_be88aed360 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_builds
|
||||
ADD CONSTRAINT fk_befce0568a FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY design_management_versions
|
||||
ADD CONSTRAINT fk_c1440b4896 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
|
@ -30650,9 +30647,6 @@ ALTER TABLE ONLY resource_iteration_events
|
|||
ALTER TABLE ONLY geo_hashed_storage_migrated_events
|
||||
ADD CONSTRAINT fk_rails_687ed7d7c5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_job_token_project_scope_links
|
||||
ADD CONSTRAINT fk_rails_6904b38465 FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY plan_limits
|
||||
ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
|
@ -6,44 +6,25 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Trello Power-Up **(FREE)**
|
||||
|
||||
The GitLab Trello Power-Up enables you to seamlessly attach
|
||||
GitLab **merge requests** to Trello cards.
|
||||
You can use the Trello Power-Up for GitLab to attach
|
||||
GitLab merge requests to Trello cards.
|
||||
|
||||
![GitLab Trello PowerUp - Trello card](img/trello_card_with_gitlab_powerup.png)
|
||||
|
||||
## Configuring the Power-Up
|
||||
## Configure the Power-Up
|
||||
|
||||
In order to get started, you must configure your Power-Up.
|
||||
To configure a Power-Up for a Trello board:
|
||||
|
||||
In Trello:
|
||||
1. Go to your Trello board.
|
||||
1. Select **Power-Ups** and find the **GitLab** row.
|
||||
1. Select **Enable**.
|
||||
1. Select **Settings** (the gear icon).
|
||||
1. Select **Authorize Account**.
|
||||
1. Enter the [GitLab API URL](#get-the-api-url) and [personal access token](../user/profile/personal_access_tokens.md#create-a-personal-access-token) with the **API** scope.
|
||||
1. Select **Save**.
|
||||
|
||||
1. Go to your Trello board
|
||||
1. Select `Power-Ups` to see a listing of all the available Power-Ups
|
||||
1. Look for a row that says `GitLab` and select the `Enable` button
|
||||
1. Select the `Settings` (gear) icon
|
||||
1. In the popup menu, select `Authorize Account`
|
||||
## Get the API URL
|
||||
|
||||
In this popup, fill in your `API URL` and `Personal Access Token`. After that, you can attach any merge request to any Trello card on your selected Trello board.
|
||||
|
||||
## What is my API URL?
|
||||
|
||||
Your API URL should be your GitLab instance URL with `/api/v4` appended in the end of the URL.
|
||||
For example, if your GitLab instance URL is `https://gitlab.com`, your API URL would be `https://gitlab.com/api/v4`.
|
||||
If your instance's URL is `https://example.com`, your API URL is `https://example.com/api/v4`.
|
||||
|
||||
![configure GitLab Trello PowerUp in Trello](img/enable_trello_powerup.png)
|
||||
|
||||
## What is my Personal Access Token?
|
||||
|
||||
Your GitLab personal access token enables your GitLab account to be accessed
|
||||
from Trello.
|
||||
|
||||
To find it in GitLab:
|
||||
|
||||
1. In the top-right corner, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select **Access Tokens**.
|
||||
|
||||
Learn more about generating a personal access token in the
|
||||
[Personal Access Token Documentation](../user/profile/personal_access_tokens.md).
|
||||
Don't forget to check the API scope checkbox!
|
||||
Your API URL is your GitLab instance URL with `/api/v4` appended at the end of the URL.
|
||||
For example, if your GitLab instance URL is `https://gitlab.com`, your API URL is `https://gitlab.com/api/v4`.
|
||||
If your instance URL is `https://example.com`, your API URL is `https://example.com/api/v4`.
|
||||
|
|
|
@ -42,6 +42,9 @@ ci_job_token_project_scope_links:
|
|||
- table: projects
|
||||
column: source_project_id
|
||||
on_delete: async_delete
|
||||
- table: projects
|
||||
column: target_project_id
|
||||
on_delete: async_delete
|
||||
ci_daily_build_group_report_results:
|
||||
- table: namespaces
|
||||
column: group_id
|
||||
|
@ -88,6 +91,9 @@ ci_builds:
|
|||
- table: users
|
||||
column: user_id
|
||||
on_delete: async_nullify
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_pipelines:
|
||||
- table: merge_requests
|
||||
column: merge_request_id
|
||||
|
|
|
@ -429,6 +429,7 @@ module Gitlab
|
|||
def with_lock_retries(*args, **kwargs, &block)
|
||||
raise_on_exhaustion = !!kwargs.delete(:raise_on_exhaustion)
|
||||
merged_args = {
|
||||
connection: connection,
|
||||
klass: self.class,
|
||||
logger: Gitlab::BackgroundMigration::Logger,
|
||||
allow_savepoints: true
|
||||
|
|
|
@ -9,6 +9,10 @@ module Gitlab
|
|||
migration.class
|
||||
end
|
||||
|
||||
def migration_connection
|
||||
migration.connection
|
||||
end
|
||||
|
||||
def enable_lock_retries?
|
||||
# regular AR migrations don't have this,
|
||||
# only ones inheriting from Gitlab::Database::Migration have
|
||||
|
@ -24,6 +28,7 @@ module Gitlab
|
|||
def ddl_transaction(migration, &block)
|
||||
if use_transaction?(migration) && migration.enable_lock_retries?
|
||||
Gitlab::Database::WithLockRetries.new(
|
||||
connection: migration.migration_connection,
|
||||
klass: migration.migration_class,
|
||||
logger: Gitlab::BackgroundMigration::Logger
|
||||
).run(raise_on_exhaustion: false, &block)
|
||||
|
|
|
@ -73,6 +73,7 @@ module Gitlab
|
|||
|
||||
def with_lock_retries(&block)
|
||||
Gitlab::Database::WithLockRetries.new(
|
||||
connection: connection,
|
||||
klass: self.class,
|
||||
logger: Gitlab::BackgroundMigration::Logger
|
||||
).run(&block)
|
||||
|
|
|
@ -61,7 +61,7 @@ module Gitlab
|
|||
[10.seconds, 10.minutes]
|
||||
].freeze
|
||||
|
||||
def initialize(logger: NULL_LOGGER, allow_savepoints: true, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV, connection: ActiveRecord::Base.connection)
|
||||
def initialize(logger: NULL_LOGGER, allow_savepoints: true, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV, connection:)
|
||||
@logger = logger
|
||||
@klass = klass
|
||||
@allow_savepoints = allow_savepoints
|
||||
|
|
|
@ -6,15 +6,10 @@ module Gitlab
|
|||
class StreamingSerializer
|
||||
include Gitlab::ImportExport::CommandLineUtil
|
||||
|
||||
BATCH_SIZE = 100
|
||||
SMALLER_BATCH_SIZE = 2
|
||||
BATCH_SIZE = 2
|
||||
|
||||
def self.batch_size(exportable)
|
||||
if Feature.enabled?(:export_reduce_relation_batch_size, exportable)
|
||||
SMALLER_BATCH_SIZE
|
||||
else
|
||||
BATCH_SIZE
|
||||
end
|
||||
BATCH_SIZE
|
||||
end
|
||||
|
||||
class Raw < String
|
||||
|
|
|
@ -27983,10 +27983,13 @@ msgstr ""
|
|||
msgid "ProjectSettings|Checkbox is visible and unselected by default."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Choose the method, options, checks, and squash options for merge requests. You can also set up merge request templates for different actions."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, and merge suggestions."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
|
||||
msgid "ProjectSettings|Choose your merge method, options, checks, and squash options."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Configure your project resources and monitor their health."
|
||||
|
@ -33292,6 +33295,9 @@ msgstr ""
|
|||
msgid "SlackIntegration|GitLab for Slack"
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|GitLab for Slack was successfully installed."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|Project alias"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33307,6 +33313,9 @@ msgstr ""
|
|||
msgid "SlackIntegration|To set up this integration press \"Add to Slack\""
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackIntegration|You can now close this window and go to your Slack workspace."
|
||||
msgstr ""
|
||||
|
||||
msgid "SlackService|1. %{slash_command_link_start}Add a slash command%{slash_command_link_end} in your Slack team using this information:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34432,6 +34441,27 @@ msgstr ""
|
|||
msgid "SubscriptionBanner|Upload new license"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|%{doc_link_start}Please reach out if you have questions%{doc_link_end}, and we'll be happy to assist."
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|Additional charges for your GitLab subscription"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|Dear %{customer_name},"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|GitLab Billing Team"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|Thank you for your business!"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|You can find more information about the quarterly reconciliation process in %{doc_link_start}our documentation%{doc_link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionEmail|You have exceeded the number of seats in your GitLab subscription %{subscription_name} by %{seat_quantity}. Even if you've exceeded the seats in your subscription, you can continue to add users, and GitLab will bill you a prorated amount for any seat overages on a quarterly basis."
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionTable|Add seats"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -165,8 +165,8 @@ RSpec.describe GroupDescendantsFinder do
|
|||
end
|
||||
|
||||
context 'with nested groups' do
|
||||
let!(:project) { create(:project, namespace: group) }
|
||||
let!(:subgroup) { create(:group, :private, parent: group) }
|
||||
let_it_be(:project) { create(:project, namespace: group) }
|
||||
let_it_be_with_reload(:subgroup) { create(:group, :private, parent: group) }
|
||||
|
||||
describe '#execute' do
|
||||
it 'contains projects and subgroups' do
|
||||
|
@ -208,57 +208,69 @@ RSpec.describe GroupDescendantsFinder do
|
|||
context 'with a filter' do
|
||||
let(:params) { { filter: 'test' } }
|
||||
|
||||
it 'contains only matching projects and subgroups' do
|
||||
matching_project = create(:project, namespace: group, name: 'Testproject')
|
||||
matching_subgroup = create(:group, name: 'testgroup', parent: group)
|
||||
shared_examples 'filter examples' do
|
||||
it 'contains only matching projects and subgroups' do
|
||||
matching_project = create(:project, namespace: group, name: 'Testproject')
|
||||
matching_subgroup = create(:group, name: 'testgroup', parent: group)
|
||||
|
||||
expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
|
||||
end
|
||||
|
||||
it 'does not include subgroups the user does not have access to' do
|
||||
_invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
|
||||
other_subgroup = create(:group, :private, parent: group, name: 'test2')
|
||||
public_subgroup = create(:group, :public, parent: group, name: 'test3')
|
||||
other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
|
||||
other_user = create(:user)
|
||||
other_subgroup.add_developer(other_user)
|
||||
|
||||
finder = described_class.new(current_user: other_user,
|
||||
parent_group: group,
|
||||
params: params)
|
||||
|
||||
expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
|
||||
end
|
||||
|
||||
context 'with matching children' do
|
||||
it 'includes a group that has a subgroup matching the query and its parent' do
|
||||
matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
|
||||
|
||||
expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
|
||||
expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
|
||||
end
|
||||
|
||||
it 'includes the parent of a matching project' do
|
||||
matching_project = create(:project, namespace: subgroup, name: 'Testproject')
|
||||
it 'does not include subgroups the user does not have access to' do
|
||||
_invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
|
||||
other_subgroup = create(:group, :private, parent: group, name: 'test2')
|
||||
public_subgroup = create(:group, :public, parent: group, name: 'test3')
|
||||
other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
|
||||
other_user = create(:user)
|
||||
other_subgroup.add_developer(other_user)
|
||||
|
||||
expect(finder.execute).to contain_exactly(subgroup, matching_project)
|
||||
finder = described_class.new(current_user: other_user,
|
||||
parent_group: group,
|
||||
params: params)
|
||||
|
||||
expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
|
||||
end
|
||||
|
||||
context 'with a small page size' do
|
||||
let(:params) { { filter: 'test', per_page: 1 } }
|
||||
context 'with matching children' do
|
||||
it 'includes a group that has a subgroup matching the query and its parent' do
|
||||
matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
|
||||
|
||||
it 'contains all the ancestors of a matching subgroup regardless the page size' do
|
||||
subgroup = create(:group, :private, parent: group)
|
||||
matching = create(:group, :private, name: 'testgroup', parent: subgroup)
|
||||
expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
|
||||
end
|
||||
|
||||
expect(finder.execute).to contain_exactly(subgroup, matching)
|
||||
it 'includes the parent of a matching project' do
|
||||
matching_project = create(:project, namespace: subgroup, name: 'Testproject')
|
||||
|
||||
expect(finder.execute).to contain_exactly(subgroup, matching_project)
|
||||
end
|
||||
|
||||
context 'with a small page size' do
|
||||
let(:params) { { filter: 'test', per_page: 1 } }
|
||||
|
||||
it 'contains all the ancestors of a matching subgroup regardless the page size' do
|
||||
subgroup = create(:group, :private, parent: group)
|
||||
matching = create(:group, :private, name: 'testgroup', parent: subgroup)
|
||||
|
||||
expect(finder.execute).to contain_exactly(subgroup, matching)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not include the parent itself' do
|
||||
group.update!(name: 'test')
|
||||
|
||||
expect(finder.execute).not_to include(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not include the parent itself' do
|
||||
group.update!(name: 'test')
|
||||
it_behaves_like 'filter examples'
|
||||
|
||||
expect(finder.execute).not_to include(group)
|
||||
context 'when feature flag :linear_group_descendants_finder_upto is disabled' do
|
||||
before do
|
||||
stub_feature_flags(linear_group_descendants_finder_upto: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'filter examples'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { GlCollapse } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { stubTransition } from 'helpers/stub_transition';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { __, s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import Deployment from '~/environments/components/deployment.vue';
|
||||
import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
|
||||
|
@ -19,6 +21,7 @@ describe('~/environments/components/deployment.vue', () => {
|
|||
deployment,
|
||||
...propsData,
|
||||
},
|
||||
stubs: { transition: stubTransition() },
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -148,4 +151,29 @@ describe('~/environments/components/deployment.vue', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapse', () => {
|
||||
let collapse;
|
||||
let button;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
collapse = wrapper.findComponent(GlCollapse);
|
||||
button = wrapper.findComponent({ ref: 'details-toggle' });
|
||||
});
|
||||
|
||||
it('is collapsed by default', () => {
|
||||
expect(collapse.attributes('visible')).toBeUndefined();
|
||||
expect(button.props('icon')).toBe('expand-down');
|
||||
expect(button.text()).toBe(__('Show details'));
|
||||
});
|
||||
|
||||
it('opens on click', async () => {
|
||||
await button.trigger('click');
|
||||
|
||||
expect(button.text()).toBe(__('Hide details'));
|
||||
expect(button.props('icon')).toBe('expand-up');
|
||||
expect(collapse.attributes('visible')).toBe('visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ import { createAlert } from '~/flash';
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import RunnerHeader from '~/runner/components/runner_header.vue';
|
||||
import RunnerDetails from '~/runner/components/runner_details.vue';
|
||||
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
|
||||
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
|
||||
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
|
||||
import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
|
@ -17,7 +19,8 @@ import { runnerData } from '../mock_data';
|
|||
jest.mock('~/flash');
|
||||
jest.mock('~/runner/sentry_utils');
|
||||
|
||||
const mockRunnerGraphqlId = runnerData.data.runner.id;
|
||||
const mockRunner = runnerData.data.runner;
|
||||
const mockRunnerGraphqlId = mockRunner.id;
|
||||
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
@ -28,6 +31,16 @@ describe('AdminRunnerShowApp', () => {
|
|||
|
||||
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
|
||||
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
|
||||
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
|
||||
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
|
||||
|
||||
const mockRunnerQueryResult = (runner = {}) => {
|
||||
mockRunnerQuery = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
runner: { ...mockRunner, ...runner },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
|
||||
wrapper = mountFn(AdminRunnerShowApp, {
|
||||
|
@ -41,10 +54,6 @@ describe('AdminRunnerShowApp', () => {
|
|||
return waitForPromises();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockRunnerQuery.mockReset();
|
||||
wrapper.destroy();
|
||||
|
@ -52,6 +61,8 @@ describe('AdminRunnerShowApp', () => {
|
|||
|
||||
describe('When showing runner details', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnerQueryResult();
|
||||
|
||||
await createComponent({ mountFn: mount });
|
||||
});
|
||||
|
||||
|
@ -63,6 +74,11 @@ describe('AdminRunnerShowApp', () => {
|
|||
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
|
||||
});
|
||||
|
||||
it('displays the runner edit and pause buttons', async () => {
|
||||
expect(findRunnerEditButton().exists()).toBe(true);
|
||||
expect(findRunnerPauseButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows basic runner details', async () => {
|
||||
const expected = `Details
|
||||
Description Instance runner
|
||||
|
@ -75,6 +91,42 @@ describe('AdminRunnerShowApp', () => {
|
|||
|
||||
expect(findRunnerDetails().text()).toMatchInterpolatedText(expected);
|
||||
});
|
||||
|
||||
describe('when runner cannot be updated', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnerQueryResult({
|
||||
userPermissions: {
|
||||
updateRunner: false,
|
||||
},
|
||||
});
|
||||
|
||||
await createComponent({
|
||||
mountFn: mount,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display the runner edit and pause buttons', () => {
|
||||
expect(findRunnerEditButton().exists()).toBe(false);
|
||||
expect(findRunnerPauseButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when runner does not have an edit url ', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnerQueryResult({
|
||||
editAdminUrl: null,
|
||||
});
|
||||
|
||||
await createComponent({
|
||||
mountFn: mount,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display the runner edit button', () => {
|
||||
expect(findRunnerEditButton().exists()).toBe(false);
|
||||
expect(findRunnerPauseButton().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When there is an error', () => {
|
||||
|
|
|
@ -9,12 +9,12 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
|
||||
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
|
||||
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
|
||||
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
|
||||
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
|
||||
import { runnersData } from '../../mock_data';
|
||||
|
||||
const mockRunner = runnersData.data.runners.nodes[0];
|
||||
|
@ -32,10 +32,9 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
const mockToastShow = jest.fn();
|
||||
const runnerDeleteMutationHandler = jest.fn();
|
||||
const runnerActionsUpdateMutationHandler = jest.fn();
|
||||
|
||||
const findEditBtn = () => wrapper.findComponent(RunnerEditButton);
|
||||
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
|
||||
const findRunnerPauseBtn = () => wrapper.findComponent(RunnerPauseButton);
|
||||
const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal);
|
||||
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
|
||||
const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value;
|
||||
|
@ -52,10 +51,7 @@ describe('RunnerTypeCell', () => {
|
|||
...runner,
|
||||
},
|
||||
},
|
||||
apolloProvider: createMockApollo([
|
||||
[runnerDeleteMutation, runnerDeleteMutationHandler],
|
||||
[runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
|
||||
]),
|
||||
apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteMutationHandler]]),
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
GlModal: createMockDirective(),
|
||||
|
@ -77,21 +73,11 @@ describe('RunnerTypeCell', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
runnerActionsUpdateMutationHandler.mockResolvedValue({
|
||||
data: {
|
||||
runnerUpdate: {
|
||||
runner: mockRunner,
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockToastShow.mockReset();
|
||||
runnerDeleteMutationHandler.mockReset();
|
||||
runnerActionsUpdateMutationHandler.mockReset();
|
||||
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -123,118 +109,14 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Toggle active action', () => {
|
||||
describe.each`
|
||||
state | label | icon | isActive | newActiveValue
|
||||
${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false}
|
||||
${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
|
||||
`('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ active: isActive });
|
||||
});
|
||||
describe('Pause action', () => {
|
||||
it('Renders a compact pause button', () => {
|
||||
createComponent();
|
||||
|
||||
it(`Displays a ${icon} button`, () => {
|
||||
expect(findToggleActiveBtn().props('loading')).toBe(false);
|
||||
expect(findToggleActiveBtn().props('icon')).toBe(icon);
|
||||
expect(getTooltip(findToggleActiveBtn())).toBe(label);
|
||||
expect(findToggleActiveBtn().attributes('aria-label')).toBe(label);
|
||||
});
|
||||
|
||||
it(`After clicking the ${icon} button, the button has a loading state`, async () => {
|
||||
await findToggleActiveBtn().vm.$emit('click');
|
||||
|
||||
expect(findToggleActiveBtn().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => {
|
||||
await findToggleActiveBtn().vm.$emit('click');
|
||||
|
||||
expect(getTooltip(findToggleActiveBtn())).toBe('');
|
||||
expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
|
||||
});
|
||||
|
||||
describe(`When clicking on the ${icon} button`, () => {
|
||||
it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
|
||||
expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
|
||||
|
||||
await findToggleActiveBtn().vm.$emit('click');
|
||||
|
||||
expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
|
||||
expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: mockRunner.id,
|
||||
active: newActiveValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('The button does not have a loading state after the mutation occurs', async () => {
|
||||
await findToggleActiveBtn().vm.$emit('click');
|
||||
|
||||
expect(findToggleActiveBtn().props('loading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findToggleActiveBtn().props('loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When update fails', () => {
|
||||
describe('On a network error', () => {
|
||||
const mockErrorMsg = 'Update error!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
|
||||
|
||||
findToggleActiveBtn().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(`Network error: ${mockErrorMsg}`),
|
||||
component: 'RunnerActionsCell',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('On a validation error', () => {
|
||||
const mockErrorMsg = 'Runner not found!';
|
||||
const mockErrorMsg2 = 'User not allowed!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerActionsUpdateMutationHandler.mockResolvedValue({
|
||||
data: {
|
||||
runnerUpdate: {
|
||||
runner: mockRunner,
|
||||
errors: [mockErrorMsg, mockErrorMsg2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
findToggleActiveBtn().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
|
||||
component: 'RunnerActionsCell',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(findRunnerPauseBtn().props('compact')).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render the runner toggle active button when user cannot update', () => {
|
||||
it('Does not render the runner pause button when user cannot update', () => {
|
||||
createComponent({
|
||||
userPermissions: {
|
||||
...mockRunner.userPermissions,
|
||||
|
@ -242,7 +124,7 @@ describe('RunnerTypeCell', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(findToggleActiveBtn().exists()).toBe(false);
|
||||
expect(findRunnerPauseBtn().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import RunnerList from '~/runner/components/runner_list.vue';
|
||||
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
|
||||
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
|
||||
import { runnersData } from '../mock_data';
|
||||
|
||||
const mockRunners = runnersData.data.runners.nodes;
|
||||
|
@ -92,7 +93,8 @@ describe('RunnerList', () => {
|
|||
const actions = findCell({ fieldKey: 'actions' });
|
||||
|
||||
expect(actions.findComponent(RunnerEditButton).exists()).toBe(true);
|
||||
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
|
||||
expect(actions.findComponent(RunnerPauseButton).exists()).toBe(true);
|
||||
expect(actions.findByTestId('delete-runner').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('Table data formatting', () => {
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
import Vue from 'vue';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import runnerToggleActiveMutation from '~/runner/graphql/runner_toggle_active.mutation.graphql';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { createAlert } from '~/flash';
|
||||
|
||||
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
|
||||
import { runnersData } from '../mock_data';
|
||||
|
||||
const mockRunner = runnersData.data.runners.nodes[0];
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/runner/sentry_utils');
|
||||
|
||||
describe('RunnerPauseButton', () => {
|
||||
let wrapper;
|
||||
let runnerToggleActiveHandler;
|
||||
|
||||
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
|
||||
const findBtn = () => wrapper.findComponent(GlButton);
|
||||
|
||||
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
|
||||
const { runner, ...propsData } = props;
|
||||
|
||||
wrapper = mountFn(RunnerPauseButton, {
|
||||
propsData: {
|
||||
runner: {
|
||||
id: mockRunner.id,
|
||||
active: mockRunner.active,
|
||||
...runner,
|
||||
},
|
||||
...propsData,
|
||||
},
|
||||
apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]),
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clickAndWait = async () => {
|
||||
findBtn().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
runnerToggleActiveHandler = jest.fn().mockImplementation(({ input }) => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
runnerUpdate: {
|
||||
runner: {
|
||||
id: input.id,
|
||||
active: input.active,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('Pause/Resume action', () => {
|
||||
describe.each`
|
||||
runnerState | icon | content | isActive | newActiveValue
|
||||
${'paused'} | ${'play'} | ${'Resume'} | ${false} | ${true}
|
||||
${'active'} | ${'pause'} | ${'Pause'} | ${true} | ${false}
|
||||
`('When the runner is $runnerState', ({ icon, content, isActive, newActiveValue }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
runner: {
|
||||
active: isActive,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`Displays a ${icon} button`, () => {
|
||||
expect(findBtn().props('loading')).toBe(false);
|
||||
expect(findBtn().props('icon')).toBe(icon);
|
||||
expect(findBtn().text()).toBe(content);
|
||||
});
|
||||
|
||||
it('Does not display redundant text for screen readers', () => {
|
||||
expect(findBtn().attributes('aria-label')).toBe(undefined);
|
||||
});
|
||||
|
||||
describe(`Before the ${icon} button is clicked`, () => {
|
||||
it('The mutation has not been called', () => {
|
||||
expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Immediately after the ${icon} button is clicked`, () => {
|
||||
beforeEach(async () => {
|
||||
findBtn().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('The button has a loading state', async () => {
|
||||
expect(findBtn().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('The stale tooltip is removed', async () => {
|
||||
expect(getTooltip()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe(`After clicking on the ${icon} button`, () => {
|
||||
beforeEach(async () => {
|
||||
await clickAndWait();
|
||||
});
|
||||
|
||||
it(`The mutation to that sets active to ${newActiveValue} is called`, async () => {
|
||||
expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1);
|
||||
expect(runnerToggleActiveHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: mockRunner.id,
|
||||
active: newActiveValue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('The button does not have a loading state', () => {
|
||||
expect(findBtn().props('loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When update fails', () => {
|
||||
describe('On a network error', () => {
|
||||
const mockErrorMsg = 'Update error!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerToggleActiveHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
|
||||
|
||||
await clickAndWait();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(`Network error: ${mockErrorMsg}`),
|
||||
component: 'RunnerPauseButton',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('On a validation error', () => {
|
||||
const mockErrorMsg = 'Runner not found!';
|
||||
const mockErrorMsg2 = 'User not allowed!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerToggleActiveHandler.mockResolvedValueOnce({
|
||||
data: {
|
||||
runnerUpdate: {
|
||||
runner: {
|
||||
id: mockRunner.id,
|
||||
active: isActive,
|
||||
},
|
||||
errors: [mockErrorMsg, mockErrorMsg2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await clickAndWait();
|
||||
});
|
||||
|
||||
it('error is reported to sentry', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
|
||||
component: 'RunnerPauseButton',
|
||||
});
|
||||
});
|
||||
|
||||
it('error is shown to the user', () => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When displaying a compact button for an active runner', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
runner: {
|
||||
active: true,
|
||||
},
|
||||
compact: true,
|
||||
},
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
});
|
||||
|
||||
it('Displays no text', () => {
|
||||
expect(findBtn().text()).toBe('');
|
||||
|
||||
// Note: Use <template v-if> to ensure rendering a
|
||||
// text-less button. Ensure we don't send even empty an
|
||||
// content slot to prevent a distorted/rectangular button.
|
||||
expect(wrapper.find('.gl-button-text').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('Display correctly for screen readers', () => {
|
||||
expect(findBtn().attributes('aria-label')).toBe('Pause');
|
||||
expect(getTooltip()).toBe('Pause');
|
||||
});
|
||||
|
||||
describe('Immediately after the button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
findBtn().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('The button has a loading state', async () => {
|
||||
expect(findBtn().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('The stale tooltip is removed', async () => {
|
||||
expect(getTooltip()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -32,7 +32,7 @@ const upgradePath = '/upgrade';
|
|||
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
|
||||
const autoDevopsPath = '/autoDevopsPath';
|
||||
const gitlabCiHistoryPath = 'test/historyPath';
|
||||
const projectPath = 'namespace/project';
|
||||
const projectFullPath = 'namespace/project';
|
||||
|
||||
useLocalStorageSpy();
|
||||
|
||||
|
@ -54,7 +54,7 @@ describe('App component', () => {
|
|||
upgradePath,
|
||||
autoDevopsHelpPagePath,
|
||||
autoDevopsPath,
|
||||
projectPath,
|
||||
projectFullPath,
|
||||
glFeatures: {
|
||||
secureVulnerabilityTraining,
|
||||
},
|
||||
|
@ -274,11 +274,11 @@ describe('App component', () => {
|
|||
|
||||
describe('Auto DevOps enabled alert', () => {
|
||||
describe.each`
|
||||
context | autoDevopsEnabled | localStorageValue | shouldRender
|
||||
${'enabled'} | ${true} | ${null} | ${true}
|
||||
${'enabled, alert dismissed on other project'} | ${true} | ${['foo/bar']} | ${true}
|
||||
${'enabled, alert dismissed on this project'} | ${true} | ${[projectPath]} | ${false}
|
||||
${'not enabled'} | ${false} | ${null} | ${false}
|
||||
context | autoDevopsEnabled | localStorageValue | shouldRender
|
||||
${'enabled'} | ${true} | ${null} | ${true}
|
||||
${'enabled, alert dismissed on other project'} | ${true} | ${['foo/bar']} | ${true}
|
||||
${'enabled, alert dismissed on this project'} | ${true} | ${[projectFullPath]} | ${false}
|
||||
${'not enabled'} | ${false} | ${null} | ${false}
|
||||
`('given Auto DevOps is $context', ({ autoDevopsEnabled, localStorageValue, shouldRender }) => {
|
||||
beforeEach(() => {
|
||||
if (localStorageValue !== null) {
|
||||
|
@ -302,11 +302,11 @@ describe('App component', () => {
|
|||
|
||||
describe('dismissing', () => {
|
||||
describe.each`
|
||||
dismissedProjects | expectedWrittenValue
|
||||
${null} | ${[projectPath]}
|
||||
${[]} | ${[projectPath]}
|
||||
${['foo/bar']} | ${['foo/bar', projectPath]}
|
||||
${[projectPath]} | ${[projectPath]}
|
||||
dismissedProjects | expectedWrittenValue
|
||||
${null} | ${[projectFullPath]}
|
||||
${[]} | ${[projectFullPath]}
|
||||
${['foo/bar']} | ${['foo/bar', projectFullPath]}
|
||||
${[projectFullPath]} | ${[projectFullPath]}
|
||||
`(
|
||||
'given dismissed projects $dismissedProjects',
|
||||
({ dismissedProjects, expectedWrittenValue }) => {
|
||||
|
|
|
@ -4,11 +4,12 @@ import Vue from 'vue';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
|
||||
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
|
||||
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
securityTrainingProviders,
|
||||
createMockResolvers,
|
||||
securityTrainingProvidersResponse,
|
||||
testProjectPath,
|
||||
textProviderIds,
|
||||
} from '../mock_data';
|
||||
|
@ -19,14 +20,19 @@ describe('TrainingProviderList component', () => {
|
|||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
const createApolloProvider = ({ resolvers } = {}) => {
|
||||
apolloProvider = createMockApollo([], createMockResolvers({ resolvers }));
|
||||
const createApolloProvider = ({ resolvers, queryHandler } = {}) => {
|
||||
const defaultQueryHandler = jest.fn().mockResolvedValue(securityTrainingProvidersResponse);
|
||||
|
||||
apolloProvider = createMockApollo(
|
||||
[[securityTrainingProvidersQuery, queryHandler || defaultQueryHandler]],
|
||||
resolvers,
|
||||
);
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(TrainingProviderList, {
|
||||
provide: {
|
||||
projectPath: testProjectPath,
|
||||
projectFullPath: testProjectPath,
|
||||
},
|
||||
apolloProvider,
|
||||
});
|
||||
|
@ -49,22 +55,31 @@ describe('TrainingProviderList component', () => {
|
|||
apolloProvider = null;
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
beforeEach(() => {
|
||||
const pendingHandler = () => new Promise(() => {});
|
||||
|
||||
createApolloProvider({
|
||||
queryHandler: pendingHandler,
|
||||
});
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('shows the loader', () => {
|
||||
expect(findLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the cards', () => {
|
||||
expect(findCards().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a successful response', () => {
|
||||
beforeEach(() => {
|
||||
createApolloProvider();
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
it('shows the loader', () => {
|
||||
expect(findLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the cards', () => {
|
||||
expect(findCards().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('basic structure', () => {
|
||||
beforeEach(async () => {
|
||||
await waitForQueryToBeLoaded();
|
||||
|
@ -142,11 +157,7 @@ describe('TrainingProviderList component', () => {
|
|||
describe('when fetching training providers', () => {
|
||||
beforeEach(async () => {
|
||||
createApolloProvider({
|
||||
resolvers: {
|
||||
Query: {
|
||||
securityTrainingProviders: jest.fn().mockReturnValue(new Error()),
|
||||
},
|
||||
},
|
||||
queryHandler: jest.fn().mockReturnValue(new Error()),
|
||||
});
|
||||
createComponent();
|
||||
|
||||
|
|
|
@ -21,19 +21,9 @@ export const securityTrainingProviders = [
|
|||
|
||||
export const securityTrainingProvidersResponse = {
|
||||
data: {
|
||||
securityTrainingProviders,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultMockResolvers = {
|
||||
Query: {
|
||||
securityTrainingProviders() {
|
||||
return securityTrainingProviders;
|
||||
project: {
|
||||
id: 1,
|
||||
securityTrainingProviders,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const createMockResolvers = ({ resolvers: customMockResolvers = {} } = {}) => ({
|
||||
...defaultMockResolvers,
|
||||
...customMockResolvers,
|
||||
});
|
||||
|
|
|
@ -95,10 +95,11 @@ describe('Accessibility extension', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
findToggleCollapsedButton().vm.$emit('click');
|
||||
findToggleCollapsedButton().trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays all report list items', async () => {
|
||||
expect(findAllExtensionListItems()).toHaveLength(10);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ jest.mock('~/lib/utils/url_utility');
|
|||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const projectPath = 'namespace/project';
|
||||
const projectFullPath = 'namespace/project';
|
||||
|
||||
describe('ManageViaMr component', () => {
|
||||
let wrapper;
|
||||
|
@ -40,7 +40,7 @@ describe('ManageViaMr component', () => {
|
|||
wrapper = extendedWrapper(
|
||||
mount(ManageViaMr, {
|
||||
provide: {
|
||||
projectPath,
|
||||
projectFullPath,
|
||||
},
|
||||
propsData: {
|
||||
feature: {
|
||||
|
@ -65,7 +65,7 @@ describe('ManageViaMr component', () => {
|
|||
// the ones available in the current test context.
|
||||
const supportedReportTypes = Object.entries(featureToMutationMap).map(
|
||||
([featureType, { getMutationPayload, mutationId }]) => {
|
||||
const { mutation, variables: mutationVariables } = getMutationPayload(projectPath);
|
||||
const { mutation, variables: mutationVariables } = getMutationPayload(projectFullPath);
|
||||
return [humanize(featureType), featureType, mutation, mutationId, mutationVariables];
|
||||
},
|
||||
);
|
||||
|
|
|
@ -3,7 +3,8 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
|
||||
describe Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigrationProxyLockRetries do
|
||||
let(:migration) { double }
|
||||
let(:connection) { ActiveRecord::Base.connection }
|
||||
let(:migration) { double(connection: connection) }
|
||||
let(:return_value) { double }
|
||||
let(:class_def) do
|
||||
Class.new do
|
||||
|
@ -40,6 +41,18 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
|
|||
expect(result).to eq(return_value)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#migration_connection' do
|
||||
subject { class_def.new(migration).migration_connection }
|
||||
|
||||
it 'retrieves actual migration connection from #migration' do
|
||||
expect(migration).to receive(:connection).and_return(return_value)
|
||||
|
||||
result = subject
|
||||
|
||||
expect(result).to eq(return_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigratorLockRetries do
|
||||
|
@ -96,7 +109,8 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
|
|||
|
||||
context 'with transactions enabled and lock retries enabled' do
|
||||
let(:receiver) { double('receiver', use_transaction?: true)}
|
||||
let(:migration) { double('migration', enable_lock_retries?: true) }
|
||||
let(:migration) { double('migration', migration_connection: connection, enable_lock_retries?: true) }
|
||||
let(:connection) { ActiveRecord::Base.connection }
|
||||
|
||||
it 'calls super method' do
|
||||
p = proc { }
|
||||
|
|
|
@ -12,13 +12,11 @@ RSpec.describe 'cross-database foreign keys' do
|
|||
let(:allowed_cross_database_foreign_keys) do
|
||||
%w(
|
||||
ci_build_report_results.project_id
|
||||
ci_builds.project_id
|
||||
ci_daily_build_group_report_results.group_id
|
||||
ci_daily_build_group_report_results.project_id
|
||||
ci_freeze_periods.project_id
|
||||
ci_job_artifacts.project_id
|
||||
ci_job_token_project_scope_links.added_by_id
|
||||
ci_job_token_project_scope_links.target_project_id
|
||||
ci_pending_builds.namespace_id
|
||||
ci_pending_builds.project_id
|
||||
ci_pipeline_schedules.owner_id
|
||||
|
|
|
@ -5,7 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
||||
let(:env) { {} }
|
||||
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
|
||||
let(:subject) { described_class.new(env: env, logger: logger, timing_configuration: timing_configuration) }
|
||||
let(:subject) { described_class.new(connection: connection, env: env, logger: logger, timing_configuration: timing_configuration) }
|
||||
let(:connection) { ActiveRecord::Base.retrieve_connection }
|
||||
|
||||
let(:timing_configuration) do
|
||||
[
|
||||
|
@ -67,7 +68,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
|
||||
"""
|
||||
|
||||
expect(ActiveRecord::Base.connection.execute(check_exclusive_lock_query).to_a).to be_present
|
||||
expect(connection.execute(check_exclusive_lock_query).to_a).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -96,8 +97,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
lock_fiber.resume
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
|
||||
connection.transaction do
|
||||
connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
|
||||
lock_acquired = true
|
||||
end
|
||||
end
|
||||
|
@ -115,7 +116,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
context 'setting the idle transaction timeout' do
|
||||
context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
|
||||
it 'does not disable the idle transaction timeout' do
|
||||
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
|
||||
allow(connection).to receive(:transaction_open?).and_return(false)
|
||||
allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
|
||||
allow(subject).to receive(:run_block_with_lock_timeout).once
|
||||
|
||||
|
@ -127,7 +128,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
|
||||
context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
|
||||
it 'disables the idle transaction timeout so the code can sleep and retry' do
|
||||
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
|
||||
allow(connection).to receive(:transaction_open?).and_return(true)
|
||||
|
||||
n = 0
|
||||
allow(subject).to receive(:run_block_with_lock_timeout).twice do
|
||||
|
@ -184,8 +185,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
subject.run(raise_on_exhaustion: true) do
|
||||
lock_attempts += 1
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
|
||||
connection.transaction do
|
||||
connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
|
||||
lock_acquired = true
|
||||
end
|
||||
end
|
||||
|
@ -199,11 +200,11 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
context 'when statement timeout is reached' do
|
||||
it 'raises StatementInvalid error' do
|
||||
lock_acquired = false
|
||||
ActiveRecord::Base.connection.execute("SET statement_timeout='100ms'")
|
||||
connection.execute("SET statement_timeout='100ms'")
|
||||
|
||||
expect do
|
||||
subject.run do
|
||||
ActiveRecord::Base.connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
|
||||
connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
|
||||
lock_acquired = true
|
||||
end
|
||||
end.to raise_error(ActiveRecord::StatementInvalid)
|
||||
|
@ -216,11 +217,11 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
|
||||
context 'restore local database variables' do
|
||||
it do
|
||||
expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW lock_timeout").to_a }
|
||||
expect { subject.run {} }.not_to change { connection.execute("SHOW lock_timeout").to_a }
|
||||
end
|
||||
|
||||
it do
|
||||
expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
|
||||
expect { subject.run {} }.not_to change { connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -228,8 +229,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
|
|||
let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
|
||||
|
||||
it 'executes `SET lock_timeout` using the configured timeout value in milliseconds' do
|
||||
expect(ActiveRecord::Base.connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
|
||||
expect(ActiveRecord::Base.connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
|
||||
expect(connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
|
||||
expect(connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
|
||||
|
||||
subject.run { }
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Database::WithLockRetries do
|
||||
let(:env) { {} }
|
||||
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
|
||||
let(:subject) { described_class.new(env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
|
||||
let(:subject) { described_class.new(connection: connection, env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
|
||||
let(:allow_savepoints) { true }
|
||||
let(:connection) { ActiveRecord::Base.retrieve_connection }
|
||||
|
||||
|
|
|
@ -183,24 +183,8 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
|
|||
end
|
||||
|
||||
describe '.batch_size' do
|
||||
context 'when export_reduce_relation_batch_size feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(export_reduce_relation_batch_size: true)
|
||||
end
|
||||
|
||||
it 'returns 20' do
|
||||
expect(described_class.batch_size(exportable)).to eq(described_class::SMALLER_BATCH_SIZE)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when export_reduce_relation_batch_size feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(export_reduce_relation_batch_size: false)
|
||||
end
|
||||
|
||||
it 'returns default batch size' do
|
||||
expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
|
||||
end
|
||||
it 'returns default batch size' do
|
||||
expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,35 +8,17 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do
|
|||
let(:tree) { {} }
|
||||
|
||||
describe '#serialize' do
|
||||
shared_examples 'FastHashSerializer with batch size' do |batch_size|
|
||||
let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
|
||||
let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
|
||||
|
||||
it 'uses FastHashSerializer' do
|
||||
expect(Gitlab::ImportExport::FastHashSerializer)
|
||||
.to receive(:new)
|
||||
.with(exportable, tree, batch_size: batch_size)
|
||||
.and_return(serializer)
|
||||
it 'uses FastHashSerializer' do
|
||||
expect(Gitlab::ImportExport::FastHashSerializer)
|
||||
.to receive(:new)
|
||||
.with(exportable, tree, batch_size: Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE)
|
||||
.and_return(serializer)
|
||||
|
||||
expect(serializer).to receive(:execute)
|
||||
expect(serializer).to receive(:execute)
|
||||
|
||||
relation_tree_saver.serialize(exportable, tree)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when export_reduce_relation_batch_size feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(export_reduce_relation_batch_size: true)
|
||||
end
|
||||
|
||||
include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::SMALLER_BATCH_SIZE
|
||||
end
|
||||
|
||||
context 'when export_reduce_relation_batch_size feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(export_reduce_relation_batch_size: false)
|
||||
end
|
||||
|
||||
include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE
|
||||
relation_tree_saver.serialize(exportable, tree)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,4 +95,11 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
|
|||
let!(:model) { create(:ci_job_token_project_scope_link, source_project: parent) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'loose foreign key on ci_job_token_project_scope_links.target_project_id' do
|
||||
it_behaves_like 'cleanup by a loose foreign key' do
|
||||
let!(:parent) { create(:project) }
|
||||
let!(:model) { create(:ci_job_token_project_scope_link, target_project: parent) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -987,4 +987,11 @@ RSpec.describe CommitStatus do
|
|||
commit_status.expire_etag_cache!
|
||||
end
|
||||
end
|
||||
|
||||
context 'loose foreign key on ci_builds.project_id' do
|
||||
it_behaves_like 'cleanup by a loose foreign key' do
|
||||
let!(:parent) { create(:project) }
|
||||
let!(:model) { create(:ci_build, project: parent) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,6 +53,10 @@ RSpec.shared_examples 'export worker' do
|
|||
it 'does not raise an exception when strategy is invalid' do
|
||||
expect(::Projects::ImportExport::ExportService).not_to receive(:new)
|
||||
|
||||
expect_next_instance_of(ProjectExportJob) do |job|
|
||||
expect(job).to receive(:finish)
|
||||
end
|
||||
|
||||
expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
|
||||
end
|
||||
|
||||
|
@ -63,6 +67,18 @@ RSpec.shared_examples 'export worker' do
|
|||
it 'does not raise error when user cannot be found' do
|
||||
expect { subject.perform(non_existing_record_id, project.id, {}) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'fails the export job status' do
|
||||
expect_next_instance_of(::Projects::ImportExport::ExportService) do |service|
|
||||
expect(service).to receive(:execute).and_raise(Gitlab::ImportExport::Error)
|
||||
end
|
||||
|
||||
expect_next_instance_of(ProjectExportJob) do |job|
|
||||
expect(job).to receive(:fail_op)
|
||||
end
|
||||
|
||||
expect { subject.perform(user.id, project.id, {}) }.to raise_error(Gitlab::ImportExport::Error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe AutoDevops::DisableWorker, '#perform' do
|
|||
let(:namespace) { create(:namespace, owner: owner) }
|
||||
let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) }
|
||||
|
||||
it 'sends an email to pipeline user and project owner' do
|
||||
it 'sends an email to pipeline user and project owner(s)' do
|
||||
expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email, owner.email])
|
||||
|
||||
subject.perform(pipeline.id)
|
||||
|
|
Loading…
Reference in New Issue