Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7feabd8d8e
commit
b502933498
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
|
||||
import { getGlobalAlerts, setGlobalAlerts, removeGlobalAlertById } from '~/lib/utils/global_alerts';
|
||||
|
||||
export default {
|
||||
name: 'GlobalAlerts',
|
||||
components: { GlAlert },
|
||||
data() {
|
||||
return {
|
||||
alerts: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const { page } = document.body.dataset;
|
||||
const alerts = getGlobalAlerts();
|
||||
|
||||
const alertsToPersist = alerts.filter((alert) => alert.persistOnPages.length);
|
||||
const alertsToRender = alerts.filter(
|
||||
(alert) => alert.persistOnPages.length === 0 || alert.persistOnPages.includes(page),
|
||||
);
|
||||
|
||||
this.alerts = alertsToRender;
|
||||
|
||||
// Once we render the global alerts, we re-set the global alerts to only store persistent alerts for the next load.
|
||||
setGlobalAlerts(alertsToPersist);
|
||||
},
|
||||
methods: {
|
||||
onDismiss(index) {
|
||||
const alert = this.alerts[index];
|
||||
this.alerts.splice(index, 1);
|
||||
removeGlobalAlertById(alert.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="alerts.length">
|
||||
<gl-alert
|
||||
v-for="(alert, index) in alerts"
|
||||
:key="alert.id"
|
||||
:variant="alert.variant"
|
||||
:title="alert.title"
|
||||
:dismissible="alert.dismissible"
|
||||
@dismiss="onDismiss(index)"
|
||||
>{{ alert.message }}</gl-alert
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import GlobalAlerts from './components/global_alerts.vue';
|
||||
|
||||
export const initGlobalAlerts = () => {
|
||||
const el = document.getElementById('js-global-alerts');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'GlobalAlertsRoot',
|
||||
render(createElement) {
|
||||
return createElement(GlobalAlerts);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -9,6 +9,7 @@ import './quick_submit';
|
|||
import './requires_input';
|
||||
import initPageShortcuts from './shortcuts';
|
||||
import { initToastMessages } from './toasts';
|
||||
import { initGlobalAlerts } from './global_alerts';
|
||||
import './toggler_behavior';
|
||||
import './preview_markdown';
|
||||
|
||||
|
|
@ -24,6 +25,8 @@ initCollapseSidebarOnWindowResize();
|
|||
|
||||
initToastMessages();
|
||||
|
||||
initGlobalAlerts();
|
||||
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
// Check if we have to Load GFM Input
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
customEnvScope: null,
|
||||
isDropdownShown: false,
|
||||
selectedEnvironment: '',
|
||||
searchTerm: '',
|
||||
|
|
@ -68,13 +69,20 @@ export default {
|
|||
filtered = uniq([...filtered, '*']);
|
||||
}
|
||||
|
||||
// add custom env scope if it matches the search term
|
||||
if (this.customEnvScope && this.customEnvScope.startsWith(this.searchTerm)) {
|
||||
filtered = uniq([...filtered, this.customEnvScope]);
|
||||
}
|
||||
|
||||
return filtered.sort().map((environment) => ({
|
||||
value: environment,
|
||||
text: environment,
|
||||
}));
|
||||
},
|
||||
shouldRenderCreateButton() {
|
||||
return this.searchTerm && !this.environments.includes(this.searchTerm);
|
||||
return (
|
||||
this.searchTerm && ![...this.environments, this.customEnvScope].includes(this.searchTerm)
|
||||
);
|
||||
},
|
||||
shouldRenderDivider() {
|
||||
return (
|
||||
|
|
@ -98,7 +106,7 @@ export default {
|
|||
this.selectedEnvironment = selected;
|
||||
},
|
||||
createEnvironmentScope() {
|
||||
this.$emit('create-environment-scope', this.searchTerm);
|
||||
this.customEnvScope = this.searchTerm;
|
||||
this.selectEnvironment(this.searchTerm);
|
||||
},
|
||||
toggleDropdownShown(isShown) {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import {
|
|||
GlFormTextarea,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
|
@ -36,10 +38,11 @@ import { awsTokenList } from './ci_variable_autocomplete_tokens';
|
|||
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
|
||||
|
||||
export const i18n = {
|
||||
addVariable: s__('CiVariables|Add Variable'),
|
||||
addVariable: s__('CiVariables|Add variable'),
|
||||
cancel: __('Cancel'),
|
||||
defaultScope: allEnvironments.text,
|
||||
editVariable: s__('CiVariables|Edit Variable'),
|
||||
deleteVariable: s__('CiVariables|Delete variable'),
|
||||
editVariable: s__('CiVariables|Edit variable'),
|
||||
environments: __('Environments'),
|
||||
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
|
||||
expandedField: s__('CiVariables|Expand variable reference'),
|
||||
|
|
@ -51,6 +54,7 @@ export const i18n = {
|
|||
maskedDescription: s__(
|
||||
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
|
||||
),
|
||||
modalDeleteMessage: s__('CiVariables|Do you want to delete the variable %{key}?'),
|
||||
protectedField: s__('CiVariables|Protect variable'),
|
||||
protectedDescription: s__(
|
||||
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
|
||||
|
|
@ -86,8 +90,12 @@ export default {
|
|||
GlFormTextarea,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
|
||||
props: {
|
||||
|
|
@ -170,6 +178,9 @@ export default {
|
|||
modalActionText() {
|
||||
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
|
||||
},
|
||||
removeVariableMessage() {
|
||||
return sprintf(this.$options.i18n.modalDeleteMessage, { key: this.variable.key });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
variable: {
|
||||
|
|
@ -188,6 +199,13 @@ export default {
|
|||
close() {
|
||||
this.$emit('close-form');
|
||||
},
|
||||
deleteVariable() {
|
||||
this.$emit('delete-variable', this.variable);
|
||||
this.close();
|
||||
},
|
||||
setEnvironmentScope(scope) {
|
||||
this.variable = { ...this.variable, environmentScope: scope };
|
||||
},
|
||||
getTrackingErrorProperty() {
|
||||
if (this.isValueEmpty) {
|
||||
return null;
|
||||
|
|
@ -225,164 +243,206 @@ export default {
|
|||
}),
|
||||
i18n,
|
||||
variableOptions,
|
||||
deleteModal: {
|
||||
actionPrimary: {
|
||||
text: __('Delete'),
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
},
|
||||
},
|
||||
actionSecondary: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-drawer
|
||||
open
|
||||
data-testid="ci-variable-drawer"
|
||||
:header-height="getDrawerHeaderHeight"
|
||||
:z-index="$options.DRAWER_Z_INDEX"
|
||||
@close="close"
|
||||
>
|
||||
<template #title>
|
||||
<h2 class="gl-m-0">{{ modalActionText }}</h2>
|
||||
</template>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.type"
|
||||
label-for="ci-variable-type"
|
||||
class="gl-border-none"
|
||||
:class="{
|
||||
'gl-mb-n5': !hideEnvironmentScope,
|
||||
'gl-mb-n1': hideEnvironmentScope,
|
||||
}"
|
||||
<div>
|
||||
<gl-drawer
|
||||
open
|
||||
data-testid="ci-variable-drawer"
|
||||
:header-height="getDrawerHeaderHeight"
|
||||
:z-index="$options.DRAWER_Z_INDEX"
|
||||
@close="close"
|
||||
>
|
||||
<gl-form-select
|
||||
id="ci-variable-type"
|
||||
v-model="variable.variableType"
|
||||
:options="$options.variableOptions"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
v-if="!hideEnvironmentScope"
|
||||
class="gl-border-none gl-mb-n5"
|
||||
label-for="ci-variable-env"
|
||||
data-testid="environment-scope"
|
||||
>
|
||||
<template #label>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<span class="gl-mr-2">
|
||||
{{ $options.i18n.environments }}
|
||||
</span>
|
||||
<gl-link
|
||||
class="gl-display-flex"
|
||||
:title="$options.i18n.environmentScopeLinkTitle"
|
||||
:href="environmentScopeLink"
|
||||
target="_blank"
|
||||
data-testid="environment-scope-link"
|
||||
>
|
||||
<gl-icon name="question-o" :size="14" />
|
||||
</gl-link>
|
||||
</div>
|
||||
<template #title>
|
||||
<h2 class="gl-m-0">{{ modalActionText }}</h2>
|
||||
</template>
|
||||
<ci-environments-dropdown
|
||||
v-if="areScopedVariablesAvailable"
|
||||
class="gl-mb-5"
|
||||
has-env-scope-query
|
||||
:are-environments-loading="areEnvironmentsLoading"
|
||||
:environments="environments"
|
||||
:selected-environment-scope="variable.environmentScope"
|
||||
/>
|
||||
<gl-form-input
|
||||
v-else
|
||||
:value="$options.i18n.defaultScope"
|
||||
class="gl-w-full gl-mb-5"
|
||||
readonly
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group class="gl-border-none gl-mb-n8">
|
||||
<template #label>
|
||||
<div class="gl-display-flex gl-align-items-center gl-mb-n3">
|
||||
<span class="gl-mr-2">
|
||||
{{ $options.i18n.flags }}
|
||||
</span>
|
||||
<gl-link
|
||||
class="gl-display-flex"
|
||||
:title="$options.i18n.flagsLinkTitle"
|
||||
:href="$options.flagLink"
|
||||
target="_blank"
|
||||
>
|
||||
<gl-icon name="question-o" :size="14" />
|
||||
</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
<gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
|
||||
{{ $options.i18n.protectedField }}
|
||||
<p class="gl-text-secondary">
|
||||
{{ $options.i18n.protectedDescription }}
|
||||
</p>
|
||||
</gl-form-checkbox>
|
||||
<gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
|
||||
{{ $options.i18n.maskedField }}
|
||||
<p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
|
||||
</gl-form-checkbox>
|
||||
<gl-form-checkbox
|
||||
data-testid="ci-variable-expanded-checkbox"
|
||||
:checked="isExpanded"
|
||||
@change="setRaw"
|
||||
<gl-form-group
|
||||
:label="$options.i18n.type"
|
||||
label-for="ci-variable-type"
|
||||
class="gl-border-none"
|
||||
:class="{
|
||||
'gl-mb-n5': !hideEnvironmentScope,
|
||||
'gl-mb-n1': hideEnvironmentScope,
|
||||
}"
|
||||
>
|
||||
{{ $options.i18n.expandedField }}
|
||||
<p class="gl-text-secondary">
|
||||
<gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</gl-form-checkbox>
|
||||
</gl-form-group>
|
||||
<gl-form-combobox
|
||||
v-model="variable.key"
|
||||
:token-list="$options.awsTokenList"
|
||||
:label-text="$options.i18n.key"
|
||||
class="gl-border-none gl-pb-0! gl-mb-n5"
|
||||
data-testid="ci-variable-key"
|
||||
data-qa-selector="ci_variable_key_field"
|
||||
/>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.value"
|
||||
label-for="ci-variable-value"
|
||||
class="gl-border-none gl-mb-n2"
|
||||
data-testid="ci-variable-value-label"
|
||||
:invalid-feedback="maskedReqsNotMetText"
|
||||
:state="isValueValid"
|
||||
>
|
||||
<gl-form-textarea
|
||||
id="ci-variable-value"
|
||||
v-model="variable.value"
|
||||
class="gl-border-none gl-font-monospace!"
|
||||
rows="3"
|
||||
max-rows="10"
|
||||
data-testid="ci-variable-value"
|
||||
data-qa-selector="ci_variable_value_field"
|
||||
spellcheck="false"
|
||||
<gl-form-select
|
||||
id="ci-variable-type"
|
||||
v-model="variable.variableType"
|
||||
:options="$options.variableOptions"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
v-if="!hideEnvironmentScope"
|
||||
class="gl-border-none gl-mb-n5"
|
||||
label-for="ci-variable-env"
|
||||
data-testid="environment-scope"
|
||||
>
|
||||
<template #label>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<span class="gl-mr-2">
|
||||
{{ $options.i18n.environments }}
|
||||
</span>
|
||||
<gl-link
|
||||
class="gl-display-flex"
|
||||
:title="$options.i18n.environmentScopeLinkTitle"
|
||||
:href="environmentScopeLink"
|
||||
target="_blank"
|
||||
data-testid="environment-scope-link"
|
||||
>
|
||||
<gl-icon name="question-o" :size="14" />
|
||||
</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
<ci-environments-dropdown
|
||||
v-if="areScopedVariablesAvailable"
|
||||
class="gl-mb-5"
|
||||
has-env-scope-query
|
||||
:are-environments-loading="areEnvironmentsLoading"
|
||||
:environments="environments"
|
||||
:selected-environment-scope="variable.environmentScope"
|
||||
@select-environment="setEnvironmentScope"
|
||||
@search-environment-scope="$emit('search-environment-scope', $event)"
|
||||
/>
|
||||
<gl-form-input
|
||||
v-else
|
||||
:value="$options.i18n.defaultScope"
|
||||
class="gl-w-full gl-mb-5"
|
||||
readonly
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group class="gl-border-none gl-mb-n8">
|
||||
<template #label>
|
||||
<div class="gl-display-flex gl-align-items-center gl-mb-n3">
|
||||
<span class="gl-mr-2">
|
||||
{{ $options.i18n.flags }}
|
||||
</span>
|
||||
<gl-link
|
||||
class="gl-display-flex"
|
||||
:title="$options.i18n.flagsLinkTitle"
|
||||
:href="$options.flagLink"
|
||||
target="_blank"
|
||||
>
|
||||
<gl-icon name="question-o" :size="14" />
|
||||
</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
<gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
|
||||
{{ $options.i18n.protectedField }}
|
||||
<p class="gl-text-secondary">
|
||||
{{ $options.i18n.protectedDescription }}
|
||||
</p>
|
||||
</gl-form-checkbox>
|
||||
<gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
|
||||
{{ $options.i18n.maskedField }}
|
||||
<p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
|
||||
</gl-form-checkbox>
|
||||
<gl-form-checkbox
|
||||
data-testid="ci-variable-expanded-checkbox"
|
||||
:checked="isExpanded"
|
||||
@change="setRaw"
|
||||
>
|
||||
{{ $options.i18n.expandedField }}
|
||||
<p class="gl-text-secondary">
|
||||
<gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</gl-form-checkbox>
|
||||
</gl-form-group>
|
||||
<gl-form-combobox
|
||||
v-model="variable.key"
|
||||
:token-list="$options.awsTokenList"
|
||||
:label-text="$options.i18n.key"
|
||||
class="gl-border-none gl-pb-0! gl-mb-n5"
|
||||
data-testid="ci-variable-key"
|
||||
data-qa-selector="ci_variable_key_field"
|
||||
/>
|
||||
<p v-if="variable.raw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
|
||||
{{ $options.i18n.valueFeedback.rawHelpText }}
|
||||
</p>
|
||||
</gl-form-group>
|
||||
<gl-alert
|
||||
v-if="hasVariableReference"
|
||||
:title="$options.i18n.variableReferenceTitle"
|
||||
:dismissible="false"
|
||||
variant="warning"
|
||||
class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
|
||||
data-testid="has-variable-reference-alert"
|
||||
<gl-form-group
|
||||
:label="$options.i18n.value"
|
||||
label-for="ci-variable-value"
|
||||
class="gl-border-none gl-mb-n2"
|
||||
data-testid="ci-variable-value-label"
|
||||
:invalid-feedback="maskedReqsNotMetText"
|
||||
:state="isValueValid"
|
||||
>
|
||||
<gl-form-textarea
|
||||
id="ci-variable-value"
|
||||
v-model="variable.value"
|
||||
class="gl-border-none gl-font-monospace!"
|
||||
rows="3"
|
||||
max-rows="10"
|
||||
data-testid="ci-variable-value"
|
||||
data-qa-selector="ci_variable_value_field"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p
|
||||
v-if="variable.raw"
|
||||
class="gl-mt-2 gl-mb-0 text-secondary"
|
||||
data-testid="raw-variable-tip"
|
||||
>
|
||||
{{ $options.i18n.valueFeedback.rawHelpText }}
|
||||
</p>
|
||||
</gl-form-group>
|
||||
<gl-alert
|
||||
v-if="hasVariableReference"
|
||||
:title="$options.i18n.variableReferenceTitle"
|
||||
:dismissible="false"
|
||||
variant="warning"
|
||||
class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
|
||||
data-testid="has-variable-reference-alert"
|
||||
>
|
||||
{{ $options.i18n.variableReferenceDescription }}
|
||||
</gl-alert>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
|
||||
>{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-if="isEditing"
|
||||
v-gl-modal-directive="`delete-variable-${variable.key}`"
|
||||
variant="danger"
|
||||
category="secondary"
|
||||
class="gl-mr-3"
|
||||
data-testid="ci-variable-delete-btn"
|
||||
>{{ $options.i18n.deleteVariable }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:disabled="!canSubmit"
|
||||
data-testid="ci-variable-confirm-btn"
|
||||
@click="submit"
|
||||
>{{ modalActionText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-drawer>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
:modal-id="`delete-variable-${variable.key}`"
|
||||
:title="$options.i18n.deleteVariable"
|
||||
:action-primary="$options.deleteModal.actionPrimary"
|
||||
:action-secondary="$options.deleteModal.actionSecondary"
|
||||
data-testid="ci-variable-drawer-confirm-delete-modal"
|
||||
@primary="deleteVariable"
|
||||
>
|
||||
{{ $options.i18n.variableReferenceDescription }}
|
||||
</gl-alert>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
|
||||
>{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:disabled="!canSubmit"
|
||||
data-testid="ci-variable-confirm-btn"
|
||||
@click="submit"
|
||||
>{{ modalActionText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-drawer>
|
||||
{{ removeVariableMessage }}
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -211,9 +211,6 @@ export default {
|
|||
addVariable() {
|
||||
this.$emit('add-variable', this.variable);
|
||||
},
|
||||
createEnvironmentScope(env) {
|
||||
this.newEnvironments.push(env);
|
||||
},
|
||||
deleteVariable() {
|
||||
this.$emit('delete-variable', this.variable);
|
||||
},
|
||||
|
|
@ -411,7 +408,6 @@ export default {
|
|||
:selected-environment-scope="variable.environmentScope"
|
||||
:environments="environmentsList"
|
||||
@select-environment="setEnvironmentScope"
|
||||
@create-environment-scope="createEnvironmentScope"
|
||||
@search-environment-scope="$emit('search-environment-scope', $event)"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
export const GLOBAL_ALERTS_SESSION_STORAGE_KEY = 'vueGlobalAlerts';
|
||||
|
||||
/**
|
||||
* Get global alerts from session storage
|
||||
*/
|
||||
export const getGlobalAlerts = () => {
|
||||
return JSON.parse(sessionStorage.getItem(GLOBAL_ALERTS_SESSION_STORAGE_KEY) || '[]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set alerts in session storage
|
||||
* @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts
|
||||
*/
|
||||
export const setGlobalAlerts = (alerts) => {
|
||||
sessionStorage.setItem(
|
||||
GLOBAL_ALERTS_SESSION_STORAGE_KEY,
|
||||
JSON.stringify([
|
||||
...alerts.map(({ dismissible = true, persistOnPages = [], ...alert }) => ({
|
||||
dismissible,
|
||||
persistOnPages,
|
||||
...alert,
|
||||
})),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove global alert by id
|
||||
* @param {String} id
|
||||
*/
|
||||
export const removeGlobalAlertById = (id) => {
|
||||
const existingAlerts = getGlobalAlerts();
|
||||
sessionStorage.setItem(
|
||||
GLOBAL_ALERTS_SESSION_STORAGE_KEY,
|
||||
JSON.stringify(existingAlerts.filter((alert) => alert.id !== id)),
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { getGlobalAlerts, setGlobalAlerts } from './global_alerts';
|
||||
|
||||
export const DASH_SCOPE = '-';
|
||||
|
||||
export const PATH_SEPARATOR = '/';
|
||||
|
|
@ -721,6 +723,20 @@ export function visitUrl(destination, external = false) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to a URL and display alerts.
|
||||
*
|
||||
* If destination is a querystring, it will be automatically transformed into a fully qualified URL.
|
||||
* If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry.
|
||||
*
|
||||
* @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring.
|
||||
* @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts - Alerts to display
|
||||
*/
|
||||
export function visitUrlWithAlerts(destination, alerts) {
|
||||
setGlobalAlerts([...getGlobalAlerts(), ...alerts]);
|
||||
visitUrl(destination);
|
||||
}
|
||||
|
||||
export function refreshCurrentPage() {
|
||||
visitUrl(window.location.href);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ class ProjectSetting < ApplicationRecord
|
|||
jitsu_administrator_email
|
||||
], remove_with: '16.5', remove_after: '2023-09-22'
|
||||
|
||||
ignore_column :jitsu_key, remove_with: '16.7', remove_after: '2023-11-17'
|
||||
|
||||
attr_encrypted :cube_api_key,
|
||||
mode: :per_attribute_iv,
|
||||
key: Settings.attr_encrypted_db_key_base_32,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,20 @@ module WorkItems
|
|||
def children
|
||||
work_item.work_item_children_by_relative_position
|
||||
end
|
||||
|
||||
def self.quick_action_commands
|
||||
[:set_parent]
|
||||
end
|
||||
|
||||
def self.quick_action_params
|
||||
[:set_parent]
|
||||
end
|
||||
|
||||
def self.process_quick_action_param(param_name, value)
|
||||
return super unless param_name == :set_parent && value
|
||||
|
||||
{ parent: value }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ module Notes
|
|||
service_errors = if service_response.respond_to?(:errors)
|
||||
service_response.errors.full_messages
|
||||
elsif service_response.respond_to?(:[]) && service_response[:status] == :error
|
||||
service_response[:message]
|
||||
Array.wrap(service_response[:message])
|
||||
end
|
||||
|
||||
service_errors.blank? ? ServiceResponse.success : ServiceResponse.error(message: service_errors)
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@
|
|||
- elsif value
|
||||
= render Pajamas::AlertComponent.new(variant: type_to_variant[key], dismissible: closable.include?(key), alert_options: {class: "flash-#{key}", data: { testid: "alert-#{type_to_variant[key]}" }}) do |c|
|
||||
= c.with_body { value }
|
||||
#js-global-alerts
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: deploy_key_for_protected_tags
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110238
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389237
|
||||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
key_path: redis_hll_counters.quickactions.i_quickactions_set_parent_weekly
|
||||
name: quickactions_set_parent_weekly
|
||||
description: Count of WAU using the `/set_parent` quick action
|
||||
product_section: dev
|
||||
product_stage: plan
|
||||
product_group: product_planning
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "16.5"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122911
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_quickactions_set_parent
|
||||
performance_indicator_type: []
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
key_path: redis_hll_counters.quickactions.i_quickactions_set_parent_monthly
|
||||
name: quickactions_set_parent_monthly
|
||||
description: Count of WAU using the `/set_parent` quick action
|
||||
product_section: dev
|
||||
product_stage: plan
|
||||
product_group: product_planning
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "16.5"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122911
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_quickactions_set_parent
|
||||
performance_indicator_type: []
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropIndexBtreeNamespacesTraversalIds < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = :namespaces
|
||||
INDEX_NAME = :index_btree_namespaces_traversal_ids
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index TABLE_NAME, :traversal_ids, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
8e09b2216c8d64273e5025a813ee74e64ce549754f2e2b5f1aaf2d12a9bf5c95
|
||||
|
|
@ -31385,8 +31385,6 @@ CREATE INDEX index_boards_on_project_id ON boards USING btree (project_id);
|
|||
|
||||
CREATE INDEX index_broadcast_message_on_ends_at_and_broadcast_type_and_id ON broadcast_messages USING btree (ends_at, broadcast_type, id);
|
||||
|
||||
CREATE INDEX index_btree_namespaces_traversal_ids ON namespaces USING btree (traversal_ids);
|
||||
|
||||
CREATE INDEX index_bulk_import_batch_trackers_on_tracker_id ON bulk_import_batch_trackers USING btree (tracker_id);
|
||||
|
||||
CREATE INDEX index_bulk_import_configurations_on_bulk_import_id ON bulk_import_configurations USING btree (bulk_import_id);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ test locally for verification.
|
|||
|
||||
To copy the name of all failed tests, at the top of the **Test summary** panel,
|
||||
select **Copy failed tests**. The failed tests are listed as a string with the tests
|
||||
separated by spaces.
|
||||
separated by spaces. This option is only available if the JUnit report populates
|
||||
the `<file>` attributes for failed tests.
|
||||
|
||||
To copy the name of a single failed test:
|
||||
|
||||
|
|
|
|||
|
|
@ -397,6 +397,29 @@ This approach has a few benefits:
|
|||
- Accessing a global variable is not required, except in the application's
|
||||
[entry point](#accessing-the-gl-object).
|
||||
|
||||
#### Redirecting to page and displaying alerts
|
||||
|
||||
If you need to redirect to another page and display alerts, you can use the [`visitUrlWithAlerts`](https://gitlab.com/gitlab-org/gitlab/-/blob/7063dce68b8231442567707024b2f29e48ce2f64/app/assets/javascripts/lib/utils/url_utility.js#L731) util.
|
||||
This can be useful when you're redirecting to a newly created resource and showing a success alert.
|
||||
|
||||
By default the alerts will be cleared when the page is reloaded. If you need an alert to be persisted on a page you can set the
|
||||
`persistOnPages` key to an array of Rails controller actions. To find the Rails controller action run `document.body.dataset.page` in your console.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
visitUrlWithAlerts('/dashboard/groups', [
|
||||
{
|
||||
id: 'resource-building-in-background',
|
||||
message: 'Resource is being built in the background.',
|
||||
variant: 'info',
|
||||
persistOnPages: ['dashboard:groups:index'],
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
If you need to manually remove a persisted alert, you can use the [`removeGlobalAlertById`](https://gitlab.com/gitlab-org/gitlab/-/blob/7063dce68b8231442567707024b2f29e48ce2f64/app/assets/javascripts/lib/utils/global_alerts.js#L31) util.
|
||||
|
||||
### A folder for Components
|
||||
|
||||
This folder holds all components that are specific to this new feature.
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ To auto-format this table, use the VS Code Markdown Table formatter: `https://do
|
|||
| `/relabel ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Replace current labels with those specified. |
|
||||
| `/remove_due_date` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes | Remove due date. |
|
||||
| `/reopen` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Reopen. |
|
||||
| `/set_parent <work_item>` | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes | Set parent work item to `<work_item>`. The `<work_item>` value should be in the format of `#iid`, `group/project#iid`, or a URL to a work item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420798) in GitLab 16.5. |
|
||||
| `/shrug <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `¯\_(ツ)_/¯`. |
|
||||
| `/subscribe` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Subscribe to notifications. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420796) in GitLab 16.4 |
|
||||
| `/tableflip <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `(╯°□°)╯︵ ┻━┻`. |
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ module Gitlab
|
|||
return unless enabled?
|
||||
|
||||
observation.query_statistics = connection.execute(<<~SQL)
|
||||
SELECT query, calls, total_time, max_time, mean_time, rows
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
total_exec_time + total_plan_time AS total_time,
|
||||
max_exec_time + max_plan_time AS max_time,
|
||||
mean_exec_time + mean_plan_time AS mean_time,
|
||||
"rows"
|
||||
FROM pg_stat_statements
|
||||
WHERE pg_get_userbyid(userid) = current_user
|
||||
ORDER BY total_time DESC
|
||||
|
|
|
|||
|
|
@ -16,17 +16,6 @@ module Gitlab
|
|||
|
||||
attr_reader :deploy_key
|
||||
|
||||
def protected_tag_accessible_to?(ref, action:)
|
||||
if Feature.enabled?(:deploy_key_for_protected_tags, project)
|
||||
super
|
||||
else
|
||||
assert_project!
|
||||
# a deploy key can always push a protected tag
|
||||
# (which is not always the case when pushing to a protected branch)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def can_collaborate?(_ref)
|
||||
assert_project!
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,18 @@ module Gitlab
|
|||
command :promote_to do |type_name|
|
||||
@execution_message[:promote_to] = update_type(type_name, :promote_to)
|
||||
end
|
||||
|
||||
desc { _('Change work item parent') }
|
||||
explanation do |parent_param|
|
||||
format(_("Change work item's parent to %{parent_ref}."), parent_ref: parent_param)
|
||||
end
|
||||
types WorkItem
|
||||
params 'Parent #iid, reference or URL'
|
||||
condition { supports_parent? && can_admin_link? }
|
||||
command :set_parent do |parent_param|
|
||||
@updates[:set_parent] = extract_work_item(parent_param)
|
||||
@execution_message[:set_parent] = success_msg[:set_parent]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -52,6 +64,16 @@ module Gitlab
|
|||
nil
|
||||
end
|
||||
|
||||
def extract_work_item(params)
|
||||
return if params.nil?
|
||||
|
||||
issuable_type = params.include?('work_items') ? :work_item : :issue
|
||||
issuable = extract_references(params, issuable_type).first
|
||||
return unless issuable
|
||||
|
||||
WorkItem.find(issuable.id)
|
||||
end
|
||||
|
||||
def validate_promote_to(type)
|
||||
return error_msg(:not_found, action: 'promote') unless type && supports_promote_to?(type.name)
|
||||
return if current_user.can?(:"create_#{type.base_type}", quick_action_target)
|
||||
|
|
@ -78,8 +100,8 @@ module Gitlab
|
|||
def error_msg(reason, action: 'convert')
|
||||
message = {
|
||||
not_found: 'Provided type is not supported',
|
||||
same_type: 'Types are the same',
|
||||
forbidden: 'You have insufficient permissions'
|
||||
forbidden: 'You have insufficient permissions',
|
||||
same_type: 'Types are the same'
|
||||
}.freeze
|
||||
|
||||
format(_("Failed to %{action} this work item: %{reason}."), { action: action, reason: message[reason] })
|
||||
|
|
@ -88,9 +110,18 @@ module Gitlab
|
|||
def success_msg
|
||||
{
|
||||
type: _('Type changed successfully.'),
|
||||
promote_to: _("Work item promoted successfully.")
|
||||
promote_to: _("Work item promoted successfully."),
|
||||
set_parent: _('Work item parent set successfully')
|
||||
}
|
||||
end
|
||||
|
||||
def supports_parent?
|
||||
::WorkItems::HierarchyRestriction.find_by_child_type_id(quick_action_target.work_item_type_id).present?
|
||||
end
|
||||
|
||||
def can_admin_link?
|
||||
current_user.can?(:admin_issue_link, quick_action_target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9557,9 +9557,15 @@ msgstr ""
|
|||
msgid "Change title"
|
||||
msgstr ""
|
||||
|
||||
msgid "Change work item parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "Change work item type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Change work item's parent to %{parent_ref}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Change your password"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10242,9 +10248,6 @@ msgstr ""
|
|||
msgid "CiStatusText|Warning"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Add Variable"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Add variable"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10263,7 +10266,7 @@ msgstr ""
|
|||
msgid "CiVariables|Do you want to delete the variable %{key}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Edit Variable"
|
||||
msgid "CiVariables|Edit variable"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Environments"
|
||||
|
|
@ -53632,6 +53635,9 @@ msgstr ""
|
|||
msgid "Work in progress limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Work item parent set successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Work item promoted successfully."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GlobalAlerts from '~/behaviors/components/global_alerts.vue';
|
||||
import { getGlobalAlerts, setGlobalAlerts, removeGlobalAlertById } from '~/lib/utils/global_alerts';
|
||||
|
||||
jest.mock('~/lib/utils/global_alerts');
|
||||
|
||||
describe('GlobalAlerts', () => {
|
||||
const alert1 = {
|
||||
dismissible: true,
|
||||
persistOnPages: [],
|
||||
id: 'foo',
|
||||
variant: 'success',
|
||||
title: 'Foo title',
|
||||
message: 'Foo',
|
||||
};
|
||||
const alert2 = {
|
||||
dismissible: true,
|
||||
persistOnPages: [],
|
||||
id: 'bar',
|
||||
variant: 'danger',
|
||||
message: 'Bar',
|
||||
};
|
||||
const alert3 = {
|
||||
dismissible: true,
|
||||
persistOnPages: ['dashboard:groups:index'],
|
||||
id: 'baz',
|
||||
variant: 'info',
|
||||
message: 'Baz',
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
||||
const createComponent = async () => {
|
||||
wrapper = shallowMountExtended(GlobalAlerts);
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
const findAllAlerts = () => wrapper.findAllComponents(GlAlert);
|
||||
|
||||
describe('when there are alerts to display', () => {
|
||||
beforeEach(() => {
|
||||
getGlobalAlerts.mockImplementationOnce(() => [alert1, alert2]);
|
||||
});
|
||||
|
||||
it('displays alerts and removes them from session storage', async () => {
|
||||
await createComponent();
|
||||
|
||||
const alerts = findAllAlerts();
|
||||
|
||||
expect(alerts.at(0).text()).toBe('Foo');
|
||||
expect(alerts.at(0).props()).toMatchObject({
|
||||
title: 'Foo title',
|
||||
variant: 'success',
|
||||
dismissible: true,
|
||||
});
|
||||
|
||||
expect(alerts.at(1).text()).toBe('Bar');
|
||||
expect(alerts.at(1).props()).toMatchObject({
|
||||
variant: 'danger',
|
||||
dismissible: true,
|
||||
});
|
||||
|
||||
expect(setGlobalAlerts).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
describe('when alert is dismissed', () => {
|
||||
it('removes alert', async () => {
|
||||
await createComponent();
|
||||
|
||||
wrapper.findComponent(GlAlert).vm.$emit('dismiss');
|
||||
await nextTick();
|
||||
|
||||
expect(findAllAlerts().length).toBe(1);
|
||||
expect(removeGlobalAlertById).toHaveBeenCalledWith(alert1.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alert has `persistOnPages` key set', () => {
|
||||
const alerts = [alert3];
|
||||
|
||||
beforeEach(() => {
|
||||
getGlobalAlerts.mockImplementationOnce(() => alerts);
|
||||
});
|
||||
|
||||
describe('when page matches specified page', () => {
|
||||
beforeEach(() => {
|
||||
document.body.dataset.page = 'dashboard:groups:index';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete document.body.dataset.page;
|
||||
});
|
||||
|
||||
it('renders alert and does not remove it from session storage', async () => {
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.findComponent(GlAlert).text()).toBe('Baz');
|
||||
expect(setGlobalAlerts).toHaveBeenCalledWith(alerts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when page does not match specified page', () => {
|
||||
beforeEach(() => {
|
||||
document.body.dataset.page = 'dashboard:groups:show';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete document.body.dataset.page;
|
||||
});
|
||||
|
||||
it('does not render alert and does not remove it from session storage', async () => {
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||
expect(setGlobalAlerts).toHaveBeenCalledWith(alerts);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no alerts to display', () => {
|
||||
beforeEach(() => {
|
||||
getGlobalAlerts.mockImplementationOnce(() => []);
|
||||
});
|
||||
|
||||
it('renders nothing', async () => {
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.html()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
GlListboxItem,
|
||||
GlCollapsibleListbox,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { GlListboxItem, GlCollapsibleListbox, GlDropdownDivider, GlIcon } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
|
||||
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
|
||||
|
|
@ -25,7 +19,7 @@ describe('Ci environments dropdown', () => {
|
|||
const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon);
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findListboxText = () => findListbox().props('toggleText');
|
||||
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
|
||||
const findCreateWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
|
||||
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
|
||||
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
|
||||
|
||||
|
|
@ -188,16 +182,35 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when creating a new environment from a search term', () => {
|
||||
const search = 'new-env';
|
||||
describe('when creating a new environment scope from a search term', () => {
|
||||
const searchTerm = 'new-env';
|
||||
beforeEach(() => {
|
||||
createComponent({ searchTerm: search });
|
||||
createComponent({ searchTerm, props: { hasEnvScopeQuery: true } });
|
||||
});
|
||||
|
||||
it('emits create-environment-scope', () => {
|
||||
findCreateWildcardButton().vm.$emit('click');
|
||||
it('sets new environment scope as the selected environment scope', async () => {
|
||||
findCreateWildcardButton().trigger('click');
|
||||
|
||||
expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
|
||||
await findListbox().vm.$emit('search', searchTerm);
|
||||
|
||||
expect(findListbox().props('selected')).toBe(searchTerm);
|
||||
});
|
||||
|
||||
it('includes new environment scope in search if it matches search term', async () => {
|
||||
findCreateWildcardButton().trigger('click');
|
||||
|
||||
await findListbox().vm.$emit('search', searchTerm);
|
||||
|
||||
expect(findAllListboxItems()).toHaveLength(envs.length + 1);
|
||||
expect(findListboxItemByIndex(1).text()).toBe(searchTerm);
|
||||
});
|
||||
|
||||
it('excludes new environment scope in search if it does not match the search term', async () => {
|
||||
findCreateWildcardButton().trigger('click');
|
||||
|
||||
await findListbox().vm.$emit('search', 'not-new-env');
|
||||
|
||||
expect(findAllListboxItems()).toHaveLength(envs.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect, GlModal } from '@gitlab/ui';
|
||||
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
|
||||
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
|
||||
|
|
@ -67,6 +68,8 @@ describe('CI Variable Drawer', () => {
|
|||
};
|
||||
|
||||
const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn');
|
||||
const findConfirmDeleteModal = () => wrapper.findComponent(GlModal);
|
||||
const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-btn');
|
||||
const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput);
|
||||
const findDrawer = () => wrapper.findComponent(GlDrawer);
|
||||
const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
|
||||
|
|
@ -363,22 +366,118 @@ describe('CI Variable Drawer', () => {
|
|||
});
|
||||
|
||||
it('title and confirm button renders the correct text', () => {
|
||||
expect(findTitle().text()).toBe('Add Variable');
|
||||
expect(findConfirmBtn().text()).toBe('Add Variable');
|
||||
expect(findTitle().text()).toBe('Add variable');
|
||||
expect(findConfirmBtn().text()).toBe('Add variable');
|
||||
});
|
||||
|
||||
it('does not render delete button', () => {
|
||||
expect(findDeleteBtn().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('dispatches the add-variable event', async () => {
|
||||
await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
|
||||
await findProtectedCheckbox().vm.$emit('input', false);
|
||||
await findExpandedCheckbox().vm.$emit('input', true);
|
||||
await findMaskedCheckbox().vm.$emit('input', true);
|
||||
await findValueField().vm.$emit('input', 'NEW_VALUE');
|
||||
|
||||
findConfirmBtn().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('add-variable')).toEqual([
|
||||
[
|
||||
{
|
||||
environmentScope: '*',
|
||||
key: 'NEW_VARIABLE',
|
||||
masked: true,
|
||||
protected: false,
|
||||
raw: false, // opposite of expanded
|
||||
value: 'NEW_VALUE',
|
||||
variableType: 'ENV_VAR',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editing a variable', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: { mode: EDIT_VARIABLE_ACTION },
|
||||
props: { mode: EDIT_VARIABLE_ACTION, selectedVariable: mockProjectVariableFileType },
|
||||
stubs: { GlDrawer },
|
||||
});
|
||||
});
|
||||
|
||||
it('title and confirm button renders the correct text', () => {
|
||||
expect(findTitle().text()).toBe('Edit Variable');
|
||||
expect(findConfirmBtn().text()).toBe('Edit Variable');
|
||||
expect(findTitle().text()).toBe('Edit variable');
|
||||
expect(findConfirmBtn().text()).toBe('Edit variable');
|
||||
});
|
||||
|
||||
it('dispatches the edit-variable event', async () => {
|
||||
await findValueField().vm.$emit('input', 'EDITED_VALUE');
|
||||
|
||||
findConfirmBtn().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('update-variable')).toEqual([
|
||||
[
|
||||
{
|
||||
...mockProjectVariableFileType,
|
||||
value: 'EDITED_VALUE',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when deleting a variable', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
props: { mode: EDIT_VARIABLE_ACTION, selectedVariable: mockProjectVariableFileType },
|
||||
});
|
||||
});
|
||||
|
||||
it('bubbles up the delete-variable event', async () => {
|
||||
findDeleteBtn().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
findConfirmDeleteModal().vm.$emit('primary');
|
||||
|
||||
expect(wrapper.emitted('delete-variable')).toEqual([[mockProjectVariableFileType]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment scope events', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mountExtended,
|
||||
props: {
|
||||
mode: EDIT_VARIABLE_ACTION,
|
||||
selectedVariable: mockProjectVariableFileType,
|
||||
areScopedVariablesAvailable: true,
|
||||
hideEnvironmentScope: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the environment scope', async () => {
|
||||
await findEnvironmentScopeDropdown().vm.$emit('select-environment', 'staging');
|
||||
await findConfirmBtn().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('update-variable')).toEqual([
|
||||
[
|
||||
{
|
||||
...mockProjectVariableFileType,
|
||||
environmentScope: 'staging',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('bubbles up the search event', async () => {
|
||||
await findEnvironmentScopeDropdown().vm.$emit('search-environment-scope', 'staging');
|
||||
|
||||
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
getGlobalAlerts,
|
||||
setGlobalAlerts,
|
||||
removeGlobalAlertById,
|
||||
GLOBAL_ALERTS_SESSION_STORAGE_KEY,
|
||||
} from '~/lib/utils/global_alerts';
|
||||
|
||||
describe('global alerts utils', () => {
|
||||
describe('getGlobalAlerts', () => {
|
||||
describe('when there are alerts', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(Storage.prototype, 'getItem')
|
||||
.mockImplementation(() => '[{"id":"foo","variant":"danger","message":"Foo"}]');
|
||||
});
|
||||
|
||||
it('returns alerts from session storage', () => {
|
||||
expect(getGlobalAlerts()).toEqual([{ id: 'foo', variant: 'danger', message: 'Foo' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no alerts', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
it('returns empty array', () => {
|
||||
expect(getGlobalAlerts()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGlobalAlerts', () => {
|
||||
it('sets alerts in session storage', () => {
|
||||
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
|
||||
|
||||
setGlobalAlerts([
|
||||
{
|
||||
id: 'foo',
|
||||
variant: 'danger',
|
||||
message: 'Foo',
|
||||
},
|
||||
{
|
||||
id: 'bar',
|
||||
variant: 'success',
|
||||
message: 'Bar',
|
||||
persistOnPages: ['dashboard:groups:index'],
|
||||
dismissible: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(setItemSpy).toHaveBeenCalledWith(
|
||||
GLOBAL_ALERTS_SESSION_STORAGE_KEY,
|
||||
'[{"dismissible":true,"persistOnPages":[],"id":"foo","variant":"danger","message":"Foo"},{"dismissible":false,"persistOnPages":["dashboard:groups:index"],"id":"bar","variant":"success","message":"Bar"}]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeGlobalAlertById', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(Storage.prototype, 'getItem')
|
||||
.mockImplementation(
|
||||
() =>
|
||||
'[{"id":"foo","variant":"success","message":"Foo"},{"id":"bar","variant":"danger","message":"Bar"}]',
|
||||
);
|
||||
});
|
||||
|
||||
it('removes alert', () => {
|
||||
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
|
||||
|
||||
removeGlobalAlertById('bar');
|
||||
|
||||
expect(setItemSpy).toHaveBeenCalledWith(
|
||||
GLOBAL_ALERTS_SESSION_STORAGE_KEY,
|
||||
'[{"id":"foo","variant":"success","message":"Foo"}]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,20 @@
|
|||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import { setGlobalAlerts } from '~/lib/utils/global_alerts';
|
||||
import { safeUrls, unsafeUrls } from './mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/global_alerts', () => ({
|
||||
getGlobalAlerts: jest.fn().mockImplementation(() => [
|
||||
{
|
||||
id: 'foo',
|
||||
message: 'Foo',
|
||||
variant: 'success',
|
||||
},
|
||||
]),
|
||||
setGlobalAlerts: jest.fn(),
|
||||
}));
|
||||
|
||||
const shas = {
|
||||
valid: [
|
||||
'ad9be38573f9ee4c4daec22673478c2dd1d81cd8',
|
||||
|
|
@ -482,6 +494,48 @@ describe('URL utility', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('visitUrlWithAlerts', () => {
|
||||
let originalLocation;
|
||||
|
||||
beforeAll(() => {
|
||||
originalLocation = window.location;
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: {
|
||||
assign: jest.fn(),
|
||||
protocol: 'http:',
|
||||
host: TEST_HOST,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.location = originalLocation;
|
||||
});
|
||||
|
||||
it('sets alerts and then visits url', () => {
|
||||
const url = '/foo/bar';
|
||||
const alert = {
|
||||
id: 'bar',
|
||||
message: 'Bar',
|
||||
variant: 'danger',
|
||||
};
|
||||
|
||||
urlUtils.visitUrlWithAlerts(url, [alert]);
|
||||
|
||||
expect(setGlobalAlerts).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 'foo',
|
||||
message: 'Foo',
|
||||
variant: 'success',
|
||||
},
|
||||
alert,
|
||||
]);
|
||||
expect(window.location.assign).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateHistory', () => {
|
||||
const state = { key: 'prop' };
|
||||
const title = 'TITLE';
|
||||
|
|
|
|||
|
|
@ -41,7 +41,13 @@ RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do
|
|||
let(:result) { double }
|
||||
let(:pgss_query) do
|
||||
<<~SQL
|
||||
SELECT query, calls, total_time, max_time, mean_time, rows
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
total_exec_time + total_plan_time AS total_time,
|
||||
max_exec_time + max_plan_time AS max_time,
|
||||
mean_exec_time + mean_plan_time AS mean_time,
|
||||
"rows"
|
||||
FROM pg_stat_statements
|
||||
WHERE pg_get_userbyid(userid) = current_user
|
||||
ORDER BY total_time DESC
|
||||
|
|
|
|||
|
|
@ -23,16 +23,6 @@ RSpec.describe Gitlab::DeployKeyAccess, feature_category: :source_code_managemen
|
|||
it 'returns false' do
|
||||
expect(access.can_create_tag?('v0.1.2')).to be_falsey
|
||||
end
|
||||
|
||||
context 'when deploy_key_for_protected_tags FF is disabled' do
|
||||
before do
|
||||
stub_feature_flags(deploy_key_for_protected_tags: false)
|
||||
end
|
||||
|
||||
it 'allows to push the tag' do
|
||||
expect(access.can_create_tag?('v0.1.2')).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'push tag that matches a protected tag pattern via a deploy key' do
|
||||
|
|
|
|||
|
|
@ -164,7 +164,6 @@ project_setting:
|
|||
- selective_code_owner_removals
|
||||
- show_diff_preview_in_email
|
||||
- suggested_reviewers_enabled
|
||||
- jitsu_key
|
||||
- mirror_branch_regex
|
||||
- allow_pipeline_trigger_approve_deployment
|
||||
- pages_unique_domain_enabled
|
||||
|
|
|
|||
|
|
@ -334,6 +334,45 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
describe '/set_parent' do
|
||||
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
||||
let_it_be_with_reload(:parent) { create(:work_item, :objective, project: project) }
|
||||
let_it_be(:note_text) { "/set_parent #{parent.to_reference}" }
|
||||
let_it_be(:note) { create(:note, noteable: noteable, project: project, note: note_text) }
|
||||
|
||||
shared_examples 'sets work item parent' do
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note)).to be_empty
|
||||
end
|
||||
|
||||
it 'sets work item parent' do
|
||||
execute(note)
|
||||
|
||||
expect(parent.valid?).to be_truthy
|
||||
expect(noteable.work_item_parent).to eq(parent)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using work item reference' do
|
||||
let_it_be(:note_text) { "/set_parent #{project.full_path}#{parent.to_reference}" }
|
||||
|
||||
it_behaves_like 'sets work item parent'
|
||||
end
|
||||
|
||||
context 'when using work item iid' do
|
||||
let_it_be(:note_text) { "/set_parent #{parent.to_reference}" }
|
||||
|
||||
it_behaves_like 'sets work item parent'
|
||||
end
|
||||
|
||||
context 'when using work item URL' do
|
||||
let_it_be(:url) { "#{Gitlab.config.gitlab.url}/#{project.full_path}/work_items/#{parent.iid}" }
|
||||
let_it_be(:note_text) { "/set_parent #{url}" }
|
||||
|
||||
it_behaves_like 'sets work item parent'
|
||||
end
|
||||
end
|
||||
|
||||
describe '/promote_to' do
|
||||
shared_examples 'promotes work item' do |from:, to:|
|
||||
it 'leaves the note empty' do
|
||||
|
|
|
|||
|
|
@ -2478,6 +2478,26 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
|
|||
end
|
||||
|
||||
it_behaves_like 'quick actions that change work item type'
|
||||
|
||||
context '/set_parent command' do
|
||||
let_it_be(:parent) { create(:work_item, :issue, project: project) }
|
||||
let_it_be(:work_item) { create(:work_item, :task, project: project) }
|
||||
let_it_be(:parent_ref) { parent.to_reference(project) }
|
||||
|
||||
let(:content) { "/set_parent #{parent_ref}" }
|
||||
|
||||
it 'returns success message' do
|
||||
_, _, message = service.execute(content, work_item)
|
||||
|
||||
expect(message).to eq('Work item parent set successfully')
|
||||
end
|
||||
|
||||
it 'sets correct update params' do
|
||||
_, updates, _ = service.execute(content, work_item)
|
||||
|
||||
expect(updates).to eq(set_parent: parent)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#explain' do
|
||||
|
|
@ -3022,6 +3042,55 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '/set_parent command' do
|
||||
let_it_be(:parent) { create(:work_item, :issue, project: project) }
|
||||
let_it_be(:work_item) { create(:work_item, :task, project: project) }
|
||||
let_it_be(:parent_ref) { parent.to_reference(project) }
|
||||
|
||||
let(:command) { "/set_parent #{parent_ref}" }
|
||||
|
||||
shared_examples 'command is available' do
|
||||
it 'explanation contains correct message' do
|
||||
_, explanations = service.explain(command, work_item)
|
||||
|
||||
expect(explanations)
|
||||
.to contain_exactly("Change work item's parent to #{parent_ref}.")
|
||||
end
|
||||
|
||||
it 'contains command' do
|
||||
expect(service.available_commands(work_item)).to include(a_hash_including(name: :set_parent))
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'command is not available' do
|
||||
it 'explanation is empty' do
|
||||
_, explanations = service.explain(command, work_item)
|
||||
|
||||
expect(explanations).to eq([])
|
||||
end
|
||||
|
||||
it 'does not contain command' do
|
||||
expect(service.available_commands(work_item)).not_to include(a_hash_including(name: :set_parent))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can admin link' do
|
||||
it_behaves_like 'command is available'
|
||||
|
||||
context 'when work item type does not support a parent' do
|
||||
let_it_be(:work_item) { build(:work_item, :incident, project: project) }
|
||||
|
||||
it_behaves_like 'command is not available'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot admin link' do
|
||||
subject(:service) { described_class.new(project, create(:user)) }
|
||||
|
||||
it_behaves_like 'command is not available'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#available_commands' do
|
||||
|
|
|
|||
|
|
@ -1,23 +1,237 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'variable list drawer' do
|
||||
it 'adds a new CI variable' do
|
||||
click_button('Add variable')
|
||||
it 'renders the list drawer' do
|
||||
open_drawer
|
||||
|
||||
# For now, we just check that the drawer is displayed
|
||||
expect(page).to have_selector('[data-testid="ci-variable-drawer"]')
|
||||
end
|
||||
|
||||
# TODO: Add tests for ADDING a variable via drawer when feature is available
|
||||
it 'adds a new CI variable' do
|
||||
open_drawer
|
||||
|
||||
fill_variable('NEW_KEY', 'NEW_VALUE')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
expect(first(".js-ci-variable-row td[data-label='#{s_('CiVariables|Key')}']")).to have_content('NEW_KEY')
|
||||
|
||||
click_button('Reveal values')
|
||||
|
||||
expect(first(".js-ci-variable-row td[data-label='#{s_('CiVariables|Value')}']")).to have_content('NEW_VALUE')
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows variable with empty value to be created' do
|
||||
open_drawer
|
||||
|
||||
fill_variable('NEW_KEY')
|
||||
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
expect(find_button('Add variable', disabled: false)).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'defaults to unmasked, expanded' do
|
||||
open_drawer
|
||||
|
||||
fill_variable('NEW_KEY')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
key_column = first(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")
|
||||
|
||||
expect(key_column).not_to have_content(s_('CiVariables|Masked'))
|
||||
expect(key_column).to have_content(s_('CiVariables|Expanded'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with application setting for protected attribute' do
|
||||
context 'when application setting is true' do
|
||||
before do
|
||||
stub_application_setting(protected_ci_variables: true)
|
||||
|
||||
visit page_path
|
||||
end
|
||||
|
||||
it 'defaults to protected' do
|
||||
open_drawer
|
||||
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when application setting is false' do
|
||||
before do
|
||||
stub_application_setting(protected_ci_variables: false)
|
||||
|
||||
visit page_path
|
||||
end
|
||||
|
||||
it 'defaults to unprotected' do
|
||||
open_drawer
|
||||
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits a variable' do
|
||||
key_column = first(".js-ci-variable-row td[data-label='#{s_('CiVariables|Key')}']")
|
||||
value_column = first(".js-ci-variable-row td[data-label='#{s_('CiVariables|Value')}']")
|
||||
|
||||
expect(key_column).to have_content('test_key')
|
||||
expect(key_column).not_to have_content(s_('CiVariables|Protected'))
|
||||
expect(key_column).to have_content(s_('CiVariables|Masked'))
|
||||
expect(key_column).to have_content(s_('CiVariables|Expanded'))
|
||||
|
||||
click_button('Edit')
|
||||
|
||||
fill_variable('EDITED_KEY', 'EDITED_VALUE')
|
||||
toggle_protected
|
||||
toggle_masked
|
||||
toggle_expanded
|
||||
click_button('Edit variable')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Edit')
|
||||
expect(key_column).to have_content('EDITED_KEY')
|
||||
expect(key_column).to have_content(s_('CiVariables|Protected'))
|
||||
expect(key_column).not_to have_content(s_('CiVariables|Masked'))
|
||||
expect(key_column).not_to have_content(s_('CiVariables|Expanded'))
|
||||
|
||||
click_button('Reveal values')
|
||||
|
||||
expect(value_column).to have_content('EDITED_VALUE')
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows validation error for duplicate keys' do
|
||||
open_drawer
|
||||
|
||||
fill_variable('NEW_KEY', 'NEW_VALUE')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
open_drawer
|
||||
|
||||
fill_variable('NEW_KEY', 'NEW_VALUE')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.flash-container')).to be_present
|
||||
expect(find('[data-testid="alert-danger"]').text).to have_content('(NEW_KEY) has already been taken')
|
||||
end
|
||||
|
||||
it 'shows validation error for unmaskable values' do
|
||||
open_drawer
|
||||
|
||||
toggle_masked
|
||||
fill_variable('EMPTY_MASK_KEY', '???')
|
||||
|
||||
expect(page).to have_content('This variable value does not meet the masking requirements.')
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
expect(find_button('Add variable', disabled: true)).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles multiple edits and a deletion' do
|
||||
# Create two variables
|
||||
open_drawer
|
||||
fill_variable('akey', 'akeyvalue')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
open_drawer
|
||||
fill_variable('zkey', 'zkeyvalue')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.js-ci-variable-row', count: 3)
|
||||
|
||||
# Remove the `akey` variable
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
page.within('.js-ci-variable-row:first-child') do
|
||||
click_button('Edit')
|
||||
end
|
||||
end
|
||||
|
||||
# For now, we just check that the drawer is displayed
|
||||
expect(page).to have_selector('[data-testid="ci-variable-drawer"]')
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
click_button('Delete variable') # opens confirmation modal
|
||||
end
|
||||
|
||||
# TODO: Add tests for EDITING a variable via drawer when feature is available
|
||||
page.within('[data-testid="ci-variable-drawer-confirm-delete-modal"]') do
|
||||
click_button('Delete')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
# Add another variable
|
||||
open_drawer
|
||||
fill_variable('ckey', 'ckeyvalue')
|
||||
click_add_variable
|
||||
|
||||
wait_for_requests
|
||||
|
||||
# expect to find 3 rows of variables in alphabetical order
|
||||
expect(page).to have_selector('.js-ci-variable-row', count: 3)
|
||||
rows = all('.js-ci-variable-row')
|
||||
expect(rows[0].find('td[data-label="Key"]')).to have_content('ckey')
|
||||
expect(rows[1].find('td[data-label="Key"]')).to have_content('test_key')
|
||||
expect(rows[2].find('td[data-label="Key"]')).to have_content('zkey')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def open_drawer
|
||||
page.within('[data-testid="ci-variable-table"]') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
end
|
||||
|
||||
def click_add_variable
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
end
|
||||
|
||||
def fill_variable(key, value = '')
|
||||
wait_for_requests
|
||||
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
find('[data-testid="ci-variable-key"] input').set(key)
|
||||
find('[data-testid="ci-variable-value"]').set(value) if value.present?
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_protected
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
find('[data-testid="ci-variable-protected-checkbox"]').click
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_masked
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
find('[data-testid="ci-variable-masked-checkbox"]').click
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_expanded
|
||||
page.within('[data-testid="ci-variable-drawer"]') do
|
||||
find('[data-testid="ci-variable-expanded-checkbox"]').click
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue