Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a27c21b5af
commit
3a8d221b7e
|
|
@ -456,7 +456,6 @@ Lint/UnusedMethodArgument:
|
||||||
- 'lib/gitlab/diff/file_collection/merge_request_diff_base.rb'
|
- 'lib/gitlab/diff/file_collection/merge_request_diff_base.rb'
|
||||||
- 'lib/gitlab/diff/line.rb'
|
- 'lib/gitlab/diff/line.rb'
|
||||||
- 'lib/gitlab/encoding_helper.rb'
|
- 'lib/gitlab/encoding_helper.rb'
|
||||||
- 'lib/gitlab/error_tracking/error_repository/active_record_strategy.rb'
|
|
||||||
- 'lib/gitlab/fogbugz_import/importer.rb'
|
- 'lib/gitlab/fogbugz_import/importer.rb'
|
||||||
- 'lib/gitlab/git/commit_stats.rb'
|
- 'lib/gitlab/git/commit_stats.rb'
|
||||||
- 'lib/gitlab/git/conflict/parser.rb'
|
- 'lib/gitlab/git/conflict/parser.rb'
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ Naming/InclusiveLanguage:
|
||||||
- 'spec/rubocop/cop/graphql/id_type_spec.rb'
|
- 'spec/rubocop/cop/graphql/id_type_spec.rb'
|
||||||
- 'spec/services/application_settings/update_service_spec.rb'
|
- 'spec/services/application_settings/update_service_spec.rb'
|
||||||
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
|
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
|
||||||
- 'spec/services/error_tracking/list_issues_service_spec.rb'
|
|
||||||
- 'spec/services/projects/download_service_spec.rb'
|
- 'spec/services/projects/download_service_spec.rb'
|
||||||
- 'spec/support/import_export/export_file_helper.rb'
|
- 'spec/support/import_export/export_file_helper.rb'
|
||||||
- 'spec/support/shared_contexts/upload_type_check_shared_context.rb'
|
- 'spec/support/shared_contexts/upload_type_check_shared_context.rb'
|
||||||
|
|
|
||||||
|
|
@ -2670,7 +2670,6 @@ RSpec/ContextWording:
|
||||||
- 'spec/services/draft_notes/publish_service_spec.rb'
|
- 'spec/services/draft_notes/publish_service_spec.rb'
|
||||||
- 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
|
- 'spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
|
||||||
- 'spec/services/environments/stop_service_spec.rb'
|
- 'spec/services/environments/stop_service_spec.rb'
|
||||||
- 'spec/services/error_tracking/list_issues_service_spec.rb'
|
|
||||||
- 'spec/services/error_tracking/list_projects_service_spec.rb'
|
- 'spec/services/error_tracking/list_projects_service_spec.rb'
|
||||||
- 'spec/services/events/destroy_service_spec.rb'
|
- 'spec/services/events/destroy_service_spec.rb'
|
||||||
- 'spec/services/feature_flags/update_service_spec.rb'
|
- 'spec/services/feature_flags/update_service_spec.rb'
|
||||||
|
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/* eslint-disable no-restricted-globals */
|
|
||||||
|
|
||||||
import { logger } from '@rails/actioncable';
|
|
||||||
|
|
||||||
// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
|
|
||||||
// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.
|
|
||||||
|
|
||||||
// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
|
|
||||||
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
|
|
||||||
|
|
||||||
const now = () => new Date().getTime();
|
|
||||||
|
|
||||||
const secondsSince = (time) => (now() - time) / 1000;
|
|
||||||
class ConnectionMonitor {
|
|
||||||
constructor(connection) {
|
|
||||||
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
|
||||||
this.connection = connection;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (!this.isRunning()) {
|
|
||||||
this.startedAt = now();
|
|
||||||
delete this.stoppedAt;
|
|
||||||
this.startPolling();
|
|
||||||
addEventListener('visibilitychange', this.visibilityDidChange);
|
|
||||||
logger.log(
|
|
||||||
`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (this.isRunning()) {
|
|
||||||
this.stoppedAt = now();
|
|
||||||
this.stopPolling();
|
|
||||||
removeEventListener('visibilitychange', this.visibilityDidChange);
|
|
||||||
logger.log('ConnectionMonitor stopped');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isRunning() {
|
|
||||||
return this.startedAt && !this.stoppedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
recordPing() {
|
|
||||||
this.pingedAt = now();
|
|
||||||
}
|
|
||||||
|
|
||||||
recordConnect() {
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.recordPing();
|
|
||||||
delete this.disconnectedAt;
|
|
||||||
logger.log('ConnectionMonitor recorded connect');
|
|
||||||
}
|
|
||||||
|
|
||||||
recordDisconnect() {
|
|
||||||
this.disconnectedAt = now();
|
|
||||||
logger.log('ConnectionMonitor recorded disconnect');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private
|
|
||||||
|
|
||||||
startPolling() {
|
|
||||||
this.stopPolling();
|
|
||||||
this.poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
stopPolling() {
|
|
||||||
clearTimeout(this.pollTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
poll() {
|
|
||||||
this.pollTimeout = setTimeout(() => {
|
|
||||||
this.reconnectIfStale();
|
|
||||||
this.poll();
|
|
||||||
}, this.getPollInterval());
|
|
||||||
}
|
|
||||||
|
|
||||||
getPollInterval() {
|
|
||||||
const { staleThreshold, reconnectionBackoffRate } = this.constructor;
|
|
||||||
const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
|
|
||||||
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
|
|
||||||
const jitter = jitterMax * Math.random();
|
|
||||||
return staleThreshold * 1000 * backoff * (1 + jitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnectIfStale() {
|
|
||||||
if (this.connectionIsStale()) {
|
|
||||||
logger.log(
|
|
||||||
`ConnectionMonitor detected stale connection. reconnectAttempts = ${
|
|
||||||
this.reconnectAttempts
|
|
||||||
}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
|
|
||||||
this.constructor.staleThreshold
|
|
||||||
} s`,
|
|
||||||
);
|
|
||||||
this.reconnectAttempts += 1;
|
|
||||||
if (this.disconnectedRecently()) {
|
|
||||||
logger.log(
|
|
||||||
`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
|
|
||||||
this.disconnectedAt,
|
|
||||||
)} s`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.log('ConnectionMonitor reopening');
|
|
||||||
this.connection.reopen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get refreshedAt() {
|
|
||||||
return this.pingedAt ? this.pingedAt : this.startedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionIsStale() {
|
|
||||||
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedRecently() {
|
|
||||||
return (
|
|
||||||
this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
visibilityDidChange() {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
|
||||||
logger.log(
|
|
||||||
`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
|
|
||||||
);
|
|
||||||
this.connection.reopen();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
|
||||||
ConnectionMonitor.reconnectionBackoffRate = 0.15;
|
|
||||||
|
|
||||||
export default ConnectionMonitor;
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
import { createConsumer } from '@rails/actioncable';
|
import { createConsumer } from '@rails/actioncable';
|
||||||
import ConnectionMonitor from './actioncable_connection_monitor';
|
|
||||||
|
|
||||||
const consumer = createConsumer();
|
export default createConsumer();
|
||||||
|
|
||||||
if (consumer.connection) {
|
|
||||||
consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default consumer;
|
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,6 @@ export default {
|
||||||
isSearchFiltered() {
|
isSearchFiltered() {
|
||||||
return isSearchFiltered(this.search);
|
return isSearchFiltered(this.search);
|
||||||
},
|
},
|
||||||
shouldShowCreateRunnerWorkflow() {
|
|
||||||
// create_runner_workflow_for_admin feature flag
|
|
||||||
return this.glFeatures.createRunnerWorkflowForAdmin;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search: {
|
search: {
|
||||||
|
|
@ -193,7 +189,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="gl-w-full gl-md-w-auto gl-display-flex">
|
<div class="gl-w-full gl-md-w-auto gl-display-flex">
|
||||||
<gl-button v-if="shouldShowCreateRunnerWorkflow" :href="newRunnerPath" variant="confirm">
|
<gl-button :href="newRunnerPath" variant="confirm">
|
||||||
{{ s__('Runners|New instance runner') }}
|
{{ s__('Runners|New instance runner') }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<registration-dropdown
|
<registration-dropdown
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ export default {
|
||||||
showInstallationInstructions: s__(
|
showInstallationInstructions: s__(
|
||||||
'Runners|Show runner installation and registration instructions',
|
'Runners|Show runner installation and registration instructions',
|
||||||
),
|
),
|
||||||
|
supportForRegistrationTokensDeprecated: s__(
|
||||||
|
'Runners|Support for registration tokens is deprecated',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
GlDropdown,
|
GlDropdown,
|
||||||
|
|
@ -51,14 +54,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isDeprecated() {
|
|
||||||
// Show a compact version when used as secondary option
|
|
||||||
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
|
|
||||||
return (
|
|
||||||
this.glFeatures?.createRunnerWorkflowForAdmin ||
|
|
||||||
this.glFeatures?.createRunnerWorkflowForNamespace
|
|
||||||
);
|
|
||||||
},
|
|
||||||
actionText() {
|
actionText() {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case INSTANCE_TYPE:
|
case INSTANCE_TYPE:
|
||||||
|
|
@ -71,30 +66,6 @@ export default {
|
||||||
return I18N_REGISTER_RUNNER;
|
return I18N_REGISTER_RUNNER;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dropdownText() {
|
|
||||||
if (this.isDeprecated) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return this.actionText;
|
|
||||||
},
|
|
||||||
dropdownToggleClass() {
|
|
||||||
if (this.isDeprecated) {
|
|
||||||
return ['gl-px-3!'];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
dropdownCategory() {
|
|
||||||
if (this.isDeprecated) {
|
|
||||||
return 'tertiary';
|
|
||||||
}
|
|
||||||
return 'primary';
|
|
||||||
},
|
|
||||||
dropdownVariant() {
|
|
||||||
if (this.isDeprecated) {
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
return 'confirm';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onShowInstructionsClick() {
|
onShowInstructionsClick() {
|
||||||
|
|
@ -113,22 +84,21 @@ export default {
|
||||||
<gl-dropdown
|
<gl-dropdown
|
||||||
ref="runnerRegistrationDropdown"
|
ref="runnerRegistrationDropdown"
|
||||||
menu-class="gl-w-auto!"
|
menu-class="gl-w-auto!"
|
||||||
:text="dropdownText"
|
toggle-class="gl-px-3!"
|
||||||
:toggle-class="dropdownToggleClass"
|
variant="default"
|
||||||
:variant="dropdownVariant"
|
category="tertiary"
|
||||||
:category="dropdownCategory"
|
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<template v-if="isDeprecated" #button-content>
|
<template #button-content>
|
||||||
<span class="gl-sr-only">{{ actionText }}</span>
|
<span class="gl-sr-only">{{ actionText }}</span>
|
||||||
<gl-icon name="ellipsis_v" />
|
<gl-icon name="ellipsis_v" />
|
||||||
</template>
|
</template>
|
||||||
<gl-dropdown-form class="gl-p-4!">
|
<gl-dropdown-form class="gl-p-4!">
|
||||||
<registration-token input-id="token-value" :value="currentRegistrationToken">
|
<registration-token input-id="token-value" :value="currentRegistrationToken">
|
||||||
<template v-if="isDeprecated" #label-description>
|
<template #label-description>
|
||||||
<gl-icon name="warning" class="gl-text-orange-500" />
|
<gl-icon name="warning" class="gl-text-orange-500" />
|
||||||
<span class="gl-text-secondary">
|
<span class="gl-text-secondary">
|
||||||
{{ s__('Runners|Support for registration tokens is deprecated') }}
|
{{ $options.i18n.supportForRegistrationTokensDeprecated }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</registration-token>
|
</registration-token>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
I18N_CREATE_RUNNER_LINK,
|
I18N_CREATE_RUNNER_LINK,
|
||||||
I18N_STILL_USING_REGISTRATION_TOKENS,
|
I18N_STILL_USING_REGISTRATION_TOKENS,
|
||||||
I18N_CONTACT_ADMIN_TO_REGISTER,
|
I18N_CONTACT_ADMIN_TO_REGISTER,
|
||||||
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
|
|
||||||
I18N_NO_RESULTS,
|
I18N_NO_RESULTS,
|
||||||
I18N_EDIT_YOUR_SEARCH,
|
I18N_EDIT_YOUR_SEARCH,
|
||||||
} from '~/ci/runner/constants';
|
} from '~/ci/runner/constants';
|
||||||
|
|
@ -44,15 +43,6 @@ export default {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
shouldShowCreateRunnerWorkflow() {
|
|
||||||
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
|
|
||||||
return (
|
|
||||||
this.glFeatures?.createRunnerWorkflowForAdmin ||
|
|
||||||
this.glFeatures?.createRunnerWorkflowForNamespace
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
modalId: 'runners-empty-state-instructions-modal',
|
modalId: 'runners-empty-state-instructions-modal',
|
||||||
svgHeight: 145,
|
svgHeight: 145,
|
||||||
EMPTY_STATE_SVG_URL,
|
EMPTY_STATE_SVG_URL,
|
||||||
|
|
@ -63,7 +53,6 @@ export default {
|
||||||
I18N_CREATE_RUNNER_LINK,
|
I18N_CREATE_RUNNER_LINK,
|
||||||
I18N_STILL_USING_REGISTRATION_TOKENS,
|
I18N_STILL_USING_REGISTRATION_TOKENS,
|
||||||
I18N_CONTACT_ADMIN_TO_REGISTER,
|
I18N_CONTACT_ADMIN_TO_REGISTER,
|
||||||
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
|
|
||||||
I18N_NO_RESULTS,
|
I18N_NO_RESULTS,
|
||||||
I18N_EDIT_YOUR_SEARCH,
|
I18N_EDIT_YOUR_SEARCH,
|
||||||
};
|
};
|
||||||
|
|
@ -85,7 +74,6 @@ export default {
|
||||||
>
|
>
|
||||||
<template #description>
|
<template #description>
|
||||||
{{ $options.I18N_RUNNERS_ARE_AGENTS }}
|
{{ $options.I18N_RUNNERS_ARE_AGENTS }}
|
||||||
<template v-if="shouldShowCreateRunnerWorkflow">
|
|
||||||
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
|
<gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
|
||||||
<template #link="{ content }">
|
<template #link="{ content }">
|
||||||
<gl-link :href="newRunnerPath">{{ content }}</gl-link>
|
<gl-link :href="newRunnerPath">{{ content }}</gl-link>
|
||||||
|
|
@ -105,21 +93,5 @@ export default {
|
||||||
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
|
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<gl-sprintf
|
|
||||||
v-else-if="registrationToken"
|
|
||||||
:message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS"
|
|
||||||
>
|
|
||||||
<template #link="{ content }">
|
|
||||||
<gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
|
|
||||||
<runner-instructions-modal
|
|
||||||
:modal-id="$options.modalId"
|
|
||||||
:registration-token="registrationToken"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</gl-sprintf>
|
|
||||||
<template v-else>
|
|
||||||
{{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</gl-empty-state>
|
</gl-empty-state>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,6 @@ export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using reg
|
||||||
export const I18N_CONTACT_ADMIN_TO_REGISTER = s__(
|
export const I18N_CONTACT_ADMIN_TO_REGISTER = s__(
|
||||||
'Runners|To register new runners, contact your administrator.',
|
'Runners|To register new runners, contact your administrator.',
|
||||||
);
|
);
|
||||||
export const I18N_FOLLOW_REGISTRATION_INSTRUCTIONS = s__(
|
|
||||||
'Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// No runners found
|
// No runners found
|
||||||
export const I18N_NO_RESULTS = s__('Runners|No results found');
|
export const I18N_NO_RESULTS = s__('Runners|No results found');
|
||||||
|
|
|
||||||
|
|
@ -155,10 +155,6 @@ export default {
|
||||||
isSearchFiltered() {
|
isSearchFiltered() {
|
||||||
return isSearchFiltered(this.search);
|
return isSearchFiltered(this.search);
|
||||||
},
|
},
|
||||||
shouldShowCreateRunnerWorkflow() {
|
|
||||||
// create_runner_workflow_for_namespace feature flag
|
|
||||||
return this.glFeatures.createRunnerWorkflowForNamespace;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search: {
|
search: {
|
||||||
|
|
@ -231,11 +227,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="gl-w-full gl-md-w-auto gl-display-flex">
|
<div class="gl-w-full gl-md-w-auto gl-display-flex">
|
||||||
<gl-button
|
<gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm">
|
||||||
v-if="shouldShowCreateRunnerWorkflow && newRunnerPath"
|
|
||||||
:href="newRunnerPath"
|
|
||||||
variant="confirm"
|
|
||||||
>
|
|
||||||
{{ s__('Runners|New group runner') }}
|
{{ s__('Runners|New group runner') }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<registration-dropdown
|
<registration-dropdown
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ export default {
|
||||||
locked: {
|
locked: {
|
||||||
icon: 'lock',
|
icon: 'lock',
|
||||||
class: 'value',
|
class: 'value',
|
||||||
iconClass: 'is-active',
|
displayText: __('Locked'),
|
||||||
},
|
},
|
||||||
unlocked: {
|
unlocked: {
|
||||||
class: ['no-value hide-collapsed'],
|
class: ['no-value hide-collapsed'],
|
||||||
icon: 'lock-open',
|
icon: 'lock-open',
|
||||||
iconClass: '',
|
displayText: __('Unlocked'),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
EditForm,
|
EditForm,
|
||||||
|
|
@ -49,8 +49,6 @@ export default {
|
||||||
issueCapitalized: __('Issue'),
|
issueCapitalized: __('Issue'),
|
||||||
mergeRequest: __('merge request'),
|
mergeRequest: __('merge request'),
|
||||||
mergeRequestCapitalized: __('Merge request'),
|
mergeRequestCapitalized: __('Merge request'),
|
||||||
locked: __('Locked'),
|
|
||||||
unlocked: __('Unlocked'),
|
|
||||||
lockingMergeRequest: __('Locking %{issuableDisplayName}'),
|
lockingMergeRequest: __('Locking %{issuableDisplayName}'),
|
||||||
unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
|
unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
|
||||||
lockMergeRequest: __('Lock %{issuableDisplayName}'),
|
lockMergeRequest: __('Lock %{issuableDisplayName}'),
|
||||||
|
|
@ -84,10 +82,7 @@ export default {
|
||||||
return this.getNoteableData.discussion_locked;
|
return this.getNoteableData.discussion_locked;
|
||||||
},
|
},
|
||||||
lockStatus() {
|
lockStatus() {
|
||||||
return this.isLocked ? this.$options.i18n.locked : this.$options.i18n.unlocked;
|
return this.isLocked ? this.$options.locked : this.$options.unlocked;
|
||||||
},
|
|
||||||
tooltipLabel() {
|
|
||||||
return this.isLocked ? this.$options.i18n.locked : this.$options.i18n.unlocked;
|
|
||||||
},
|
},
|
||||||
lockToggleInProgressText() {
|
lockToggleInProgressText() {
|
||||||
return this.isLocked ? this.unlockingMergeRequestText : this.lockingMergeRequestText;
|
return this.isLocked ? this.unlockingMergeRequestText : this.lockingMergeRequestText;
|
||||||
|
|
@ -205,7 +200,7 @@ export default {
|
||||||
</gl-disclosure-dropdown-item>
|
</gl-disclosure-dropdown-item>
|
||||||
<div v-else class="block issuable-sidebar-item lock">
|
<div v-else class="block issuable-sidebar-item lock">
|
||||||
<div
|
<div
|
||||||
v-gl-tooltip.left.viewport="{ title: tooltipLabel }"
|
v-gl-tooltip.left.viewport="{ title: lockStatus.displayText }"
|
||||||
class="sidebar-collapsed-icon"
|
class="sidebar-collapsed-icon"
|
||||||
data-testid="sidebar-collapse-icon"
|
data-testid="sidebar-collapse-icon"
|
||||||
@click="toggleForm"
|
@click="toggleForm"
|
||||||
|
|
@ -239,7 +234,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
|
<div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
|
||||||
{{ lockStatus }}
|
{{ lockStatus.displayText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -122,14 +122,6 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showDeprecationAlert() {
|
|
||||||
return (
|
|
||||||
// create_runner_workflow_for_admin
|
|
||||||
this.glFeatures.createRunnerWorkflowForAdmin ||
|
|
||||||
// create_runner_workflow_for_namespace
|
|
||||||
this.glFeatures.createRunnerWorkflowForNamespace
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
// Refocus on dom changes, after loading data
|
// Refocus on dom changes, after loading data
|
||||||
|
|
@ -200,12 +192,7 @@ export default {
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
@shown="onShown"
|
@shown="onShown"
|
||||||
>
|
>
|
||||||
<gl-alert
|
<gl-alert :title="$options.i18n.deprecationAlertTitle" variant="warning" :dismissible="false">
|
||||||
v-if="showDeprecationAlert"
|
|
||||||
:title="$options.i18n.deprecationAlertTitle"
|
|
||||||
variant="warning"
|
|
||||||
:dismissible="false"
|
|
||||||
>
|
|
||||||
<gl-sprintf :message="$options.i18n.deprecationAlertContent">
|
<gl-sprintf :message="$options.i18n.deprecationAlertContent">
|
||||||
<template #link="{ content }">
|
<template #link="{ content }">
|
||||||
<gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL"
|
<gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL"
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,6 @@ class Admin::RunnersController < Admin::ApplicationController
|
||||||
|
|
||||||
before_action :runner, except: [:index, :new, :tag_list, :runner_setup_scripts]
|
before_action :runner, except: [:index, :new, :tag_list, :runner_setup_scripts]
|
||||||
|
|
||||||
before_action only: [:index] do
|
|
||||||
push_frontend_feature_flag(:create_runner_workflow_for_admin, current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
feature_category :runner
|
feature_category :runner
|
||||||
urgency :low
|
urgency :low
|
||||||
|
|
||||||
|
|
@ -23,12 +19,10 @@ class Admin::RunnersController < Admin::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def register
|
def register
|
||||||
render_404 unless Feature.enabled?(:create_runner_workflow_for_admin, current_user) &&
|
render_404 unless runner.registration_available?
|
||||||
runner.registration_available?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ class Groups::RunnersController < Groups::ApplicationController
|
||||||
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
|
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
|
||||||
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]
|
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]
|
||||||
|
|
||||||
before_action only: [:index] do
|
|
||||||
push_frontend_feature_flag(:create_runner_workflow_for_namespace, group)
|
|
||||||
end
|
|
||||||
|
|
||||||
feature_category :runner
|
feature_category :runner
|
||||||
urgency :low
|
urgency :low
|
||||||
|
|
||||||
|
|
@ -35,11 +31,10 @@ class Groups::RunnersController < Groups::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
render_404 unless create_runner_workflow_for_namespace_enabled?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def register
|
def register
|
||||||
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available?
|
render_404 unless runner.registration_available?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
@ -67,10 +62,6 @@ class Groups::RunnersController < Groups::ApplicationController
|
||||||
|
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_runner_workflow_for_namespace_enabled?
|
|
||||||
Feature.enabled?(:create_runner_workflow_for_namespace, group)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Groups::RunnersController.prepend_mod
|
Groups::RunnersController.prepend_mod
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,10 @@ class Projects::RunnersController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
render_404 unless create_runner_workflow_for_namespace_enabled?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def register
|
def register
|
||||||
render_404 unless create_runner_workflow_for_namespace_enabled? && runner.registration_available?
|
render_404 unless runner.registration_available?
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|
@ -84,8 +83,4 @@ class Projects::RunnersController < Projects::ApplicationController
|
||||||
def runner_params
|
def runner_params
|
||||||
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
|
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_runner_workflow_for_namespace_enabled?
|
|
||||||
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ module Projects
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:ci_variables_pages, current_user)
|
push_frontend_feature_flag(:ci_variables_pages, current_user)
|
||||||
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
|
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
|
||||||
push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace)
|
|
||||||
push_frontend_feature_flag(:frozen_outbound_job_token_scopes, @project)
|
push_frontend_feature_flag(:frozen_outbound_job_token_scopes, @project)
|
||||||
push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project)
|
push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,6 @@ module Mutations
|
||||||
|
|
||||||
parse_gid(**args)
|
parse_gid(**args)
|
||||||
|
|
||||||
check_feature_flag(**args)
|
|
||||||
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -79,28 +77,6 @@ module Mutations
|
||||||
GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project)
|
GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_feature_flag(**args)
|
|
||||||
case args[:runner_type]
|
|
||||||
when 'instance_type'
|
|
||||||
if Feature.disabled?(:create_runner_workflow_for_admin, current_user)
|
|
||||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
|
|
||||||
'`create_runner_workflow_for_admin` feature flag is disabled.'
|
|
||||||
end
|
|
||||||
when 'group_type'
|
|
||||||
namespace = find_object(**args).sync
|
|
||||||
if Feature.disabled?(:create_runner_workflow_for_namespace, namespace)
|
|
||||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
|
|
||||||
'`create_runner_workflow_for_namespace` feature flag is disabled.'
|
|
||||||
end
|
|
||||||
when 'project_type'
|
|
||||||
project = find_object(**args).sync
|
|
||||||
if project && Feature.disabled?(:create_runner_workflow_for_namespace, project.namespace)
|
|
||||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
|
|
||||||
'`create_runner_workflow_for_namespace` feature flag is disabled.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,6 @@ class GlobalPolicy < BasePolicy
|
||||||
condition(:project_bot, scope: :user) { @user&.project_bot? }
|
condition(:project_bot, scope: :user) { @user&.project_bot? }
|
||||||
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
|
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
|
||||||
|
|
||||||
condition(:create_runner_workflow_enabled, scope: :user) do
|
|
||||||
Feature.enabled?(:create_runner_workflow_for_admin, @user)
|
|
||||||
end
|
|
||||||
|
|
||||||
condition(:service_account, scope: :user) { @user&.service_account? }
|
condition(:service_account, scope: :user) { @user&.service_account? }
|
||||||
|
|
||||||
rule { anonymous }.policy do
|
rule { anonymous }.policy do
|
||||||
|
|
@ -128,10 +124,6 @@ class GlobalPolicy < BasePolicy
|
||||||
enable :create_instance_runner
|
enable :create_instance_runner
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { ~create_runner_workflow_enabled }.policy do
|
|
||||||
prevent :create_instance_runner
|
|
||||||
end
|
|
||||||
|
|
||||||
# We can't use `read_statistics` because the user may have different permissions for different projects
|
# We can't use `read_statistics` because the user may have different permissions for different projects
|
||||||
rule { admin }.enable :use_project_statistics_filters
|
rule { admin }.enable :use_project_statistics_filters
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
||||||
with_scope :subject
|
with_scope :subject
|
||||||
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
|
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
|
||||||
|
|
||||||
condition(:create_runner_workflow_enabled) do
|
|
||||||
Feature.enabled?(:create_runner_workflow_for_namespace, group)
|
|
||||||
end
|
|
||||||
|
|
||||||
condition(:achievements_enabled, scope: :subject) do
|
condition(:achievements_enabled, scope: :subject) do
|
||||||
Feature.enabled?(:achievements, @subject)
|
Feature.enabled?(:achievements, @subject)
|
||||||
end
|
end
|
||||||
|
|
@ -375,10 +371,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
|
||||||
enable :admin_observability
|
enable :admin_observability
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { ~create_runner_workflow_enabled }.policy do
|
|
||||||
prevent :create_runner
|
|
||||||
end
|
|
||||||
|
|
||||||
# Should be matched with ProjectPolicy#read_internal_note
|
# Should be matched with ProjectPolicy#read_internal_note
|
||||||
rule { admin | reporter }.enable :read_internal_note
|
rule { admin | reporter }.enable :read_internal_note
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,10 +253,6 @@ class ProjectPolicy < BasePolicy
|
||||||
!Gitlab.config.terraform_state.enabled
|
!Gitlab.config.terraform_state.enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
condition(:create_runner_workflow_enabled) do
|
|
||||||
Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
|
|
||||||
end
|
|
||||||
|
|
||||||
condition(:namespace_catalog_available) { namespace_catalog_available? }
|
condition(:namespace_catalog_available) { namespace_catalog_available? }
|
||||||
|
|
||||||
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
|
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
|
||||||
|
|
@ -886,10 +882,6 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :read_code
|
enable :read_code
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { ~create_runner_workflow_enabled }.policy do
|
|
||||||
prevent :create_runner
|
|
||||||
end
|
|
||||||
|
|
||||||
# Should be matched with GroupPolicy#read_internal_note
|
# Should be matched with GroupPolicy#read_internal_note
|
||||||
rule { admin | can?(:reporter_access) }.enable :read_internal_note
|
rule { admin | can?(:reporter_access) }.enable :read_internal_note
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
- if Feature.enabled?(:edit_user_profile_vue, current_user)
|
- if Feature.enabled?(:edit_user_profile_vue, current_user)
|
||||||
.js-user-profile
|
.js-user-profile
|
||||||
- else
|
- else
|
||||||
= 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|
|
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
|
||||||
.row.js-search-settings-section
|
.js-search-settings-section.gl-pb-6
|
||||||
.col-lg-4.profile-settings-sidebar
|
.profile-settings-sidebar
|
||||||
%h4.gl-mt-0
|
%h4.gl-my-0
|
||||||
= s_("Profiles|Public avatar")
|
= s_("Profiles|Public avatar")
|
||||||
%p
|
%p.gl-text-secondary
|
||||||
- if @user.avatar?
|
- if @user.avatar?
|
||||||
- if gravatar_enabled?
|
- if gravatar_enabled?
|
||||||
= s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
|
= s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
- if current_appearance&.profile_image_guidelines?
|
- if current_appearance&.profile_image_guidelines?
|
||||||
.md
|
.md
|
||||||
= brand_profile_image_guidelines
|
= brand_profile_image_guidelines
|
||||||
.col-lg-8
|
|
||||||
.avatar-image
|
.avatar-image
|
||||||
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
|
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
|
||||||
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
|
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
|
||||||
|
|
@ -44,13 +43,12 @@
|
||||||
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
|
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
|
||||||
method: :delete) do
|
method: :delete) do
|
||||||
= s_("Profiles|Remove avatar")
|
= s_("Profiles|Remove avatar")
|
||||||
.col-lg-12
|
|
||||||
%hr
|
.js-search-settings-section.gl-border-t.gl-py-6
|
||||||
.row.js-search-settings-section
|
.profile-settings-sidebar
|
||||||
.col-lg-4.profile-settings-sidebar
|
%h4.gl-my-0= s_("Profiles|Current status")
|
||||||
%h4.gl-mt-0= s_("Profiles|Current status")
|
%p.gl-text-secondary= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
|
||||||
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
|
.gl-max-w-80
|
||||||
.col-lg-8
|
|
||||||
#js-user-profile-set-status-form
|
#js-user-profile-set-status-form
|
||||||
= f.fields_for :status, @user.status do |status_form|
|
= f.fields_for :status, @user.status do |status_form|
|
||||||
= status_form.hidden_field :emoji, data: { js_name: 'emoji' }
|
= status_form.hidden_field :emoji, data: { js_name: 'emoji' }
|
||||||
|
|
@ -59,30 +57,25 @@
|
||||||
= status_form.hidden_field :clear_status_after,
|
= status_form.hidden_field :clear_status_after,
|
||||||
value: user_clear_status_at(@user),
|
value: user_clear_status_at(@user),
|
||||||
data: { js_name: 'clearStatusAfter' }
|
data: { js_name: 'clearStatusAfter' }
|
||||||
.col-lg-12
|
|
||||||
%hr
|
.user-time-preferences.js-search-settings-section.gl-border-t.gl-py-6
|
||||||
.row.user-time-preferences.js-search-settings-section
|
.profile-settings-sidebar
|
||||||
.col-lg-4.profile-settings-sidebar
|
%h4.gl-my-0= s_("Profiles|Time settings")
|
||||||
%h4.gl-mt-0= s_("Profiles|Time settings")
|
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
|
||||||
%p= s_("Profiles|Set your local time zone.")
|
|
||||||
.col-lg-8
|
|
||||||
= f.label :user_timezone, _("Time zone")
|
= f.label :user_timezone, _("Time zone")
|
||||||
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
|
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
|
||||||
.col-lg-12
|
|
||||||
%hr
|
.js-search-settings-section.gl-border-t.gl-py-6
|
||||||
.row.js-search-settings-section
|
.profile-settings-sidebar
|
||||||
.col-lg-4.profile-settings-sidebar
|
%h4.gl-my-0
|
||||||
%h4.gl-mt-0
|
|
||||||
= s_("Profiles|Main settings")
|
= s_("Profiles|Main settings")
|
||||||
%p
|
%p.gl-text-secondary
|
||||||
= s_("Profiles|This information will appear on your profile.")
|
= s_("Profiles|This information will appear on your profile.")
|
||||||
- if current_user.ldap_user?
|
- if current_user.ldap_user?
|
||||||
= s_("Profiles|Some options are unavailable for LDAP accounts")
|
= s_("Profiles|Some options are unavailable for LDAP accounts")
|
||||||
.col-lg-8
|
.form-group.gl-form-group.rspec-full-name.gl-max-w-80
|
||||||
.row
|
|
||||||
.form-group.gl-form-group.col-md-9.rspec-full-name
|
|
||||||
= render 'profiles/name', form: f, user: @user
|
= render 'profiles/name', form: f, user: @user
|
||||||
.form-group.gl-form-group.col-md-3
|
.form-group.gl-form-group.gl-md-form-input-lg
|
||||||
= f.label :id, s_('Profiles|User ID')
|
= f.label :id, s_('Profiles|User ID')
|
||||||
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
|
= f.text_field :id, class: 'gl-form-input form-control', readonly: true
|
||||||
.form-group.gl-form-group
|
.form-group.gl-form-group
|
||||||
|
|
@ -145,32 +138,30 @@
|
||||||
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
|
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||||
%small.form-text.text-gl-muted
|
%small.form-text.text-gl-muted
|
||||||
= s_("Profiles|Who you represent or work for.")
|
= s_("Profiles|Who you represent or work for.")
|
||||||
.form-group.gl-form-group
|
.form-group.gl-form-group.gl-mb-6.gl-max-w-80
|
||||||
= f.label :bio, s_('Profiles|Bio')
|
= f.label :bio, s_('Profiles|Bio')
|
||||||
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
|
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
|
||||||
%small.form-text.text-gl-muted
|
%small.form-text.text-gl-muted
|
||||||
= s_("Profiles|Tell us about yourself in fewer than 250 characters.")
|
= s_("Profiles|Tell us about yourself in fewer than 250 characters.")
|
||||||
%hr
|
.gl-border-t.gl-pt-6
|
||||||
%fieldset.form-group.gl-form-group
|
%fieldset.form-group.gl-form-group
|
||||||
%legend.col-form-label.col-form-label
|
%legend.col-form-label
|
||||||
= _('Private profile')
|
= _('Private profile')
|
||||||
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
|
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
|
||||||
- private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
|
- private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
|
||||||
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
|
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
|
||||||
%fieldset.form-group.gl-form-group
|
%fieldset.form-group.gl-form-group
|
||||||
%legend.col-form-label.col-form-label
|
%legend.col-form-label
|
||||||
= s_("Profiles|Private contributions")
|
= s_("Profiles|Private contributions")
|
||||||
= f.gitlab_ui_checkbox_component :include_private_contributions,
|
= f.gitlab_ui_checkbox_component :include_private_contributions,
|
||||||
s_('Profiles|Include private contributions on your profile'),
|
s_('Profiles|Include private contributions on your profile'),
|
||||||
help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
|
help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
|
||||||
%fieldset.form-group.gl-form-group
|
%fieldset.form-group.gl-form-group.gl-mb-0
|
||||||
%legend.col-form-label.col-form-label
|
%legend.col-form-label
|
||||||
= s_("Profiles|Achievements")
|
= s_("Profiles|Achievements")
|
||||||
= f.gitlab_ui_checkbox_component :achievements_enabled,
|
= f.gitlab_ui_checkbox_component :achievements_enabled,
|
||||||
s_('Profiles|Display achievements on your profile')
|
s_('Profiles|Display achievements on your profile')
|
||||||
.row.js-hide-when-nothing-matches-search
|
.js-hide-when-nothing-matches-search.gl-border-t.gl-py-6
|
||||||
.col-lg-12
|
|
||||||
%hr
|
|
||||||
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
|
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
|
||||||
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
|
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
|
||||||
= s_('TagsPage|Cancel')
|
= s_('TagsPage|Cancel')
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
.bs-callout.help-callout
|
.bs-callout.help-callout
|
||||||
%p= s_('Runners|These runners are assigned to this project.')
|
%p= s_('Runners|These runners are assigned to this project.')
|
||||||
- if Feature.enabled?(:create_runner_workflow_for_namespace, @project.namespace)
|
|
||||||
- if can?(current_user, :create_runner, @project)
|
- if can?(current_user, :create_runner, @project)
|
||||||
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
|
= render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
|
||||||
= s_('Runners|New project runner')
|
= s_('Runners|New project runner')
|
||||||
|
|
@ -12,17 +11,6 @@
|
||||||
- else
|
- else
|
||||||
= _('Please contact an admin to create runners.')
|
= _('Please contact an admin to create runners.')
|
||||||
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
||||||
- else
|
|
||||||
- if can?(current_user, :register_project_runners, @project)
|
|
||||||
= render partial: 'ci/runner/how_to_setup_runner',
|
|
||||||
locals: { registration_token: @project.runners_token,
|
|
||||||
type: _('project'),
|
|
||||||
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
|
|
||||||
project_path: @project.path_with_namespace,
|
|
||||||
group_path: '' }
|
|
||||||
- else
|
|
||||||
= _('Please contact an admin to register runners.')
|
|
||||||
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
|
||||||
|
|
||||||
%hr
|
%hr
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: create_runner_workflow_for_admin
|
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109497
|
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389269
|
|
||||||
milestone: '15.9'
|
|
||||||
type: development
|
|
||||||
group: group::runner
|
|
||||||
default_enabled: true
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: create_runner_workflow_for_namespace
|
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113535
|
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393919
|
|
||||||
milestone: '15.10'
|
|
||||||
type: development
|
|
||||||
group: group::runner
|
|
||||||
default_enabled: true
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
data_category: optional
|
data_category: optional
|
||||||
key_path: usage_activity_by_stage_monthly.plan.service_desk_enabled_projects
|
key_path: usage_activity_by_stage_monthly.plan.service_desk_enabled_projects
|
||||||
description: Count creator ids from projects with service desk enabled
|
description: Count unique users who created projects (creator_id) with service desk enabled
|
||||||
product_section: ops
|
product_section: ops
|
||||||
product_stage: monitor
|
product_stage: monitor
|
||||||
product_group: respond
|
product_group: respond
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
data_category: optional
|
data_category: optional
|
||||||
key_path: usage_activity_by_stage.plan.service_desk_enabled_projects
|
key_path: usage_activity_by_stage.plan.service_desk_enabled_projects
|
||||||
description: Count creator ids from projects with service desk enabled
|
description: Count unique users who created projects (creator_id) with service desk enabled
|
||||||
product_section: ops
|
product_section: ops
|
||||||
product_stage: monitor
|
product_stage: monitor
|
||||||
product_group: respond
|
product_group: respond
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ development:
|
||||||
cluster_cache:
|
cluster_cache:
|
||||||
cluster:
|
cluster:
|
||||||
- redis://localhost:7001
|
- redis://localhost:7001
|
||||||
|
feature_flag:
|
||||||
|
cluster:
|
||||||
|
- redis://localhost:7001
|
||||||
rate_limiting:
|
rate_limiting:
|
||||||
cluster:
|
cluster:
|
||||||
- redis://localhost:7001
|
- redis://localhost:7001
|
||||||
|
|
@ -17,6 +20,9 @@ test:
|
||||||
cluster_cache:
|
cluster_cache:
|
||||||
cluster:
|
cluster:
|
||||||
- redis://localhost:7001
|
- redis://localhost:7001
|
||||||
|
feature_flag:
|
||||||
|
cluster:
|
||||||
|
- redis://localhost:7001
|
||||||
rate_limiting:
|
rate_limiting:
|
||||||
cluster:
|
cluster:
|
||||||
- redis://localhost:7001
|
- redis://localhost:7001
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,6 @@ concurrency is set to:
|
||||||
When `min_concurrency` is greater than `max_concurrency`, it is treated as
|
When `min_concurrency` is greater than `max_concurrency`, it is treated as
|
||||||
being equal to `max_concurrency`.
|
being equal to `max_concurrency`.
|
||||||
|
|
||||||
You can find example values used by GitLab.com by searching for `concurrency:`
|
|
||||||
in [the Helm charts](https://gitlab.com/gitlab-com/gl-infra/k8s-workloads/gitlab-com/-/blob/master/releases/gitlab/values/gprd.yaml.gotmpl).
|
|
||||||
The values vary according to the work each specific deployment of Sidekiq does.
|
The values vary according to the work each specific deployment of Sidekiq does.
|
||||||
Any other specialized deployments with processes dedicated to specific queues
|
Any other specialized deployments with processes dedicated to specific queues
|
||||||
should have the concurrency tuned according to:
|
should have the concurrency tuned according to:
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,7 @@ If you are using GitLab.com:
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383139) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_admin` [flag](../../administration/feature_flags.md)
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383139) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_admin` [flag](../../administration/feature_flags.md)
|
||||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/389269) in GitLab 16.0.
|
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/389269) in GitLab 16.0.
|
||||||
|
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
|
||||||
FLAG:
|
|
||||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
|
||||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_admin`.
|
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
|
|
@ -195,10 +192,7 @@ Group runners process jobs by using a first in, first out ([FIFO](https://en.wik
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
|
||||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
|
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
|
||||||
|
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
|
||||||
FLAG:
|
|
||||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
|
||||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_namespace`.
|
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
|
|
@ -328,10 +322,7 @@ A fork *does* copy the CI/CD settings of the cloned repository.
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383143) in GitLab 15.10. Deployed behind the `create_runner_workflow_for_namespace` [flag](../../administration/feature_flags.md). Disabled by default.
|
||||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
|
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/393919) in GitLab 16.0.
|
||||||
|
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/415447) in GitLab 16.2. Feature flag `create_runner_workflow_for_admin` removed.
|
||||||
FLAG:
|
|
||||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
|
||||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `create_runner_workflow_for_namespace`.
|
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -863,9 +863,9 @@ PUT /namespaces/:id/subscription_add_on_purchase/:add_on_name
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
|:------------|:--------|:---------|:------------|
|
|:------------|:--------|:---------|:------------|
|
||||||
| `quantity` | integer | yes | Amount of units in the subscription add-on purchase (Example: Number of seats for a code suggestions add-on) |
|
| `quantity` | integer | no | Amount of units in the subscription add-on purchase (Example: Number of seats for a code suggestions add-on) |
|
||||||
| `expires_on` | date | yes | Expiration date of the subscription add-on purchase |
|
| `expires_on` | date | yes | Expiration date of the subscription add-on purchase |
|
||||||
| `purchase_xid` | string | yes | Identifier for the subscription add-on purchase (Example: Subscription name for a code suggestions add-on) |
|
| `purchase_xid` | string | no | Identifier for the subscription add-on purchase (Example: Subscription name for a code suggestions add-on) |
|
||||||
|
|
||||||
Example request:
|
Example request:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
stage: Create
|
stage: Manage
|
||||||
group: Ecosystem
|
group: Authentication and Authorization
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,12 +347,13 @@ If you have difficulty during the renewal process, contact the
|
||||||
|
|
||||||
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
|
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
|
||||||
|
|
||||||
To change the contacts:
|
For information about how to transfer ownership of the Customers Portal account to another person, see
|
||||||
|
[Change account owner information](../customers_portal.md#change-account-owner-information).
|
||||||
|
|
||||||
|
To add a secondary contact for your subscription:
|
||||||
|
|
||||||
1. Ensure an account exists in the
|
1. Ensure an account exists in the
|
||||||
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
|
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
|
||||||
1. Verify you have access to at least one of
|
|
||||||
[these requirements](https://about.gitlab.com/handbook/support/license-and-renewals/workflows/customersdot/associating_purchases.html).
|
|
||||||
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
|
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
|
||||||
|
|
||||||
## Compute
|
## Compute
|
||||||
|
|
|
||||||
|
|
@ -428,12 +428,13 @@ The new tier takes effect when the new license is activated.
|
||||||
|
|
||||||
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
|
Contacts can renew a subscription, cancel a subscription, or transfer the subscription to a different namespace.
|
||||||
|
|
||||||
To change the contacts:
|
For information about how to transfer ownership of the Customers Portal account to another person, see
|
||||||
|
[Change account owner information](../customers_portal.md#change-account-owner-information).
|
||||||
|
|
||||||
|
To add a secondary contact for your subscription:
|
||||||
|
|
||||||
1. Ensure an account exists in the
|
1. Ensure an account exists in the
|
||||||
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
|
[Customers Portal](https://customers.gitlab.com/customers/sign_in) for the user you want to add.
|
||||||
1. Verify you have access to at least one of
|
|
||||||
[these requirements](https://about.gitlab.com/handbook/support/license-and-renewals/workflows/customersdot/associating_purchases.html).
|
|
||||||
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
|
1. [Create a ticket with the Support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293). Include any relevant material in your request.
|
||||||
|
|
||||||
## Subscription expiry
|
## Subscription expiry
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ You can use push options to skip a CI/CD pipeline, or pass CI/CD variables.
|
||||||
| ------------------------------ | ------------------------------------------------------------------------------------------- |---------------------- |
|
| ------------------------------ | ------------------------------------------------------------------------------------------- |---------------------- |
|
||||||
| `ci.skip` | Do not create a CI pipeline for the latest push. Only skips branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). This does not skip pipelines for CI integrations, such as Jenkins. | [11.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) |
|
| `ci.skip` | Do not create a CI pipeline for the latest push. Only skips branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). This does not skip pipelines for CI integrations, such as Jenkins. | [11.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) |
|
||||||
| `ci.variable="<name>=<value>"` | Provide [CI/CD variables](../../ci/variables/index.md) to be used in a CI pipeline, if one is created due to the push. Only passes variables to branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). | [12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/27983) |
|
| `ci.variable="<name>=<value>"` | Provide [CI/CD variables](../../ci/variables/index.md) to be used in a CI pipeline, if one is created due to the push. Only passes variables to branch pipelines and not [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md). | [12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/27983) |
|
||||||
|
| `integrations.skip_ci` | Skip push events for CI integrations, such as Atlassian Bamboo, Buildkite, Drone, Jenkins, and JetBrains TeamCity. | [16.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123837) |
|
||||||
|
|
||||||
An example of using `ci.skip`:
|
An example of using `ci.skip`:
|
||||||
|
|
||||||
|
|
@ -50,6 +51,12 @@ An example of passing some CI/CD variables for a pipeline:
|
||||||
git push -o ci.variable="MAX_RETRIES=10" -o ci.variable="MAX_TIME=600"
|
git push -o ci.variable="MAX_RETRIES=10" -o ci.variable="MAX_TIME=600"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
An example of using `integrations.skip_ci`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git push -o integrations.skip_ci
|
||||||
|
```
|
||||||
|
|
||||||
## Push options for merge requests
|
## Push options for merge requests
|
||||||
|
|
||||||
You can use Git push options to perform certain actions for merge requests at the same
|
You can use Git push options to perform certain actions for merge requests at the same
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,11 @@ module Gitlab
|
||||||
with_redis do |redis|
|
with_redis do |redis|
|
||||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||||
if Gitlab::Redis::ClusterUtil.cluster?(redis)
|
if Gitlab::Redis::ClusterUtil.cluster?(redis)
|
||||||
|
redis.with_readonly_pipeline do
|
||||||
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
|
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
|
||||||
keys.each { |key| pipeline.get(key) }
|
keys.each { |key| pipeline.get(key) }
|
||||||
end
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
redis.mget(keys)
|
redis.mget(keys)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,7 @@ module Gitlab
|
||||||
#
|
#
|
||||||
# @return [self]
|
# @return [self]
|
||||||
def self.build(project)
|
def self.build(project)
|
||||||
strategy =
|
strategy = OpenApiStrategy.new(project)
|
||||||
if Feature.enabled?(:gitlab_error_tracking, project)
|
|
||||||
OpenApiStrategy.new(project)
|
|
||||||
else
|
|
||||||
ActiveRecordStrategy.new(project)
|
|
||||||
end
|
|
||||||
|
|
||||||
new(strategy)
|
new(strategy)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Gitlab
|
|
||||||
module ErrorTracking
|
|
||||||
class ErrorRepository
|
|
||||||
class ActiveRecordStrategy
|
|
||||||
def initialize(project)
|
|
||||||
@project = project
|
|
||||||
end
|
|
||||||
|
|
||||||
def report_error(
|
|
||||||
name:, description:, actor:, platform:,
|
|
||||||
environment:, level:, occurred_at:, payload:
|
|
||||||
)
|
|
||||||
error = project_errors.report_error(
|
|
||||||
name: name, # Example: ActionView::MissingTemplate
|
|
||||||
description: description, # Example: Missing template posts/show in...
|
|
||||||
actor: actor, # Example: PostsController#show
|
|
||||||
platform: platform, # Example: ruby
|
|
||||||
timestamp: occurred_at
|
|
||||||
)
|
|
||||||
|
|
||||||
# The payload field contains all the data on error including stacktrace in jsonb.
|
|
||||||
# Together with occurred_at these are 2 main attributes that we need to save here.
|
|
||||||
error.events.create!(
|
|
||||||
environment: environment,
|
|
||||||
description: description,
|
|
||||||
level: level,
|
|
||||||
occurred_at: occurred_at,
|
|
||||||
payload: payload
|
|
||||||
)
|
|
||||||
rescue ActiveRecord::ActiveRecordError => e
|
|
||||||
handle_exceptions(e)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_error(id)
|
|
||||||
project_error(id).to_sentry_detailed_error
|
|
||||||
rescue ActiveRecord::ActiveRecordError => e
|
|
||||||
handle_exceptions(e)
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_errors(filters:, query:, sort:, limit:, cursor:)
|
|
||||||
errors = project_errors
|
|
||||||
errors = filter_by_status(errors, filters[:status])
|
|
||||||
errors = sort(errors, sort)
|
|
||||||
errors = errors.keyset_paginate(cursor: cursor, per_page: limit)
|
|
||||||
# query is not supported
|
|
||||||
|
|
||||||
pagination = ErrorRepository::Pagination.new(errors.cursor_for_next_page, errors.cursor_for_previous_page)
|
|
||||||
|
|
||||||
[errors.map(&:to_sentry_error), pagination]
|
|
||||||
end
|
|
||||||
|
|
||||||
def last_event_for(id)
|
|
||||||
project_error(id).last_event&.to_sentry_error_event
|
|
||||||
rescue ActiveRecord::ActiveRecordError => e
|
|
||||||
handle_exceptions(e)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_error(id, **attributes)
|
|
||||||
project_error(id).update(attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
def dsn_url(public_key)
|
|
||||||
gitlab = Settings.gitlab
|
|
||||||
|
|
||||||
custom_port = Settings.gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
|
|
||||||
|
|
||||||
base_url = [
|
|
||||||
gitlab.protocol,
|
|
||||||
"://",
|
|
||||||
public_key,
|
|
||||||
'@',
|
|
||||||
gitlab.host,
|
|
||||||
custom_port,
|
|
||||||
gitlab.relative_url_root
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
"#{base_url}/api/v4/error_tracking/collector/#{project.id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
attr_reader :project
|
|
||||||
|
|
||||||
def project_errors
|
|
||||||
::ErrorTracking::Error.where(project: project) # rubocop:disable CodeReuse/ActiveRecord
|
|
||||||
end
|
|
||||||
|
|
||||||
def project_error(id)
|
|
||||||
project_errors.find(id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_by_status(errors, status)
|
|
||||||
return errors unless ::ErrorTracking::Error.statuses.key?(status)
|
|
||||||
|
|
||||||
errors.for_status(status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def sort(errors, sort)
|
|
||||||
return errors.order_id_desc unless sort
|
|
||||||
|
|
||||||
errors.sort_by_attribute(sort)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_exceptions(exception)
|
|
||||||
case exception
|
|
||||||
when ActiveRecord::RecordInvalid
|
|
||||||
raise RecordInvalidError, exception.message
|
|
||||||
else
|
|
||||||
raise DatabaseError, exception.message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -11,6 +11,7 @@ module Gitlab
|
||||||
|
|
||||||
Gitlab::Redis::Cache.with do |r|
|
Gitlab::Redis::Cache.with do |r|
|
||||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||||
|
r.with_readonly_pipeline do
|
||||||
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
|
Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
|
||||||
subjects.each do |subject|
|
subjects.each do |subject|
|
||||||
results[subject.cache_key] = new(subject).read(pipeline)
|
results[subject.cache_key] = new(subject).read(pipeline)
|
||||||
|
|
@ -18,6 +19,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
results
|
results
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,13 @@ module Gitlab
|
||||||
keys = names.map { |name| normalize_key(name, options) }
|
keys = names.map { |name| normalize_key(name, options) }
|
||||||
|
|
||||||
values = failsafe(:patched_read_multi_mget, returning: {}) do
|
values = failsafe(:patched_read_multi_mget, returning: {}) do
|
||||||
redis.with { |c| pipeline_mget(c, keys) }
|
redis.with do |c|
|
||||||
|
if c.is_a?(Gitlab::Redis::MultiStore)
|
||||||
|
c.with_readonly_pipeline { pipeline_mget(c, keys) }
|
||||||
|
else
|
||||||
|
pipeline_mget(c, keys)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
names.zip(values).each_with_object({}) do |(name, value), results|
|
names.zip(values).each_with_object({}) do |(name, value), results|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class NestedReadonlyPipelineError < StandardError
|
||||||
|
def message
|
||||||
|
'Nested use of with_readonly_pipeline is detected.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :primary_store, :secondary_store, :instance_name
|
attr_reader :primary_store, :secondary_store, :instance_name
|
||||||
|
|
||||||
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis default_store.'
|
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis default_store.'
|
||||||
|
|
@ -100,6 +106,25 @@ module Gitlab
|
||||||
validate_stores!
|
validate_stores!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Pipelines are sent to both instances by default since
|
||||||
|
# they could execute both read and write commands.
|
||||||
|
#
|
||||||
|
# But for pipelines that only consists of read commands, this method
|
||||||
|
# can be used to scope the pipeline and send it only to the default store.
|
||||||
|
def with_readonly_pipeline
|
||||||
|
raise NestedReadonlyPipelineError if readonly_pipeline?
|
||||||
|
|
||||||
|
Thread.current[:readonly_pipeline] = true
|
||||||
|
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
Thread.current[:readonly_pipeline] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def readonly_pipeline?
|
||||||
|
Thread.current[:readonly_pipeline].present?
|
||||||
|
end
|
||||||
|
|
||||||
# rubocop:disable GitlabSecurity/PublicSend
|
# rubocop:disable GitlabSecurity/PublicSend
|
||||||
READ_COMMANDS.each do |name|
|
READ_COMMANDS.each do |name|
|
||||||
define_method(name) do |*args, **kwargs, &block|
|
define_method(name) do |*args, **kwargs, &block|
|
||||||
|
|
@ -123,7 +148,7 @@ module Gitlab
|
||||||
|
|
||||||
PIPELINED_COMMANDS.each do |name|
|
PIPELINED_COMMANDS.each do |name|
|
||||||
define_method(name) do |*args, **kwargs, &block|
|
define_method(name) do |*args, **kwargs, &block|
|
||||||
if use_primary_and_secondary_stores?
|
if use_primary_and_secondary_stores? && !readonly_pipeline?
|
||||||
pipelined_both(name, *args, **kwargs, &block)
|
pipelined_both(name, *args, **kwargs, &block)
|
||||||
else
|
else
|
||||||
send_command(default_store, name, *args, **kwargs, &block)
|
send_command(default_store, name, *args, **kwargs, &block)
|
||||||
|
|
|
||||||
|
|
@ -34249,9 +34249,6 @@ msgstr ""
|
||||||
msgid "Please contact an admin to create runners."
|
msgid "Please contact an admin to create runners."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Please contact an admin to register runners."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Please contact your GitLab administrator if you think this is an error."
|
msgid "Please contact your GitLab administrator if you think this is an error."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -39678,9 +39675,6 @@ msgstr ""
|
||||||
msgid "Runners|Filter projects"
|
msgid "Runners|Filter projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Runners|Get started with runners"
|
msgid "Runners|Get started with runners"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(response).to render_template(:new)
|
expect(response).to render_template(:new)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_admin is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns :not_found' do
|
|
||||||
get :new
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#register' do
|
describe '#register' do
|
||||||
|
|
@ -78,20 +66,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_admin is disabled' do
|
|
||||||
let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns :not_found' do
|
|
||||||
register
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#edit' do
|
describe '#edit' do
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,6 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#new' do
|
describe '#new' do
|
||||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is owner' do
|
context 'when user is owner' do
|
||||||
before do
|
before do
|
||||||
group.add_owner(user)
|
group.add_owner(user)
|
||||||
|
|
@ -96,33 +91,9 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is owner' do
|
|
||||||
before do
|
|
||||||
group.add_owner(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a 404' do
|
|
||||||
get :new, params: { group_id: group }
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#register' do
|
describe '#register' do
|
||||||
subject(:register) { get :register, params: { group_id: group, id: new_runner } }
|
subject(:register) { get :register, params: { group_id: group, id: new_runner } }
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is owner' do
|
context 'when user is owner' do
|
||||||
before do
|
before do
|
||||||
group.add_owner(user)
|
group.add_owner(user)
|
||||||
|
|
@ -167,27 +138,6 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
let_it_be(:new_runner) { create(:ci_runner, :group, groups: [group], registration_type: :authenticated_user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is owner' do
|
|
||||||
before do
|
|
||||||
group.add_owner(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns :not_found' do
|
|
||||||
register
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#show' do
|
describe '#show' do
|
||||||
context 'when user is owner' do
|
context 'when user is owner' do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,6 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is maintainer' do
|
context 'when user is maintainer' do
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
|
|
@ -59,33 +54,9 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is maintainer' do
|
|
||||||
before do
|
|
||||||
project.add_maintainer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a 404' do
|
|
||||||
get :new, params: params
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#register' do
|
describe '#register' do
|
||||||
subject(:register) { get :register, params: { namespace_id: project.namespace, project_id: project, id: new_runner } }
|
subject(:register) { get :register, params: { namespace_id: project.namespace, project_id: project, id: new_runner } }
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is maintainer' do
|
context 'when user is maintainer' do
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
|
|
@ -130,27 +101,6 @@ RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
let_it_be(:new_runner) { create(:ci_runner, :project, projects: [project], registration_type: :authenticated_user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is maintainer' do
|
|
||||||
before do
|
|
||||||
project.add_maintainer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns :not_found' do
|
|
||||||
register
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#update' do
|
describe '#update' do
|
||||||
it 'updates the runner and ticks the queue' do
|
it 'updates the runner and ticks the queue' do
|
||||||
new_desc = runner.description.swapcase
|
new_desc = runner.description.swapcase
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "runners registration" do
|
describe "runners registration" do
|
||||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(create_runner_workflow_for_admin: true)
|
|
||||||
|
|
||||||
visit admin_runners_path
|
visit admin_runners_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -45,20 +42,6 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
|
||||||
|
|
||||||
visit admin_runners_path
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like "shows and resets runner registration token" do
|
|
||||||
let(:dropdown_text) { s_('Runners|Register an instance runner') }
|
|
||||||
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when there are runners" do
|
context "when there are runners" do
|
||||||
context "with an instance runner" do
|
context "with an instance runner" do
|
||||||
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
|
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,6 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Group runners page", :js do
|
describe "Group runners page", :js do
|
||||||
describe "legacy runners registration" do
|
|
||||||
let_it_be(:group_registration_token) { group.runners_token }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
|
|
||||||
visit group_runners_path(group)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like "shows and resets runner registration token" do
|
|
||||||
let(:dropdown_text) { 'Register a group runner' }
|
|
||||||
let(:registration_token) { group_registration_token }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with no runners" do
|
context "with no runners" do
|
||||||
before do
|
before do
|
||||||
visit group_runners_path(group)
|
visit group_runners_path(group)
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,11 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
|
||||||
stub_feature_flags(project_runners_vue_ui: false)
|
stub_feature_flags(project_runners_vue_ui: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user views runners page' do
|
context 'with user as project maintainer' do
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be(:project) { create(:project).tap { |project| project.add_maintainer(user) } }
|
||||||
|
|
||||||
|
context 'when user views runners page', :js do
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is enabled', :js do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
|
||||||
|
|
||||||
visit project_runners_path(project)
|
visit project_runners_path(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -38,11 +32,8 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user views new runner page' do
|
context 'when user views new runner page', :js do
|
||||||
context 'when create_runner_workflow_for_namespace is enabled', :js do
|
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
|
||||||
|
|
||||||
visit new_project_runner_path(project)
|
visit new_project_runner_path(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -57,43 +48,6 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'user can see a link with instructions on how to install GitLab Runner' do
|
|
||||||
visit project_runners_path(project)
|
|
||||||
|
|
||||||
expect(page).to have_link('Install GitLab Runner and ensure it\'s running.', href: "https://docs.gitlab.com/runner/install/")
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'runners registration token' do
|
|
||||||
let!(:token) { project.runners_token }
|
|
||||||
|
|
||||||
before do
|
|
||||||
visit project_runners_path(project)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'has a registration token' do
|
|
||||||
expect(page.find('#registration_token')).to have_content(token)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'reload registration token' do
|
|
||||||
let(:page_token) { find('#registration_token').text }
|
|
||||||
|
|
||||||
before do
|
|
||||||
click_link 'Reset registration token'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'changes registration token' do
|
|
||||||
expect(page_token).not_to eq token
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a project has enabled shared_runners' do
|
context 'when a project has enabled shared_runners' do
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be(:project) { create(:project) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import ConnectionMonitor from '~/actioncable_connection_monitor';
|
|
||||||
|
|
||||||
describe('ConnectionMonitor', () => {
|
|
||||||
let monitor;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
monitor = new ConnectionMonitor({});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getPollInterval', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Math.originalRandom = Math.random;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
Math.random = Math.originalRandom;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor;
|
|
||||||
const backoffFactor = 1 + reconnectionBackoffRate;
|
|
||||||
const ms = 1000;
|
|
||||||
|
|
||||||
it('uses exponential backoff', () => {
|
|
||||||
Math.random = () => 0;
|
|
||||||
|
|
||||||
monitor.reconnectAttempts = 0;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
|
|
||||||
|
|
||||||
monitor.reconnectAttempts = 1;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms);
|
|
||||||
|
|
||||||
monitor.reconnectAttempts = 2;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(
|
|
||||||
staleThreshold * backoffFactor * backoffFactor * ms,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('caps exponential backoff after some number of reconnection attempts', () => {
|
|
||||||
Math.random = () => 0;
|
|
||||||
monitor.reconnectAttempts = 42;
|
|
||||||
const cappedPollInterval = monitor.getPollInterval();
|
|
||||||
|
|
||||||
monitor.reconnectAttempts = 9001;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(cappedPollInterval);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses 100% jitter when 0 reconnection attempts', () => {
|
|
||||||
Math.random = () => 0;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
|
|
||||||
|
|
||||||
Math.random = () => 0.5;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => {
|
|
||||||
monitor.reconnectAttempts = 1;
|
|
||||||
|
|
||||||
Math.random = () => 0.25;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(
|
|
||||||
staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms,
|
|
||||||
);
|
|
||||||
|
|
||||||
Math.random = () => 0.5;
|
|
||||||
expect(monitor.getPollInterval()).toEqual(
|
|
||||||
staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies jitter after capped exponential backoff', () => {
|
|
||||||
monitor.reconnectAttempts = 9001;
|
|
||||||
|
|
||||||
Math.random = () => 0;
|
|
||||||
const withoutJitter = monitor.getPollInterval();
|
|
||||||
Math.random = () => 0.5;
|
|
||||||
const withJitter = monitor.getPollInterval();
|
|
||||||
|
|
||||||
expect(withJitter).toBeGreaterThan(withoutJitter);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -41,7 +41,9 @@ describe('RegistrationDropdown', () => {
|
||||||
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
|
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
|
||||||
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
|
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
|
||||||
const findRegistrationTokenInput = () =>
|
const findRegistrationTokenInput = () =>
|
||||||
wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
|
wrapper.findByLabelText(
|
||||||
|
`${RegistrationToken.i18n.registrationToken} ${RegistrationDropdown.i18n.supportForRegistrationTokensDeprecated}`,
|
||||||
|
);
|
||||||
const findTokenResetDropdownItem = () =>
|
const findTokenResetDropdownItem = () =>
|
||||||
wrapper.findComponent(RegistrationTokenResetDropdownItem);
|
wrapper.findComponent(RegistrationTokenResetDropdownItem);
|
||||||
const findModal = () => wrapper.findComponent(GlModal);
|
const findModal = () => wrapper.findComponent(GlModal);
|
||||||
|
|
@ -107,12 +109,12 @@ describe('RegistrationDropdown', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
expect(findDropdown().props()).toMatchObject({
|
expect(findDropdown().props()).toMatchObject({
|
||||||
category: 'primary',
|
category: 'tertiary',
|
||||||
variant: 'confirm',
|
variant: 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findDropdown().attributes()).toMatchObject({
|
expect(findDropdown().attributes()).toMatchObject({
|
||||||
toggleclass: '',
|
toggleclass: 'gl-px-3!',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -217,14 +219,9 @@ describe('RegistrationDropdown', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe('When showing a "deprecated" warning', () => {
|
||||||
{ createRunnerWorkflowForAdmin: true },
|
|
||||||
{ createRunnerWorkflowForNamespace: true },
|
|
||||||
])('When showing a "deprecated" warning', (glFeatures) => {
|
|
||||||
it('passes deprecated variant props and attributes to dropdown', () => {
|
it('passes deprecated variant props and attributes to dropdown', () => {
|
||||||
createComponent({
|
createComponent();
|
||||||
provide: { glFeatures },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findDropdown().props()).toMatchObject({
|
expect(findDropdown().props()).toMatchObject({
|
||||||
category: 'tertiary',
|
category: 'tertiary',
|
||||||
|
|
@ -249,12 +246,7 @@ describe('RegistrationDropdown', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows warning text', () => {
|
it('shows warning text', () => {
|
||||||
createComponent(
|
createComponent({}, mountExtended);
|
||||||
{
|
|
||||||
provide: { glFeatures },
|
|
||||||
},
|
|
||||||
mountExtended,
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
|
const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
|
||||||
|
|
||||||
|
|
@ -262,12 +254,7 @@ describe('RegistrationDropdown', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('button shows ellipsis icon', () => {
|
it('button shows ellipsis icon', () => {
|
||||||
createComponent(
|
createComponent({}, mountExtended);
|
||||||
{
|
|
||||||
provide: { glFeatures },
|
|
||||||
},
|
|
||||||
mountExtended,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
|
expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
|
||||||
expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
|
expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
I18N_CREATE_RUNNER_LINK,
|
I18N_CREATE_RUNNER_LINK,
|
||||||
I18N_STILL_USING_REGISTRATION_TOKENS,
|
I18N_STILL_USING_REGISTRATION_TOKENS,
|
||||||
I18N_CONTACT_ADMIN_TO_REGISTER,
|
I18N_CONTACT_ADMIN_TO_REGISTER,
|
||||||
I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
|
|
||||||
I18N_NO_RESULTS,
|
I18N_NO_RESULTS,
|
||||||
I18N_EDIT_YOUR_SEARCH,
|
I18N_EDIT_YOUR_SEARCH,
|
||||||
} from '~/ci/runner/constants';
|
} from '~/ci/runner/constants';
|
||||||
|
|
@ -59,14 +58,6 @@ describe('RunnerListEmptyState', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when search is not filtered', () => {
|
describe('when search is not filtered', () => {
|
||||||
describe.each([
|
|
||||||
{ createRunnerWorkflowForAdmin: true },
|
|
||||||
{ createRunnerWorkflowForNamespace: true },
|
|
||||||
])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
|
|
||||||
beforeEach(() => {
|
|
||||||
glFeatures = currentGlFeatures;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each`
|
describe.each`
|
||||||
newRunnerPath | registrationToken | expectedMessages
|
newRunnerPath | registrationToken | expectedMessages
|
||||||
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
|
${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
|
||||||
|
|
@ -149,50 +140,6 @@ describe('RunnerListEmptyState', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when createRunnerWorkflow is disabled', () => {
|
|
||||||
describe('when there is a registration token', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({
|
|
||||||
props: {
|
|
||||||
registrationToken: mockRegistrationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an illustration', () => {
|
|
||||||
expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('opens a runner registration instructions modal with a link', () => {
|
|
||||||
const { value } = getBinding(findLink().element, 'gl-modal');
|
|
||||||
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays text with registration instructions', () => {
|
|
||||||
expectTitleToBe(I18N_GET_STARTED);
|
|
||||||
|
|
||||||
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when there is no registration token', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({ props: { registrationToken: null } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays "contact admin" text', () => {
|
|
||||||
expectTitleToBe(I18N_GET_STARTED);
|
|
||||||
|
|
||||||
expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has no registration instructions link', () => {
|
|
||||||
expect(findLink().exists()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when search is filtered', () => {
|
describe('when search is filtered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({ props: { isSearchFiltered: true } });
|
createComponent({ props: { isSearchFiltered: true } });
|
||||||
|
|
|
||||||
|
|
@ -483,35 +483,15 @@ describe('GroupRunnersApp', () => {
|
||||||
expect(findRegistrationDropdown().exists()).toBe(true);
|
expect(findRegistrationDropdown().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when create_runner_workflow_for_namespace is enabled', () => {
|
it('shows the create runner button', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
props: {
|
props: {
|
||||||
newRunnerPath,
|
newRunnerPath,
|
||||||
},
|
},
|
||||||
provide: {
|
|
||||||
glFeatures: {
|
|
||||||
createRunnerWorkflowForNamespace: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
|
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when create_runner_workflow_for_namespace is disabled', () => {
|
|
||||||
createComponent({
|
|
||||||
props: {
|
|
||||||
newRunnerPath,
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
glFeatures: {
|
|
||||||
createRunnerWorkflowForNamespace: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findNewRunnerBtn().exists()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user has no permission to register group runner', () => {
|
describe('when user has no permission to register group runner', () => {
|
||||||
|
|
@ -524,16 +504,11 @@ describe('GroupRunnersApp', () => {
|
||||||
expect(findRegistrationDropdown().exists()).toBe(false);
|
expect(findRegistrationDropdown().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when create_runner_workflow_for_namespace is enabled', () => {
|
it('shows the create runner button', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
props: {
|
props: {
|
||||||
newRunnerPath: null,
|
newRunnerPath: null,
|
||||||
},
|
},
|
||||||
provide: {
|
|
||||||
glFeatures: {
|
|
||||||
createRunnerWorkflowForNamespace: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findNewRunnerBtn().exists()).toBe(false);
|
expect(findNewRunnerBtn().exists()).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
|
||||||
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
|
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
|
||||||
import eventHub from '~/issues/show/event_hub';
|
import eventHub from '~/issues/show/event_hub';
|
||||||
|
|
||||||
|
jest.mock('~/issues/show/event_hub');
|
||||||
|
|
||||||
describe('TaskListItemActions component', () => {
|
describe('TaskListItemActions component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
|
@ -37,16 +39,12 @@ describe('TaskListItemActions component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits event when `Convert to task` dropdown item is clicked', () => {
|
it('emits event when `Convert to task` dropdown item is clicked', () => {
|
||||||
jest.spyOn(eventHub, '$emit');
|
|
||||||
|
|
||||||
findConvertToTaskItem().vm.$emit('action');
|
findConvertToTaskItem().vm.$emit('action');
|
||||||
|
|
||||||
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
|
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits event when `Delete` dropdown item is clicked', () => {
|
it('emits event when `Delete` dropdown item is clicked', () => {
|
||||||
jest.spyOn(eventHub, '$emit');
|
|
||||||
|
|
||||||
findDeleteItem().vm.$emit('action');
|
findDeleteItem().vm.$emit('action');
|
||||||
|
|
||||||
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
|
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,37 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { nextTick } from 'vue';
|
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
|
import GfmAutoComplete from '~/gfm_auto_complete';
|
||||||
import { TYPE_ISSUE } from '~/issues/constants';
|
import { TYPE_ISSUE } from '~/issues/constants';
|
||||||
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
|
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
|
||||||
import { PathIdSeparator } from '~/related_issues/constants';
|
import { PathIdSeparator } from '~/related_issues/constants';
|
||||||
|
|
||||||
jest.mock('ee_else_ce/gfm_auto_complete', () => {
|
jest.mock('~/gfm_auto_complete');
|
||||||
return function gfmAutoComplete() {
|
|
||||||
return {
|
|
||||||
constructor() {},
|
|
||||||
setup() {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('RelatedIssuableInput', () => {
|
describe('RelatedIssuableInput', () => {
|
||||||
let propsData;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
const autoCompleteSources = {
|
||||||
propsData = {
|
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mountComponent = (props = {}) => {
|
||||||
|
wrapper = shallowMount(RelatedIssuableInput, {
|
||||||
|
propsData: {
|
||||||
inputValue: '',
|
inputValue: '',
|
||||||
references: [],
|
references: [],
|
||||||
pathIdSeparator: PathIdSeparator.Issue,
|
pathIdSeparator: PathIdSeparator.Issue,
|
||||||
issuableType: TYPE_ISSUE,
|
issuableType: TYPE_ISSUE,
|
||||||
autoCompleteSources: {
|
autoCompleteSources,
|
||||||
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
|
...props,
|
||||||
},
|
},
|
||||||
};
|
attachTo: document.body,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
describe('autocomplete', () => {
|
describe('autocomplete', () => {
|
||||||
describe('with autoCompleteSources', () => {
|
describe('with autoCompleteSources', () => {
|
||||||
it('shows placeholder text', () => {
|
it('shows placeholder text', () => {
|
||||||
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
|
mountComponent();
|
||||||
|
|
||||||
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
|
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
|
||||||
'Paste issue link or <#issue id>',
|
'Paste issue link or <#issue id>',
|
||||||
|
|
@ -40,51 +39,32 @@ describe('RelatedIssuableInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has GfmAutoComplete', () => {
|
it('has GfmAutoComplete', () => {
|
||||||
const wrapper = shallowMount(RelatedIssuableInput, { propsData });
|
mountComponent();
|
||||||
|
|
||||||
expect(wrapper.vm.gfmAutoComplete).toBeDefined();
|
expect(GfmAutoComplete).toHaveBeenCalledWith(autoCompleteSources);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with no autoCompleteSources', () => {
|
describe('with no autoCompleteSources', () => {
|
||||||
it('shows placeholder text', () => {
|
it('shows placeholder text', () => {
|
||||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
mountComponent({ references: ['!1', '!2'] });
|
||||||
propsData: {
|
|
||||||
...propsData,
|
|
||||||
references: ['!1', '!2'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
|
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not have GfmAutoComplete', () => {
|
it('does not have GfmAutoComplete', () => {
|
||||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
mountComponent({ autoCompleteSources: {} });
|
||||||
propsData: {
|
|
||||||
...propsData,
|
|
||||||
autoCompleteSources: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.vm.gfmAutoComplete).not.toBeDefined();
|
expect(GfmAutoComplete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('focus', () => {
|
describe('focus', () => {
|
||||||
it('when clicking anywhere on the input wrapper it should focus the input', async () => {
|
it('when clicking anywhere on the input wrapper it should focus the input', async () => {
|
||||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
mountComponent({ references: ['foo', 'bar'] });
|
||||||
propsData: {
|
|
||||||
...propsData,
|
|
||||||
references: ['foo', 'bar'],
|
|
||||||
},
|
|
||||||
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
|
|
||||||
attachTo: document.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.find('li').trigger('click');
|
await wrapper.find('li').trigger('click');
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
|
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
|
||||||
});
|
});
|
||||||
|
|
@ -92,11 +72,7 @@ describe('RelatedIssuableInput', () => {
|
||||||
|
|
||||||
describe('when filling in the input', () => {
|
describe('when filling in the input', () => {
|
||||||
it('emits addIssuableFormInput with data', () => {
|
it('emits addIssuableFormInput with data', () => {
|
||||||
const wrapper = shallowMount(RelatedIssuableInput, {
|
mountComponent();
|
||||||
propsData,
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.vm.$emit = jest.fn();
|
|
||||||
|
|
||||||
const newInputValue = 'filling in things';
|
const newInputValue = 'filling in things';
|
||||||
const untouchedRawReferences = newInputValue.trim().split(/\s/);
|
const untouchedRawReferences = newInputValue.trim().split(/\s/);
|
||||||
|
|
@ -108,12 +84,16 @@ describe('RelatedIssuableInput', () => {
|
||||||
input.element.selectionEnd = newInputValue.length;
|
input.element.selectionEnd = newInputValue.length;
|
||||||
input.trigger('input');
|
input.trigger('input');
|
||||||
|
|
||||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', {
|
expect(wrapper.emitted('addIssuableFormInput')).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
newValue: newInputValue,
|
newValue: newInputValue,
|
||||||
caretPos: newInputValue.length,
|
caretPos: newInputValue.length,
|
||||||
untouchedRawReferences,
|
untouchedRawReferences,
|
||||||
touchedReference,
|
touchedReference,
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { GlIcon } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import Vue, { nextTick } from 'vue';
|
import Vue, { nextTick } from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
|
@ -154,6 +155,13 @@ describe('IssuableLockForm', () => {
|
||||||
expect(tooltip).toBeDefined();
|
expect(tooltip).toBeDefined();
|
||||||
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
|
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders lock icon', () => {
|
||||||
|
const icon = findSidebarCollapseIcon().findComponent(GlIcon).props('name');
|
||||||
|
const expected = isLocked ? 'lock' : 'lock-open';
|
||||||
|
|
||||||
|
expect(icon).toBe(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -148,3 +148,16 @@ export const getMrTimelogsQueryResponse = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteTimelogMutationResponse = {
|
||||||
|
data: {
|
||||||
|
timelogDelete: {
|
||||||
|
errors: [],
|
||||||
|
timelog: {
|
||||||
|
id: 'gid://gitlab/Issue/148',
|
||||||
|
issue: {},
|
||||||
|
mergeRequest: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.gr
|
||||||
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
|
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
|
||||||
import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
|
import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
|
||||||
import {
|
import {
|
||||||
|
deleteTimelogMutationResponse,
|
||||||
getIssueTimelogsQueryResponse,
|
getIssueTimelogsQueryResponse,
|
||||||
getMrTimelogsQueryResponse,
|
getMrTimelogsQueryResponse,
|
||||||
timelogToRemoveId,
|
timelogToRemoveId,
|
||||||
|
|
@ -22,7 +23,7 @@ jest.mock('~/alert');
|
||||||
describe('Issuable Time Tracking Report', () => {
|
describe('Issuable Time Tracking Report', () => {
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let fakeApollo;
|
|
||||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||||
const findDeleteButton = () => wrapper.findByTestId('deleteButton');
|
const findDeleteButton = () => wrapper.findByTestId('deleteButton');
|
||||||
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
|
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
|
||||||
|
|
@ -30,30 +31,27 @@ describe('Issuable Time Tracking Report', () => {
|
||||||
|
|
||||||
const mountComponent = ({
|
const mountComponent = ({
|
||||||
queryHandler = successIssueQueryHandler,
|
queryHandler = successIssueQueryHandler,
|
||||||
|
mutationHandler,
|
||||||
issuableType = 'issue',
|
issuableType = 'issue',
|
||||||
mountFunction = shallowMount,
|
mountFunction = shallowMount,
|
||||||
limitToHours = false,
|
limitToHours = false,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
fakeApollo = createMockApollo([
|
|
||||||
[getIssueTimelogsQuery, queryHandler],
|
|
||||||
[getMrTimelogsQuery, queryHandler],
|
|
||||||
]);
|
|
||||||
wrapper = extendedWrapper(
|
wrapper = extendedWrapper(
|
||||||
mountFunction(Report, {
|
mountFunction(Report, {
|
||||||
|
apolloProvider: createMockApollo([
|
||||||
|
[getIssueTimelogsQuery, queryHandler],
|
||||||
|
[getMrTimelogsQuery, queryHandler],
|
||||||
|
[deleteTimelogMutation, mutationHandler],
|
||||||
|
]),
|
||||||
provide: {
|
provide: {
|
||||||
issuableId: 1,
|
issuableId: 1,
|
||||||
issuableType,
|
issuableType,
|
||||||
},
|
},
|
||||||
propsData: { limitToHours, issuableId: '1' },
|
propsData: { limitToHours, issuableId: '1' },
|
||||||
apolloProvider: fakeApollo,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fakeApollo = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render loading spinner', () => {
|
it('should render loading spinner', () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
|
|
@ -135,50 +133,27 @@ describe('Issuable Time Tracking Report', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when clicking on the delete timelog button', () => {
|
describe('when clicking on the delete timelog button', () => {
|
||||||
beforeEach(() => {
|
|
||||||
mountComponent({ mountFunction: mount });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
|
it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
|
||||||
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
|
const mutateSpy = jest.fn().mockResolvedValue(deleteTimelogMutationResponse);
|
||||||
data: {
|
mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
|
||||||
timelogDelete: {
|
|
||||||
errors: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
await findDeleteButton().trigger('click');
|
await findDeleteButton().trigger('click');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(createAlert).not.toHaveBeenCalled();
|
expect(createAlert).not.toHaveBeenCalled();
|
||||||
expect(mutateSpy).toHaveBeenCalledWith({
|
expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
|
||||||
mutation: deleteTimelogMutation,
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
id: timelogToRemoveId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
|
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
|
||||||
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
|
const mutateSpy = jest.fn().mockRejectedValue({});
|
||||||
|
mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
await findDeleteButton().trigger('click');
|
await findDeleteButton().trigger('click');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(mutateSpy).toHaveBeenCalledWith({
|
expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
|
||||||
mutation: deleteTimelogMutation,
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
id: timelogToRemoveId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(createAlert).toHaveBeenCalledWith({
|
expect(createAlert).toHaveBeenCalledWith({
|
||||||
message: 'An error occurred while removing the timelog.',
|
message: 'An error occurred while removing the timelog.',
|
||||||
captureError: true,
|
captureError: true,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ describe('CrmContactToken', () => {
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let fakeApollo;
|
|
||||||
|
|
||||||
const getBaseToken = () => wrapper.findComponent(BaseToken);
|
const getBaseToken = () => wrapper.findComponent(BaseToken);
|
||||||
|
|
||||||
|
|
@ -58,9 +57,8 @@ describe('CrmContactToken', () => {
|
||||||
listeners = {},
|
listeners = {},
|
||||||
queryHandler = searchGroupCrmContactsQueryHandler,
|
queryHandler = searchGroupCrmContactsQueryHandler,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]);
|
|
||||||
|
|
||||||
wrapper = mount(CrmContactToken, {
|
wrapper = mount(CrmContactToken, {
|
||||||
|
apolloProvider: createMockApollo([[searchCrmContactsQuery, queryHandler]]),
|
||||||
propsData: {
|
propsData: {
|
||||||
config,
|
config,
|
||||||
value,
|
value,
|
||||||
|
|
@ -75,14 +73,9 @@ describe('CrmContactToken', () => {
|
||||||
},
|
},
|
||||||
stubs,
|
stubs,
|
||||||
listeners,
|
listeners,
|
||||||
apolloProvider: fakeApollo,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fakeApollo = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('methods', () => {
|
describe('methods', () => {
|
||||||
describe('fetchContacts', () => {
|
describe('fetchContacts', () => {
|
||||||
describe('for groups', () => {
|
describe('for groups', () => {
|
||||||
|
|
@ -160,9 +153,7 @@ describe('CrmContactToken', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls `createAlert` with alert error message when request fails', async () => {
|
it('calls `createAlert` with alert error message when request fails', async () => {
|
||||||
mountComponent();
|
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
|
||||||
|
|
||||||
getBaseToken().vm.$emit('fetch-suggestions');
|
getBaseToken().vm.$emit('fetch-suggestions');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
@ -173,12 +164,9 @@ describe('CrmContactToken', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets `loading` to false when request completes', async () => {
|
it('sets `loading` to false when request completes', async () => {
|
||||||
mountComponent();
|
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
|
||||||
|
|
||||||
getBaseToken().vm.$emit('fetch-suggestions');
|
getBaseToken().vm.$emit('fetch-suggestions');
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(getBaseToken().props('suggestionsLoading')).toBe(false);
|
expect(getBaseToken().props('suggestionsLoading')).toBe(false);
|
||||||
|
|
@ -195,13 +183,7 @@ describe('CrmContactToken', () => {
|
||||||
value: { data: '1' },
|
value: { data: '1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseTokenEl = wrapper.findComponent(BaseToken);
|
expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
|
||||||
|
|
||||||
expect(baseTokenEl.exists()).toBe(true);
|
|
||||||
expect(baseTokenEl.props()).toMatchObject({
|
|
||||||
suggestions: mockCrmContacts,
|
|
||||||
getActiveTokenValue: wrapper.vm.getActiveContact,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
|
it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
|
||||||
|
|
@ -270,12 +252,9 @@ describe('CrmContactToken', () => {
|
||||||
|
|
||||||
it('emits listeners in the base-token', () => {
|
it('emits listeners in the base-token', () => {
|
||||||
const mockInput = jest.fn();
|
const mockInput = jest.fn();
|
||||||
mountComponent({
|
mountComponent({ listeners: { input: mockInput } });
|
||||||
listeners: {
|
|
||||||
input: mockInput,
|
getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
||||||
},
|
|
||||||
});
|
|
||||||
wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
|
||||||
|
|
||||||
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
|
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ describe('CrmOrganizationToken', () => {
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let fakeApollo;
|
|
||||||
|
|
||||||
const getBaseToken = () => wrapper.findComponent(BaseToken);
|
const getBaseToken = () => wrapper.findComponent(BaseToken);
|
||||||
|
|
||||||
|
|
@ -58,8 +57,8 @@ describe('CrmOrganizationToken', () => {
|
||||||
listeners = {},
|
listeners = {},
|
||||||
queryHandler = searchGroupCrmOrganizationsQueryHandler,
|
queryHandler = searchGroupCrmOrganizationsQueryHandler,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]);
|
|
||||||
wrapper = mount(CrmOrganizationToken, {
|
wrapper = mount(CrmOrganizationToken, {
|
||||||
|
apolloProvider: createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]),
|
||||||
propsData: {
|
propsData: {
|
||||||
config,
|
config,
|
||||||
value,
|
value,
|
||||||
|
|
@ -74,14 +73,9 @@ describe('CrmOrganizationToken', () => {
|
||||||
},
|
},
|
||||||
stubs,
|
stubs,
|
||||||
listeners,
|
listeners,
|
||||||
apolloProvider: fakeApollo,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fakeApollo = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('methods', () => {
|
describe('methods', () => {
|
||||||
describe('fetchOrganizations', () => {
|
describe('fetchOrganizations', () => {
|
||||||
describe('for groups', () => {
|
describe('for groups', () => {
|
||||||
|
|
@ -159,9 +153,7 @@ describe('CrmOrganizationToken', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls `createAlert` when request fails', async () => {
|
it('calls `createAlert` when request fails', async () => {
|
||||||
mountComponent();
|
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
|
||||||
|
|
||||||
getBaseToken().vm.$emit('fetch-suggestions');
|
getBaseToken().vm.$emit('fetch-suggestions');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
@ -172,9 +164,7 @@ describe('CrmOrganizationToken', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets `loading` to false when request completes', async () => {
|
it('sets `loading` to false when request completes', async () => {
|
||||||
mountComponent();
|
mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
|
|
||||||
|
|
||||||
getBaseToken().vm.$emit('fetch-suggestions');
|
getBaseToken().vm.$emit('fetch-suggestions');
|
||||||
|
|
||||||
|
|
@ -194,13 +184,7 @@ describe('CrmOrganizationToken', () => {
|
||||||
value: { data: '1' },
|
value: { data: '1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseTokenEl = wrapper.findComponent(BaseToken);
|
expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
|
||||||
|
|
||||||
expect(baseTokenEl.exists()).toBe(true);
|
|
||||||
expect(baseTokenEl.props()).toMatchObject({
|
|
||||||
suggestions: mockCrmOrganizations,
|
|
||||||
getActiveTokenValue: wrapper.vm.getActiveOrganization,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
|
it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
|
||||||
|
|
@ -269,12 +253,9 @@ describe('CrmOrganizationToken', () => {
|
||||||
|
|
||||||
it('emits listeners in the base-token', () => {
|
it('emits listeners in the base-token', () => {
|
||||||
const mockInput = jest.fn();
|
const mockInput = jest.fn();
|
||||||
mountComponent({
|
mountComponent({ listeners: { input: mockInput } });
|
||||||
listeners: {
|
|
||||||
input: mockInput,
|
getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
||||||
},
|
|
||||||
});
|
|
||||||
wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
|
|
||||||
|
|
||||||
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
|
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -90,14 +90,6 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show alert', () => {
|
|
||||||
expect(findAlert().exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show deprecation alert', () => {
|
|
||||||
expect(findAlert('warning').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a number of platforms buttons', () => {
|
it('should contain a number of platforms buttons', () => {
|
||||||
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
|
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
|
||||||
|
|
||||||
|
|
@ -112,19 +104,8 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
|
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each`
|
it('alert is shown', () => {
|
||||||
glFeatures | deprecationAlertExists
|
expect(findAlert('warning').exists()).toBe(true);
|
||||||
${{}} | ${false}
|
|
||||||
${{ createRunnerWorkflowForAdmin: true }} | ${true}
|
|
||||||
${{ createRunnerWorkflowForNamespace: true }} | ${true}
|
|
||||||
`('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({ provide: { glFeatures } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
|
|
||||||
expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the modal resizes', () => {
|
describe('when the modal resizes', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { GlSearchBoxByType } from '@gitlab/ui';
|
import { GlSearchBoxByType } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import Vue, { nextTick } from 'vue';
|
import Vue, { nextTick } from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||||
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
|
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
|
||||||
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
|
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
|
||||||
|
|
@ -44,20 +44,20 @@ Vue.use(VueApollo);
|
||||||
describe('User select dropdown', () => {
|
describe('User select dropdown', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let fakeApollo;
|
let fakeApollo;
|
||||||
|
const hideDropdownMock = jest.fn();
|
||||||
|
|
||||||
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
|
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
|
||||||
const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
|
const findParticipantsLoading = () => wrapper.findByTestId('loading-participants');
|
||||||
const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
|
const findSelectedParticipants = () => wrapper.findAllByTestId('selected-participant');
|
||||||
const findSelectedParticipantByIndex = (index) =>
|
const findSelectedParticipantByIndex = (index) =>
|
||||||
findSelectedParticipants().at(index).findComponent(SidebarParticipant);
|
findSelectedParticipants().at(index).findComponent(SidebarParticipant);
|
||||||
const findUnselectedParticipants = () =>
|
const findUnselectedParticipants = () => wrapper.findAllByTestId('unselected-participant');
|
||||||
wrapper.findAll('[data-testid="unselected-participant"]');
|
|
||||||
const findUnselectedParticipantByIndex = (index) =>
|
const findUnselectedParticipantByIndex = (index) =>
|
||||||
findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
|
findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
|
||||||
const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
|
const findCurrentUser = () => wrapper.findAllByTestId('current-user');
|
||||||
const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
|
const findIssuableAuthor = () => wrapper.findAllByTestId('issuable-author');
|
||||||
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
|
const findUnassignLink = () => wrapper.findByTestId('unassign');
|
||||||
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
|
const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
|
||||||
|
|
||||||
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
|
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
|
||||||
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
|
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
|
||||||
|
|
@ -72,7 +72,7 @@ describe('User select dropdown', () => {
|
||||||
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
|
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
|
||||||
[getIssueParticipantsQuery, participantsQueryHandler],
|
[getIssueParticipantsQuery, participantsQueryHandler],
|
||||||
]);
|
]);
|
||||||
wrapper = shallowMount(UserSelect, {
|
wrapper = shallowMountExtended(UserSelect, {
|
||||||
apolloProvider: fakeApollo,
|
apolloProvider: fakeApollo,
|
||||||
propsData: {
|
propsData: {
|
||||||
headerText: 'test',
|
headerText: 'test',
|
||||||
|
|
@ -97,7 +97,7 @@ describe('User select dropdown', () => {
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
methods: {
|
methods: {
|
||||||
hide: jest.fn(),
|
hide: hideDropdownMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -106,6 +106,7 @@ describe('User select dropdown', () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
fakeApollo = null;
|
fakeApollo = null;
|
||||||
|
hideDropdownMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a loading spinner if participants are loading', () => {
|
it('renders a loading spinner if participants are loading', () => {
|
||||||
|
|
@ -290,12 +291,12 @@ describe('User select dropdown', () => {
|
||||||
value: [assignee],
|
value: [assignee],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
wrapper.vm.$refs.dropdown.hide = jest.fn();
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
findUnassignLink().trigger('click');
|
findUnassignLink().trigger('click');
|
||||||
|
|
||||||
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
|
expect(hideDropdownMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits an empty array after unselecting the only selected assignee', async () => {
|
it('emits an empty array after unselecting the only selected assignee', async () => {
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,7 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach
|
||||||
Gitlab::Redis::Cache.with do |redis|
|
Gitlab::Redis::Cache.with do |redis|
|
||||||
expect(redis).to receive(:pipelined).and_call_original
|
expect(redis).to receive(:pipelined).and_call_original
|
||||||
|
|
||||||
times = Gitlab::Redis::ClusterUtil.cluster?(redis) ? 2 : 1
|
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||||
|
|
||||||
expect_next_instances_of(Redis::PipelinedConnection, times) do |pipeline|
|
|
||||||
expect(pipeline).to receive(:mapped_hmget).once.and_call_original
|
expect(pipeline).to receive(:mapped_hmget).once.and_call_original
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -832,6 +832,30 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when with_readonly_pipeline is used' do
|
||||||
|
it 'calls the default store only' do
|
||||||
|
expect(primary_store).to receive(:send).and_call_original
|
||||||
|
expect(secondary_store).not_to receive(:send).and_call_original
|
||||||
|
|
||||||
|
multi_store.with_readonly_pipeline { subject }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when used in a nested manner' do
|
||||||
|
subject(:nested_subject) do
|
||||||
|
multi_store.with_readonly_pipeline do
|
||||||
|
multi_store.with_readonly_pipeline { subject }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error' do
|
||||||
|
expect { nested_subject }.to raise_error(Gitlab::Redis::MultiStore::NestedReadonlyPipelineError)
|
||||||
|
expect { nested_subject }.to raise_error { |e|
|
||||||
|
expect(e.message).to eq('Nested use of with_readonly_pipeline is detected.')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -694,59 +694,5 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
it { is_expected.to be_disallowed(:create_instance_runner) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'create_runner_workflow_for_admin flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'admin' do
|
|
||||||
let(:current_user) { admin_user }
|
|
||||||
|
|
||||||
context 'when admin mode is enabled', :enable_admin_mode do
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when admin mode is disabled' do
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with project_bot' do
|
|
||||||
let(:current_user) { project_bot }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with migration_bot' do
|
|
||||||
let(:current_user) { migration_bot }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with security_bot' do
|
|
||||||
let(:current_user) { security_bot }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with llm_bot' do
|
|
||||||
let(:current_user) { llm_bot }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runners) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with regular user' do
|
|
||||||
let(:current_user) { user }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with anonymous' do
|
|
||||||
let(:current_user) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_instance_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1483,11 +1483,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'create_runner_workflow_for_namespace flag enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'admin' do
|
context 'admin' do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
|
@ -1566,75 +1561,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with create_runner_workflow_for_namespace flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
|
|
||||||
end
|
|
||||||
|
|
||||||
let_it_be(:other_group) { create(:group) }
|
|
||||||
|
|
||||||
context 'admin' do
|
|
||||||
let(:current_user) { admin }
|
|
||||||
|
|
||||||
context 'when admin mode is enabled', :enable_admin_mode do
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
|
|
||||||
context 'with specific group runner registration disabled' do
|
|
||||||
before do
|
|
||||||
group.runner_registration_enabled = false
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'disallowed when group runner registration disabled'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when admin mode is disabled' do
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with owner' do
|
|
||||||
let(:current_user) { owner }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
|
|
||||||
it_behaves_like 'disallowed when group runner registration disabled'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with maintainer' do
|
|
||||||
let(:current_user) { maintainer }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with reporter' do
|
|
||||||
let(:current_user) { reporter }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with guest' do
|
|
||||||
let(:current_user) { guest }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with developer' do
|
|
||||||
let(:current_user) { developer }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with anonymous' do
|
|
||||||
let(:current_user) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'read_group_all_available_runners' do
|
describe 'read_group_all_available_runners' do
|
||||||
context 'admin' do
|
context 'admin' do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
|
||||||
|
|
@ -2879,11 +2879,6 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'create_runner' do
|
describe 'create_runner' do
|
||||||
context 'create_runner_workflow_for_namespace flag enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'admin' do
|
context 'admin' do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
|
@ -2965,93 +2960,6 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'create_runner_workflow_for_namespace flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'admin' do
|
|
||||||
let(:current_user) { admin }
|
|
||||||
|
|
||||||
context 'when admin mode is enabled', :enable_admin_mode do
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
|
|
||||||
context 'with project runner registration disabled' do
|
|
||||||
before do
|
|
||||||
stub_application_setting(valid_runner_registrars: ['group'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with specific project runner registration disabled' do
|
|
||||||
before do
|
|
||||||
project.update!(runner_registration_enabled: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when admin mode is disabled' do
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with owner' do
|
|
||||||
let(:current_user) { owner }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
|
|
||||||
context 'with project runner registration disabled' do
|
|
||||||
before do
|
|
||||||
stub_application_setting(valid_runner_registrars: ['group'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with specific project runner registration disabled' do
|
|
||||||
before do
|
|
||||||
project.update!(runner_registration_enabled: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with maintainer' do
|
|
||||||
let(:current_user) { maintainer }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with reporter' do
|
|
||||||
let(:current_user) { reporter }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with guest' do
|
|
||||||
let(:current_user) { guest }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with developer' do
|
|
||||||
let(:current_user) { developer }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with anonymous' do
|
|
||||||
let(:current_user) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to be_disallowed(:create_runner) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'admin_project_runners' do
|
describe 'admin_project_runners' do
|
||||||
context 'admin' do
|
context 'admin' do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
|
||||||
|
|
@ -95,18 +95,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an error' do
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
|
||||||
|
|
||||||
expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_examples 'when runner is created successfully' do
|
shared_examples 'when runner is created successfully' do
|
||||||
it do
|
it do
|
||||||
expected_args = { user: current_user, params: anything }
|
expected_args = { user: current_user, params: anything }
|
||||||
|
|
@ -139,18 +127,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
||||||
context 'when user has permissions', :enable_admin_mode do
|
context 'when user has permissions', :enable_admin_mode do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
context 'when :create_runner_workflow_for_admin feature flag is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_admin: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns an error' do
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
|
||||||
|
|
||||||
expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'when runner is created successfully'
|
it_behaves_like 'when runner is created successfully'
|
||||||
it_behaves_like 'when model is invalid returns error'
|
it_behaves_like 'when model is invalid returns error'
|
||||||
end
|
end
|
||||||
|
|
@ -164,17 +140,12 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: [group])
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'when user does not have permissions'
|
it_behaves_like 'when user does not have permissions'
|
||||||
|
|
||||||
context 'when user has permissions' do
|
context 'when user has permissions' do
|
||||||
context 'when user is group owner' do
|
context 'when user is group owner' do
|
||||||
let(:current_user) { group_owner }
|
let(:current_user) { group_owner }
|
||||||
|
|
||||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
|
||||||
it_behaves_like 'when runner is created successfully'
|
it_behaves_like 'when runner is created successfully'
|
||||||
it_behaves_like 'when model is invalid returns error'
|
it_behaves_like 'when model is invalid returns error'
|
||||||
|
|
||||||
|
|
@ -226,7 +197,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
||||||
context 'when user is admin in admin mode', :enable_admin_mode do
|
context 'when user is admin in admin mode', :enable_admin_mode do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
|
||||||
it_behaves_like 'when runner is created successfully'
|
it_behaves_like 'when runner is created successfully'
|
||||||
it_behaves_like 'when model is invalid returns error'
|
it_behaves_like 'when model is invalid returns error'
|
||||||
end
|
end
|
||||||
|
|
@ -249,7 +219,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
||||||
context 'when user is group owner' do
|
context 'when user is group owner' do
|
||||||
let(:current_user) { group_owner }
|
let(:current_user) { group_owner }
|
||||||
|
|
||||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
|
||||||
it_behaves_like 'when runner is created successfully'
|
it_behaves_like 'when runner is created successfully'
|
||||||
it_behaves_like 'when model is invalid returns error'
|
it_behaves_like 'when model is invalid returns error'
|
||||||
|
|
||||||
|
|
@ -304,7 +273,6 @@ RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
|
||||||
context 'when user is admin in admin mode', :enable_admin_mode do
|
context 'when user is admin in admin mode', :enable_admin_mode do
|
||||||
let(:current_user) { admin }
|
let(:current_user) { admin }
|
||||||
|
|
||||||
it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
|
|
||||||
it_behaves_like 'when runner is created successfully'
|
it_behaves_like 'when runner is created successfully'
|
||||||
it_behaves_like 'when model is invalid returns error'
|
it_behaves_like 'when model is invalid returns error'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
|
RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
|
||||||
include_context 'sentry error tracking context'
|
include_context 'sentry error tracking context'
|
||||||
|
|
||||||
subject { described_class.new(project, user, params) }
|
subject(:service) { described_class.new(project, user, params) }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
context 'with authorized user' do
|
context 'with authorized user' do
|
||||||
|
|
@ -41,26 +41,41 @@ RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_trac
|
||||||
include_examples 'error tracking service http status handling', :issue_details
|
include_examples 'error tracking service http status handling', :issue_details
|
||||||
|
|
||||||
context 'with integrated error tracking' do
|
context 'with integrated error tracking' do
|
||||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||||
|
let(:params) { { issue_id: issue_id } }
|
||||||
let(:params) { { issue_id: error.id } }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
error_tracking_setting.update!(integrated: true)
|
error_tracking_setting.update!(integrated: true)
|
||||||
|
|
||||||
|
allow(service).to receive(:error_repository).and_return(error_repository)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when error is found' do
|
||||||
|
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||||
|
let(:issue_id) { error.fingerprint }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(error_repository).to receive(:find_error).with(issue_id).and_return(error)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the error in detailed format' do
|
it 'returns the error in detailed format' do
|
||||||
expect(result[:status]).to eq(:success)
|
expect(result[:status]).to eq(:success)
|
||||||
expect(result[:issue].to_json).to eq(error.to_sentry_detailed_error.to_json)
|
expect(result[:issue]).to eq(error)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when error does not exist' do
|
context 'when error does not exist' do
|
||||||
let(:params) { { issue_id: non_existing_record_id } }
|
let(:issue_id) { non_existing_record_id }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(error_repository).to receive(:find_error).with(issue_id)
|
||||||
|
.and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns the error in detailed format' do
|
it 'returns the error in detailed format' do
|
||||||
expect(result).to match(
|
expect(result).to match(
|
||||||
status: :error,
|
status: :error,
|
||||||
message: /Couldn't find ErrorTracking::Error/,
|
message: /Error not found/,
|
||||||
http_status: :bad_request
|
http_status: :bad_request
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
|
||||||
|
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
|
|
||||||
subject { described_class.new(project, user, params) }
|
subject(:service) { described_class.new(project, user, params) }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
context 'with authorized user' do
|
context 'with authorized user' do
|
||||||
|
|
@ -29,27 +29,42 @@ RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_
|
||||||
include_examples 'error tracking service http status handling', :issue_latest_event
|
include_examples 'error tracking service http status handling', :issue_latest_event
|
||||||
|
|
||||||
context 'with integrated error tracking' do
|
context 'with integrated error tracking' do
|
||||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||||
let_it_be(:event) { create(:error_tracking_error_event, error: error) }
|
let(:params) { { issue_id: issue_id } }
|
||||||
|
|
||||||
let(:params) { { issue_id: error.id } }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
error_tracking_setting.update!(integrated: true)
|
error_tracking_setting.update!(integrated: true)
|
||||||
|
|
||||||
|
allow(service).to receive(:error_repository).and_return(error_repository)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when error is found' do
|
||||||
|
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||||
|
let(:event) { build_stubbed(:error_tracking_open_api_error_event, fingerprint: error.fingerprint) }
|
||||||
|
let(:issue_id) { error.fingerprint }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(error_repository).to receive(:last_event_for).with(issue_id).and_return(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the latest event in expected format' do
|
it 'returns the latest event in expected format' do
|
||||||
expect(result[:status]).to eq(:success)
|
expect(result[:status]).to eq(:success)
|
||||||
expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json)
|
expect(result[:latest_event]).to eq(event)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when error does not exist' do
|
context 'when error does not exist' do
|
||||||
let(:params) { { issue_id: non_existing_record_id } }
|
let(:issue_id) { non_existing_record_id }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(error_repository).to receive(:last_event_for).with(issue_id)
|
||||||
|
.and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns the error in detailed format' do
|
it 'returns the error in detailed format' do
|
||||||
expect(result).to match(
|
expect(result).to match(
|
||||||
status: :error,
|
status: :error,
|
||||||
message: /Couldn't find ErrorTracking::Error/,
|
message: /Error not found/,
|
||||||
http_status: :bad_request
|
http_status: :bad_request
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -113,17 +113,45 @@ RSpec.describe ErrorTracking::IssueUpdateService, feature_category: :error_track
|
||||||
include_examples 'error tracking service sentry error handling', :update_issue
|
include_examples 'error tracking service sentry error handling', :update_issue
|
||||||
|
|
||||||
context 'with integrated error tracking' do
|
context 'with integrated error tracking' do
|
||||||
let(:error) { create(:error_tracking_error, project: project) }
|
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||||
let(:arguments) { { issue_id: error.id, status: 'resolved' } }
|
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||||
let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } }
|
let(:issue_id) { error.fingerprint }
|
||||||
|
let(:arguments) { { issue_id: issue_id, status: 'resolved' } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
error_tracking_setting.update!(integrated: true)
|
error_tracking_setting.update!(integrated: true)
|
||||||
|
|
||||||
|
allow(update_service).to receive(:error_repository).and_return(error_repository)
|
||||||
|
allow(error_repository).to receive(:update_error)
|
||||||
|
.with(issue_id, status: 'resolved').and_return(updated)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'resolves the error and responds with expected format' do
|
context 'when update succeeded' do
|
||||||
expect(update_service.execute).to eq(update_issue_response)
|
let(:updated) { true }
|
||||||
expect(error.reload.status).to eq('resolved')
|
|
||||||
|
it 'returns success with updated true' do
|
||||||
|
expect(project.error_tracking_setting).to receive(:expire_issues_cache)
|
||||||
|
|
||||||
|
expect(update_service.execute).to eq(
|
||||||
|
status: :success,
|
||||||
|
updated: true,
|
||||||
|
closed_issue_iid: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when update failed' do
|
||||||
|
let(:updated) { false }
|
||||||
|
|
||||||
|
it 'returns success with updated false' do
|
||||||
|
expect(project.error_tracking_setting).to receive(:expire_issues_cache)
|
||||||
|
|
||||||
|
expect(update_service.execute).to eq(
|
||||||
|
status: :success,
|
||||||
|
updated: false,
|
||||||
|
closed_issue_iid: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
|
||||||
|
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
|
|
||||||
subject { described_class.new(project, user, params) }
|
subject(:service) { described_class.new(project, user, params) }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
context 'Sentry backend' do
|
context 'with Sentry backend' do
|
||||||
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
|
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
|
||||||
|
|
||||||
let(:list_sentry_issues_args) do
|
let(:list_sentry_issues_args) do
|
||||||
|
|
@ -42,7 +42,7 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
|
||||||
expect(result).to eq(status: :success, pagination: {}, issues: issues)
|
expect(result).to eq(status: :success, pagination: {}, issues: issues)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns bad request for an issue_status not on the whitelist' do
|
it 'returns bad request with invalid issue_status' do
|
||||||
params[:issue_status] = 'assigned'
|
params[:issue_status] = 'assigned'
|
||||||
|
|
||||||
expect(error_tracking_setting).not_to receive(:list_sentry_issues)
|
expect(error_tracking_setting).not_to receive(:list_sentry_issues)
|
||||||
|
|
@ -65,22 +65,84 @@ RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracki
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'GitLab backend' do
|
context 'with integrated error tracking' do
|
||||||
let_it_be(:error1) { create(:error_tracking_error, name: 'foo', project: project) }
|
let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
|
||||||
let_it_be(:error2) { create(:error_tracking_error, name: 'bar', project: project) }
|
let(:errors) { [] }
|
||||||
|
let(:pagination) { Gitlab::ErrorTracking::ErrorRepository::Pagination.new(nil, nil) }
|
||||||
|
let(:opts) { default_opts }
|
||||||
|
|
||||||
let(:params) { { limit: '1' } }
|
let(:default_opts) do
|
||||||
|
{
|
||||||
|
filters: { status: described_class::DEFAULT_ISSUE_STATUS },
|
||||||
|
query: nil,
|
||||||
|
sort: described_class::DEFAULT_SORT,
|
||||||
|
limit: described_class::DEFAULT_LIMIT,
|
||||||
|
cursor: nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
error_tracking_setting.update!(integrated: true)
|
error_tracking_setting.update!(integrated: true)
|
||||||
|
|
||||||
|
allow(service).to receive(:error_repository).and_return(error_repository)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the error in expected format' do
|
context 'when errors are found' do
|
||||||
|
let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
|
||||||
|
let(:errors) { [error] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(error_repository).to receive(:list_errors)
|
||||||
|
.with(**opts)
|
||||||
|
.and_return([errors, pagination])
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without params' do
|
||||||
|
it 'returns the errors without pagination' do
|
||||||
expect(result[:status]).to eq(:success)
|
expect(result[:status]).to eq(:success)
|
||||||
expect(result[:issues].size).to eq(1)
|
expect(result[:issues]).to eq(errors)
|
||||||
expect(result[:issues].first.to_json).to eq(error2.to_sentry_error.to_json)
|
expect(result[:pagination]).to eq({})
|
||||||
expect(result[:pagination][:next][:cursor]).to be_present
|
expect(error_repository).to have_received(:list_errors).with(**opts)
|
||||||
expect(result[:pagination][:previous]).to be_nil
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with pagination' do
|
||||||
|
context 'with next page' do
|
||||||
|
before do
|
||||||
|
pagination.next = 'next cursor'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has next cursor' do
|
||||||
|
expect(result[:pagination]).to eq(next: { cursor: 'next cursor' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with prev page' do
|
||||||
|
before do
|
||||||
|
pagination.prev = 'prev cursor'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has prev cursor' do
|
||||||
|
expect(result[:pagination]).to eq(previous: { cursor: 'prev cursor' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with next and prev page' do
|
||||||
|
before do
|
||||||
|
pagination.next = 'next cursor'
|
||||||
|
pagination.prev = 'prev cursor'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has both cursors' do
|
||||||
|
expect(result[:pagination]).to eq(
|
||||||
|
next: { cursor: 'next cursor' },
|
||||||
|
previous: { cursor: 'prev cursor' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9123,10 +9123,6 @@
|
||||||
- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
|
- './spec/services/environments/schedule_to_delete_review_apps_service_spec.rb'
|
||||||
- './spec/services/environments/stop_service_spec.rb'
|
- './spec/services/environments/stop_service_spec.rb'
|
||||||
- './spec/services/error_tracking/base_service_spec.rb'
|
- './spec/services/error_tracking/base_service_spec.rb'
|
||||||
- './spec/services/error_tracking/issue_details_service_spec.rb'
|
|
||||||
- './spec/services/error_tracking/issue_latest_event_service_spec.rb'
|
|
||||||
- './spec/services/error_tracking/issue_update_service_spec.rb'
|
|
||||||
- './spec/services/error_tracking/list_issues_service_spec.rb'
|
|
||||||
- './spec/services/error_tracking/list_projects_service_spec.rb'
|
- './spec/services/error_tracking/list_projects_service_spec.rb'
|
||||||
- './spec/services/event_create_service_spec.rb'
|
- './spec/services/event_create_service_spec.rb'
|
||||||
- './spec/services/events/destroy_service_spec.rb'
|
- './spec/services/events/destroy_service_spec.rb'
|
||||||
|
|
|
||||||
|
|
@ -15,44 +15,6 @@ RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category:
|
||||||
allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
|
allow(view).to receive(:reset_registration_token_namespace_project_settings_ci_cd_path).and_return('banana_url')
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project runner registration is allowed' do
|
|
||||||
before do
|
|
||||||
stub_application_setting(valid_runner_registrars: ['project'])
|
|
||||||
allow(view).to receive(:can?).with(user, :register_project_runners, project).and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'enables the Remove project button for a project' do
|
|
||||||
render 'projects/runners/project_runners', project: project
|
|
||||||
|
|
||||||
expect(rendered).to have_selector '#js-install-runner'
|
|
||||||
expect(rendered).not_to have_content 'Please contact an admin to register runners.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project runner registration is not allowed' do
|
|
||||||
before do
|
|
||||||
stub_application_setting(valid_runner_registrars: ['group'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not enable the Remove project button for a project' do
|
|
||||||
render 'projects/runners/project_runners', project: project
|
|
||||||
|
|
||||||
expect(rendered).to have_content 'Please contact an admin to register runners.'
|
|
||||||
expect(rendered).not_to have_selector '#js-install-runner'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when create_runner_workflow_for_namespace is enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(create_runner_workflow_for_namespace: project.namespace)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user can create project runner' do
|
context 'when user can create project runner' do
|
||||||
before do
|
before do
|
||||||
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
|
allow(view).to receive(:can?).with(user, :create_runner, project).and_return(true)
|
||||||
|
|
@ -77,5 +39,4 @@ RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue