Skip to content
Snippets Groups Projects
Commit 76250df3 authored by Mireya Andres's avatar Mireya Andres :red_circle:
Browse files

Merge branch 'ma/ci-variable-drawer-ff' into 'master'

Draft: Add feature flag for CI variables drawer

See merge request gitlab-org/gitlab!126197



Merged-by: default avatarMireya Andres <mandres@gitlab.com>
parents 9129e11c cf10121f
No related branches found
No related tags found
No related merge requests found
Showing
with 344 additions and 40 deletions
<script>
import {
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormTextarea,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import {
ENVIRONMENT_SCOPE_LINK_TITLE,
EXPANDED_VARIABLES_NOTE,
VARIABLE_ACTIONS,
variableOptions,
} from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokenList } from './ci_variable_autocomplete_tokens';
const i18n = {
header: s__('CiVariables|Add Variable'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
expandedDescription: EXPANDED_VARIABLES_NOTE,
maskedField: s__('CiVariables|Mask variable'),
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
protectedField: s__('CiVariables|Protect variable'),
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
),
};
export default {
DRAWER_Z_INDEX,
components: {
CiEnvironmentsDropdown,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormTextarea,
GlSprintf,
},
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
hasEnvScopeQuery: {
type: Boolean,
required: true,
},
mode: {
type: String,
required: true,
validator(val) {
return VARIABLE_ACTIONS.includes(val);
},
},
},
data() {
return {
key: '',
};
},
computed: {
environmentsList() {
return [];
},
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
},
methods: {
close() {
this.$emit('close-form');
},
},
awsTokenList,
i18n,
variableOptions,
};
</script>
<template>
<gl-drawer
open
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
@close="close"
>
<template #title>
<h2 class="gl-m-0">{{ $options.i18n.header }}</h2>
</template>
<gl-form-group :label="__('Type')" label-for="ci-variable-type" class="gl-border-none gl-mb-n5">
<gl-form-select id="ci-variable-type" :options="$options.variableOptions" />
</gl-form-group>
<gl-form-group
:label="__('Environments')"
class="gl-border-none gl-mb-n8"
label-for="ci-variable-env"
data-testid="environment-scope"
>
<ci-environments-dropdown
class="gl-mb-5"
:are-environments-loading="areEnvironmentsLoading"
:environments="environmentsList"
:has-env-scope-query="hasEnvScopeQuery"
selected-environment-scope=""
/>
<gl-form-checkbox data-testid="ci-variable-protected-checkbox">
{{ $options.i18n.protectedField }}
<p class="gl-text-secondary">
{{ $options.i18n.protectedDescription }}
</p>
</gl-form-checkbox>
<gl-form-checkbox 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">
{{ $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="key"
:token-list="$options.awsTokenList"
:label-text="__('Key')"
class="gl-border-none gl-mb-n7"
data-testid="pipeline-form-ci-variable-key"
data-qa-selector="ci_variable_key_field"
/>
<gl-form-group
:label="__('Value')"
label-for="ci-variable-value"
class="gl-border-none gl-mb-n5"
>
<gl-form-textarea
id="ci-variable-value"
class="gl-border-none gl-font-monospace!"
rows="3"
max-rows="10"
data-testid="pipeline-form-ci-variable-value"
data-qa-selector="ci_variable_value_field"
spellcheck="false"
/>
</gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button category="primary" class="gl-mr-3" data-testid="cancel-button" @click="close"
>{{ __('Cancel') }}
</gl-button>
<gl-button category="primary" variant="confirm" data-testid="confirm-button"
>{{ __('Add Variable') }}
</gl-button>
</div>
</gl-drawer>
</template>
......@@ -241,7 +241,7 @@ export default {
this.resetVariableData();
this.resetValidationErrorEvents();
this.$emit('hideModal');
this.$emit('close-form');
},
resetVariableData() {
this.variable = { ...defaultVariableState };
......
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
import CiVariableDrawer from './ci_variable_drawer.vue';
import CiVariableTable from './ci_variable_table.vue';
import CiVariableModal from './ci_variable_modal.vue';
export default {
components: {
CiVariableDrawer,
CiVariableTable,
CiVariableModal,
},
mixins: [glFeatureFlagsMixin()],
props: {
areEnvironmentsLoading: {
type: Boolean,
......@@ -62,23 +66,32 @@ export default {
};
},
computed: {
showModal() {
showForm() {
return VARIABLE_ACTIONS.includes(this.mode);
},
useDrawerForm() {
return this.glFeatures?.ciVariableDrawer;
},
showDrawer() {
return this.showForm && this.useDrawerForm;
},
showModal() {
return this.showForm && !this.useDrawerForm;
},
},
methods: {
addVariable(variable) {
this.$emit('add-variable', variable);
},
closeForm() {
this.mode = null;
},
deleteVariable(variable) {
this.$emit('delete-variable', variable);
},
updateVariable(variable) {
this.$emit('update-variable', variable);
},
hideModal() {
this.mode = null;
},
setSelectedVariable(variable = null) {
if (!variable) {
this.selectedVariable = {};
......@@ -118,10 +131,18 @@ export default {
:selected-variable="selectedVariable"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@hideModal="hideModal"
@close-form="closeForm"
@update-variable="updateVariable"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
<ci-variable-drawer
v-if="showDrawer"
:are-environments-loading="areEnvironmentsLoading"
:has-env-scope-query="hasEnvScopeQuery"
:mode="mode"
v-on="$listeners"
@close-form="closeForm"
/>
</div>
</div>
</template>
......@@ -153,3 +153,16 @@
.gl-fill-red-500 {
fill: $red-500;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3569
.gl-mb-n5 {
margin-bottom: -$gl-spacing-scale-5;
}
.gl-mb-n7 {
margin-bottom: -$gl-spacing-scale-7;
}
.gl-mb-n8 {
margin-bottom: -$gl-spacing-scale-8;
}
......@@ -16,6 +16,7 @@ class CiCdController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:ci_group_env_scope_graphql, group)
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
urgency :low
......
......@@ -14,6 +14,7 @@ class CiCdController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
helper_method :highlight_badge
......
---
name: ci_variable_drawer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126197
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/418005
milestone: '16.2'
type: development
group: group::pipeline security
default_enabled: false
......@@ -2654,6 +2654,9 @@ msgstr ""
msgid "Add README"
msgstr ""
 
msgid "Add Variable"
msgstr ""
msgid "Add Wiki"
msgstr ""
 
......@@ -9814,6 +9817,9 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
 
msgid "CiVariables|Add Variable"
msgstr ""
msgid "CiVariables|Attributes"
msgstr ""
 
......@@ -9826,9 +9832,15 @@ msgstr ""
msgid "CiVariables|Environments"
msgstr ""
 
msgid "CiVariables|Expand variable reference"
msgstr ""
msgid "CiVariables|Expanded"
msgstr ""
 
msgid "CiVariables|Export variable to pipelines running on protected branches and tags only."
msgstr ""
msgid "CiVariables|File"
msgstr ""
 
......@@ -9844,6 +9856,9 @@ msgstr ""
msgid "CiVariables|Key"
msgstr ""
 
msgid "CiVariables|Mask variable"
msgstr ""
msgid "CiVariables|Masked"
msgstr ""
 
......@@ -9853,6 +9868,9 @@ msgstr ""
msgid "CiVariables|Maximum number of variables reached."
msgstr ""
 
msgid "CiVariables|Protect variable"
msgstr ""
msgid "CiVariables|Protected"
msgstr ""
 
......@@ -9892,6 +9910,9 @@ msgstr ""
msgid "CiVariables|Value"
msgstr ""
 
msgid "CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements."
msgstr ""
msgid "CiVariables|Variables"
msgstr ""
 
......@@ -10,6 +10,7 @@
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
stub_feature_flags(ci_variable_drawer: false)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
......
......@@ -9,6 +9,7 @@
let(:page_path) { group_settings_ci_cd_path(group) }
before do
stub_feature_flags(ci_variable_drawer: false)
group.add_owner(user)
gitlab_sign_in(user)
visit page_path
......
......@@ -9,6 +9,7 @@
let(:page_path) { project_settings_ci_cd_path(project) }
before do
stub_feature_flags(ci_variable_drawer: false)
sign_in(user)
project.add_maintainer(user)
project.variables << variable
......
import { GlDrawer } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
import { ADD_VARIABLE_ACTION, variableOptions } from '~/ci/ci_variable_list/constants';
describe('CI Variable Drawer', () => {
let wrapper;
const defaultProps = {
areEnvironmentsLoading: false,
hasEnvScopeQuery: true,
mode: ADD_VARIABLE_ACTION,
};
const createComponent = ({ mountFn = shallowMountExtended, props = {} } = {}) => {
wrapper = mountFn(CiVariableDrawer, {
propsData: {
...defaultProps,
...props,
},
});
};
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findTypeDropdown = () => wrapper.find('#ci-variable-type');
describe('validations', () => {
beforeEach(() => {
createComponent({ mountFn: mountExtended });
});
describe('type dropdown', () => {
it('adds each type option as a dropdown item', () => {
expect(findTypeDropdown().findAll('option')).toHaveLength(variableOptions.length);
variableOptions.forEach((v) => {
expect(findTypeDropdown().text()).toContain(v.text);
});
});
});
});
describe('drawer events', () => {
beforeEach(() => {
createComponent();
});
it('emits `close-form` when closing the drawer', async () => {
expect(wrapper.emitted('close-form')).toBeUndefined();
await findDrawer().vm.$emit('close');
expect(wrapper.emitted('close-form')).toHaveLength(1);
});
});
});
......@@ -122,9 +122,9 @@ describe('Ci variable modal', () => {
expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]);
});
it('Dispatches the `hideModal` event when dismissing', () => {
it('Dispatches the `close-form` event when dismissing', () => {
findModal().vm.$emit('hidden');
expect(wrapper.emitted('hideModal')).toEqual([[]]);
expect(wrapper.emitted('close-form')).toEqual([[]]);
});
});
});
......@@ -313,9 +313,9 @@ describe('Ci variable modal', () => {
expect(wrapper.emitted('update-variable')).toEqual([[variable]]);
});
it('Propagates the `hideModal` event', () => {
it('Propagates the `close-form` event', () => {
findModal().vm.$emit('hidden');
expect(wrapper.emitted('hideModal')).toEqual([[]]);
expect(wrapper.emitted('close-form')).toEqual([[]]);
});
it('dispatches `delete-variable` with correct variable to delete', () => {
......
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import ciVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
import {
ADD_VARIABLE_ACTION,
EDIT_VARIABLE_ACTION,
......@@ -27,15 +29,22 @@ describe('Ci variable table', () => {
variables: mockVariablesWithScopes(projectString),
};
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
const findCiVariableModal = () => wrapper.findComponent(ciVariableModal);
const findCiVariableDrawer = () => wrapper.findComponent(CiVariableDrawer);
const findCiVariableTable = () => wrapper.findComponent(CiVariableTable);
const findCiVariableModal = () => wrapper.findComponent(CiVariableModal);
const createComponent = ({ props = {} } = {}) => {
const createComponent = ({ props = {}, featureFlags = {} } = {}) => {
wrapper = shallowMount(CiVariableSettings, {
propsData: {
...defaultProps,
...props,
},
provide: {
glFeatures: {
ciVariableDrawer: false,
...featureFlags,
},
},
});
};
......@@ -70,51 +79,51 @@ describe('Ci variable table', () => {
});
});
describe('modal mode', () => {
describe.each`
bool | flagStatus | elementName | findElement
${false} | ${'disabled'} | ${'modal'} | ${findCiVariableModal}
${true} | ${'enabled'} | ${'drawer'} | ${findCiVariableDrawer}
`('when ciVariableDrawer feature flag is $flagStatus', ({ bool, elementName, findElement }) => {
beforeEach(() => {
createComponent();
createComponent({ featureFlags: { ciVariableDrawer: bool } });
});
it('passes down ADD mode when receiving an empty variable', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
it(`${elementName} hidden by default`, () => {
expect(findElement().exists()).toBe(false);
});
it('passes down EDIT mode when receiving a variable', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
it(`shows ${elementName} when adding a new variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
expect(findElement().exists()).toBe(true);
});
});
describe('variable modal', () => {
beforeEach(() => {
createComponent();
});
it(`shows ${elementName} when updating a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
it('is hidden by default', () => {
expect(findCiVariableModal().exists()).toBe(false);
expect(findElement().exists()).toBe(true);
});
it('shows modal when adding a new variable', async () => {
it(`hides ${elementName} when closing the form`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().exists()).toBe(true);
});
expect(findElement().isVisible()).toBe(true);
it('shows modal when updating a variable', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
await findElement().vm.$emit('close-form');
expect(findCiVariableModal().exists()).toBe(true);
expect(findElement().exists()).toBe(false);
});
it('hides modal when receiving the event from the modal', async () => {
it(`passes down ADD mode to ${elementName} when receiving an empty variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
await findCiVariableModal().vm.$emit('hideModal');
expect(findElement().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
it(`passes down EDIT mode to ${elementName} when receiving a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().exists()).toBe(false);
expect(findElement().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment