Skip to content
Snippets Groups Projects
Commit ab85af0f authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent a6c2be7c
No related branches found
No related tags found
No related merge requests found
Showing
with 657 additions and 45 deletions
<script>
import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import {
GlModal,
GlFormSelect,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlLink,
GlIcon,
} from '@gitlab/ui';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
components: {
GlModal,
GlFormSelect,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlLink,
GlIcon,
},
computed: {
...mapState([
'projectId',
'environments',
'typeOptions',
'variable',
'variableBeingEdited',
'isGroup',
'maskableRegex',
]),
canSubmit() {
return this.variableData.key !== '' && this.variableData.secret_value !== '';
},
canMask() {
const regex = RegExp(this.maskableRegex);
return regex.test(this.variableData.secret_value);
},
variableData() {
return this.variableBeingEdited || this.variable;
},
modalActionText() {
return this.variableBeingEdited ? __('Update Variable') : __('Add variable');
},
primaryAction() {
return {
text: this.modalActionText,
attributes: { variant: 'success', disabled: !this.canSubmit },
};
},
cancelAction() {
return {
text: __('Cancel'),
};
},
},
methods: {
...mapActions([
'addVariable',
'updateVariable',
'resetEditing',
'displayInputValue',
'clearModal',
]),
updateOrAddVariable() {
if (this.variableBeingEdited) {
this.updateVariable(this.variableBeingEdited);
} else {
this.addVariable();
}
},
resetModalHandler() {
if (this.variableBeingEdited) {
this.resetEditing();
} else {
this.clearModal();
}
},
},
};
</script>
<template>
<gl-modal
:modal-id="$options.modalId"
:title="modalActionText"
:action-primary="primaryAction"
:action-cancel="cancelAction"
@ok="updateOrAddVariable"
@hidden="resetModalHandler"
>
<form>
<gl-form-group label="Type" label-for="ci-variable-type">
<gl-form-select
id="ci-variable-type"
v-model="variableData.variable_type"
:options="typeOptions"
/>
</gl-form-group>
<div class="d-flex">
<gl-form-group label="Key" label-for="ci-variable-key" class="w-50 append-right-15">
<gl-form-input
id="ci-variable-key"
v-model="variableData.key"
type="text"
data-qa-selector="variable_key"
/>
</gl-form-group>
<gl-form-group label="Value" label-for="ci-variable-value" class="w-50">
<gl-form-input
id="ci-variable-value"
v-model="variableData.secret_value"
type="text"
data-qa-selector="variable_value"
/>
</gl-form-group>
</div>
<gl-form-group v-if="!isGroup" label="Environment scope" label-for="ci-variable-env">
<gl-form-select
id="ci-variable-env"
v-model="variableData.environment_scope"
:options="environments"
/>
</gl-form-group>
<gl-form-group label="Flags" label-for="ci-variable-flags">
<gl-form-checkbox v-model="variableData.protected" class="mb-0">
{{ __('Protect variable') }}
<gl-link href="/help/ci/variables/README#protected-environment-variables">
<gl-icon name="question" :size="12" />
</gl-link>
<p class="prepend-top-4 clgray">
{{ __('Allow variables to run on protected branches and tags.') }}
</p>
</gl-form-checkbox>
<gl-form-checkbox
ref="masked-ci-variable"
v-model="variableData.masked"
:disabled="!canMask"
data-qa-selector="variable_masked"
>
{{ __('Mask variable') }}
<gl-link href="/help/ci/variables/README#masked-variables">
<gl-icon name="question" :size="12" />
</gl-link>
<p class="prepend-top-4 append-bottom-0 clgray">
{{
__(
'Variables will be masked in job logs. Requires values to meet regular expression requirements.',
)
}}
<gl-link href="/help/ci/variables/README#masked-variables">{{
__('More information')
}}</gl-link>
</p>
</gl-form-checkbox>
</gl-form-group>
</form>
</gl-modal>
</template>
<script>
import CiVariableModal from './ci_variable_modal.vue';
import CiVariableTable from './ci_variable_table.vue';
import { mapState, mapActions } from 'vuex';
export default {
components: {
CiVariableModal,
CiVariableTable,
},
computed: {
...mapState(['isGroup']),
},
mounted() {
if (!this.isGroup) {
this.fetchEnvironments();
}
},
methods: {
...mapActions(['fetchEnvironments']),
},
};
</script>
<template>
<div class="row">
<div class="col-lg-12">
<ci-variable-table />
<ci-variable-modal />
</div>
</div>
</template>
<script>
import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { mapState, mapActions } from 'vuex';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
fields: [
{
key: 'variable_type',
label: s__('CiVariables|Type'),
},
{
key: 'key',
label: s__('CiVariables|Key'),
},
{
key: 'value',
label: s__('CiVariables|Value'),
tdClass: 'qa-ci-variable-input-value',
},
{
key: 'protected',
label: s__('CiVariables|Protected'),
},
{
key: 'masked',
label: s__('CiVariables|Masked'),
},
{
key: 'environment_scope',
label: s__('CiVariables|Environment Scope'),
},
{
key: 'actions',
label: '',
},
],
components: {
GlTable,
GlButton,
GlIcon,
},
directives: {
GlModalDirective,
},
computed: {
...mapState(['variables', 'valuesHidden', 'isGroup', 'isLoading', 'isDeleting']),
valuesButtonText() {
return this.valuesHidden ? __('Reveal values') : __('Hide values');
},
tableIsNotEmpty() {
return this.variables && this.variables.length > 0;
},
fields() {
if (this.isGroup) {
return this.$options.fields.filter(field => field.key !== 'environment_scope');
}
return this.$options.fields;
},
},
mounted() {
this.fetchVariables();
},
methods: {
...mapActions(['fetchVariables', 'deleteVariable', 'toggleValues', 'editVariable']),
},
};
</script>
<template>
<div class="ci-variable-table">
<gl-table
:fields="fields"
:items="variables"
responsive
show-empty
tbody-tr-class="js-ci-variable-row"
>
<template #cell(value)="data">
<span v-if="valuesHidden">*****************</span>
<span v-else>{{ data.value }}</span>
</template>
<template #cell(actions)="data">
<gl-button
ref="edit-ci-variable"
v-gl-modal-directive="$options.modalId"
@click="editVariable(data.item)"
>
<gl-icon name="pencil" />
</gl-button>
<gl-button
ref="delete-ci-variable"
category="secondary"
variant="danger"
@click="deleteVariable(data.item)"
>
<gl-icon name="remove" />
</gl-button>
</template>
<template #empty>
<p ref="empty-variables" class="settings-message text-center empty-variables">
{{
__(
'There are currently no variables, add a variable with the Add Variable button below.',
)
}}
</p>
</template>
</gl-table>
<div class="ci-variable-actions d-flex justify-content-end">
<gl-button
v-if="tableIsNotEmpty"
ref="secret-value-reveal-button"
data-qa-selector="reveal_ci_variable_value"
class="append-right-8"
@click="toggleValues(!valuesHidden)"
>{{ valuesButtonText }}</gl-button
>
<gl-button
ref="add-ci-variable"
v-gl-modal-directive="$options.modalId"
data-qa-selector="add_ci_variable"
variant="success"
>{{ __('Add Variable') }}</gl-button
>
</div>
</div>
</template>
// eslint-disable-next-line import/prefer-default-export
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
import Vue from 'vue';
import CiVariableSettings from './components/ci_variable_settings.vue';
import createStore from './store';
import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
const el = document.getElementById('js-ci-project-variables');
const { endpoint, projectId, group, maskableRegex } = el.dataset;
const isGroup = parseBoolean(group);
const store = createStore({
endpoint,
projectId,
isGroup,
maskableRegex,
});
return new Vue({
el,
store,
render(createElement) {
return createElement(CiVariableSettings);
},
});
};
import { __ } from '~/locale';
import { cloneDeep } from 'lodash';
 
