Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
133b8a64b6
commit
bced7d8bcc
|
|
@ -6,7 +6,7 @@ workflow:
|
|||
|
||||
include:
|
||||
- project: gitlab-org/quality/pipeline-common
|
||||
ref: 7.2.3
|
||||
ref: 7.5.0
|
||||
file:
|
||||
- /ci/base.gitlab-ci.yml
|
||||
- /ci/knapsack-report.yml
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0183d30e998cfab0b4f93fd1aa60200fba8e3771
|
||||
46892cb4fa5b0e86b7e86fbb7acda5effac412d6
|
||||
|
|
|
|||
|
|
@ -1,262 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import SecretValues from '~/behaviors/secret_values';
|
||||
import CreateItemDropdown from '~/create_item_dropdown';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
|
||||
|
||||
function createEnvironmentItem(value) {
|
||||
return {
|
||||
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
|
||||
id: value,
|
||||
text: value === '*' ? s__('CiVariable|* (All environments)') : value,
|
||||
};
|
||||
}
|
||||
|
||||
export default class VariableList {
|
||||
constructor({ container, formField, maskableRegex }) {
|
||||
this.$container = $(container);
|
||||
this.formField = formField;
|
||||
this.maskableRegex = new RegExp(maskableRegex);
|
||||
this.environmentDropdownMap = new WeakMap();
|
||||
|
||||
this.inputMap = {
|
||||
id: {
|
||||
selector: '.js-ci-variable-input-id',
|
||||
default: '',
|
||||
},
|
||||
variable_type: {
|
||||
selector: '.js-ci-variable-input-variable-type',
|
||||
default: 'env_var',
|
||||
},
|
||||
key: {
|
||||
selector: '.js-ci-variable-input-key',
|
||||
default: '',
|
||||
},
|
||||
secret_value: {
|
||||
selector: '.js-ci-variable-input-value',
|
||||
default: '',
|
||||
},
|
||||
protected: {
|
||||
selector: '.js-ci-variable-input-protected',
|
||||
// use `attr` instead of `data` as we don't want the value to be
|
||||
// converted. we need the value as a string.
|
||||
default: $('.js-ci-variable-input-protected').attr('data-default'),
|
||||
},
|
||||
masked: {
|
||||
selector: '.js-ci-variable-input-masked',
|
||||
// use `attr` instead of `data` as we don't want the value to be
|
||||
// converted. we need the value as a string.
|
||||
default: $('.js-ci-variable-input-masked').attr('data-default'),
|
||||
},
|
||||
environment_scope: {
|
||||
// We can't use a `.js-` class here because
|
||||
// deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class
|
||||
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458
|
||||
selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
|
||||
default: '*',
|
||||
},
|
||||
_destroy: {
|
||||
selector: '.js-ci-variable-input-destroy',
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
|
||||
this.secretValues = new SecretValues({
|
||||
container: this.$container[0],
|
||||
valueSelector: '.js-row:not(:last-child) .js-secret-value',
|
||||
placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.secretValues.init();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$container.find('.js-row').each((index, rowEl) => {
|
||||
this.initRow(rowEl);
|
||||
});
|
||||
|
||||
this.$container.on('click', '.js-row-remove-button', (e) => {
|
||||
e.preventDefault();
|
||||
this.removeRow($(e.currentTarget).closest('.js-row'));
|
||||
});
|
||||
|
||||
const inputSelector = Object.keys(this.inputMap)
|
||||
.map((name) => this.inputMap[name].selector)
|
||||
.join(',');
|
||||
|
||||
// Remove any empty rows except the last row
|
||||
this.$container.on('blur', inputSelector, (e) => {
|
||||
const $row = $(e.currentTarget).closest('.js-row');
|
||||
|
||||
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
|
||||
this.removeRow($row);
|
||||
}
|
||||
});
|
||||
|
||||
this.$container.on('input trigger-change', inputSelector, (e) => {
|
||||
// Always make sure there is an empty last row
|
||||
const $lastRow = this.$container.find('.js-row').last();
|
||||
|
||||
if (this.checkIfRowTouched($lastRow)) {
|
||||
this.insertRow($lastRow);
|
||||
}
|
||||
|
||||
// If masked, validate value against regex
|
||||
this.validateMaskability($(e.currentTarget).closest('.js-row'));
|
||||
});
|
||||
}
|
||||
|
||||
initRow(rowEl) {
|
||||
const $row = $(rowEl);
|
||||
|
||||
// Reset the resizable textarea
|
||||
$row.find(this.inputMap.secret_value.selector).css('height', '');
|
||||
|
||||
const $environmentSelect = $row.find('.js-variable-environment-toggle');
|
||||
if ($environmentSelect.length) {
|
||||
const createItemDropdown = new CreateItemDropdown({
|
||||
$dropdown: $environmentSelect,
|
||||
defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
|
||||
fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
|
||||
getData: (term, callback) => callback(this.getEnvironmentValues()),
|
||||
createNewItemFromValue: createEnvironmentItem,
|
||||
onSelect: () => {
|
||||
// Refresh the other dropdowns in the variable list
|
||||
// so they have the new value we just picked
|
||||
this.refreshDropdownData();
|
||||
|
||||
$row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
|
||||
},
|
||||
});
|
||||
|
||||
// Clear out any data that might have been left-over from the row clone
|
||||
createItemDropdown.clearDropdown();
|
||||
|
||||
this.environmentDropdownMap.set($row[0], createItemDropdown);
|
||||
}
|
||||
}
|
||||
|
||||
insertRow($row) {
|
||||
const $rowClone = $row.clone();
|
||||
$rowClone.removeAttr('data-is-persisted');
|
||||
|
||||
// Reset the inputs to their defaults
|
||||
Object.keys(this.inputMap).forEach((name) => {
|
||||
const entry = this.inputMap[name];
|
||||
$rowClone.find(entry.selector).val(entry.default);
|
||||
});
|
||||
|
||||
// Close any dropdowns
|
||||
$rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
|
||||
$dropdown.classList.remove('show');
|
||||
});
|
||||
|
||||
this.initRow($rowClone);
|
||||
|
||||
$row.after($rowClone);
|
||||
}
|
||||
|
||||
removeRow(row) {
|
||||
const $row = $(row);
|
||||
const isPersisted = parseBoolean($row.attr('data-is-persisted'));
|
||||
|
||||
if (isPersisted) {
|
||||
$row.hide();
|
||||
$row
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
.find(this.inputMap._destroy.selector)
|
||||
.val(true);
|
||||
} else {
|
||||
$row.remove();
|
||||
}
|
||||
|
||||
// Refresh the other dropdowns in the variable list
|
||||
// so any value with the variable deleted is gone
|
||||
this.refreshDropdownData();
|
||||
}
|
||||
|
||||
checkIfRowTouched($row) {
|
||||
return Object.keys(this.inputMap).some((name) => {
|
||||
// Row should not qualify as touched if only switches have been touched
|
||||
if (['protected', 'masked'].includes(name)) return false;
|
||||
|
||||
const entry = this.inputMap[name];
|
||||
const $el = $row.find(entry.selector);
|
||||
return $el.length && $el.val() !== entry.default;
|
||||
});
|
||||
}
|
||||
|
||||
validateMaskability($row) {
|
||||
const invalidInputClass = 'gl-field-error-outline';
|
||||
|
||||
const variableValue = $row.find(this.inputMap.secret_value.selector).val();
|
||||
const isValueMaskable = this.maskableRegex.test(variableValue) || variableValue === '';
|
||||
const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
|
||||
|
||||
// Show a validation error if the user wants to mask an unmaskable variable value
|
||||
$row
|
||||
.find(this.inputMap.secret_value.selector)
|
||||
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
|
||||
$row
|
||||
.find('.js-secret-value-placeholder')
|
||||
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
|
||||
$row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
|
||||
}
|
||||
|
||||
toggleEnableRow(isEnabled = true) {
|
||||
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
|
||||
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
|
||||
}
|
||||
|
||||
hideValues() {
|
||||
this.secretValues.updateDom(false);
|
||||
}
|
||||
|
||||
getAllData() {
|
||||
// Ignore the last empty row because we don't want to try persist
|
||||
// a blank variable and run into validation problems.
|
||||
const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
|
||||
|
||||
return validRows.map((rowEl) => {
|
||||
const resultant = {};
|
||||
Object.keys(this.inputMap).forEach((name) => {
|
||||
const entry = this.inputMap[name];
|
||||
const $input = $(rowEl).find(entry.selector);
|
||||
if ($input.length) {
|
||||
resultant[name] = $input.val();
|
||||
}
|
||||
});
|
||||
|
||||
return resultant;
|
||||
});
|
||||
}
|
||||
|
||||
getEnvironmentValues() {
|
||||
const valueMap = this.$container
|
||||
.find(this.inputMap.environment_scope.selector)
|
||||
.toArray()
|
||||
.reduce(
|
||||
(prevValueMap, envInput) => ({
|
||||
...prevValueMap,
|
||||
[envInput.value]: envInput.value,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
return Object.keys(valueMap).map(createEnvironmentItem);
|
||||
}
|
||||
|
||||
refreshDropdownData() {
|
||||
this.$container.find('.js-row').each((index, rowEl) => {
|
||||
const environmentDropdown = this.environmentDropdownMap.get(rowEl);
|
||||
if (environmentDropdown) {
|
||||
environmentDropdown.refreshData();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import VariableList from './ci_variable_list';
|
||||
|
||||
// Used for the variable list on scheduled pipeline edit page
|
||||
export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
|
||||
const $container = $(container);
|
||||
|
||||
const variableList = new VariableList({
|
||||
container: $container,
|
||||
formField,
|
||||
});
|
||||
variableList.init();
|
||||
|
||||
// Clear out the names in the empty last row so it
|
||||
// doesn't get submitted and throw validation errors
|
||||
$container.closest('form').on('submit trigger-submit', () => {
|
||||
const $lastRow = $container.find('.js-row').last();
|
||||
|
||||
const isTouched = variableList.checkIfRowTouched($lastRow);
|
||||
if (!isTouched) {
|
||||
$lastRow.find('input, textarea').attr('name', '');
|
||||
$lastRow.find('select').attr('name', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ export default {
|
|||
this.description = schedule.description;
|
||||
this.cron = schedule.cron;
|
||||
this.cronTimezone = schedule.cronTimezone;
|
||||
this.scheduleRef = schedule.ref;
|
||||
this.scheduleRef = schedule.ref || this.defaultBranch;
|
||||
this.variables = variables.map((variable) => {
|
||||
return {
|
||||
id: variable.id,
|
||||
|
|
|
|||
|
|
@ -27,10 +27,13 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-icon :name="iconName" />
|
||||
<div data-testid="pipeline-schedule-target">
|
||||
<span v-if="refPath">
|
||||
<gl-icon :name="iconName" />
|
||||
<gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ s__('PipelineSchedules|None') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<script>
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
},
|
||||
props: {
|
||||
ownershipUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
modalId: 'pipeline-take-ownership-modal',
|
||||
i18n: {
|
||||
takeOwnership: s__('PipelineSchedules|Take ownership'),
|
||||
ownershipMessage: s__(
|
||||
'PipelineSchedules|Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
|
||||
),
|
||||
cancelLabel: __('Cancel'),
|
||||
},
|
||||
computed: {
|
||||
actionCancel() {
|
||||
return { text: this.$options.i18n.cancelLabel };
|
||||
},
|
||||
actionPrimary() {
|
||||
return {
|
||||
text: this.$options.i18n.takeOwnership,
|
||||
attributes: {
|
||||
variant: 'confirm',
|
||||
category: 'primary',
|
||||
href: this.ownershipUrl,
|
||||
'data-method': 'post',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
:modal-id="$options.modalId"
|
||||
:action-primary="actionPrimary"
|
||||
:action-cancel="actionCancel"
|
||||
:title="$options.i18n.takeOwnership"
|
||||
>
|
||||
<p>{{ $options.i18n.ownershipMessage }}</p>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { EVENT_UPDATED_I18N } from '../../constants';
|
||||
import { getValueByEventTarget } from '../../utils';
|
||||
import ContributionEventBase from './contribution_event_base.vue';
|
||||
|
||||
export default {
|
||||
name: 'ContributionEventUpdated',
|
||||
components: { ContributionEventBase },
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
return getValueByEventTarget(EVENT_UPDATED_I18N, this.event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<contribution-event-base :event="event" :message="message" icon-name="pencil" />
|
||||
</template>
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
EVENT_TYPE_CLOSED,
|
||||
EVENT_TYPE_REOPENED,
|
||||
EVENT_TYPE_COMMENTED,
|
||||
EVENT_TYPE_UPDATED,
|
||||
} from '../constants';
|
||||
import ContributionEventApproved from './contribution_event/contribution_event_approved.vue';
|
||||
import ContributionEventExpired from './contribution_event/contribution_event_expired.vue';
|
||||
|
|
@ -24,6 +25,7 @@ import ContributionEventCreated from './contribution_event/contribution_event_cr
|
|||
import ContributionEventClosed from './contribution_event/contribution_event_closed.vue';
|
||||
import ContributionEventReopened from './contribution_event/contribution_event_reopened.vue';
|
||||
import ContributionEventCommented from './contribution_event/contribution_event_commented.vue';
|
||||
import ContributionEventUpdated from './contribution_event/contribution_event_updated.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -151,6 +153,9 @@ export default {
|
|||
case EVENT_TYPE_COMMENTED:
|
||||
return ContributionEventCommented;
|
||||
|
||||
case EVENT_TYPE_UPDATED:
|
||||
return ContributionEventUpdated;
|
||||
|
||||
default:
|
||||
return EmptyComponent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,14 +119,24 @@ export const EVENT_COMMENTED_I18N = Object.freeze({
|
|||
[COMMIT_NOTEABLE_TYPE]: s__(
|
||||
'ContributionEvent|Commented on commit %{noteableLink} in %{resourceParentLink}.',
|
||||
),
|
||||
fallback: s__('ContributionEvent|Commented on %{noteableLink}.'),
|
||||
[TYPE_FALLBACK]: s__('ContributionEvent|Commented on %{noteableLink}.'),
|
||||
});
|
||||
|
||||
export const EVENT_COMMENTED_SNIPPET_I18N = Object.freeze({
|
||||
[RESOURCE_PARENT_TYPE_PROJECT]: s__(
|
||||
'ContributionEvent|Commented on snippet %{noteableLink} in %{resourceParentLink}.',
|
||||
),
|
||||
fallback: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
|
||||
[TYPE_FALLBACK]: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
|
||||
});
|
||||
|
||||
export const EVENT_UPDATED_I18N = Object.freeze({
|
||||
[TARGET_TYPE_DESIGN]: s__(
|
||||
'ContributionEvent|Updated design %{targetLink} in %{resourceParentLink}.',
|
||||
),
|
||||
[TARGET_TYPE_WIKI]: s__(
|
||||
'ContributionEvent|Updated wiki page %{targetLink} in %{resourceParentLink}.',
|
||||
),
|
||||
[TYPE_FALLBACK]: s__('ContributionEvent|Updated resource.'),
|
||||
});
|
||||
|
||||
export const EVENT_CLOSED_ICONS = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<gl-disclosure-dropdown
|
||||
:text="title"
|
||||
:toggle-text="title"
|
||||
:title="title"
|
||||
:loading="isLoading"
|
||||
:aria-label="title"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
<deployment-status-link :deployment-job="item.job" :status="item.status" />
|
||||
</template>
|
||||
<template #cell(id)="{ item }">
|
||||
<strong>{{ item.id }}</strong>
|
||||
<strong data-testid="deployment-id">{{ item.id }}</strong>
|
||||
</template>
|
||||
<template #cell(triggerer)="{ item }">
|
||||
<deployment-triggerer :triggerer="item.triggerer" />
|
||||
|
|
|
|||
|
|
@ -59,9 +59,6 @@ export const initHeader = () => {
|
|||
};
|
||||
|
||||
export const initPage = async () => {
|
||||
if (!gon.features.environmentDetailsVue) {
|
||||
return null;
|
||||
}
|
||||
const EnvironmentsDetailPageModule = await import('./environment_details/index.vue');
|
||||
const EnvironmentsDetailPage = EnvironmentsDetailPageModule.default;
|
||||
const dataElement = document.getElementById('environments-detail-view');
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default {
|
|||
|
||||
<template #cell(stage)="{ item }">
|
||||
<div class="gl-text-truncate">
|
||||
<span data-testid="job-stage-name">{{ item.stage.name }}</span>
|
||||
<span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
|
||||
import initForm from '../shared/init_form';
|
||||
|
||||
if (gon.features?.pipelineSchedulesVue) {
|
||||
initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
|
||||
} else {
|
||||
initForm();
|
||||
}
|
||||
initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
|
||||
import initPipelineSchedulesApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_app';
|
||||
import PipelineSchedulesTakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
|
||||
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
|
||||
|
||||
function initPipelineSchedulesCallout() {
|
||||
|
|
@ -27,49 +25,5 @@ function initPipelineSchedulesCallout() {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO: move take ownership feature into new Vue app
|
||||
// located in directory app/assets/javascripts/pipeline_schedules/components
|
||||
function initTakeownershipModal() {
|
||||
const modalId = 'pipeline-take-ownership-modal';
|
||||
const buttonSelector = 'js-take-ownership-button';
|
||||
const el = document.getElementById(modalId);
|
||||
const takeOwnershipButtons = document.querySelectorAll(`.${buttonSelector}`);
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
takeOwnershipButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const { url } = button.dataset;
|
||||
|
||||
this.url = url;
|
||||
this.$root.$emit(BV_SHOW_MODAL, modalId, `.${buttonSelector}`);
|
||||
});
|
||||
});
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(PipelineSchedulesTakeOwnershipModalLegacy, {
|
||||
props: {
|
||||
ownershipUrl: this.url,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gon.features?.pipelineSchedulesVue) {
|
||||
initPipelineSchedulesApp();
|
||||
} else {
|
||||
initPipelineSchedulesCallout();
|
||||
initTakeownershipModal();
|
||||
}
|
||||
initPipelineSchedulesApp();
|
||||
initPipelineSchedulesCallout();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
|
||||
import initForm from '../shared/init_form';
|
||||
|
||||
if (gon.features?.pipelineSchedulesVue) {
|
||||
initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
|
||||
} else {
|
||||
initForm();
|
||||
}
|
||||
initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { __ } from '~/locale';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
|
||||
import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
|
||||
import GlFieldErrors from '~/gl_field_errors';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { initTimezoneDropdown } from '../../../profiles/init_timezone_dropdown';
|
||||
import IntervalPatternInput from './components/interval_pattern_input.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
function initIntervalPatternInput() {
|
||||
const intervalPatternMount = document.getElementById('interval-pattern-input');
|
||||
const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
|
||||
const dailyLimit = intervalPatternMount.dataset?.dailyLimit;
|
||||
|
||||
return new Vue({
|
||||
el: intervalPatternMount,
|
||||
components: {
|
||||
IntervalPatternInput,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('interval-pattern-input', {
|
||||
props: {
|
||||
initialCronInterval,
|
||||
dailyLimit,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getEnabledRefTypes() {
|
||||
return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
|
||||
}
|
||||
|
||||
function initTargetRefDropdown() {
|
||||
const $refField = document.getElementById('schedule_ref');
|
||||
const el = document.querySelector('.js-target-ref-dropdown');
|
||||
const { projectId, defaultBranch } = el.dataset;
|
||||
|
||||
if (!$refField.value) {
|
||||
$refField.value = defaultBranch;
|
||||
}
|
||||
|
||||
const refDropdown = new Vue({
|
||||
el,
|
||||
render(h) {
|
||||
return h(RefSelector, {
|
||||
props: {
|
||||
enabledRefTypes: getEnabledRefTypes(),
|
||||
projectId,
|
||||
value: $refField.value,
|
||||
useSymbolicRefNames: true,
|
||||
translations: {
|
||||
dropdownHeader: __('Select target branch or tag'),
|
||||
},
|
||||
},
|
||||
class: 'gl-w-full',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
refDropdown.$children[0].$on('input', (newRef) => {
|
||||
$refField.value = newRef;
|
||||
});
|
||||
|
||||
return refDropdown;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
/* Most of the form is written in haml, but for fields with more complex behaviors,
|
||||
* you should mount individual Vue components here. If at some point components need
|
||||
* to share state, it may make sense to refactor the whole form to Vue */
|
||||
|
||||
initIntervalPatternInput();
|
||||
|
||||
// Initialize non-Vue JS components in the form
|
||||
|
||||
const formElement = document.getElementById('new-pipeline-schedule-form');
|
||||
|
||||
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
|
||||
|
||||
initTargetRefDropdown();
|
||||
|
||||
setupNativeFormVariableList({
|
||||
container: $('.js-ci-variable-list-section'),
|
||||
formField: 'schedule',
|
||||
});
|
||||
};
|
||||
|
||||
initTimezoneDropdown();
|
||||
|
|
@ -149,7 +149,6 @@ export default {
|
|||
id="custom-email-form-forwarding"
|
||||
ref="service-desk-incoming-email"
|
||||
type="text"
|
||||
data-testid="custom-email-form-forwarding"
|
||||
:aria-label="$options.I18N_FORM_FORWARDING_LABEL"
|
||||
:value="incomingEmail"
|
||||
:disabled="true"
|
||||
|
|
@ -167,7 +166,6 @@ export default {
|
|||
<gl-form-group
|
||||
:label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
|
||||
label-for="custom-email-form-custom-email"
|
||||
data-testid="form-group-custom-email"
|
||||
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL"
|
||||
class="gl-mt-3"
|
||||
:description="$options.I18N_FORM_CUSTOM_EMAIL_DESCRIPTION"
|
||||
|
|
@ -191,7 +189,6 @@ export default {
|
|||
<gl-form-group
|
||||
:label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
|
||||
label-for="custom-email-form-smtp-address"
|
||||
data-testid="form-group-smtp-address"
|
||||
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS"
|
||||
class="gl-mt-3"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
|
||||
.ci-variable-list {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.ci-variable-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $gl-btn-padding;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-bottom: 3 * $gl-btn-padding;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.ci-variable-body-item:last-child {
|
||||
margin-right: $ci-variable-remove-button-width;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-variable-row-remove-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.ci-variable-row-body {
|
||||
margin-right: $ci-variable-remove-button-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ci-variable-row-body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding-bottom: $gl-padding;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-variable-body-item {
|
||||
flex: 1;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: $gl-btn-padding;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-right: 0;
|
||||
margin-bottom: $gl-btn-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-schedule-form {
|
||||
.gl-field-error {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-schedule-table-row {
|
||||
a {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
|
||||
layout 'project'
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:environment_details_vue, @project)
|
||||
end
|
||||
|
||||
before_action only: [:index, :edit, :new] do
|
||||
push_frontend_feature_flag(:flux_resource_for_environment)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
|
|||
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
|
||||
before_action :authorize_update_pipeline_schedule!, only: [:edit, :update]
|
||||
before_action :authorize_admin_pipeline_schedule!, only: [:take_ownership, :destroy]
|
||||
before_action :push_schedule_feature_flag, only: [:index, :new, :edit]
|
||||
|
||||
feature_category :continuous_integration
|
||||
urgency :low
|
||||
|
|
@ -120,8 +119,4 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
|
|||
def authorize_admin_pipeline_schedule!
|
||||
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
|
||||
end
|
||||
|
||||
def push_schedule_feature_flag
|
||||
push_frontend_feature_flag(:pipeline_schedules_vue, @project)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -68,7 +68,10 @@ module Types
|
|||
null: false, description: 'Timestamp of when the pipeline schedule was last updated.'
|
||||
|
||||
def ref_path
|
||||
::Gitlab::Routing.url_helpers.project_commits_path(object.project, object.ref_for_display)
|
||||
ref_for_display = object.ref_for_display
|
||||
return unless ref_for_display
|
||||
|
||||
::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_for_display)
|
||||
end
|
||||
|
||||
def edit_path
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@ module Commits
|
|||
Gitlab::Git::PreReceiveError,
|
||||
Gitlab::Git::CommandError => ex
|
||||
Gitlab::ErrorTracking.log_exception(ex)
|
||||
error(ex.message)
|
||||
|
||||
if Feature.enabled?(:errors_utf_8_encoding)
|
||||
error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message))
|
||||
else
|
||||
error(ex.message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
- form_field = local_assigns.fetch(:form_field, nil)
|
||||
- variable = local_assigns.fetch(:variable, nil)
|
||||
|
||||
- id = variable&.id
|
||||
- variable_type = variable&.variable_type
|
||||
- key = variable&.key
|
||||
- value = variable&.value
|
||||
|
||||
- id_input_name = "#{form_field}[variables_attributes][][id]"
|
||||
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
|
||||
- variable_type_input_name = "#{form_field}[variables_attributes][][variable_type]"
|
||||
- key_input_name = "#{form_field}[variables_attributes][][key]"
|
||||
- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
|
||||
|
||||
%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
|
||||
.ci-variable-row-body.border-bottom
|
||||
%input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id }
|
||||
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
|
||||
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
|
||||
= options_for_select(ci_variable_type_options, variable_type)
|
||||
%input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text",
|
||||
name: key_input_name,
|
||||
value: key,
|
||||
placeholder: s_('CiVariables|Input variable key') }
|
||||
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
|
||||
.form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) }
|
||||
= '*' * 17
|
||||
%textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id),
|
||||
rows: 1,
|
||||
name: value_input_name,
|
||||
placeholder: s_('CiVariables|Input variable value') }
|
||||
= value
|
||||
%p.masking-validation-error.gl-field-error.hide
|
||||
= s_("CiVariables|Cannot use Masked Variable with current value")
|
||||
= link_to sprite_icon('question-o'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= render Pajamas::ButtonComponent.new(icon: 'close', button_options: { class: 'js-row-remove-button ci-variable-row-remove-button table-section', 'aria-label': s_('CiVariables|Remove variable row') })
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
.table-mobile-content
|
||||
.branch-commit.cgray
|
||||
- if deployment.ref
|
||||
%span.icon-container.gl-display-inline-block
|
||||
= deployment.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
|
||||
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
|
||||
.icon-container.commit-icon
|
||||
= custom_icon("icon_commit")
|
||||
= link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha mr-0"
|
||||
|
||||
%p.commit-title.flex-truncate-parent
|
||||
%span.flex-truncate-child
|
||||
- if commit_title = deployment.commit_title
|
||||
= author_avatar(deployment.commit, size: 20)
|
||||
= link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message cgray"
|
||||
- else
|
||||
= _("Can't find HEAD commit for this branch")
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
.gl-responsive-table-row.deployment{ role: 'row' }
|
||||
.table-section.section-15{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Status")
|
||||
.table-mobile-content
|
||||
= render_deployment_status(deployment)
|
||||
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("ID")
|
||||
%strong.table-mobile-content{ data: { testid: 'deployment-id' } } ##{deployment.iid}
|
||||
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Triggerer")
|
||||
.table-mobile-content
|
||||
- if deployment.deployed_by
|
||||
= user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
|
||||
|
||||
.table-section.section-25{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Commit")
|
||||
= render 'projects/deployments/commit', deployment: deployment
|
||||
|
||||
.table-section.section-10.build-column{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Job")
|
||||
- if deployment.deployable
|
||||
.table-mobile-content
|
||||
.flex-truncate-parent
|
||||
.flex-truncate-child.has-tooltip.gl-white-space-normal.gl-md-white-space-nowrap{ :title => "#{deployment.deployable.name} (##{deployment.deployable.id})", data: { container: 'body' } }
|
||||
= link_to deployment_path(deployment), class: 'build-link' do
|
||||
#{deployment.deployable.name} (##{deployment.deployable.id})
|
||||
- else
|
||||
= gl_badge_tag s_('Deployment|API'), { variant: :info }, { class: 'gl-cursor-help', data: { toggle: 'tooltip' }, title: s_('Deployment|This deployment was created using the API') }
|
||||
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Created")
|
||||
%span.table-mobile-content.flex-truncate-parent
|
||||
%span.flex-truncate-child
|
||||
= time_ago_with_tooltip(deployment.created_at)
|
||||
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Deployed")
|
||||
- if deployment.deployed_at
|
||||
%span.table-mobile-content.flex-truncate-parent
|
||||
%span.flex-truncate-child
|
||||
= time_ago_with_tooltip(deployment.deployed_at)
|
||||
|
||||
.table-section.section-10.table-button-footer{ role: 'gridcell' }
|
||||
.btn-group.table-action-buttons
|
||||
= render 'projects/deployments/actions', deployment: deployment
|
||||
= render 'projects/deployments/rollback', deployment: deployment
|
||||
= render_if_exists 'projects/deployments/approvals', deployment: deployment
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
- if deployment.deployable && can?(current_user, :play_job, deployment.deployable)
|
||||
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
|
||||
- icon = deployment.last? ? 'repeat' : 'redo'
|
||||
= render Pajamas::ButtonComponent.new(icon: icon, button_options: { title: tooltip, class: 'js-confirm-rollback-modal-button has-tooltip', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) } })
|
||||
|
|
@ -8,33 +8,4 @@
|
|||
#environments-detail-view{ data: { details: environments_detail_data_json(current_user, @project, @environment) } }
|
||||
#environments-detail-view-header
|
||||
|
||||
- if Feature.enabled?(:environment_details_vue, @project)
|
||||
#environment_details_page
|
||||
- else
|
||||
.environments-container
|
||||
- if @deployments.blank?
|
||||
.empty-state
|
||||
.text-content
|
||||
%h4.state-title
|
||||
= _("You don't have any deployments right now.")
|
||||
%p
|
||||
= html_escape(_("Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
|
||||
.text-center
|
||||
= render Pajamas::ButtonComponent.new(variant: :confirm, href: help_page_path("ci/environments/index.md")) do
|
||||
= _('Read more')
|
||||
|
||||
- else
|
||||
.table-holder.gl-overflow-visible
|
||||
.ci-table.environments{ role: 'grid' }
|
||||
.gl-responsive-table-row.table-row-header{ role: 'row' }
|
||||
.table-section.section-15{ role: 'columnheader' }= _('Status')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('ID')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
|
||||
.table-section.section-25{ role: 'columnheader' }= _('Commit')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Job')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Created')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
|
||||
|
||||
= render @deployments
|
||||
|
||||
= paginate @deployments, theme: 'gitlab'
|
||||
#environment_details_page
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
= gitlab_ui_form_for [@project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f|
|
||||
= form_errors(@schedule)
|
||||
.form-group.row
|
||||
.col-md-9
|
||||
= f.label :description, _('Description'), class: 'label-bold'
|
||||
= f.text_field :description, class: 'form-control gl-form-input', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline')
|
||||
.form-group.row
|
||||
.col-md-9
|
||||
= f.label :cron, _('Interval Pattern'), class: 'label-bold'
|
||||
#interval-pattern-input{ data: { initial_interval: @schedule.cron, daily_limit: @schedule.daily_limit } }
|
||||
.form-group.row
|
||||
.col-md-9{ data: { testid: 'schedule-timezone' } }
|
||||
= f.label :cron_timezone, _("Cron Timezone")
|
||||
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @schedule.cron_timezone, name: 'schedule[cron_timezone]' } }
|
||||
|
||||
.form-group.row
|
||||
.col-md-9
|
||||
= f.label :ref, _('Target branch or tag'), class: 'label-bold'
|
||||
%div{ data: { testid: 'schedule-target-ref' } }
|
||||
.js-target-ref-dropdown{ data: { project_id: @project.id, default_branch: @project.default_branch } }
|
||||
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
|
||||
.form-group.row.js-ci-variable-list-section
|
||||
.col-md-9
|
||||
%label.label-bold
|
||||
#{ s_('PipelineSchedules|Variables') }
|
||||
%ul.ci-variable-list
|
||||
- @schedule.variables.each do |variable|
|
||||
= render 'ci/variables/variable_row', form_field: 'schedule', variable: variable
|
||||
= render 'ci/variables/variable_row', form_field: 'schedule'
|
||||
- if @schedule.variables.size > 0
|
||||
= render Pajamas::ButtonComponent.new(category: :secondary, variant: :confirm, button_options: { class: 'gl-mt-3 js-secret-value-reveal-button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" }}) do
|
||||
- if @schedule.variables.size == 0
|
||||
= n_('Hide value', 'Hide values', @schedule.variables.size)
|
||||
- else
|
||||
= n_('Reveal value', 'Reveal values', @schedule.variables.size)
|
||||
.form-group.row
|
||||
.col-md-9
|
||||
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
|
||||
%div
|
||||
= f.gitlab_ui_checkbox_component :active, _('Active'), checkbox_options: { value: @schedule.active, required: false }
|
||||
.footer-block
|
||||
= f.submit _('Save pipeline schedule'), pajamas_button: true
|
||||
= link_button_to _('Cancel'), pipeline_schedules_path(@project)
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
- if pipeline_schedule
|
||||
%tr.pipeline-schedule-table-row
|
||||
%td{ role: 'cell', data: { label: _('Description') } }
|
||||
%div
|
||||
= pipeline_schedule.description
|
||||
%td.branch-name-cell.gl-text-truncate{ role: 'cell', data: { label: s_("PipelineSchedules|Target") } }
|
||||
%div
|
||||
- if pipeline_schedule.for_tag?
|
||||
= sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!')
|
||||
- else
|
||||
= sprite_icon('fork', size: 12, css_class: 'gl-vertical-align-middle!')
|
||||
- if pipeline_schedule.ref.present?
|
||||
= link_to pipeline_schedule.ref_for_display, project_ref_path(@project, pipeline_schedule.ref_for_display), class: "ref-name"
|
||||
%td{ role: 'cell', data: { label: _("Last Pipeline") } }
|
||||
%div
|
||||
- if pipeline_schedule.last_pipeline
|
||||
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
|
||||
= link_to project_pipeline_path(@project, pipeline_schedule.last_pipeline.id) do
|
||||
= ci_icon_for_status(pipeline_schedule.last_pipeline.status)
|
||||
%span.gl-text-blue-500! ##{pipeline_schedule.last_pipeline.id}
|
||||
- else
|
||||
= s_("PipelineSchedules|None")
|
||||
%td.gl-text-gray-500{ role: 'cell', data: { label: s_("PipelineSchedules|Next Run") }, 'data-testid': 'next-run-cell' }
|
||||
%div
|
||||
- if pipeline_schedule.active? && pipeline_schedule.next_run_at
|
||||
= time_ago_with_tooltip(pipeline_schedule.real_next_run)
|
||||
- else
|
||||
= s_("PipelineSchedules|Inactive")
|
||||
%td{ role: 'cell', data: { label: _("Owner") } }
|
||||
%div
|
||||
- if pipeline_schedule.owner
|
||||
= render Pajamas::AvatarComponent.new(pipeline_schedule.owner, size: 24, class: "gl-mr-2")
|
||||
= link_to user_path(pipeline_schedule.owner) do
|
||||
= pipeline_schedule.owner&.name
|
||||
%td{ role: 'cell', data: { label: _('Actions') } }
|
||||
.float-right.btn-group
|
||||
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
|
||||
= link_button_to nil, play_pipeline_schedule_path(pipeline_schedule), method: :post, title: _('Play'), icon: 'play'
|
||||
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) && pipeline_schedule.owner != current_user
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-take-ownership-button has-tooltip', title: s_('PipelineSchedule|Take ownership to edit'), data: { url: take_ownership_pipeline_schedule_path(pipeline_schedule) } }) do
|
||||
= s_('PipelineSchedules|Take ownership')
|
||||
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
|
||||
= link_button_to nil, edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), icon: 'pencil'
|
||||
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
|
||||
= link_button_to nil, pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, aria: { label: _('Delete pipeline schedule') }, data: { confirm: _("Are you sure you want to delete this pipeline schedule?"), confirm_btn_variant: 'danger' }, variant: :danger, icon: 'remove'
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
.table-holder
|
||||
%table.table.ci-table.responsive-table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
|
||||
%thead{ role: 'rowgroup' }
|
||||
%tr{ role: 'row' }
|
||||
%th.table-th-transparent.border-bottom{ role: 'cell', style: 'width: 34%' }= _("Description")
|
||||
%th.table-th-transparent.border-bottom{ role: 'cell' }= s_("PipelineSchedules|Target")
|
||||
%th.table-th-transparent.border-bottom{ role: 'cell' }= _("Last Pipeline")
|
||||
%th.table-th-transparent.border-bottom{ role: 'cell' }= s_("PipelineSchedules|Next Run")
|
||||
%th.table-th-transparent.border-bottom{ role: 'cell' }= _("Owner")
|
||||
%th.table-th-transparent.border-bottom{ role: 'cell' }
|
||||
%tbody{ role: 'rowgroup' }
|
||||
= render partial: "pipeline_schedule", collection: @schedules
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
= gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-0' }) do
|
||||
= gl_tab_link_to schedule_path_proc.call(nil), { item_active: active_when(scope.nil?) } do
|
||||
= s_("PipelineSchedules|All")
|
||||
= gl_tab_counter_badge(number_with_delimiter(all_schedules.count(:id)), { class: 'js-totalbuilds-count' })
|
||||
|
||||
= gl_tab_link_to schedule_path_proc.call('active'), { item_active: active_when(scope == 'active') } do
|
||||
= s_("PipelineSchedules|Active")
|
||||
= gl_tab_counter_badge(number_with_delimiter(all_schedules.active.count(:id)))
|
||||
|
||||
= gl_tab_link_to schedule_path_proc.call('inactive'), { item_active: active_when(scope == 'inactive') } do
|
||||
= s_("PipelineSchedules|Inactive")
|
||||
= gl_tab_counter_badge(number_with_delimiter(all_schedules.inactive.count(:id)))
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project)
|
||||
- breadcrumb_title "##{@schedule.id}"
|
||||
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
|
||||
- add_page_specific_style 'page_bundles/pipeline_schedules'
|
||||
|
||||
%h1.page-title.gl-font-size-h-display
|
||||
= _("Edit Pipeline Schedule")
|
||||
|
||||
- if Feature.enabled?(:pipeline_schedules_vue, @project)
|
||||
#pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
|
||||
- else
|
||||
= render "form"
|
||||
#pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
|
||||
|
|
|
|||
|
|
@ -1,28 +1,6 @@
|
|||
- breadcrumb_title _("Schedules")
|
||||
- page_title _("Pipeline Schedules")
|
||||
- add_page_specific_style 'page_bundles/pipeline_schedules'
|
||||
- add_page_specific_style 'page_bundles/ci_status'
|
||||
- add_page_specific_style 'page_bundles/merge_request'
|
||||
|
||||
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), illustration_url: image_path('illustrations/pipeline_schedule_callout.svg') } }
|
||||
|
||||
- if Feature.enabled?(:pipeline_schedules_vue, @project)
|
||||
#pipeline-schedules-app{ data: { full_path: @project.full_path, pipelines_path: project_pipelines_path(@project), new_schedule_path: new_project_pipeline_schedule_path(@project) } }
|
||||
- else
|
||||
.top-area
|
||||
- schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
|
||||
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
|
||||
|
||||
- if can?(current_user, :create_pipeline_schedule, @project)
|
||||
.nav-controls
|
||||
= link_button_to new_project_pipeline_schedule_path(@project), variant: :confirm do
|
||||
= _('New schedule')
|
||||
|
||||
- if @schedules.present?
|
||||
%ul.content-list
|
||||
= render partial: "table"
|
||||
- else
|
||||
.nothing-here-block
|
||||
= _("No schedules")
|
||||
|
||||
#pipeline-take-ownership-modal
|
||||
#pipeline-schedules-app{ data: { full_path: @project.full_path, pipelines_path: project_pipelines_path(@project), new_schedule_path: new_project_pipeline_schedule_path(@project) } }
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
- breadcrumb_title _('Schedules')
|
||||
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
|
||||
- page_title _("New Pipeline Schedule")
|
||||
- add_page_specific_style 'page_bundles/pipeline_schedules'
|
||||
|
||||
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
|
||||
|
||||
%h1.page-title.gl-font-size-h-display
|
||||
= _("Schedule a new pipeline")
|
||||
|
||||
- if Feature.enabled?(:pipeline_schedules_vue, @project)
|
||||
#pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
|
||||
- else
|
||||
= render "form"
|
||||
#pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: environment_details_vue
|
||||
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105527"
|
||||
rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/384914"
|
||||
milestone: '15.7'
|
||||
type: development
|
||||
group: group::configure
|
||||
default_enabled: true
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: pipeline_schedules_vue
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99155
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/375139
|
||||
milestone: '15.5'
|
||||
name: errors_utf_8_encoding
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129217
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422061
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: explain_vulnerability_vertex
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118754
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409182
|
||||
milestone: '16.0'
|
||||
name: explain_vulnerability_anthropic
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128118
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420608
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
||||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RebalancePartitionIdCiPipeline < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
TABLE = :ci_pipelines
|
||||
BATCH_SIZE = 2_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RebalancePartitionIdCiBuild < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes.freeze
|
||||
TABLE = :ci_builds
|
||||
BATCH_SIZE = 5_000
|
||||
SUB_BATCH_SIZE = 500
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiPipelineVariable < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes.freeze
|
||||
TABLE = :ci_pipeline_variables
|
||||
BATCH_SIZE = 2_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiJobArtifact < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
TABLE = :ci_job_artifacts
|
||||
BATCH_SIZE = 5_000
|
||||
SUB_BATCH_SIZE = 500
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiStage < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes.freeze
|
||||
TABLE = :ci_stages
|
||||
BATCH_SIZE = 3_000
|
||||
SUB_BATCH_SIZE = 300
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiBuildReportResult < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes.freeze
|
||||
TABLE = :ci_build_report_results
|
||||
BATCH_SIZE = 2_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:build_id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :build_id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiBuildTraceMetadata < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes.freeze
|
||||
TABLE = :ci_build_trace_metadata
|
||||
BATCH_SIZE = 3_000
|
||||
SUB_BATCH_SIZE = 300
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:build_id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :build_id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,30 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiBuildMetadata < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'RebalancePartitionId'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
TABLE = :p_ci_builds_metadata
|
||||
BATCH_SIZE = 5_000
|
||||
SUB_BATCH_SIZE = 500
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
TABLE,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, TABLE, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsForCiJobVariables < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
BATCH_SIZE = 50
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
define_batchable_model(:ci_job_variables)
|
||||
.where(partition_id: 101)
|
||||
.each_batch(of: BATCH_SIZE) do |batch|
|
||||
batch.update_all(partition_id: 100)
|
||||
sleep 0.1
|
||||
end
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -1,36 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixPartitionIdsOnCiSourcesPipelines < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
BATCH_SIZE = 50
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
model = define_batchable_model(:ci_sources_pipelines)
|
||||
|
||||
batch_update_records(model, :partition_id, from: 101, to: 100, source_partition_id: 100)
|
||||
batch_update_records(model, :source_partition_id, from: 101, to: 100)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch_update_records(model, column, from:, to:, **updates)
|
||||
updates.reverse_merge!(column => to)
|
||||
|
||||
model
|
||||
.where(model.arel_table[column].eq(from))
|
||||
.each_batch(of: BATCH_SIZE) { |batch| update_records(batch, updates) }
|
||||
end
|
||||
|
||||
def update_records(relation, updates)
|
||||
relation.update_all(updates)
|
||||
sleep 0.1
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ The following API resources are available in the project context:
|
|||
| [Conan distributions](packages/conan.md) | `/projects/:id/packages/conan` (also available standalone) |
|
||||
| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) |
|
||||
| [Debian packages](packages/debian.md) | `/projects/:id/packages/debian` (also available for groups) |
|
||||
| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
|
||||
| [Dependencies](dependencies.md) **(ULTIMATE ALL)** | `/projects/:id/dependencies` |
|
||||
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
|
||||
| [Deploy tokens](deploy_tokens.md) | `/projects/:id/deploy_tokens` (also available for groups and standalone) |
|
||||
| [Deployments](deployments.md) | `/projects/:id/deployments` |
|
||||
|
|
@ -82,7 +82,7 @@ The following API resources are available in the project context:
|
|||
| [Project milestones](milestones.md) | `/projects/:id/milestones` |
|
||||
| [Project snippets](project_snippets.md) | `/projects/:id/snippets` |
|
||||
| [Project templates](project_templates.md) | `/projects/:id/templates` |
|
||||
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` |
|
||||
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE ALL)** | `/projects/:id/vulnerabilities` |
|
||||
| [Project wikis](wikis.md) | `/projects/:id/wikis` |
|
||||
| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` |
|
||||
| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) |
|
||||
|
|
@ -104,9 +104,9 @@ The following API resources are available in the project context:
|
|||
| [Terraform modules](packages/terraform-modules.md) | `/projects/:id/packages/terraform/modules` (also available standalone) |
|
||||
| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` |
|
||||
| [Visual Review discussions](visual_review_discussions.md) **(PREMIUM)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
|
||||
| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/vulnerabilities/:id` |
|
||||
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` |
|
||||
| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` |
|
||||
| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE ALL)** | `/vulnerabilities/:id` |
|
||||
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE ALL)** | `/projects/:id/vulnerability_exports` |
|
||||
| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE ALL)** | `/projects/:id/vulnerability_findings` |
|
||||
|
||||
## Group resources
|
||||
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ Parameters:
|
|||
| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
|
||||
| `min_access_level` | integer | no | Limit to projects where current user has at least this [role (`access_level`)](members.md#roles) |
|
||||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
|
||||
| `with_security_reports` **(ULTIMATE)** | boolean | no | Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
|
||||
| `with_security_reports` **(ULTIMATE ALL)** | boolean | no | Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
|
||||
|
||||
1. Order by similarity: Orders the results by a similarity score calculated from the provided `search`
|
||||
URL parameter. When using `order_by=similarity`, the `sort` parameter is ignored. When the `search`
|
||||
|
|
@ -992,11 +992,11 @@ PUT /groups/:id
|
|||
| `membership_lock` **(PREMIUM)** | boolean | no | Users cannot be added to projects in this group. |
|
||||
| `prevent_forking_outside_group` **(PREMIUM)** | boolean | no | When enabled, users can **not** fork projects from this group to external namespaces. |
|
||||
| `shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Can be set by administrators only. Maximum number of monthly compute minutes for this group. Can be `nil` (default; inherit system default), `0` (unlimited), or `> 0`. |
|
||||
| `unique_project_download_limit` **(ULTIMATE)** | integer | no | Maximum number of unique projects a user can download in the specified time period before they are banned. Available only on top-level groups. Default: 0, Maximum: 10,000. |
|
||||
| `unique_project_download_limit_interval_in_seconds` **(ULTIMATE)** | integer | no | Time period during which a user can download a maximum amount of projects before they are banned. Available only on top-level groups. Default: 0, Maximum: 864,000 seconds (10 days). |
|
||||
| `unique_project_download_limit_allowlist` **(ULTIMATE)** | array of strings | no | List of usernames excluded from the unique project download limit. Available only on top-level groups. Default: `[]`, Maximum: 100 usernames. |
|
||||
| `unique_project_download_limit_alertlist` **(ULTIMATE)** | array of integers | no | List of user IDs that are emailed when the unique project download limit is exceeded. Available only on top-level groups. Default: `[]`, Maximum: 100 user IDs. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110201) in GitLab 15.9. |
|
||||
| `auto_ban_user_on_excessive_projects_download` **(ULTIMATE)** | boolean | no | When enabled, users are automatically banned from the group when they download more than the maximum number of unique projects specified by `unique_project_download_limit` and `unique_project_download_limit_interval_in_seconds`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94159) in GitLab 15.4. |
|
||||
| `unique_project_download_limit` **(ULTIMATE ALL)** | integer | no | Maximum number of unique projects a user can download in the specified time period before they are banned. Available only on top-level groups. Default: 0, Maximum: 10,000. |
|
||||
| `unique_project_download_limit_interval_in_seconds` **(ULTIMATE ALL)** | integer | no | Time period during which a user can download a maximum amount of projects before they are banned. Available only on top-level groups. Default: 0, Maximum: 864,000 seconds (10 days). |
|
||||
| `unique_project_download_limit_allowlist` **(ULTIMATE ALL)** | array of strings | no | List of usernames excluded from the unique project download limit. Available only on top-level groups. Default: `[]`, Maximum: 100 usernames. |
|
||||
| `unique_project_download_limit_alertlist` **(ULTIMATE ALL)** | array of integers | no | List of user IDs that are emailed when the unique project download limit is exceeded. Available only on top-level groups. Default: `[]`, Maximum: 100 user IDs. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110201) in GitLab 15.9. |
|
||||
| `auto_ban_user_on_excessive_projects_download` **(ULTIMATE ALL)** | boolean | no | When enabled, users are automatically banned from the group when they download more than the maximum number of unique projects specified by `unique_project_download_limit` and `unique_project_download_limit_interval_in_seconds`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94159) in GitLab 15.4. |
|
||||
| `ip_restriction_ranges` **(PREMIUM)** | string | no | Comma-separated list of IP addresses or subnet masks to restrict group access. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351493) in GitLab 15.4. |
|
||||
| `wiki_access_level` **(PREMIUM)** | string | no | The wiki access level. Can be `disabled`, `private`, or `enabled`. |
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ GET /issues?state=opened
|
|||
| `created_before` | datetime | no | Return issues created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
|
||||
| `due_date` | string | no | Return issues that have no due date, are overdue, or whose due date is this week, this month, or between two weeks ago and next month. Accepts: `0` (no due date), `any`, `today`, `tomorrow`, `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`. |
|
||||
| `epic_id` **(PREMIUM)** | integer | no | Return issues associated with the given epic ID. `None` returns issues that are not associated with an epic. `Any` returns issues that are associated with an epic. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46887) in GitLab 13.6)_
|
||||
| `health_status` **(ULTIMATE)** | string | no | Return issues with the specified `health_status`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370721) in GitLab 15.4)._ In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/370721), `None` returns issues with no health status assigned, and `Any` returns issues with a health status assigned.
|
||||
| `health_status` **(ULTIMATE ALL)** | string | no | Return issues with the specified `health_status`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/370721) in GitLab 15.4)._ In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/370721), `None` returns issues with no health status assigned, and `Any` returns issues with a health status assigned.
|
||||
| `iids[]` | integer array | no | Return only the issues having the given `iid` |
|
||||
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
|
||||
| `issue_type` | string | no | Filter to a given type of issue. One of `issue`, `incident`, or `test_case`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/260375) in GitLab 13.12)_ |
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ PUT /projects/:id/members/:user_id
|
|||
| `user_id` | integer | yes | The user ID of the member |
|
||||
| `access_level` | integer | yes | A valid access level |
|
||||
| `expires_at` | string | no | A date string in the format `YEAR-MONTH-DAY` |
|
||||
| `member_role_id` | integer | no | The ID of a member role **(ULTIMATE)** |
|
||||
| `member_role_id` | integer | no | The ID of a member role **(ULTIMATE ALL)** |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/members/:user_id?access_level=40"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ If the `custom` level is used, specific email events can be controlled. Availabl
|
|||
- `success_pipeline`
|
||||
- `moved_project`
|
||||
- `merge_when_pipeline_succeeds`
|
||||
- `new_epic` **(ULTIMATE)**
|
||||
- `new_epic` **(ULTIMATE ALL)**
|
||||
|
||||
## Global notification settings
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
|
|||
| `success_pipeline` | boolean | no | Enable/disable this notification |
|
||||
| `moved_project` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30371) in GitLab 13.3) |
|
||||
| `merge_when_pipeline_succeeds` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/244840) in GitLab 13.9) |
|
||||
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE)** |
|
||||
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE ALL)** |
|
||||
|
||||
Example response:
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
|
|||
| `success_pipeline` | boolean | no | Enable/disable this notification |
|
||||
| `moved_project` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30371) in GitLab 13.3) |
|
||||
| `merge_when_pipeline_succeeds` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/244840) in GitLab 13.9) |
|
||||
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE)** |
|
||||
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5863) in GitLab 11.3) **(ULTIMATE ALL)** |
|
||||
|
||||
Example responses:
|
||||
|
||||
|
|
|
|||
|
|
@ -1495,7 +1495,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your-token>" \
|
|||
| `name` | string | **{check-circle}** Yes (if `path` isn't provided) | The name of the new project. Equals path if not provided. |
|
||||
| `path` | string | **{check-circle}** Yes (if `name` isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). Starting with GitLab 14.9, path must not start or end with a special character and must not contain consecutive special characters. |
|
||||
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
|
||||
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
|
||||
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE ALL)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
|
||||
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
|
||||
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/353097) in GitLab 16.0. |
|
||||
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This action toggles between an enabled state and a disabled state; it is not a boolean. |
|
||||
|
|
@ -1583,7 +1583,7 @@ POST /projects/user/:user_id
|
|||
| `user_id` | integer | **{check-circle}** Yes | The user ID of the project owner. |
|
||||
| `name` | string | **{check-circle}** Yes | The name of the new project. |
|
||||
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
|
||||
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
|
||||
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE ALL)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
|
||||
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
|
||||
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/353097) in GitLab 16.0. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
|
||||
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This action toggles between an enabled state and a disabled state; it is not a boolean. |
|
||||
|
|
@ -1684,7 +1684,7 @@ Supported attributes:
|
|||
| `id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
|
||||
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
|
||||
| `allow_pipeline_trigger_approve_deployment` **(PREMIUM)** | boolean | **{dotted-circle}** No | Set whether or not a pipeline triggerer is allowed to approve deployments. |
|
||||
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false.<br/><br/>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. The feature flag was enabled by default in GitLab 15.9. |
|
||||
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE ALL)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false.<br/><br/>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. The feature flag was enabled by default in GitLab 15.9. |
|
||||
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
|
||||
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/353097) in GitLab 16.0. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
|
||||
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This action toggles between an enabled state and a disabled state; it is not a boolean. |
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ of possible security breaches in our code:
|
|||
|
||||
Remember to run
|
||||
[SAST](../../user/application_security/sast/index.md) and [Dependency Scanning](../../user/application_security/dependency_scanning/index.md)
|
||||
**(ULTIMATE)** on your project (or at least the
|
||||
**(ULTIMATE ALL)** on your project (or at least the
|
||||
[`gosec` analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)),
|
||||
and to follow our [Security requirements](../code_review.md#security).
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ The following events are supported for integrations:
|
|||
| [Pipeline event](../../user/project/integrations/webhook_events.md#pipeline-events) | | `pipeline` | A pipeline status changes.
|
||||
| [Push event](../../user/project/integrations/webhook_events.md#push-events) | ✓ | `push` | A push is made to the repository.
|
||||
| [Tag push event](../../user/project/integrations/webhook_events.md#tag-events) | ✓ | `tag_push` | New tags are pushed to the repository.
|
||||
| Vulnerability event **(ULTIMATE)** | | `vulnerability` | A new, unique vulnerability is recorded.
|
||||
| Vulnerability event **(ULTIMATE ALL)** | | `vulnerability` | A new, unique vulnerability is recorded.
|
||||
| [Wiki page event](../../user/project/integrations/webhook_events.md#wiki-page-events) | ✓ | `wiki_page` | A wiki page is created or updated.
|
||||
|
||||
#### Event examples
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ content directly. Instead, it enhances the results with additional properties, i
|
|||
|
||||
- CWEs.
|
||||
- Location tracking fields.
|
||||
- A means of identifying false positives or insignificant findings. **(ULTIMATE)**
|
||||
- A means of identifying false positives or insignificant findings. **(ULTIMATE ALL)**
|
||||
|
||||
## Transition to Semgrep-based scanning
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# This rebalances partition_id to fix invalid records in production
|
||||
class RebalancePartitionId < BatchedMigrationJob
|
||||
INVALID_PARTITION_ID = 101
|
||||
VALID_PARTITION_ID = 100
|
||||
|
||||
scope_to ->(relation) { relation.where(partition_id: INVALID_PARTITION_ID) }
|
||||
operation_name :update_all
|
||||
feature_category :continuous_integration
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
sub_batch.update_all(partition_id: VALID_PARTITION_ID)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -45,6 +45,11 @@ msgstr ""
|
|||
msgid " and %{sliced}"
|
||||
msgstr ""
|
||||
|
||||
msgid " except branch:"
|
||||
msgid_plural " except branches:"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid " or "
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1438,6 +1443,9 @@ msgstr ""
|
|||
msgid "+ %{count} more"
|
||||
msgstr ""
|
||||
|
||||
msgid "+ %{hiddenBranchesLength} more"
|
||||
msgstr ""
|
||||
|
||||
msgid "+ %{moreCount} more"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6215,9 +6223,6 @@ msgstr ""
|
|||
msgid "Are you sure you want to delete this label?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete this pipeline schedule?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10132,9 +10137,6 @@ msgstr ""
|
|||
msgid "CiVariables|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Cannot use Masked Variable with current value"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Delete variable"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10195,9 +10197,6 @@ msgstr ""
|
|||
msgid "CiVariables|Remove variable"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Remove variable row"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Run job"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10240,9 +10239,6 @@ msgstr ""
|
|||
msgid "CiVariables|You have reached the maximum number of variables available. To add new variables, you must reduce the number of defined variables."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|* (All environments)"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|All environments"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13218,6 +13214,15 @@ msgstr ""
|
|||
msgid "ContributionEvent|Reopened test case %{targetLink} in %{resourceParentLink}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionEvent|Updated design %{targetLink} in %{resourceParentLink}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionEvent|Updated resource."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionEvent|Updated wiki page %{targetLink} in %{resourceParentLink}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionEvent|…and %{count} more commits. %{linkStart}Compare%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14142,9 +14147,6 @@ msgstr ""
|
|||
msgid "Crm|Organization has been updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron Timezone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron time zone"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15478,9 +15480,6 @@ msgstr ""
|
|||
msgid "Define custom rules for what constitutes spam, independent of Akismet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here."
|
||||
msgstr ""
|
||||
|
||||
msgid "Define how approval rules are applied to merge requests."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15603,9 +15602,6 @@ msgstr ""
|
|||
msgid "Delete pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete pipeline schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete project"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16344,9 +16340,6 @@ msgstr ""
|
|||
msgid "Deployments|You don't have any deployments right now."
|
||||
msgstr ""
|
||||
|
||||
msgid "Deployment|API"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deployment|Cancelled"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16395,9 +16388,6 @@ msgstr ""
|
|||
msgid "Deployment|Sync status is unknown. %{linkStart}How do I configure Flux for my deployment?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deployment|This deployment was created using the API"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deployment|Triggerer"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -25565,9 +25555,6 @@ msgstr ""
|
|||
msgid "Interval"
|
||||
msgstr ""
|
||||
|
||||
msgid "Interval Pattern"
|
||||
msgstr ""
|
||||
|
||||
msgid "Introducing Your DevOps Reports"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27563,9 +27550,6 @@ msgstr ""
|
|||
msgid "Last Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last Pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last Seen"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31180,9 +31164,6 @@ msgstr ""
|
|||
msgid "New runners registration token has been generated!"
|
||||
msgstr ""
|
||||
|
||||
msgid "New schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "New snippet"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31501,9 +31482,6 @@ msgstr ""
|
|||
msgid "No runner executable"
|
||||
msgstr ""
|
||||
|
||||
msgid "No schedules"
|
||||
msgstr ""
|
||||
|
||||
msgid "No service accounts"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34295,9 +34273,6 @@ msgstr ""
|
|||
msgid "PipelineSchedules|A scheduled pipeline starts automatically at regular intervals, like daily or weekly. The pipeline: "
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSchedules|Activated"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSchedules|Active"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34412,12 +34387,6 @@ msgstr ""
|
|||
msgid "PipelineSchedules|There was a problem taking ownership of the pipeline schedule."
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSchedules|Variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSchedule|Take ownership to edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineSource|API"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -41379,9 +41348,6 @@ msgstr ""
|
|||
msgid "Save password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save pipeline schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saving"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -42214,6 +42180,12 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|%{agent} for %{namespaces}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|%{branchName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|%{branchName} (in %{codeStart}%{fullPath}%{codeEnd})"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|%{cadence} on %{branches}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -42361,6 +42333,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Hide extra branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|If any scanner finds a newly detected critical vulnerability in an open merge request targeting the master branch, then require two approvals from any member of App security."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -42645,13 +42620,13 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Vulnerability age requires previously existing vulnerability states (detected, confirmed, resolved, or dismissed)"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|When %{scanners} %{vulnerabilitiesAllowed} %{vulnerability} in an open merge request %{targeting}%{branches}%{criteriaApply}"
|
||||
msgid "SecurityOrchestration|When %{scanners} %{vulnerabilitiesAllowed} %{vulnerability} in an open merge request %{targeting}%{branches}%{branchExceptionsString}%{criteriaApply}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|When license scanner finds any license except %{licenses}%{detection} in an open merge request %{targeting}%{branches}."
|
||||
msgid "SecurityOrchestration|When license scanner finds any license except %{licenses}%{detection} in an open merge request %{targeting}%{branches}%{branchExceptionsString}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|When license scanner finds any license matching %{licenses}%{detection} in an open merge request %{targeting}%{branches}."
|
||||
msgid "SecurityOrchestration|When license scanner finds any license matching %{licenses}%{detection} in an open merge request %{targeting}%{branches}%{branchExceptionsString}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|When this policy is enabled, protected branches cannot be unprotected and users will not be allowed to force push to protected branches."
|
||||
|
|
@ -46639,9 +46614,6 @@ msgstr ""
|
|||
msgid "Target branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target branch or tag"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target branch: %{target_branch}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54317,9 +54289,6 @@ msgstr ""
|
|||
msgid "You don't have any authorized applications."
|
||||
msgstr ""
|
||||
|
||||
msgid "You don't have any deployments right now."
|
||||
msgstr ""
|
||||
|
||||
msgid "You don't have any open merge requests"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
link :upgrade_to_ultimate
|
||||
|
||||
# Subscription details
|
||||
strong :subscription_header
|
||||
h5 :subscription_header
|
||||
button :refresh_seats
|
||||
|
||||
# Usage
|
||||
|
|
|
|||
|
|
@ -10,24 +10,19 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
before do
|
||||
sign_in(user)
|
||||
project.add_role(user, role)
|
||||
stub_feature_flags(environment_details_vue: false)
|
||||
end
|
||||
|
||||
def auto_stop_button_selector
|
||||
%q{button[title="Prevent environment from auto-stopping"]}
|
||||
end
|
||||
|
||||
describe 'environment details page vue' do
|
||||
describe 'environment details page', :js do
|
||||
let_it_be(:environment) { create(:environment, project: project) }
|
||||
let!(:permissions) {}
|
||||
let!(:deployment) {}
|
||||
let!(:action) {}
|
||||
let!(:cluster) {}
|
||||
|
||||
before do
|
||||
stub_feature_flags(environment_details_vue: true)
|
||||
end
|
||||
|
||||
context 'with auto-stop' do
|
||||
let_it_be(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
|
||||
|
||||
|
|
@ -35,122 +30,16 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
visit_environment(environment)
|
||||
end
|
||||
|
||||
it 'shows auto stop info', :js do
|
||||
it 'shows auto stop info' do
|
||||
expect(page).to have_content('Auto stops')
|
||||
end
|
||||
|
||||
it 'shows auto stop button', :js do
|
||||
it 'shows auto stop button' do
|
||||
expect(page).to have_selector(auto_stop_button_selector)
|
||||
expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
|
||||
end
|
||||
|
||||
it 'allows user to cancel auto stop', :js do
|
||||
page.find(auto_stop_button_selector).click
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content('Auto stop successfully canceled.')
|
||||
expect(page).not_to have_selector(auto_stop_button_selector)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without deployments' do
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
it 'does not show deployments', :js do
|
||||
expect(page).to have_content('You don\'t have any deployments right now.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deployments' do
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
context 'when there is a successful deployment' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
|
||||
|
||||
let(:deployment) do
|
||||
create(:deployment, :success, environment: environment, deployable: build)
|
||||
end
|
||||
|
||||
it 'does show deployments', :js do
|
||||
wait_for_requests
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a failed deployment' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
let(:deployment) do
|
||||
create(:deployment, :failed, environment: environment, deployable: build)
|
||||
end
|
||||
|
||||
it 'does show deployments', :js do
|
||||
wait_for_requests
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with related deployable present' do
|
||||
let_it_be(:previous_pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
let_it_be(:previous_build) do
|
||||
create(:ci_build, :success, pipeline: previous_pipeline, environment: environment.name)
|
||||
end
|
||||
|
||||
let_it_be(:previous_deployment) do
|
||||
create(:deployment, :success, environment: environment, deployable: previous_build)
|
||||
end
|
||||
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) }
|
||||
|
||||
let_it_be(:deployment) do
|
||||
create(:deployment, :success, environment: environment, deployable: build)
|
||||
end
|
||||
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
it 'shows deployment information and buttons', :js do
|
||||
wait_for_requests
|
||||
expect(page).to have_button('Re-deploy to environment')
|
||||
expect(page).to have_button('Rollback environment')
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'environment details page' do
|
||||
let_it_be(:environment) { create(:environment, project: project) }
|
||||
let!(:permissions) {}
|
||||
let!(:deployment) {}
|
||||
let!(:action) {}
|
||||
let!(:cluster) {}
|
||||
|
||||
context 'with auto-stop' do
|
||||
let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
|
||||
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
it 'shows auto stop info', :js do
|
||||
expect(page).to have_content('Auto stops')
|
||||
end
|
||||
|
||||
it 'shows auto stop button', :js do
|
||||
expect(page).to have_selector(auto_stop_button_selector)
|
||||
expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
|
||||
end
|
||||
|
||||
it 'allows user to cancel auto stop', :js do
|
||||
it 'allows user to cancel auto stop' do
|
||||
page.find(auto_stop_button_selector).click
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content('Auto stop successfully canceled.')
|
||||
|
|
@ -208,10 +97,6 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
it 'does show deployments' do
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
end
|
||||
|
||||
it 'shows a tooltip on the job name' do
|
||||
expect(page).to have_css("[title=\"#{build.name} (##{build.id})\"].has-tooltip")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a failed deployment' do
|
||||
|
|
@ -227,26 +112,6 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with many deployments' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
let!(:second) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: Time.current) }
|
||||
let!(:first) { create(:deployment, environment: environment, deployable: build, status: :running) }
|
||||
let!(:last) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: 2.days.ago) }
|
||||
let!(:third) { create(:deployment, environment: environment, deployable: build, status: :canceled, finished_at: 1.day.ago) }
|
||||
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
it 'shows all of them in ordered way' do
|
||||
ids = find_all('[data-testid="deployment-id"]').map { |e| e.text }
|
||||
expected_ordered_ids = [first, second, third, last].map { |d| "##{d.iid}" }
|
||||
expect(ids).to eq(expected_ordered_ids)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with upcoming deployments' do
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
|
@ -265,7 +130,7 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/350618 for more information.
|
||||
it 'shows upcoming deployments in unordered way' do
|
||||
displayed_ids = find_all('[data-testid="deployment-id"]').map { |e| e.text }
|
||||
internal_ids = [runnind_deployment_1, runnind_deployment_2, success_without_finished_at].map { |d| "##{d.iid}" }
|
||||
internal_ids = [runnind_deployment_1, runnind_deployment_2, success_without_finished_at].map { |d| d.iid.to_s }
|
||||
expect(displayed_ids).to match_array(internal_ids)
|
||||
end
|
||||
end
|
||||
|
|
@ -309,20 +174,19 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
it 'does show a play button' do
|
||||
expect(page).to have_link(action.name)
|
||||
expect(page).to have_button(action.name, visible: :all)
|
||||
end
|
||||
|
||||
it 'does allow to play manual action', :js do
|
||||
it 'does allow to play manual action' do
|
||||
expect(action).to be_manual
|
||||
|
||||
find('button.dropdown').click
|
||||
click_button('Deploy to...')
|
||||
|
||||
expect { click_link(action.name) }
|
||||
expect { click_button(action.name) }
|
||||
.not_to change { Ci::Pipeline.count }
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
expect(page).to have_content(action.name)
|
||||
expect(action.reload).to be_pending
|
||||
end
|
||||
end
|
||||
|
|
@ -347,38 +211,6 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with terminal' do
|
||||
context 'when user configured kubernetes from CI/CD > Clusters' do
|
||||
let!(:cluster) do
|
||||
create(:cluster, :project, :provided_by_gcp, projects: [project])
|
||||
end
|
||||
|
||||
context 'for project maintainer' do
|
||||
let(:role) { :maintainer }
|
||||
|
||||
context 'web terminal', :js do
|
||||
before do
|
||||
# Stub #terminals as it causes js-enabled feature specs to
|
||||
# render the page incorrectly
|
||||
#
|
||||
# In EE we have to stub EE::Environment since it overwrites
|
||||
# the "terminals" method.
|
||||
allow_next_instance_of(Gitlab.ee? ? EE::Environment : Environment) do |instance|
|
||||
allow(instance).to receive(:terminals) { nil }
|
||||
end
|
||||
|
||||
visit terminal_project_environment_path(project, environment)
|
||||
end
|
||||
|
||||
it 'displays a web terminal' do
|
||||
expect(page).to have_selector('#terminal')
|
||||
expect(page).to have_link(nil, href: environment.external_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment is available' do
|
||||
context 'with stop action' do
|
||||
let(:build) { create(:ci_build, :success, pipeline: pipeline, environment: environment.name) }
|
||||
|
|
@ -446,6 +278,8 @@ RSpec.describe 'Environment', feature_category: :groups_and_projects do
|
|||
visit folder_project_environments_path(project, id: 'staging-1.0')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(reqs.first.status_code).to eq(200)
|
||||
expect(page).to have_content('Environments / staging-1.0')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,389 +12,301 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :groups_and_projects
|
|||
let!(:user) { create(:user) }
|
||||
let!(:maintainer) { create(:user) }
|
||||
|
||||
context 'with pipeline_schedules_vue feature flag turned off' do
|
||||
context 'logged in as the pipeline schedule owner' do
|
||||
before do
|
||||
stub_feature_flags(pipeline_schedules_vue: false)
|
||||
project.add_developer(user)
|
||||
pipeline_schedule.update!(owner: user)
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
||||
context 'logged in as the pipeline schedule owner' do
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
pipeline_schedule.update!(owner: user)
|
||||
gitlab_sign_in(user)
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
it 'edits the pipeline' do
|
||||
page.find('[data-testid="edit-pipeline-schedule-btn"]').click
|
||||
|
||||
it 'edits the pipeline' do
|
||||
page.within('.pipeline-schedule-table-row') do
|
||||
click_link 'Edit'
|
||||
end
|
||||
expect(page).to have_content(s_('PipelineSchedules|Edit pipeline schedule'))
|
||||
end
|
||||
end
|
||||
|
||||
expect(page).to have_content('Edit Pipeline Schedule')
|
||||
end
|
||||
describe 'PATCH /projects/pipelines_schedules/:id/edit' do
|
||||
before do
|
||||
edit_pipeline_schedule
|
||||
end
|
||||
|
||||
describe 'PATCH /projects/pipelines_schedules/:id/edit' do
|
||||
it 'displays existing properties' do
|
||||
description = find_field('schedule-description').value
|
||||
expect(description).to eq('pipeline schedule')
|
||||
expect(page).to have_button('master')
|
||||
expect(page).to have_button(_('Select timezone'))
|
||||
end
|
||||
|
||||
it 'edits the scheduled pipeline' do
|
||||
fill_in 'schedule-description', with: 'my brand new description'
|
||||
|
||||
save_pipeline_schedule
|
||||
|
||||
expect(page).to have_content('my brand new description')
|
||||
end
|
||||
|
||||
context 'when ref is nil' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, nil)
|
||||
edit_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'displays existing properties' do
|
||||
description = find_field('schedule_description').value
|
||||
expect(description).to eq('pipeline schedule')
|
||||
expect(page).to have_button('master')
|
||||
expect(page).to have_button('Select timezone')
|
||||
end
|
||||
|
||||
it 'edits the scheduled pipeline' do
|
||||
fill_in 'schedule_description', with: 'my brand new description'
|
||||
|
||||
save_pipeline_schedule
|
||||
|
||||
expect(page).to have_content('my brand new description')
|
||||
end
|
||||
|
||||
context 'when ref is nil' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, nil)
|
||||
edit_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'shows the pipeline schedule with default ref' do
|
||||
page.within('[data-testid="schedule-target-ref"]') do
|
||||
expect(first('.gl-button-text').text).to eq('master')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is empty' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, '')
|
||||
edit_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'shows the pipeline schedule with default ref' do
|
||||
page.within('[data-testid="schedule-target-ref"]') do
|
||||
expect(first('.gl-button-text').text).to eq('master')
|
||||
end
|
||||
it 'shows the pipeline schedule with default ref' do
|
||||
page.within('#schedule-target-branch-tag') do
|
||||
expect(first('.gl-button-text').text).to eq('master')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'logged in as a project maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
context 'when ref is empty' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
pipeline_schedule.update_attribute(:ref, '')
|
||||
edit_pipeline_schedule
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'displays the required information description' do
|
||||
page.within('.pipeline-schedule-table-row') do
|
||||
expect(page).to have_content('pipeline schedule')
|
||||
expect(find("[data-testid='next-run-cell'] time")['title'])
|
||||
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
|
||||
expect(page).to have_link('master')
|
||||
expect(page).to have_link("##{pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new scheduled pipeline' do
|
||||
click_link 'New schedule'
|
||||
|
||||
expect(page).to have_content('Schedule a new pipeline')
|
||||
end
|
||||
|
||||
it 'changes ownership of the pipeline' do
|
||||
click_button 'Take ownership'
|
||||
|
||||
page.within('#pipeline-take-ownership-modal') do
|
||||
click_link 'Take ownership'
|
||||
end
|
||||
|
||||
page.within('.pipeline-schedule-table-row') do
|
||||
expect(page).not_to have_content('No owner')
|
||||
expect(page).to have_link('Sidney Jones')
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes the pipeline' do
|
||||
click_link 'Delete'
|
||||
|
||||
accept_gl_confirm(button_text: 'Delete pipeline schedule')
|
||||
|
||||
expect(page).not_to have_css(".pipeline-schedule-table-row")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is nil' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, nil)
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
it 'shows a list of the pipeline schedules with empty ref column' do
|
||||
expect(first('.branch-name-cell').text).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is empty' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, '')
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
it 'shows a list of the pipeline schedules with empty ref column' do
|
||||
expect(first('.branch-name-cell').text).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/pipeline_schedules/new' do
|
||||
before do
|
||||
visit_new_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'sets defaults for timezone and target branch' do
|
||||
expect(page).to have_button('master')
|
||||
expect(page).to have_button('Select timezone')
|
||||
end
|
||||
|
||||
it 'creates a new scheduled pipeline' do
|
||||
fill_in_schedule_form
|
||||
save_pipeline_schedule
|
||||
|
||||
expect(page).to have_content('my fancy description')
|
||||
end
|
||||
|
||||
it 'prevents an invalid form from being submitted' do
|
||||
save_pipeline_schedule
|
||||
|
||||
expect(page).to have_content('This field is required')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user creates a new pipeline schedule with variables' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
click_link 'New schedule'
|
||||
fill_in_schedule_form
|
||||
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
|
||||
all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
|
||||
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
|
||||
all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
|
||||
save_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
page.within('.ci-variable-list') do
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
|
||||
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
|
||||
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user edits a variable of a pipeline schedule' do
|
||||
before do
|
||||
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
|
||||
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
|
||||
end
|
||||
|
||||
visit_pipelines_schedules
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
find('.js-ci-variable-list-section .js-secret-value-reveal-button').click
|
||||
first('.js-ci-variable-input-key').set('foo')
|
||||
first('.js-ci-variable-input-value').set('bar')
|
||||
click_button 'Save pipeline schedule'
|
||||
end
|
||||
|
||||
it 'user sees the updated variable in edit window' do
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
page.within('.ci-variable-list') do
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user removes a variable of a pipeline schedule' do
|
||||
before do
|
||||
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
|
||||
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
|
||||
end
|
||||
|
||||
visit_pipelines_schedules
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
find('.ci-variable-list .ci-variable-row-remove-button').click
|
||||
click_button 'Save pipeline schedule'
|
||||
end
|
||||
|
||||
it 'user does not see the removed variable in edit window' do
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
page.within('.ci-variable-list') do
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when active is true and next_run_at is NULL' do
|
||||
before do
|
||||
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
|
||||
pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'user edit and recover the problematic pipeline schedule' do
|
||||
visit_pipelines_schedules
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
fill_in 'schedule_cron', with: '* 1 2 3 4'
|
||||
click_button 'Save pipeline schedule'
|
||||
|
||||
page.within('.pipeline-schedule-table-row:nth-child(1)') do
|
||||
expect(page).to have_css("[data-testid='next-run-cell'] time")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'logged in as non-member' do
|
||||
before do
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'does not show create schedule button' do
|
||||
expect(page).not_to have_link('New schedule')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'not logged in' do
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'does not show create schedule button' do
|
||||
expect(page).not_to have_link('New schedule')
|
||||
it 'shows the pipeline schedule with default ref' do
|
||||
page.within('#schedule-target-branch-tag') do
|
||||
expect(first('.gl-button-text').text).to eq('master')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pipeline_schedules_vue feature flag turned on' do
|
||||
context 'logged in as a project maintainer' do
|
||||
context 'logged in as a project maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
pipeline_schedule.update!(owner: maintainer)
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
project.add_maintainer(maintainer)
|
||||
pipeline_schedule.update!(owner: user)
|
||||
gitlab_sign_in(maintainer)
|
||||
visit_pipelines_schedules
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
|
||||
wait_for_requests
|
||||
describe 'The view' do
|
||||
it 'displays the required information description' do
|
||||
page.within('[data-testid="pipeline-schedule-table-row"]') do
|
||||
expect(page).to have_content('pipeline schedule')
|
||||
expect(find('[data-testid="next-run-cell"] time')['title'])
|
||||
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
|
||||
expect(page).to have_link('master')
|
||||
expect(find("[data-testid='last-pipeline-status'] a")['href']).to include(pipeline.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'displays the required information description' do
|
||||
page.within('[data-testid="pipeline-schedule-table-row"]') do
|
||||
expect(page).to have_content('pipeline schedule')
|
||||
expect(find("[data-testid='next-run-cell'] time")['title'])
|
||||
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
|
||||
expect(page).to have_link('master')
|
||||
expect(find("[data-testid='last-pipeline-status'] a")['href']).to include(pipeline.id.to_s)
|
||||
end
|
||||
end
|
||||
it 'creates a new scheduled pipeline' do
|
||||
click_link 'New schedule'
|
||||
|
||||
it 'changes ownership of the pipeline' do
|
||||
click_button 'Take ownership'
|
||||
expect(page).to have_content('Schedule a new pipeline')
|
||||
end
|
||||
|
||||
page.within('#pipeline-take-ownership-modal') do
|
||||
click_button 'Take ownership'
|
||||
it 'changes ownership of the pipeline' do
|
||||
find("[data-testid='take-ownership-pipeline-schedule-btn']").click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
page.within('[data-testid="pipeline-schedule-table-row"]') do
|
||||
expect(page).not_to have_content('No owner')
|
||||
expect(page).to have_link('Sidney Jones')
|
||||
end
|
||||
end
|
||||
|
||||
it 'runs the pipeline' do
|
||||
click_button 'Run pipeline schedule'
|
||||
page.within('#pipeline-take-ownership-modal') do
|
||||
click_button s_('PipelineSchedules|Take ownership')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content("Successfully scheduled a pipeline to run. Go to the Pipelines page for details.")
|
||||
end
|
||||
|
||||
it 'deletes the pipeline' do
|
||||
click_button 'Delete pipeline schedule'
|
||||
|
||||
accept_gl_confirm(button_text: 'Delete pipeline schedule')
|
||||
|
||||
expect(page).not_to have_css('[data-testid="pipeline-schedule-table-row"]')
|
||||
page.within('[data-testid="pipeline-schedule-table-row"]') do
|
||||
expect(page).not_to have_content('No owner')
|
||||
expect(page).to have_link('Sidney Jones')
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes the pipeline' do
|
||||
page.within('[data-testid="pipeline-schedule-table-row"]') do
|
||||
click_button s_('PipelineSchedules|Delete pipeline schedule')
|
||||
end
|
||||
|
||||
accept_gl_confirm(button_text: s_('PipelineSchedules|Delete pipeline schedule'))
|
||||
|
||||
expect(page).not_to have_css('[data-testid="pipeline-schedule-table-row"]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is nil' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, nil)
|
||||
visit_pipelines_schedules
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'shows a list of the pipeline schedules with empty ref column' do
|
||||
target = find('[data-testid="pipeline-schedule-target"]')
|
||||
|
||||
page.within('[data-testid="pipeline-schedule-table-row"]') do
|
||||
expect(target.text).to eq(s_('PipelineSchedules|None'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref is empty' do
|
||||
before do
|
||||
pipeline_schedule.update_attribute(:ref, '')
|
||||
visit_pipelines_schedules
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'shows a list of the pipeline schedules with empty ref column' do
|
||||
target = find('[data-testid="pipeline-schedule-target"]')
|
||||
|
||||
expect(target.text).to eq(s_('PipelineSchedules|None'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'logged in as non-member' do
|
||||
describe 'POST /projects/pipeline_schedules/new' do
|
||||
before do
|
||||
gitlab_sign_in(user)
|
||||
visit_new_pipeline_schedule
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
it 'sets defaults for timezone and target branch' do
|
||||
expect(page).to have_button('master')
|
||||
expect(page).to have_button('Select timezone')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
it 'creates a new scheduled pipeline' do
|
||||
fill_in_schedule_form
|
||||
create_pipeline_schedule
|
||||
|
||||
describe 'The view' do
|
||||
it 'does not show create schedule button' do
|
||||
expect(page).not_to have_link('New schedule')
|
||||
end
|
||||
expect(page).to have_content('my fancy description')
|
||||
end
|
||||
|
||||
it 'prevents an invalid form from being submitted' do
|
||||
create_pipeline_schedule
|
||||
|
||||
expect(page).to have_content("Cron timezone can't be blank")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user creates a new pipeline schedule with variables' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
click_link 'New schedule'
|
||||
fill_in_schedule_form
|
||||
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
|
||||
all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
|
||||
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
|
||||
all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
|
||||
create_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
|
||||
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
|
||||
page.within('.ci-variable-list') do
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
|
||||
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
|
||||
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
|
||||
expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'not logged in' do
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
|
||||
wait_for_requests
|
||||
context 'when user edits a variable of a pipeline schedule' do
|
||||
before do
|
||||
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
|
||||
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'does not show create schedule button' do
|
||||
expect(page).not_to have_link('New schedule')
|
||||
end
|
||||
visit_pipelines_schedules
|
||||
first('[data-testid="edit-pipeline-schedule-btn"]').click
|
||||
click_button _('Reveal values')
|
||||
first('[data-testid="pipeline-form-ci-variable-key"]').set('foo')
|
||||
first('[data-testid="pipeline-form-ci-variable-value"]').set('bar')
|
||||
save_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'user sees the updated variable' do
|
||||
first('[data-testid="edit-pipeline-schedule-btn"]').click
|
||||
|
||||
expect(first('[data-testid="pipeline-form-ci-variable-key"]').value).to eq('foo')
|
||||
expect(first('[data-testid="pipeline-form-ci-variable-value"]').value).to eq('')
|
||||
|
||||
click_button _('Reveal values')
|
||||
|
||||
expect(first('[data-testid="pipeline-form-ci-variable-value"]').value).to eq('bar')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user removes a variable of a pipeline schedule' do
|
||||
before do
|
||||
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
|
||||
create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule)
|
||||
end
|
||||
|
||||
visit_pipelines_schedules
|
||||
first('[data-testid="edit-pipeline-schedule-btn"]').click
|
||||
find('[data-testid="remove-ci-variable-row"]').click
|
||||
save_pipeline_schedule
|
||||
end
|
||||
|
||||
it 'user does not see the removed variable in edit window' do
|
||||
first('[data-testid="edit-pipeline-schedule-btn"]').click
|
||||
|
||||
expect(first('[data-testid="pipeline-form-ci-variable-key"]').value).to eq('')
|
||||
expect(first('[data-testid="pipeline-form-ci-variable-value"]').value).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when active is true and next_run_at is NULL' do
|
||||
before do
|
||||
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
|
||||
pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'user edit and recover the problematic pipeline schedule' do
|
||||
visit_pipelines_schedules
|
||||
first('[data-testid="edit-pipeline-schedule-btn"]').click
|
||||
fill_in 'schedule_cron', with: '* 1 2 3 4'
|
||||
save_pipeline_schedule
|
||||
|
||||
page.within(first('[data-testid="pipeline-schedule-table-row"]')) do
|
||||
expect(page).to have_css("[data-testid='next-run-cell'] time")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'logged in as non-member' do
|
||||
before do
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'does not show create schedule button' do
|
||||
expect(page).not_to have_link('New schedule')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'not logged in' do
|
||||
describe 'GET /projects/pipeline_schedules' do
|
||||
before do
|
||||
visit_pipelines_schedules
|
||||
end
|
||||
|
||||
describe 'The view' do
|
||||
it 'does not show create schedule button' do
|
||||
expect(page).not_to have_link('New schedule')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -413,7 +325,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :groups_and_projects
|
|||
end
|
||||
|
||||
def select_timezone
|
||||
find('[data-testid="schedule-timezone"] .gl-new-dropdown-toggle').click
|
||||
find('#schedule-timezone .gl-new-dropdown-toggle').click
|
||||
find("li", text: "Arizona").click
|
||||
end
|
||||
|
||||
|
|
@ -421,12 +333,16 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :groups_and_projects
|
|||
click_button 'master'
|
||||
end
|
||||
|
||||
def create_pipeline_schedule
|
||||
click_button s_('PipelineSchedules|Create pipeline schedule')
|
||||
end
|
||||
|
||||
def save_pipeline_schedule
|
||||
click_button 'Save pipeline schedule'
|
||||
click_button s_('PipelineSchedules|Edit pipeline schedule')
|
||||
end
|
||||
|
||||
def fill_in_schedule_form
|
||||
fill_in 'schedule_description', with: 'my fancy description'
|
||||
fill_in 'schedule-description', with: 'my fancy description'
|
||||
fill_in 'schedule_cron', with: '* 1 2 3 4'
|
||||
|
||||
select_timezone
|
||||
|
|
|
|||
|
|
@ -843,12 +843,10 @@ RSpec.describe 'Pipeline', :js, feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
it 'displays the PipelineSchedule in an inactive state' do
|
||||
stub_feature_flags(pipeline_schedules_vue: false)
|
||||
|
||||
visit project_pipeline_schedules_path(project)
|
||||
page.click_link('Inactive')
|
||||
|
||||
expect(page).to have_selector('table.ci-table > tbody > tr > td', text: 'blocked user schedule')
|
||||
expect(page).to have_selector('[data-testid="pipeline-schedule-description"]', text: 'blocked user schedule')
|
||||
end
|
||||
|
||||
it 'does not create a new Pipeline', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408215' do
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
|
||||
import htmlPipelineSchedulesEditWithVariables from 'test_fixtures/pipeline_schedules/edit_with_variables.html';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import VariableList from '~/ci/ci_variable_list/ci_variable_list';
|
||||
|
||||
const HIDE_CLASS = 'hide';
|
||||
|
||||
describe('VariableList', () => {
|
||||
let $wrapper;
|
||||
let variableList;
|
||||
|
||||
describe('with only key/value inputs', () => {
|
||||
describe('with no variables', () => {
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(htmlPipelineSchedulesEdit);
|
||||
$wrapper = $('.js-ci-variable-list-section');
|
||||
|
||||
variableList = new VariableList({
|
||||
container: $wrapper,
|
||||
formField: 'schedule',
|
||||
});
|
||||
variableList.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('should remove the row when clicking the remove button', () => {
|
||||
$wrapper.find('.js-row-remove-button').trigger('click');
|
||||
|
||||
expect($wrapper.find('.js-row').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should add another row when editing the last rows key input', () => {
|
||||
const $row = $wrapper.find('.js-row');
|
||||
$row.find('.js-ci-variable-input-key').val('foo').trigger('input');
|
||||
|
||||
expect($wrapper.find('.js-row').length).toBe(2);
|
||||
|
||||
// Check for the correct default in the new row
|
||||
const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
|
||||
|
||||
expect($keyInput.val()).toBe('');
|
||||
});
|
||||
|
||||
it('should add another row when editing the last rows value textarea', () => {
|
||||
const $row = $wrapper.find('.js-row');
|
||||
$row.find('.js-ci-variable-input-value').val('foo').trigger('input');
|
||||
|
||||
expect($wrapper.find('.js-row').length).toBe(2);
|
||||
|
||||
// Check for the correct default in the new row
|
||||
const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
|
||||
|
||||
expect($valueInput.val()).toBe('');
|
||||
});
|
||||
|
||||
it('should remove empty row after blurring', () => {
|
||||
const $row = $wrapper.find('.js-row');
|
||||
$row.find('.js-ci-variable-input-key').val('foo').trigger('input');
|
||||
|
||||
expect($wrapper.find('.js-row').length).toBe(2);
|
||||
|
||||
$row.find('.js-ci-variable-input-key').val('').trigger('input').trigger('blur');
|
||||
|
||||
expect($wrapper.find('.js-row').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with persisted variables', () => {
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
|
||||
$wrapper = $('.js-ci-variable-list-section');
|
||||
|
||||
variableList = new VariableList({
|
||||
container: $wrapper,
|
||||
formField: 'schedule',
|
||||
});
|
||||
variableList.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('should have "Reveal values" button initially when there are already variables', () => {
|
||||
expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
|
||||
});
|
||||
|
||||
it('should reveal hidden values', () => {
|
||||
const $row = $wrapper.find('.js-row:first-child');
|
||||
const $inputValue = $row.find('.js-ci-variable-input-value');
|
||||
const $placeholder = $row.find('.js-secret-value-placeholder');
|
||||
|
||||
expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
|
||||
expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
|
||||
|
||||
// Reveal values
|
||||
$wrapper.find('.js-secret-value-reveal-button').click();
|
||||
|
||||
expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
|
||||
expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleEnableRow method', () => {
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
|
||||
$wrapper = $('.js-ci-variable-list-section');
|
||||
|
||||
variableList = new VariableList({
|
||||
container: $wrapper,
|
||||
formField: 'variables',
|
||||
});
|
||||
variableList.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('should disable all key inputs', () => {
|
||||
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
|
||||
|
||||
variableList.toggleEnableRow(false);
|
||||
|
||||
expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
|
||||
});
|
||||
|
||||
it('should disable all remove buttons', () => {
|
||||
expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
|
||||
|
||||
variableList.toggleEnableRow(false);
|
||||
|
||||
expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
|
||||
});
|
||||
|
||||
it('should enable all remove buttons', () => {
|
||||
variableList.toggleEnableRow(false);
|
||||
|
||||
expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
|
||||
|
||||
variableList.toggleEnableRow(true);
|
||||
|
||||
expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
|
||||
});
|
||||
|
||||
it('should enable all key inputs', () => {
|
||||
variableList.toggleEnableRow(false);
|
||||
|
||||
expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
|
||||
|
||||
variableList.toggleEnableRow(true);
|
||||
|
||||
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
|
||||
|
||||
describe('NativeFormVariableList', () => {
|
||||
let $wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(htmlPipelineSchedulesEdit);
|
||||
$wrapper = $('.js-ci-variable-list-section');
|
||||
|
||||
setupNativeFormVariableList({
|
||||
container: $wrapper,
|
||||
formField: 'schedule',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
describe('onFormSubmit', () => {
|
||||
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
|
||||
const $row = $wrapper.find('.js-row');
|
||||
|
||||
expect($row.find('.js-ci-variable-input-key').attr('name')).toBe(
|
||||
'schedule[variables_attributes][][key]',
|
||||
);
|
||||
|
||||
expect($row.find('.js-ci-variable-input-value').attr('name')).toBe(
|
||||
'schedule[variables_attributes][][secret_value]',
|
||||
);
|
||||
|
||||
$wrapper.closest('form').trigger('trigger-submit');
|
||||
|
||||
expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('');
|
||||
expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { s__ } from '~/locale';
|
||||
import PipelineScheduleTarget from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
|
||||
import { mockPipelineScheduleNodes } from '../../../mock_data';
|
||||
|
||||
|
|
@ -20,18 +21,35 @@ describe('Pipeline schedule target', () => {
|
|||
|
||||
const findIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findLink = () => wrapper.findComponent(GlLink);
|
||||
const findTarget = () => wrapper.findComponent('[data-testid="pipeline-schedule-target"]');
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
describe('with ref', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('displays icon', () => {
|
||||
expect(findIcon().exists()).toBe(true);
|
||||
expect(findIcon().props('name')).toBe('fork');
|
||||
});
|
||||
|
||||
it('displays ref link', () => {
|
||||
expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
|
||||
expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays icon', () => {
|
||||
expect(findIcon().exists()).toBe(true);
|
||||
expect(findIcon().props('name')).toBe('fork');
|
||||
});
|
||||
describe('without refPath', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
schedule: { ...mockPipelineScheduleNodes[0], refPath: null, refForDisplay: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('displays ref link', () => {
|
||||
expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
|
||||
expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
|
||||
it('displays none for the target', () => {
|
||||
expect(findIcon().exists()).toBe(false);
|
||||
expect(findLink().exists()).toBe(false);
|
||||
expect(findTarget().text()).toBe(s__('PipelineSchedules|None'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import TakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
|
||||
|
||||
describe('Take ownership modal', () => {
|
||||
let wrapper;
|
||||
const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMountExtended(TakeOwnershipModalLegacy, {
|
||||
propsData: {
|
||||
ownershipUrl: url,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('has a primary action set to a url and a post data-method', () => {
|
||||
const actionPrimary = findModal().props('actionPrimary');
|
||||
|
||||
expect(actionPrimary.attributes).toEqual(
|
||||
expect.objectContaining({
|
||||
category: 'primary',
|
||||
variant: 'confirm',
|
||||
href: url,
|
||||
'data-method': 'post',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a take ownership message', () => {
|
||||
expect(findModal().text()).toBe(
|
||||
'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ContributionEventUpdated from '~/contribution_events/components/contribution_event/contribution_event_updated.vue';
|
||||
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
|
||||
import { eventDesignUpdated, eventWikiPageUpdated } from '../../utils';
|
||||
|
||||
describe('ContributionEventUpdated', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ propsData }) => {
|
||||
wrapper = shallowMountExtended(ContributionEventUpdated, {
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
describe.each`
|
||||
event | expectedMessage
|
||||
${eventDesignUpdated()} | ${'Updated design %{targetLink} in %{resourceParentLink}.'}
|
||||
${eventWikiPageUpdated()} | ${'Updated wiki page %{targetLink} in %{resourceParentLink}.'}
|
||||
${{ target: { type: 'unsupported type' } }} | ${'Updated resource.'}
|
||||
`('when event target type is $event.target.type', ({ event, expectedMessage }) => {
|
||||
it('renders `ContributionEventBase` with correct props', () => {
|
||||
createComponent({ propsData: { event } });
|
||||
|
||||
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
|
||||
event,
|
||||
message: expectedMessage,
|
||||
iconName: 'pencil',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ import ContributionEventCreated from '~/contribution_events/components/contribut
|
|||
import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
|
||||
import ContributionEventReopened from '~/contribution_events/components/contribution_event/contribution_event_reopened.vue';
|
||||
import ContributionEventCommented from '~/contribution_events/components/contribution_event/contribution_event_commented.vue';
|
||||
import ContributionEventUpdated from '~/contribution_events/components/contribution_event/contribution_event_updated.vue';
|
||||
import {
|
||||
eventApproved,
|
||||
eventExpired,
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
eventClosed,
|
||||
eventReopened,
|
||||
eventCommented,
|
||||
eventUpdated,
|
||||
} from '../utils';
|
||||
|
||||
describe('ContributionEvents', () => {
|
||||
|
|
@ -43,6 +45,7 @@ describe('ContributionEvents', () => {
|
|||
eventClosed(),
|
||||
eventReopened(),
|
||||
eventCommented(),
|
||||
eventUpdated(),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
@ -61,6 +64,7 @@ describe('ContributionEvents', () => {
|
|||
${ContributionEventClosed} | ${eventClosed()}
|
||||
${ContributionEventReopened} | ${eventReopened()}
|
||||
${ContributionEventCommented} | ${eventCommented()}
|
||||
${ContributionEventUpdated} | ${eventUpdated()}
|
||||
`(
|
||||
'renders `$expectedComponent.name` component and passes expected event',
|
||||
({ expectedComponent, expectedEvent }) => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
EVENT_TYPE_CLOSED,
|
||||
EVENT_TYPE_REOPENED,
|
||||
EVENT_TYPE_COMMENTED,
|
||||
EVENT_TYPE_UPDATED,
|
||||
PUSH_EVENT_REF_TYPE_BRANCH,
|
||||
PUSH_EVENT_REF_TYPE_TAG,
|
||||
EVENT_TYPE_CREATED,
|
||||
|
|
@ -32,22 +33,12 @@ import {
|
|||
COMMIT_NOTEABLE_TYPE,
|
||||
} from '~/notes/constants';
|
||||
|
||||
// Private finders
|
||||
const findEventByAction = (action) => () => events.find((event) => event.action === action);
|
||||
const findEventByActionAndTargetType = (action, targetType) => () =>
|
||||
events.find((event) => event.action === action && event.target?.type === targetType);
|
||||
const findEventByActionAndIssueType = (action, issueType) => () =>
|
||||
events.find((event) => event.action === action && event.target.issue_type === issueType);
|
||||
|
||||
export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
|
||||
|
||||
export const eventExpired = findEventByAction(EVENT_TYPE_EXPIRED);
|
||||
|
||||
export const eventJoined = findEventByAction(EVENT_TYPE_JOINED);
|
||||
|
||||
export const eventLeft = findEventByAction(EVENT_TYPE_LEFT);
|
||||
|
||||
export const eventMerged = findEventByAction(EVENT_TYPE_MERGED);
|
||||
|
||||
const findPushEvent = ({
|
||||
isNew = false,
|
||||
isRemoved = false,
|
||||
|
|
@ -62,6 +53,43 @@ const findPushEvent = ({
|
|||
ref.type === refType &&
|
||||
commit.count === commitCount,
|
||||
);
|
||||
const findEventByActionAndNoteableType = (action, noteableType) => () =>
|
||||
events.find((event) => event.action === action && event.noteable?.type === noteableType);
|
||||
const findCommentedSnippet = (resourceParentType) => () =>
|
||||
events.find(
|
||||
(event) =>
|
||||
event.action === EVENT_TYPE_COMMENTED &&
|
||||
event.noteable?.type === SNIPPET_NOTEABLE_TYPE &&
|
||||
event.resource_parent?.type === resourceParentType,
|
||||
);
|
||||
const findUpdatedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_UPDATED, targetType);
|
||||
|
||||
// Finders that are used by EE
|
||||
export const findCreatedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
|
||||
export const findWorkItemCreatedEvent = (issueType) =>
|
||||
findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
|
||||
export const findClosedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
|
||||
export const findWorkItemClosedEvent = (issueType) =>
|
||||
findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
|
||||
export const findReopenedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_REOPENED, targetType);
|
||||
export const findWorkItemReopenedEvent = (issueType) =>
|
||||
findEventByActionAndIssueType(EVENT_TYPE_REOPENED, issueType);
|
||||
export const findCommentedEvent = (noteableType) =>
|
||||
findEventByActionAndNoteableType(EVENT_TYPE_COMMENTED, noteableType);
|
||||
|
||||
export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
|
||||
|
||||
export const eventExpired = findEventByAction(EVENT_TYPE_EXPIRED);
|
||||
|
||||
export const eventJoined = findEventByAction(EVENT_TYPE_JOINED);
|
||||
|
||||
export const eventLeft = findEventByAction(EVENT_TYPE_LEFT);
|
||||
|
||||
export const eventMerged = findEventByAction(EVENT_TYPE_MERGED);
|
||||
|
||||
export const eventPushedNewBranch = findPushEvent({ isNew: true });
|
||||
export const eventPushedNewTag = findPushEvent({ isNew: true, refType: PUSH_EVENT_REF_TYPE_TAG });
|
||||
|
|
@ -77,12 +105,6 @@ export const eventBulkPushedBranch = findPushEvent({ commitCount: 5 });
|
|||
export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE });
|
||||
|
||||
export const eventCreated = findEventByAction(EVENT_TYPE_CREATED);
|
||||
|
||||
export const findCreatedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
|
||||
export const findWorkItemCreatedEvent = (issueType) =>
|
||||
findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
|
||||
|
||||
export const eventProjectCreated = findCreatedEvent(undefined);
|
||||
export const eventMilestoneCreated = findCreatedEvent(TARGET_TYPE_MILESTONE);
|
||||
export const eventIssueCreated = findCreatedEvent(TARGET_TYPE_ISSUE);
|
||||
|
|
@ -93,12 +115,6 @@ export const eventTaskCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_TA
|
|||
export const eventIncidentCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
|
||||
|
||||
export const eventClosed = findEventByAction(EVENT_TYPE_CLOSED);
|
||||
|
||||
export const findClosedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
|
||||
export const findWorkItemClosedEvent = (issueType) =>
|
||||
findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
|
||||
|
||||
export const eventMilestoneClosed = findClosedEvent(TARGET_TYPE_MILESTONE);
|
||||
export const eventIssueClosed = findClosedEvent(TARGET_TYPE_ISSUE);
|
||||
export const eventMergeRequestClosed = findClosedEvent(TARGET_TYPE_MERGE_REQUEST);
|
||||
|
|
@ -108,12 +124,6 @@ export const eventTaskClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_TASK
|
|||
export const eventIncidentClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
|
||||
|
||||
export const eventReopened = findEventByAction(EVENT_TYPE_REOPENED);
|
||||
|
||||
export const findReopenedEvent = (targetType) =>
|
||||
findEventByActionAndTargetType(EVENT_TYPE_REOPENED, targetType);
|
||||
export const findWorkItemReopenedEvent = (issueType) =>
|
||||
findEventByActionAndIssueType(EVENT_TYPE_REOPENED, issueType);
|
||||
|
||||
export const eventMilestoneReopened = findReopenedEvent(TARGET_TYPE_MILESTONE);
|
||||
export const eventMergeRequestReopened = findReopenedEvent(TARGET_TYPE_MERGE_REQUEST);
|
||||
export const eventWikiPageReopened = findReopenedEvent(TARGET_TYPE_WIKI);
|
||||
|
|
@ -123,19 +133,6 @@ export const eventTaskReopened = findWorkItemReopenedEvent(WORK_ITEM_ISSUE_TYPE_
|
|||
export const eventIncidentReopened = findWorkItemReopenedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
|
||||
|
||||
export const eventCommented = findEventByAction(EVENT_TYPE_COMMENTED);
|
||||
|
||||
const findEventByActionAndNoteableType = (action, noteableType) => () =>
|
||||
events.find((event) => event.action === action && event.noteable?.type === noteableType);
|
||||
export const findCommentedEvent = (noteableType) =>
|
||||
findEventByActionAndNoteableType(EVENT_TYPE_COMMENTED, noteableType);
|
||||
export const findCommentedSnippet = (resourceParentType) => () =>
|
||||
events.find(
|
||||
(event) =>
|
||||
event.action === EVENT_TYPE_COMMENTED &&
|
||||
event.noteable?.type === SNIPPET_NOTEABLE_TYPE &&
|
||||
event.resource_parent?.type === resourceParentType,
|
||||
);
|
||||
|
||||
export const eventCommentedIssue = findCommentedEvent(ISSUE_NOTEABLE_TYPE);
|
||||
export const eventCommentedMergeRequest = findCommentedEvent(MERGE_REQUEST_NOTEABLE_TYPE);
|
||||
export const eventCommentedSnippet = findCommentedEvent(SNIPPET_NOTEABLE_TYPE);
|
||||
|
|
@ -153,3 +150,7 @@ export const eventCommentedCommit = () => ({
|
|||
first_line_in_markdown: '\u003cp\u003eMy title 9\u003c/p\u003e',
|
||||
},
|
||||
});
|
||||
|
||||
export const eventUpdated = findEventByAction(EVENT_TYPE_UPDATED);
|
||||
export const eventDesignUpdated = findUpdatedEvent(TARGET_TYPE_DESIGN);
|
||||
export const eventWikiPageUpdated = findUpdatedEvent(TARGET_TYPE_WIKI);
|
||||
|
|
|
|||
|
|
@ -16,35 +16,6 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
|
|||
let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
|
||||
let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) }
|
||||
|
||||
describe Projects::PipelineSchedulesController, type: :controller do
|
||||
render_views
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
stub_feature_flags(pipeline_schedules_vue: false)
|
||||
end
|
||||
|
||||
it 'pipeline_schedules/edit.html' do
|
||||
get :edit, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: pipeline_schedule.id
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it 'pipeline_schedules/edit_with_variables.html' do
|
||||
get :edit, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: pipeline_schedule_populated.id
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
before do
|
||||
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,15 @@ describe('Jobs Table', () => {
|
|||
});
|
||||
expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
|
||||
});
|
||||
|
||||
describe('when stage of a job is missing', () => {
|
||||
it('shows no stage', () => {
|
||||
const stagelessJob = { ...mockJobsNodes[0], stage: null };
|
||||
createComponent({ jobs: [stagelessJob] });
|
||||
|
||||
expect(findJobStage().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('regular user', () => {
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::RebalancePartitionId,
|
||||
:migration,
|
||||
schema: 20230125093723,
|
||||
feature_category: :continuous_integration do
|
||||
let(:ci_builds_table) { table(:ci_builds, database: :ci) }
|
||||
let(:ci_pipelines_table) { table(:ci_pipelines, database: :ci) }
|
||||
|
||||
let!(:valid_ci_pipeline) { ci_pipelines_table.create!(id: 1, partition_id: 100) }
|
||||
let!(:invalid_ci_pipeline) { ci_pipelines_table.create!(id: 2, partition_id: 101) }
|
||||
|
||||
describe '#perform' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:table_name, :invalid_record, :valid_record) do
|
||||
:ci_pipelines | invalid_ci_pipeline | valid_ci_pipeline
|
||||
end
|
||||
|
||||
subject(:perform) do
|
||||
described_class.new(
|
||||
start_id: 1,
|
||||
end_id: 2,
|
||||
batch_table: table_name,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 1,
|
||||
pause_ms: 0,
|
||||
connection: Ci::ApplicationRecord.connection
|
||||
).perform
|
||||
end
|
||||
|
||||
shared_examples 'fix invalid records' do
|
||||
it 'rebalances partition_id to 100 when partition_id is 101' do
|
||||
expect { perform }
|
||||
.to change { invalid_record.reload.partition_id }.from(101).to(100)
|
||||
.and not_change { valid_record.reload.partition_id }
|
||||
end
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'fix invalid records'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RebalancePartitionIdCiPipeline, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on sass' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_builds' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_pipelines,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RebalancePartitionIdCiBuild, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on sass' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_builds' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_builds,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiPipelineVariable, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on saas' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_pipeline_variables' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_pipeline_variables,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiJobArtifact, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on saas' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_job_artifacts' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_job_artifacts,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiStage, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on saas' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_stages' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_stages,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiBuildReportResult,
|
||||
migration: :gitlab_ci,
|
||||
feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on saas' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_build_report_results' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_build_report_results,
|
||||
column_name: :build_id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiBuildTraceMetadata,
|
||||
migration: :gitlab_ci,
|
||||
feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on saas' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of ci_build_trace_metadata' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_build_trace_metadata,
|
||||
column_name: :build_id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiBuildMetadata,
|
||||
migration: :gitlab_ci,
|
||||
feature_category: :continuous_integration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
context 'when on saas' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of p_ci_builds_metadata' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :p_ci_builds_metadata,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe '#up' do
|
||||
it 'does not schedule background job' do
|
||||
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||
|
||||
migration.up
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'does not delete background job' do
|
||||
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||
|
||||
migration.down
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsForCiJobVariables, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:builds) { table(:ci_builds, database: :ci) }
|
||||
let(:job_variables) { table(:ci_job_variables, database: :ci) }
|
||||
let(:connection) { job_variables.connection }
|
||||
|
||||
around do |example|
|
||||
connection.execute "ALTER TABLE #{job_variables.quoted_table_name} DISABLE TRIGGER ALL;"
|
||||
|
||||
example.run
|
||||
ensure
|
||||
connection.execute "ALTER TABLE #{job_variables.quoted_table_name} ENABLE TRIGGER ALL;"
|
||||
end
|
||||
|
||||
before do
|
||||
job = builds.create!(partition_id: 100)
|
||||
|
||||
job_variables.insert_all!([
|
||||
{ job_id: job.id, partition_id: 100, key: 'variable-100' },
|
||||
{ job_id: job.id, partition_id: 101, key: 'variable-101' }
|
||||
])
|
||||
end
|
||||
|
||||
describe '#up', :aggregate_failures do
|
||||
context 'when on sass' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
it 'fixes partition_id' do
|
||||
expect { migrate! }.not_to raise_error
|
||||
|
||||
expect(job_variables.where(partition_id: 100).count).to eq(2)
|
||||
expect(job_variables.where(partition_id: 101).count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self managed' do
|
||||
it 'does not change partition_id' do
|
||||
expect { migrate! }.not_to raise_error
|
||||
|
||||
expect(job_variables.where(partition_id: 100).count).to eq(1)
|
||||
expect(job_variables.where(partition_id: 101).count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixPartitionIdsOnCiSourcesPipelines, migration: :gitlab_ci, feature_category: :continuous_integration do
|
||||
let(:sources_pipelines) { table(:ci_sources_pipelines, database: :ci) }
|
||||
|
||||
before do
|
||||
sources_pipelines.insert_all!([
|
||||
{ partition_id: 100, source_partition_id: 100 },
|
||||
{ partition_id: 100, source_partition_id: 101 },
|
||||
{ partition_id: 101, source_partition_id: 100 },
|
||||
{ partition_id: 101, source_partition_id: 101 }
|
||||
])
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
context 'when on sass' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
it 'fixes partition_id and source_partition_id' do
|
||||
expect { migrate! }.not_to raise_error
|
||||
|
||||
expect(sources_pipelines.where(partition_id: 100).count).to eq(4)
|
||||
expect(sources_pipelines.where(partition_id: 101).count).to eq(0)
|
||||
expect(sources_pipelines.where(source_partition_id: 100).count).to eq(4)
|
||||
expect(sources_pipelines.where(source_partition_id: 101).count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on self managed' do
|
||||
it 'does not change partition_id or source_partition_id' do
|
||||
expect { migrate! }.not_to raise_error
|
||||
|
||||
expect(sources_pipelines.where(partition_id: 100).count).to eq(2)
|
||||
expect(sources_pipelines.where(partition_id: 100).count).to eq(2)
|
||||
expect(sources_pipelines.where(source_partition_id: 101).count).to eq(2)
|
||||
expect(sources_pipelines.where(source_partition_id: 101).count).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'projects/pipeline_schedules/_pipeline_schedule' do
|
||||
let(:owner) { create(:user) }
|
||||
let(:maintainer) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
|
||||
allow(view).to receive(:current_user).and_return(user)
|
||||
allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule)
|
||||
|
||||
allow(view).to receive(:can?).and_return(true)
|
||||
end
|
||||
|
||||
context 'taking ownership of schedule' do
|
||||
context 'when non-owner is signed in' do
|
||||
let(:user) { maintainer }
|
||||
|
||||
before do
|
||||
allow(view).to receive(:can?).with(maintainer, :admin_pipeline_schedule, pipeline_schedule).and_return(true)
|
||||
end
|
||||
|
||||
it 'non-owner can take ownership of pipeline' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_button('Take ownership')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when owner is signed in' do
|
||||
let(:user) { owner }
|
||||
|
||||
before do
|
||||
allow(view).to receive(:can?).with(owner, :admin_pipeline_schedule, pipeline_schedule).and_return(false)
|
||||
end
|
||||
|
||||
it 'owner cannot take ownership of pipeline' do
|
||||
render
|
||||
|
||||
expect(rendered).not_to have_button('Take ownership')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue