Commit 63c0654e authored by Nathan Friend's avatar Nathan Friend

Merge branch 'refactor-env-scope' into 'master'

Add wildcard scope to environments

See merge request gitlab-org/gitlab!27699
parents e2d10e2b 88396383
<script>
import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { mapGetters } from 'vuex';
export default {
name: 'CiEnvironmentsDropdown',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlIcon,
},
props: {
value: {
type: String,
required: false,
default: '',
},
},
data() {
return {
searchTerm: this.value || '',
};
},
computed: {
...mapGetters(['joinedEnvironments']),
composedCreateButtonLabel() {
return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
},
shouldRenderCreateButton() {
return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm);
},
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.joinedEnvironments.filter(resultString =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
},
watch: {
value(newVal) {
this.searchTerm = newVal;
},
},
methods: {
selectEnvironment(selected) {
this.$emit('selectEnvironment', selected);
this.searchTerm = '';
},
createClicked() {
this.$emit('createClicked', this.searchTerm);
this.searchTerm = '';
},
isSelected(env) {
return this.value === env;
},
},
};
</script>
<template>
<gl-dropdown :text="value">
<gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
<gl-dropdown-item
v-for="environment in filteredResults"
:key="environment"
@click="selectEnvironment(environment)"
>
<gl-icon
:class="{ invisible: !isSelected(environment) }"
name="mobile-issue-close"
class="vertical-align-middle"
/>
{{ environment }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
__('No matching results')
}}</gl-dropdown-item>
<template v-if="shouldRenderCreateButton">
<gl-dropdown-divider />
<gl-dropdown-item @click="createClicked">
{{ composedCreateButtonLabel }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants'; import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { import {
GlButton, GlButton,
GlModal, GlModal,
...@@ -17,6 +18,7 @@ import { ...@@ -17,6 +18,7 @@ import {
export default { export default {
modalId: ADD_CI_VARIABLE_MODAL_ID, modalId: ADD_CI_VARIABLE_MODAL_ID,
components: { components: {
CiEnvironmentsDropdown,
GlButton, GlButton,
GlModal, GlModal,
GlFormSelect, GlFormSelect,
...@@ -36,6 +38,7 @@ export default { ...@@ -36,6 +38,7 @@ export default {
'variableBeingEdited', 'variableBeingEdited',
'isGroup', 'isGroup',
'maskableRegex', 'maskableRegex',
'selectedEnvironment',
]), ]),
canSubmit() { canSubmit() {
if (this.variableData.masked && this.maskedState === false) { if (this.variableData.masked && this.maskedState === false) {
...@@ -80,6 +83,10 @@ export default { ...@@ -80,6 +83,10 @@ export default {
'displayInputValue', 'displayInputValue',
'clearModal', 'clearModal',
'deleteVariable', 'deleteVariable',
'setEnvironmentScope',
'addWildCardScope',
'resetSelectedEnvironment',
'setSelectedEnvironment',
]), ]),
updateOrAddVariable() { updateOrAddVariable() {
if (this.variableBeingEdited) { if (this.variableBeingEdited) {
...@@ -95,6 +102,7 @@ export default { ...@@ -95,6 +102,7 @@ export default {
} else { } else {
this.clearModal(); this.clearModal();
} }
this.resetSelectedEnvironment();
}, },
hideModal() { hideModal() {
this.$refs.modal.hide(); this.$refs.modal.hide();
...@@ -158,10 +166,11 @@ export default { ...@@ -158,10 +166,11 @@ export default {
label-for="ci-variable-env" label-for="ci-variable-env"
class="w-50" class="w-50"
> >
<gl-form-select <ci-environments-dropdown
id="ci-variable-env" class="w-100"
v-model="variableData.environment_scope" :value="variableData.environment_scope"
:options="environments" @selectEnvironment="setEnvironmentScope"
@createClicked="addWildCardScope"
/> />
</gl-form-group> </gl-form-group>
</div> </div>
......
...@@ -92,6 +92,7 @@ export default { ...@@ -92,6 +92,7 @@ export default {
sort-by="key" sort-by="key"
sort-direction="asc" sort-direction="asc"
stacked="lg" stacked="lg"
table-class="text-secondary"
fixed fixed
show-empty show-empty
sort-icon-left sort-icon-left
......
...@@ -6,7 +6,7 @@ export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable'; ...@@ -6,7 +6,7 @@ export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = { export const displayText = {
variableText: __('Var'), variableText: __('Var'),
fileText: __('File'), fileText: __('File'),
allEnvironmentsText: __('All'), allEnvironmentsText: __('All (default)'),
}; };
export const types = { export const types = {
......
...@@ -153,3 +153,22 @@ export const fetchEnvironments = ({ dispatch, state }) => { ...@@ -153,3 +153,22 @@ export const fetchEnvironments = ({ dispatch, state }) => {
createFlash(__('There was an error fetching the environments information.')); createFlash(__('There was an error fetching the environments information.'));
}); });
}; };
export const setEnvironmentScope = ({ commit, dispatch }, environment) => {
commit(types.SET_ENVIRONMENT_SCOPE, environment);
dispatch('setSelectedEnvironment', environment);
};
export const addWildCardScope = ({ commit, dispatch }, environment) => {
commit(types.ADD_WILD_CARD_SCOPE, environment);
commit(types.SET_ENVIRONMENT_SCOPE, environment);
dispatch('setSelectedEnvironment', environment);
};
export const resetSelectedEnvironment = ({ commit }) => {
commit(types.RESET_SELECTED_ENVIRONMENT);
};
export const setSelectedEnvironment = ({ commit }, environment) => {
commit(types.SET_SELECTED_ENVIRONMENT, environment);
};
/* eslint-disable import/prefer-default-export */
// Disabling import/prefer-default-export can be
// removed once a second getter is added to this file
import { uniq } from 'lodash';
export const joinedEnvironments = state => {
const scopesFromVariables = (state.variables || []).map(variable => variable.environment_scope);
return uniq(state.environments.concat(scopesFromVariables)).sort();
};
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
...@@ -10,6 +11,7 @@ export default (initialState = {}) => ...@@ -10,6 +11,7 @@ export default (initialState = {}) =>
new Vuex.Store({ new Vuex.Store({
actions, actions,
mutations, mutations,
getters,
state: { state: {
...state(), ...state(),
...initialState, ...initialState,
......
...@@ -20,3 +20,7 @@ export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR'; ...@@ -20,3 +20,7 @@ export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR';
export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS'; export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS'; export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
export const ADD_WILD_CARD_SCOPE = 'ADD_WILD_CARD_SCOPE';
export const RESET_SELECTED_ENVIRONMENT = 'RESET_SELECTED_ENVIRONMENT';
export const SET_SELECTED_ENVIRONMENT = 'SET_SELECTED_ENVIRONMENT';
...@@ -83,4 +83,25 @@ export default { ...@@ -83,4 +83,25 @@ export default {
state.variableBeingEdited = null; state.variableBeingEdited = null;
state.showInputValue = false; state.showInputValue = false;
}, },
[types.SET_ENVIRONMENT_SCOPE](state, environment) {
if (state.variableBeingEdited) {
state.variableBeingEdited.environment_scope = environment;
} else {
state.variable.environment_scope = environment;
}
},
[types.ADD_WILD_CARD_SCOPE](state, environment) {
state.environments.push(environment);
state.environments.sort();
},
[types.RESET_SELECTED_ENVIRONMENT](state) {
state.selectedEnvironment = '';
},
[types.SET_SELECTED_ENVIRONMENT](state, environment) {
state.selectedEnvironment = environment;
},
}; };
...@@ -21,4 +21,5 @@ export default () => ({ ...@@ -21,4 +21,5 @@ export default () => ({
environments: [], environments: [],
typeOptions: [displayText.variableText, displayText.fileText], typeOptions: [displayText.variableText, displayText.fileText],
variableBeingEdited: null, variableBeingEdited: null,
selectedEnvironment: '',
}); });
...@@ -1631,6 +1631,9 @@ msgstr "" ...@@ -1631,6 +1631,9 @@ msgstr ""
msgid "All %{replicableType} are being scheduled for %{action}" msgid "All %{replicableType} are being scheduled for %{action}"
msgstr "" msgstr ""
msgid "All (default)"
msgstr ""
msgid "All Members" msgid "All Members"
msgstr "" msgstr ""
...@@ -5904,6 +5907,9 @@ msgstr "" ...@@ -5904,6 +5907,9 @@ msgstr ""
msgid "Create project label" msgid "Create project label"
msgstr "" msgstr ""
msgid "Create wildcard: %{searchTerm}"
msgstr ""
msgid "Create your first page" msgid "Create your first page"
msgstr "" msgstr ""
......
import Vuex from 'vuex';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ci environments dropdown', () => {
let wrapper;
let store;
const createComponent = term => {
store = new Vuex.Store({
getters: {
joinedEnvironments: () => ['dev', 'prod', 'staging'],
},
});
wrapper = shallowMount(CiEnvironmentsDropdown, {
store,
localVue,
propsData: {
value: term,
},
});
};
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findDropdownItemByIndex = index => wrapper.findAll(GlDropdownItem).at(index);
const findActiveIconByIndex = index => wrapper.findAll(GlIcon).at(index);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('No enviroments found', () => {
beforeEach(() => {
createComponent('stable');
});
it('renders create button with search term if enviroments do not contain search term', () => {
expect(findAllDropdownItems()).toHaveLength(2);
expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
});
it('renders empty results message', () => {
expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
});
});
describe('Search term is empty', () => {
beforeEach(() => {
createComponent('');
});
it('renders all enviroments when search term is empty', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe('dev');
expect(findDropdownItemByIndex(1).text()).toBe('prod');
expect(findDropdownItemByIndex(2).text()).toBe('staging');
});
});
describe('Enviroments found', () => {
beforeEach(() => {
createComponent('prod');
});
it('renders only the enviroment searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('prod');
});
it('should not display create button', () => {
const enviroments = findAllDropdownItems().filter(env => env.text().startsWith('Create'));
expect(enviroments).toHaveLength(0);
expect(findAllDropdownItems()).toHaveLength(1);
});
it('should not display empty results message', () => {
expect(wrapper.find({ ref: 'noMatchingResults' }).exists()).toBe(false);
});
it('should display active checkmark if active', () => {
expect(findActiveIconByIndex(0).classes('invisible')).toBe(false);
});
describe('Custom events', () => {
it('should emit selectEnvironment if an environment is clicked', () => {
findDropdownItemByIndex(0).vm.$emit('click');
expect(wrapper.emitted('selectEnvironment')).toEqual([['prod']]);
});
it('should emit createClicked if an environment is clicked', () => {
createComponent('newscope');
findDropdownItemByIndex(1).vm.$emit('click');
expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
});
});
});
});
export default { export default {
mockVariables: [ mockVariables: [
{ {
environment_scope: 'All environments', environment_scope: 'All (default)',
id: 113, id: 113,
key: 'test_var', key: 'test_var',
masked: false, masked: false,
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
mockVariablesDisplay: [ mockVariablesDisplay: [
{ {
environment_scope: 'All', environment_scope: 'All (default)',
id: 113, id: 113,
key: 'test_var', key: 'test_var',
masked: false, masked: false,
...@@ -47,7 +47,7 @@ export default { ...@@ -47,7 +47,7 @@ export default {
variable_type: 'Var', variable_type: 'Var',
}, },
{ {
environment_scope: 'All', environment_scope: 'All (default)',
id: 114, id: 114,
key: 'test_var_2', key: 'test_var_2',
masked: false, masked: false,
...@@ -88,4 +88,67 @@ export default { ...@@ -88,4 +88,67 @@ export default {
ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ
98TwDIK/39WEB/V607As+KoYazQG8drorw== 98TwDIK/39WEB/V607As+KoYazQG8drorw==
-----END CERTIFICATE REQUEST-----`, -----END CERTIFICATE REQUEST-----`,
mockVariableScopes: [
{
id: 13,
key: 'test_var_1',
value: 'test_val_1',
variable_type: 'File',
protected: true,
masked: true,
environment_scope: 'All (default)',
secret_value: 'test_val_1',
},
{
id: 28,
key: 'goku_var',
value: 'goku_val',
variable_type: 'Var',
protected: true,
masked: true,
environment_scope: 'staging',
secret_value: 'goku_val',
},
{
id: 25,
key: 'test_var_4',
value: 'test_val_4',
variable_type: 'Var',
protected: false,
masked: false,
environment_scope: 'production',
secret_value: 'test_val_4',
},
{
id: 14,
key: 'test_var_2',
value: 'test_val_2',
variable_type: 'File',
protected: false,
masked: false,
environment_scope: 'staging',
secret_value: 'test_val_2',
},
{
id: 24,
key: 'test_var_3',
value: 'test_val_3',
variable_type: 'Var',
protected: false,
masked: false,
environment_scope: 'All (default)',
secret_value: 'test_val_3',
},
{
id: 26,
key: 'test_var_5',
value: 'test_val_5',
variable_type: 'Var',
protected: false,
masked: false,
environment_scope: 'production',
secret_value: 'test_val_5',
},
],
}; };
import * as getters from '~/ci_variable_list/store/getters';
import mockData from '../services/mock_data';
describe('Ci variable getters', () => {
describe('joinedEnvironments', () => {
it('should join fetched enviroments with variable environment scopes', () => {
const state = {
environments: ['All (default)', 'staging', 'deployment', 'prod'],
variables: mockData.mockVariableScopes,
};
expect(getters.joinedEnvironments(state)).toEqual([
'All (default)',
'deployment',
'prod',
'production',
'staging',
]);
});
});
});
...@@ -4,6 +4,15 @@ import * as types from '~/ci_variable_list/store/mutation_types'; ...@@ -4,6 +4,15 @@ import * as types from '~/ci_variable_list/store/mutation_types';
describe('CI variable list mutations', () => { describe('CI variable list mutations', () => {
let stateCopy; let stateCopy;
const variableBeingEdited = {
environment_scope: '*',
id: 63,
key: 'test_var',
masked: false,
protected: false,
value: 'test_val',
variable_type: 'env_var',
};
beforeEach(() => { beforeEach(() => {
stateCopy = state(); stateCopy = state();
...@@ -21,16 +30,6 @@ describe('CI variable list mutations', () => { ...@@ -21,16 +30,6 @@ describe('CI variable list mutations', () => {
describe('VARIABLE_BEING_EDITED', () => { describe('VARIABLE_BEING_EDITED', () => {
it('should set variable that is being edited', () => { it('should set variable that is being edited', () => {
const variableBeingEdited = {
environment_scope: '*',
id: 63,
key: 'test_var',
masked: false,
protected: false,
value: 'test_val',
variable_type: 'env_var',
};
mutations[types.VARIABLE_BEING_EDITED](stateCopy, variableBeingEdited); mutations[types.VARIABLE_BEING_EDITED](stateCopy, variableBeingEdited);
expect(stateCopy.variableBeingEdited).toEqual(variableBeingEdited); expect(stateCopy.variableBeingEdited).toEqual(variableBeingEdited);
...@@ -53,7 +52,7 @@ describe('CI variable list mutations', () => { ...@@ -53,7 +52,7 @@ describe('CI variable list mutations', () => {
secret_value: '', secret_value: '',
protected: false, protected: false,
masked: false, masked: false,
environment_scope: 'All', environment_scope: 'All (default)',
}; };
mutations[types.CLEAR_MODAL](stateCopy); mutations[types.CLEAR_MODAL](stateCopy);
...@@ -61,4 +60,41 @@ describe('CI variable list mutations', () => { ...@@ -61,4 +60,41 @@ describe('CI variable list mutations', () => {
expect(stateCopy.variable).toEqual(modalState); expect(stateCopy.variable).toEqual(modalState);
}); });
}); });
describe('RECEIVE_ENVIRONMENTS_SUCCESS', () => {
it('should set environments', () => {
const environments = ['env1', 'env2'];
mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](stateCopy, environments);
expect(stateCopy.environments).toEqual(['All (default)', 'env1', 'env2']);
});
});
describe('SET_ENVIRONMENT_SCOPE', () => {
const environment = 'production';
it('should set scope to variable being updated if updating variable', () => {
stateCopy.variableBeingEdited = variableBeingEdited;
mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
expect(stateCopy.variableBeingEdited.environment_scope).toBe('production');
});
it('should set scope to variable if adding new variable', () => {
mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
expect(stateCopy.variable.environment_scope).toBe('production');
});
});
describe('ADD_WILD_CARD_SCOPE', () => {
it('should add wild card scope to enviroments array and sort', () => {
stateCopy.environments = ['dev', 'staging'];
mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production');
expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment