Commit 38048647 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'ci-variables-ui-vuex' into 'master'

Refactor project/group variables UI into Vue - 2 / 4

See merge request gitlab-org/gitlab!25263
parents bf32ad05 1e542314
......@@ -47,6 +47,7 @@ const Api = {
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
environmentsPath: '/api/:version/projects/:id/environments',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
......@@ -483,6 +484,11 @@ const Api = {
return axios.get(url, { params: { path } });
},
environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
},
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
......
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils';
export const toggleValues = ({ commit }, valueState) => {
commit(types.TOGGLE_VALUES, valueState);
};
export const clearModal = ({ commit }) => {
commit(types.CLEAR_MODAL);
};
export const resetEditing = ({ commit, dispatch }) => {
// fetch variables again if modal is being edited and then hidden
// without saving changes, to cover use case of reactivity in the table
dispatch('fetchVariables');
commit(types.RESET_EDITING);
};
export const requestAddVariable = ({ commit }) => {
commit(types.REQUEST_ADD_VARIABLE);
};
export const receiveAddVariableSuccess = ({ commit }) => {
commit(types.RECEIVE_ADD_VARIABLE_SUCCESS);
};
export const receiveAddVariableError = ({ commit }, error) => {
commit(types.RECEIVE_ADD_VARIABLE_ERROR, error);
};
export const addVariable = ({ state, dispatch }) => {
dispatch('requestAddVariable');
return axios
.patch(state.endpoint, {
variables_attributes: [prepareDataForApi(state.variable)],
})
.then(() => {
dispatch('receiveAddVariableSuccess');
dispatch('fetchVariables');
})
.catch(error => {
createFlash(error.response.data[0]);
dispatch('receiveAddVariableError', error);
});
};
export const requestUpdateVariable = ({ commit }) => {
commit(types.REQUEST_UPDATE_VARIABLE);
};
export const receiveUpdateVariableSuccess = ({ commit }) => {
commit(types.RECEIVE_UPDATE_VARIABLE_SUCCESS);
};
export const receiveUpdateVariableError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_VARIABLE_ERROR, error);
};
export const updateVariable = ({ state, dispatch }, variable) => {
dispatch('requestUpdateVariable');
const updatedVariable = prepareDataForApi(variable);
updatedVariable.secrect_value = updateVariable.value;
return axios
.patch(state.endpoint, { variables_attributes: [updatedVariable] })
.then(() => {
dispatch('receiveUpdateVariableSuccess');
dispatch('fetchVariables');
})
.catch(error => {
createFlash(error.response.data[0]);
dispatch('receiveUpdateVariableError', error);
});
};
export const editVariable = ({ commit }, variable) => {
const variableToEdit = variable;
variableToEdit.secret_value = variableToEdit.value;
commit(types.VARIABLE_BEING_EDITED, variableToEdit);
};
export const requestVariables = ({ commit }) => {
commit(types.REQUEST_VARIABLES);
};
export const receiveVariablesSuccess = ({ commit }, variables) => {
commit(types.RECEIVE_VARIABLES_SUCCESS, variables);
};
export const fetchVariables = ({ dispatch, state }) => {
dispatch('requestVariables');
return axios
.get(state.endpoint)
.then(({ data }) => {
dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables));
})
.catch(() => {
createFlash(__('There was an error fetching the variables.'));
});
};
export const requestDeleteVariable = ({ commit }) => {
commit(types.REQUEST_DELETE_VARIABLE);
};
export const receiveDeleteVariableSuccess = ({ commit }) => {
commit(types.RECEIVE_DELETE_VARIABLE_SUCCESS);
};
export const receiveDeleteVariableError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_VARIABLE_ERROR, error);
};
export const deleteVariable = ({ dispatch, state }, variable) => {
dispatch('requestDeleteVariable');
const destroy = true;
return axios
.patch(state.endpoint, { variables_attributes: [prepareDataForApi(variable, destroy)] })
.then(() => {
dispatch('receiveDeleteVariableSuccess');
dispatch('fetchVariables');
})
.catch(error => {
createFlash(error.response.data[0]);
dispatch('receiveDeleteVariableError', error);
});
};
export const requestEnvironments = ({ commit }) => {
commit(types.REQUEST_ENVIRONMENTS);
};
export const receiveEnvironmentsSuccess = ({ commit }, environments) => {
commit(types.RECEIVE_ENVIRONMENTS_SUCCESS, environments);
};
export const fetchEnvironments = ({ dispatch, state }) => {
dispatch('requestEnvironments');
return Api.environments(state.projectId)
.then(res => {
dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data));
})
.catch(() => {
createFlash(__('There was an error fetching the environments information.'));
});
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default (initialState = {}) =>
new Vuex.Store({
actions,
mutations,
state: {
...state(),
...initialState,
},
});
export const TOGGLE_VALUES = 'TOGGLE_VALUES';
export const VARIABLE_BEING_EDITED = 'VARIABLE_BEING_EDITED';
export const RESET_EDITING = 'RESET_EDITING';
export const CLEAR_MODAL = 'CLEAR_MODAL';
export const REQUEST_VARIABLES = 'REQUEST_VARIABLES';
export const RECEIVE_VARIABLES_SUCCESS = 'RECEIVE_VARIABLES_SUCCESS';
export const REQUEST_DELETE_VARIABLE = 'REQUEST_DELETE_VARIABLE';
export const RECEIVE_DELETE_VARIABLE_SUCCESS = 'RECEIVE_DELETE_VARIABLE_SUCCESS';
export const RECEIVE_DELETE_VARIABLE_ERROR = 'RECEIVE_DELETE_VARIABLE_ERROR';
export const REQUEST_ADD_VARIABLE = 'REQUEST_ADD_VARIABLE';
export const RECEIVE_ADD_VARIABLE_SUCCESS = 'RECEIVE_ADD_VARIABLE_SUCCESS';
export const RECEIVE_ADD_VARIABLE_ERROR = 'RECEIVE_ADD_VARIABLE_ERROR';
export const REQUEST_UPDATE_VARIABLE = 'REQUEST_UPDATE_VARIABLE';
export const RECEIVE_UPDATE_VARIABLE_SUCCESS = 'RECEIVE_UPDATE_VARIABLE_SUCCESS';
export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR';
export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
import * as types from './mutation_types';
import { __ } from '~/locale';
export default {
[types.REQUEST_VARIABLES](state) {
state.isLoading = true;
},
[types.RECEIVE_VARIABLES_SUCCESS](state, variables) {
state.isLoading = false;
state.variables = variables;
},
[types.REQUEST_DELETE_VARIABLE](state) {
state.isDeleting = true;
},
[types.RECEIVE_DELETE_VARIABLE_SUCCESS](state) {
state.isDeleting = false;
},
[types.RECEIVE_DELETE_VARIABLE_ERROR](state, error) {
state.isDeleting = false;
state.error = error;
},
[types.REQUEST_ADD_VARIABLE](state) {
state.isLoading = true;
},
[types.RECEIVE_ADD_VARIABLE_SUCCESS](state) {
state.isLoading = false;
},
[types.RECEIVE_ADD_VARIABLE_ERROR](state, error) {
state.isLoading = false;
state.error = error;
},
[types.REQUEST_UPDATE_VARIABLE](state) {
state.isLoading = true;
},
[types.RECEIVE_UPDATE_VARIABLE_SUCCESS](state) {
state.isLoading = false;
},
[types.RECEIVE_UPDATE_VARIABLE_ERROR](state, error) {
state.isLoading = false;
state.error = error;
},
[types.TOGGLE_VALUES](state, valueState) {
state.valuesHidden = valueState;
},
[types.REQUEST_ENVIRONMENTS](state) {
state.isLoading = true;
},
[types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) {
state.isLoading = false;
state.environments = environments;
state.environments.unshift(__('All environments'));
},
[types.VARIABLE_BEING_EDITED](state, variable) {
state.variableBeingEdited = variable;
},
[types.CLEAR_MODAL](state) {
state.variable = {
variable_type: __('Variable'),
key: '',
secret_value: '',
protected: false,
masked: false,
environment_scope: __('All environments'),
};
},
[types.RESET_EDITING](state) {
state.variableBeingEdited = null;
state.showInputValue = false;
},
};
import { __ } from '~/locale';
export default () => ({
endpoint: null,
projectId: null,
isGroup: null,
maskableRegex: null,
isLoading: false,
isDeleting: false,
variable: {
variable_type: __('Variable'),
key: '',
secret_value: '',
protected: false,
masked: false,
environment_scope: __('All environments'),
},
variables: null,
valuesHidden: true,
error: null,
environments: [],
typeOptions: [__('Variable'), __('File')],
variableBeingEdited: null,
});
import { __ } from '~/locale';
const variableType = 'env_var';
const fileType = 'file';
const variableTypeHandler = type => (type === 'Variable' ? variableType : fileType);
export const prepareDataForDisplay = variables => {
const variablesToDisplay = [];
variables.forEach(variable => {
const variableCopy = variable;
if (variableCopy.variable_type === variableType) {
variableCopy.variable_type = __('Variable');
} else {
variableCopy.variable_type = __('File');
}
if (variableCopy.environment_scope === '*') {
variableCopy.environment_scope = __('All environments');
}
variablesToDisplay.push(variableCopy);
});
return variablesToDisplay;
};
export const prepareDataForApi = (variable, destroy = false) => {
const variableCopy = variable;
variableCopy.protected.toString();
variableCopy.masked.toString();
variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type);
if (variableCopy.environment_scope === __('All environments')) {
variableCopy.environment_scope = __('*');
}
if (destroy) {
// eslint-disable-next-line
variableCopy._destroy = destroy;
}
return variableCopy;
};
export const prepareEnvironments = environments => environments.map(e => e.name);
......@@ -534,6 +534,9 @@ msgstr ""
msgid "(removed)"
msgstr ""
msgid "*"
msgstr ""
msgid "+ %{amount} more"
msgstr ""
......@@ -1544,6 +1547,9 @@ msgstr ""
msgid "All email addresses will be used to identify your commits."
msgstr ""
msgid "All environments"
msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
......@@ -8385,6 +8391,9 @@ msgstr ""
msgid "Fetching licenses failed. You are not permitted to perform this action."
msgstr ""
msgid "File"
msgstr ""
msgid "File Hooks"
msgstr ""
......@@ -19425,6 +19434,12 @@ msgstr ""
msgid "There was an error fetching the Designs"
msgstr ""
msgid "There was an error fetching the environments information."
msgstr ""
msgid "There was an error fetching the variables."
msgstr ""
msgid "There was an error fetching value stream analytics stages."
msgstr ""
......@@ -21367,6 +21382,9 @@ msgstr ""
msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
msgid "Variable"
msgstr ""
msgid "Variables"
msgstr ""
......
export default {
mockVariables: [
{
environment_scope: 'All environments',
id: 113,
key: 'test_var',
masked: false,
protected: false,
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: [
{
environment_scope: '*',
id: 113,
key: 'test_var',
masked: false,
protected: false,
value: 'test_val',
variable_type: 'env_var',
},
{
environment_scope: '*',
id: 114,
key: 'test_var_2',
masked: false,
protected: false,
value: 'test_val_2',
variable_type: 'file',
},
],
mockVariablesDisplay: [
{
environment_scope: 'All environments',
id: 113,
key: 'test_var',
masked: false,
protected: false,
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: 'File',
},
],
mockEnvironments: [
{
id: 28,
name: 'staging',
slug: 'staging',
external_url: 'https://staging.example.com',
state: 'available',
},
{
id: 29,
name: 'production',
slug: 'production',
external_url: 'https://production.example.com',
state: 'available',
},
],
};
import Api from '~/api';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import getInitialState from '~/ci_variable_list/store/state';
import * as actions from '~/ci_variable_list/store/actions';
import * as types from '~/ci_variable_list/store/mutation_types';
import mockData from '../services/mock_data';
import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
jest.mock('~/api.js');
jest.mock('~/flash.js');
describe('CI variable list store actions', () => {
let mock;
let state;
const mockVariable = {
environment_scope: '*',
id: 63,
key: 'test_var',
masked: false,
protected: false,
value: 'test_val',
variable_type: 'env_var',
_destory: true,
};
const payloadError = new Error('Request failed with status code 500');
beforeEach(() => {
mock = new MockAdapter(axios);
state = getInitialState();
state.endpoint = '/variables';
});
afterEach(() => {
mock.restore();
});
describe('toggleValues', () => {
const valuesHidden = false;
it('commits TOGGLE_VALUES mutation', () => {
testAction(actions.toggleValues, valuesHidden, {}, [
{
type: types.TOGGLE_VALUES,
payload: valuesHidden,
},
]);
});
});
describe('clearModal', () => {
it('commits CLEAR_MODAL mutation', () => {
testAction(actions.clearModal, {}, {}, [
{
type: types.CLEAR_MODAL,
},
]);
});
});
describe('resetEditing', () => {
it('commits RESET_EDITING mutation', () => {
testAction(
actions.resetEditing,
{},
{},
[
{
type: types.RESET_EDITING,
},
],
[{ type: 'fetchVariables' }],
);
});
});
describe('deleteVariable', () => {
it('dispatch correct actions on successful deleted variable', done => {
mock.onPatch(state.endpoint).reply(200);
testAction(
actions.deleteVariable,
mockVariable,
state,
[],
[
{ type: 'requestDeleteVariable' },
{ type: 'receiveDeleteVariableSuccess' },
{ type: 'fetchVariables' },
],
() => {
done();
},
);
});
it('should show flash error and set error in state on delete failure', done => {
mock.onPatch(state.endpoint).reply(500, '');
testAction(
actions.deleteVariable,
mockVariable,
state,
[],
[
{ type: 'requestDeleteVariable' },
{
type: 'receiveDeleteVariableError',
payload: payloadError,
},
],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
describe('updateVariable', () => {
it('dispatch correct actions on successful updated variable', done => {
mock.onPatch(state.endpoint).reply(200);
testAction(
actions.updateVariable,
mockVariable,
state,
[],
[
{ type: 'requestUpdateVariable' },
{ type: 'receiveUpdateVariableSuccess' },
{ type: 'fetchVariables' },
],
() => {
done();
},
);
});
it('should show flash error and set error in state on update failure', done => {
mock.onPatch(state.endpoint).reply(500, '');
testAction(
actions.updateVariable,
mockVariable,
state,
[],
[
{ type: 'requestUpdateVariable' },
{
type: 'receiveUpdateVariableError',
payload: payloadError,
},
],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
describe('addVariable', () => {
it('dispatch correct actions on successful added variable', done => {
mock.onPatch(state.endpoint).reply(200);
testAction(
actions.addVariable,
{},
state,
[],
[
{ type: 'requestAddVariable' },
{ type: 'receiveAddVariableSuccess' },
{ type: 'fetchVariables' },
],
() => {
done();
},
);
});
it('should show flash error and set error in state on add failure', done => {
mock.onPatch(state.endpoint).reply(500, '');
testAction(
actions.addVariable,
{},
state,
[],
[
{ type: 'requestAddVariable' },
{
type: 'receiveAddVariableError',
payload: payloadError,
},
],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
describe('fetchVariables', () => {
it('dispatch correct actions on fetchVariables', done => {
mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables });
testAction(
actions.fetchVariables,
{},
state,
[],
[
{ type: 'requestVariables' },
{
type: 'receiveVariablesSuccess',
payload: prepareDataForDisplay(mockData.mockVariables),
},
],
() => {
done();
},
);
});
it('should show flash error and set error in state on fetch variables failure', done => {
mock.onGet(state.endpoint).reply(500);
testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }], () => {
expect(createFlash).toHaveBeenCalledWith('There was an error fetching the variables.');
done();
});
});
});
describe('fetchEnvironments', () => {
it('dispatch correct actions on fetchEnvironments', done => {
Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments });
testAction(
actions.fetchEnvironments,
{},
state,
[],
[
{ type: 'requestEnvironments' },
{
type: 'receiveEnvironmentsSuccess',
payload: prepareEnvironments(mockData.mockEnvironments),
},
],
() => {
done();
},
);
});
it('should show flash error and set error in state on fetch environments failure', done => {
Api.environments = jest.fn().mockRejectedValue();
testAction(
actions.fetchEnvironments,
{},
state,
[],
[{ type: 'requestEnvironments' }],
() => {
expect(createFlash).toHaveBeenCalledWith(
'There was an error fetching the environments information.',
);
done();
},
);
});
});
});
import state from '~/ci_variable_list/store/state';
import mutations from '~/ci_variable_list/store/mutations';
import * as types from '~/ci_variable_list/store/mutation_types';
describe('CI variable list mutations', () => {
let stateCopy;
beforeEach(() => {
stateCopy = state();
});
describe('TOGGLE_VALUES', () => {
it('should toggle state', () => {
const valuesHidden = false;
mutations[types.TOGGLE_VALUES](stateCopy, valuesHidden);
expect(stateCopy.valuesHidden).toEqual(valuesHidden);
});
});
describe('VARIABLE_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);
expect(stateCopy.variableBeingEdited).toEqual(variableBeingEdited);
});
});
describe('RESET_EDITING', () => {
it('should reset variableBeingEdited to null', () => {
mutations[types.RESET_EDITING](stateCopy);
expect(stateCopy.variableBeingEdited).toEqual(null);
});
});
describe('CLEAR_MODAL', () => {
it('should clear modal state ', () => {
const modalState = {
variable_type: 'Variable',
key: '',
secret_value: '',
protected: false,
masked: false,
environment_scope: 'All environments',
};
mutations[types.CLEAR_MODAL](stateCopy);
expect(stateCopy.variable).toEqual(modalState);
});
});
});
import {
prepareDataForDisplay,
prepareEnvironments,
prepareDataForApi,
} from '~/ci_variable_list/store/utils';
import mockData from '../services/mock_data';
describe('CI variables store utils', () => {
it('prepares ci variables for display', () => {
expect(prepareDataForDisplay(mockData.mockVariablesApi)).toStrictEqual(
mockData.mockVariablesDisplay,
);
});
it('prepares single ci variable for api', () => {
expect(prepareDataForApi(mockData.mockVariablesDisplay[0])).toStrictEqual({
environment_scope: '*',
id: 113,
key: 'test_var',
masked: false,
protected: false,
value: 'test_val',
variable_type: 'env_var',
});
expect(prepareDataForApi(mockData.mockVariablesDisplay[1])).toStrictEqual({
environment_scope: '*',
id: 114,
key: 'test_var_2',
masked: false,
protected: false,
value: 'test_val_2',
variable_type: 'file',
});
});
it('prepares single ci variable for delete', () => {
expect(prepareDataForApi(mockData.mockVariablesDisplay[0], true)).toHaveProperty(
'_destroy',
true,
);
});
it('prepares environments for display', () => {
expect(prepareEnvironments(mockData.mockEnvironments)).toStrictEqual(['staging', 'production']);
});
});
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