const variableType = 'env_var';
const fileType = 'file';
Loading
Loading
@@ -24,9 +25,9 @@ export const prepareDataForDisplay = variables => {
};
 
export const prepareDataForApi = (variable, destroy = false) => {
const variableCopy = variable;
variableCopy.protected.toString();
variableCopy.masked.toString();
const variableCopy = cloneDeep(variable);
variableCopy.protected = variableCopy.protected.toString();
variableCopy.masked = variableCopy.masked.toString();
variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type);
 
if (variableCopy.environment_scope === __('All environments')) {
Loading
Loading
import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import initVariableList from '~/ci_variable_list';
 
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
 
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
maskableRegex: variableListEl.dataset.maskableRegex,
});
if (gon.features.newVariablesUi) {
initVariableList();
} else {
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
maskableRegex: variableListEl.dataset.maskableRegex,
});
}
});
Loading
Loading
@@ -2,6 +2,7 @@ import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list';
 
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
Loading
Loading
@@ -15,15 +16,19 @@ document.addEventListener('DOMContentLoaded', () => {
runnerTokenSecretValue.init();
}
 
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
maskableRegex: variableListEl.dataset.maskableRegex,
});
if (gon.features.newVariablesUi) {
initVariableList();
} else {
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
maskableRegex: variableListEl.dataset.maskableRegex,
});
}
 
// hide extra auto devops settings based checkbox state
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
Loading
Loading
Loading
Loading
@@ -130,6 +130,10 @@
border-radius: $border-radius-base;
}
 
.empty-variables {
padding: 20px 0;
}
.warning-title {
color: $orange-500;
}
Loading
Loading
@@ -370,3 +374,10 @@
.push-pull-table {
margin-top: 1em;
}
.ci-variable-table {
table tr th {
background-color: transparent;
border: 0;
}
}
Loading
Loading
@@ -8,8 +8,8 @@ Discuss your architecture design in an issue before writing code. This helps dec
 
## Be consistent
 
There are multiple ways of writing code to accomplish the same results. We should be as consistent as possible in how we write code across our codebases. This will make it more easier us to maintain our code across GitLab.
There are multiple ways of writing code to accomplish the same results. We should be as consistent as possible in how we write code across our codebases. This will make it easier for us to maintain our code across GitLab.
 
## Improve code [iteratively](https://about.gitlab.com/handbook/values/#iteration)
 
Whenever you see with existing code that does not follow our current style guide, update it proactively. You don't need to fix everything, but each merge request should iteratively improve our codebase, and reduce technical debt where possible.
Whenever you see existing code that does not follow our current style guide, update it proactively. You dont need to fix everything, but each merge request should iteratively improve our codebase, and reduce technical debt where possible.
Loading
Loading
@@ -1057,6 +1057,9 @@ msgstr ""
msgid "Add README"
msgstr ""
 
msgid "Add Variable"
msgstr ""
msgid "Add Zoom meeting"
msgstr ""
 
Loading
Loading
@@ -1189,6 +1192,9 @@ msgstr ""
msgid "Add users to group"
msgstr ""
 
msgid "Add variable"
msgstr ""
msgid "Add webhook"
msgstr ""
 
Loading
Loading
@@ -1638,6 +1644,9 @@ msgstr ""
msgid "Allow users to request access (if visibility is public or internal)"
msgstr ""
 
msgid "Allow variables to run on protected branches and tags."
msgstr ""
msgid "Allowed email domain restriction only permitted for top-level groups"
msgstr ""
 
Loading
Loading
@@ -3702,6 +3711,9 @@ msgstr ""
msgid "CiVariables|Cannot use Masked Variable with current value"
msgstr ""
 
msgid "CiVariables|Environment Scope"
msgstr ""
msgid "CiVariables|Input variable key"
msgstr ""
 
Loading
Loading
@@ -3714,6 +3726,9 @@ msgstr ""
msgid "CiVariables|Masked"
msgstr ""
 
msgid "CiVariables|Protected"
msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
 
Loading
Loading
@@ -11822,6 +11837,9 @@ msgstr ""
msgid "Marks this issue as related to %{issue_ref}."
msgstr ""
 
msgid "Mask variable"
msgstr ""
msgid "Match not found; try refining your search query."
msgstr ""
 
Loading
Loading
@@ -15491,6 +15509,9 @@ msgstr ""
msgid "Prompt users to upload SSH keys"
msgstr ""
 
msgid "Protect variable"
msgstr ""
msgid "Protected"
msgstr ""
 
Loading
Loading
@@ -19372,6 +19393,9 @@ msgstr ""
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgstr ""
 
msgid "There are currently no variables, add a variable with the Add Variable button below."
msgstr ""
msgid "There are no GPG keys associated with this account."
msgstr ""
 
Loading
Loading
@@ -20831,6 +20855,9 @@ msgstr ""
msgid "Update"
msgstr ""
 
msgid "Update Variable"
msgstr ""
msgid "Update all"
msgstr ""
 
Loading
Loading
@@ -21443,6 +21470,9 @@ msgstr ""
msgid "Variables"
msgstr ""
 
msgid "Variables will be masked in job logs. Requires values to meet regular expression requirements."
msgstr ""
msgid "Various container registry settings."
msgstr ""
 
Loading
Loading
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ci variable modal', () => {
let wrapper;
let store;
const createComponent = () => {
store = createStore();
wrapper = shallowMount(CiVariableModal, {
localVue,
store,
});
};
const findModal = () => wrapper.find(GlModal);
beforeEach(() => {
createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
afterEach(() => {
wrapper.destroy();
});
it('button is disabled when no key/value pair are present', () => {
expect(findModal().props('actionPrimary').attributes.disabled).toBeTruthy();
});
it('masked checkbox is disabled when value does not meet regex requirements', () => {
expect(wrapper.find({ ref: 'masked-ci-variable' }).attributes('disabled')).toBeTruthy();
});
describe('Adding a new variable', () => {
beforeEach(() => {
const [variable] = mockData.mockVariables;
store.state.variable = variable;
});
it('button is enabled when key/value pair are present', () => {
expect(findModal().props('actionPrimary').attributes.disabled).toBeFalsy();
});
it('masked checkbox is enabled when value meets regex requirements', () => {
store.state.maskableRegex = '^[a-zA-Z0-9_+=/@:-]{8,}$';
return wrapper.vm.$nextTick(() => {
expect(wrapper.find({ ref: 'masked-ci-variable' }).attributes('disabled')).toBeFalsy();
});
});
it('Add variable button dispatches addVariable action', () => {
findModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith('addVariable');
});
it('Clears the modal state once modal is hidden', () => {
findModal().vm.$emit('hidden');
expect(store.dispatch).toHaveBeenCalledWith('clearModal');
});
});
describe('Editing a variable', () => {
beforeEach(() => {
const [variable] = mockData.mockVariables;
store.state.variableBeingEdited = variable;
});
it('button text is Update variable when updating', () => {
expect(wrapper.vm.modalActionText).toBe('Update Variable');
});
it('Update variable button dispatches updateVariable with correct variable', () => {
findModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith(
'updateVariable',
store.state.variableBeingEdited,
);
});
it('Resets the editing state once modal is hidden', () => {
findModal().vm.$emit('hidden');
expect(store.dispatch).toHaveBeenCalledWith('resetEditing');
});
});
});
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
import createStore from '~/ci_variable_list/store';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ci variable table', () => {
let wrapper;
let store;
let isGroup;
const createComponent = groupState => {
store = createStore();
store.state.isGroup = groupState;
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(CiVariableSettings, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
});
it('dispatches fetchEnvironments when mounted', () => {
isGroup = false;
createComponent(isGroup);
expect(store.dispatch).toHaveBeenCalledWith('fetchEnvironments');
});
it('does not dispatch fetchenvironments when in group context', () => {
isGroup = true;
createComponent(isGroup);
expect(store.dispatch).not.toHaveBeenCalled();
});
});
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlTable } from '@gitlab/ui';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ci variable table', () => {
let wrapper;
let store;
const createComponent = () => {
store = createStore();
store.state.isGroup = true;
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mount(CiVariableTable, {
localVue,
store,
});
};
const findDeleteButton = () => wrapper.find({ ref: 'delete-ci-variable' });
const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' });
const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' });
const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' });
const findTable = () => wrapper.find(GlTable);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('dispatches fetchVariables when mounted', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
});
it('fields prop does not contain environment_scope if group', () => {
expect(findTable().props('fields')).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'environment_scope',
label: 'Environment Scope',
}),
]),
);
});
describe('Renders correct data', () => {
it('displays empty message when variables are not present', () => {
expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
});
it('displays correct amount of variables present and no empty message', () => {
store.state.variables = mockData.mockVariables;
return wrapper.vm.$nextTick(() => {
expect(wrapper.findAll('.js-ci-variable-row').length).toBe(1);
expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
});
});
});
describe('Table click actions', () => {
beforeEach(() => {
store.state.variables = mockData.mockVariables;
});
it('dispatches deleteVariable with correct variable to delete', () => {
findDeleteButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('deleteVariable', mockData.mockVariables[0]);
});
it('reveals secret values when button is clicked', () => {
findRevealButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('toggleValues', false);
});
it('dispatches editVariable with correct variable to edit', () => {
findEditButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('editVariable', mockData.mockVariables[0]);
});
});
});
Loading
Loading
@@ -9,24 +9,6 @@ export default {
value: 'test_val',
variable_type: 'Variable',
},
{
environment_scope: 'All environments',
id: 114,
key: 'test_var_2',
masked: false,
protected: false,
value: 'test_val_2',
variable_type: 'Variable',
},
{
environment_scope: 'All environments',
id: 115,
key: 'test_var_3',
masked: false,
protected: false,
value: 'test_val_3',
variable_type: 'Variable',
},
],
 
mockVariablesApi: [
Loading
Loading
Loading
Loading
@@ -17,8 +17,8 @@ describe('CI variables store utils', () => {
environment_scope: '*',
id: 113,
key: 'test_var',
masked: false,
protected: false,
masked: 'false',
protected: 'false',
value: 'test_val',
variable_type: 'env_var',
});
Loading
Loading
@@ -27,8 +27,8 @@ describe('CI variables store utils', () => {
environment_scope: '*',
id: 114,
key: 'test_var_2',
masked: false,
protected: false,
masked: 'false',
protected: 'false',
value: 'test_val_2',
variable_type: 'file',
});
Loading
Loading
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