diff options
Diffstat (limited to 'app/assets/javascripts/feature_flags/store/modules')
16 files changed, 753 insertions, 0 deletions
diff --git a/app/assets/javascripts/feature_flags/store/modules/edit/actions.js b/app/assets/javascripts/feature_flags/store/modules/edit/actions.js new file mode 100644 index 00000000000..351f36d8fa6 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/edit/actions.js @@ -0,0 +1,75 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { __ } from '~/locale'; +import { NEW_VERSION_FLAG } from '../../../constants'; +import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers'; + +/** + * Commits mutation to set the main endpoint + * @param {String} endpoint + */ +export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); + +/** + * Commits mutation to set the feature flag path. + * Used to redirect the user after form submission + * + * @param {String} path + */ +export const setPath = ({ commit }, path) => commit(types.SET_PATH, path); + +/** + * Handles the edition of a feature flag. + * + * Will dispatch `requestUpdateFeatureFlag` + * Serializes the params and makes a put request + * Dispatches an action acording to the request status. + * + * @param {Object} params + */ +export const updateFeatureFlag = ({ state, dispatch }, params) => { + dispatch('requestUpdateFeatureFlag'); + + axios + .put( + state.endpoint, + params.version === NEW_VERSION_FLAG + ? mapStrategiesToRails(params) + : mapFromScopesViewModel(params), + ) + .then(() => { + dispatch('receiveUpdateFeatureFlagSuccess'); + visitUrl(state.path); + }) + .catch(error => dispatch('receiveUpdateFeatureFlagError', error.response.data)); +}; + +export const requestUpdateFeatureFlag = ({ commit }) => commit(types.REQUEST_UPDATE_FEATURE_FLAG); +export const receiveUpdateFeatureFlagSuccess = ({ commit }) => + commit(types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS); +export const receiveUpdateFeatureFlagError = ({ commit }, error) => + commit(types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, error); + +/** + * Fetches the feature flag data for the edit form + */ +export const fetchFeatureFlag = ({ state, dispatch }) => { + dispatch('requestFeatureFlag'); + + axios + .get(state.endpoint) + .then(({ data }) => dispatch('receiveFeatureFlagSuccess', data)) + .catch(() => dispatch('receiveFeatureFlagError')); +}; + +export const requestFeatureFlag = ({ commit }) => commit(types.REQUEST_FEATURE_FLAG); +export const receiveFeatureFlagSuccess = ({ commit }, response) => + commit(types.RECEIVE_FEATURE_FLAG_SUCCESS, response); +export const receiveFeatureFlagError = ({ commit }) => { + commit(types.RECEIVE_FEATURE_FLAG_ERROR); + createFlash(__('Something went wrong on our end. Please try again!')); +}; + +export const toggleActive = ({ commit }, active) => commit(types.TOGGLE_ACTIVE, active); diff --git a/app/assets/javascripts/feature_flags/store/modules/edit/index.js b/app/assets/javascripts/feature_flags/store/modules/edit/index.js new file mode 100644 index 00000000000..665bb29a17e --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/edit/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +export default { + namespaced: true, + actions, + mutations, + state: state(), +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/edit/mutation_types.js b/app/assets/javascripts/feature_flags/store/modules/edit/mutation_types.js new file mode 100644 index 00000000000..b2715e501f4 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/edit/mutation_types.js @@ -0,0 +1,12 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; +export const SET_PATH = 'SET_PATH'; + +export const REQUEST_UPDATE_FEATURE_FLAG = 'REQUEST_UPDATE_FEATURE_FLAG'; +export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS'; +export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR'; + +export const REQUEST_FEATURE_FLAG = 'REQUEST_FEATURE_FLAG'; +export const RECEIVE_FEATURE_FLAG_SUCCESS = 'RECEIVE_FEATURE_FLAG_SUCCESS'; +export const RECEIVE_FEATURE_FLAG_ERROR = 'RECEIVE_FEATURE_FLAG_ERROR'; + +export const TOGGLE_ACTIVE = 'TOGGLE_ACTIVE'; diff --git a/app/assets/javascripts/feature_flags/store/modules/edit/mutations.js b/app/assets/javascripts/feature_flags/store/modules/edit/mutations.js new file mode 100644 index 00000000000..1d2721e037d --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/edit/mutations.js @@ -0,0 +1,45 @@ +import * as types from './mutation_types'; +import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers'; +import { LEGACY_FLAG } from '../../../constants'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.SET_PATH](state, path) { + state.path = path; + }, + [types.REQUEST_FEATURE_FLAG](state) { + state.isLoading = true; + }, + [types.RECEIVE_FEATURE_FLAG_SUCCESS](state, response) { + state.isLoading = false; + state.hasError = false; + + state.name = response.name; + state.description = response.description; + state.iid = response.iid; + state.active = response.active; + state.scopes = mapToScopesViewModel(response.scopes); + state.strategies = mapStrategiesToViewModel(response.strategies); + state.version = response.version || LEGACY_FLAG; + }, + [types.RECEIVE_FEATURE_FLAG_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, + [types.REQUEST_UPDATE_FEATURE_FLAG](state) { + state.isSendingRequest = true; + state.error = []; + }, + [types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](state) { + state.isSendingRequest = false; + }, + [types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](state, error) { + state.isSendingRequest = false; + state.error = error.message || []; + }, + [types.TOGGLE_ACTIVE](state, active) { + state.active = active; + }, +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/edit/state.js b/app/assets/javascripts/feature_flags/store/modules/edit/state.js new file mode 100644 index 00000000000..7de05b49482 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/edit/state.js @@ -0,0 +1,18 @@ +import { LEGACY_FLAG } from '../../../constants'; + +export default () => ({ + endpoint: null, + path: null, + isSendingRequest: false, + error: [], + + name: null, + description: null, + scopes: [], + isLoading: false, + hasError: false, + iid: null, + active: true, + strategies: [], + version: LEGACY_FLAG, +}); diff --git a/app/assets/javascripts/feature_flags/store/modules/helpers.js b/app/assets/javascripts/feature_flags/store/modules/helpers.js new file mode 100644 index 00000000000..5a8d7bc6af3 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/helpers.js @@ -0,0 +1,213 @@ +import { isEmpty, uniqueId, isString } from 'lodash'; +import { + ROLLOUT_STRATEGY_ALL_USERS, + ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + ROLLOUT_STRATEGY_USER_ID, + ROLLOUT_STRATEGY_GITLAB_USER_LIST, + INTERNAL_ID_PREFIX, + DEFAULT_PERCENT_ROLLOUT, + PERCENT_ROLLOUT_GROUP_ID, + fetchPercentageParams, + fetchUserIdParams, + LEGACY_FLAG, +} from '../../constants'; + +/** + * Converts raw scope objects fetched from the API into an array of scope + * objects that is easier/nicer to bind to in Vue. + * @param {Array} scopesFromRails An array of scope objects fetched from the API + */ +export const mapToScopesViewModel = scopesFromRails => + (scopesFromRails || []).map(s => { + const percentStrategy = (s.strategies || []).find( + strat => strat.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + ); + + const rolloutPercentage = fetchPercentageParams(percentStrategy) || DEFAULT_PERCENT_ROLLOUT; + + const userStrategy = (s.strategies || []).find( + strat => strat.name === ROLLOUT_STRATEGY_USER_ID, + ); + + const rolloutStrategy = + (percentStrategy && percentStrategy.name) || + (userStrategy && userStrategy.name) || + ROLLOUT_STRATEGY_ALL_USERS; + + const rolloutUserIds = (fetchUserIdParams(userStrategy) || '') + .split(',') + .filter(id => id) + .join(', '); + + return { + id: s.id, + environmentScope: s.environment_scope, + active: Boolean(s.active), + canUpdate: Boolean(s.can_update), + protected: Boolean(s.protected), + rolloutStrategy, + rolloutPercentage, + rolloutUserIds, + + // eslint-disable-next-line no-underscore-dangle + shouldBeDestroyed: Boolean(s._destroy), + shouldIncludeUserIds: rolloutUserIds.length > 0 && percentStrategy !== null, + }; + }); +/** + * Converts the parameters emitted by the Vue component into + * the shape that the Rails API expects. + * @param {Array} scopesFromVue An array of scope objects from the Vue component + */ +export const mapFromScopesViewModel = params => { + const scopes = (params.scopes || []).map(s => { + const parameters = {}; + if (s.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT) { + parameters.groupId = PERCENT_ROLLOUT_GROUP_ID; + parameters.percentage = s.rolloutPercentage; + } else if (s.rolloutStrategy === ROLLOUT_STRATEGY_USER_ID) { + parameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ','); + } + + const userIdParameters = {}; + + if (s.shouldIncludeUserIds && s.rolloutStrategy !== ROLLOUT_STRATEGY_USER_ID) { + userIdParameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ','); + } + + // Strip out any internal IDs + const id = isString(s.id) && s.id.startsWith(INTERNAL_ID_PREFIX) ? undefined : s.id; + + const strategies = [ + { + name: s.rolloutStrategy, + parameters, + }, + ]; + + if (!isEmpty(userIdParameters)) { + strategies.push({ name: ROLLOUT_STRATEGY_USER_ID, parameters: userIdParameters }); + } + + return { + id, + environment_scope: s.environmentScope, + active: s.active, + can_update: s.canUpdate, + protected: s.protected, + _destroy: s.shouldBeDestroyed, + strategies, + }; + }); + + const model = { + operations_feature_flag: { + name: params.name, + description: params.description, + active: params.active, + scopes_attributes: scopes, + version: LEGACY_FLAG, + }, + }; + + return model; +}; + +/** + * Creates a new feature flag environment scope object for use + * in a Vue component. An optional parameter can be passed to + * override the property values that are created by default. + * + * @param {Object} overrides An optional object whose + * property values will be used to override the default values. + * + */ +export const createNewEnvironmentScope = (overrides = {}, featureFlagPermissions = false) => { + const defaultScope = { + environmentScope: '', + active: false, + id: uniqueId(INTERNAL_ID_PREFIX), + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + rolloutUserIds: '', + }; + + const newScope = { + ...defaultScope, + ...overrides, + }; + + if (featureFlagPermissions) { + newScope.canUpdate = true; + newScope.protected = false; + } + + return newScope; +}; + +const mapStrategyScopesToRails = scopes => + scopes.length === 0 + ? [{ environment_scope: '*' }] + : scopes.map(s => ({ + id: s.id, + _destroy: s.shouldBeDestroyed, + environment_scope: s.environmentScope, + })); + +const mapStrategyScopesToView = scopes => + scopes.map(s => ({ + id: s.id, + // eslint-disable-next-line no-underscore-dangle + shouldBeDestroyed: Boolean(s._destroy), + environmentScope: s.environment_scope, + })); + +const mapStrategiesParametersToViewModel = params => { + if (params.userIds) { + return { ...params, userIds: params.userIds.split(',').join(', ') }; + } + return params; +}; + +export const mapStrategiesToViewModel = strategiesFromRails => + (strategiesFromRails || []).map(s => ({ + id: s.id, + name: s.name, + parameters: mapStrategiesParametersToViewModel(s.parameters), + userListId: s.user_list?.id, + // eslint-disable-next-line no-underscore-dangle + shouldBeDestroyed: Boolean(s._destroy), + scopes: mapStrategyScopesToView(s.scopes), + })); + +const mapStrategiesParametersToRails = params => { + if (params.userIds) { + return { ...params, userIds: params.userIds.split(', ').join(',') }; + } + return params; +}; + +const mapStrategyToRails = strategy => { + const mappedStrategy = { + id: strategy.id, + name: strategy.name, + _destroy: strategy.shouldBeDestroyed, + scopes_attributes: mapStrategyScopesToRails(strategy.scopes || []), + parameters: mapStrategiesParametersToRails(strategy.parameters), + }; + + if (strategy.name === ROLLOUT_STRATEGY_GITLAB_USER_LIST) { + mappedStrategy.user_list_id = strategy.userListId; + } + return mappedStrategy; +}; + +export const mapStrategiesToRails = params => ({ + operations_feature_flag: { + name: params.name, + description: params.description, + version: params.version, + active: params.active, + strategies_attributes: (params.strategies || []).map(mapStrategyToRails), + }, +}); diff --git a/app/assets/javascripts/feature_flags/store/modules/index/actions.js b/app/assets/javascripts/feature_flags/store/modules/index/actions.js new file mode 100644 index 00000000000..ed41dd34e4d --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/index/actions.js @@ -0,0 +1,107 @@ +import Api from '~/api'; +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; + +export const setFeatureFlagsEndpoint = ({ commit }, endpoint) => + commit(types.SET_FEATURE_FLAGS_ENDPOINT, endpoint); + +export const setFeatureFlagsOptions = ({ commit }, options) => + commit(types.SET_FEATURE_FLAGS_OPTIONS, options); + +export const setInstanceIdEndpoint = ({ commit }, endpoint) => + commit(types.SET_INSTANCE_ID_ENDPOINT, endpoint); + +export const setProjectId = ({ commit }, endpoint) => commit(types.SET_PROJECT_ID, endpoint); + +export const setInstanceId = ({ commit }, instanceId) => commit(types.SET_INSTANCE_ID, instanceId); + +export const fetchFeatureFlags = ({ state, dispatch }) => { + dispatch('requestFeatureFlags'); + + axios + .get(state.endpoint, { + params: state.options, + }) + .then(response => + dispatch('receiveFeatureFlagsSuccess', { + data: response.data || {}, + headers: response.headers, + }), + ) + .catch(() => dispatch('receiveFeatureFlagsError')); +}; + +export const requestFeatureFlags = ({ commit }) => commit(types.REQUEST_FEATURE_FLAGS); +export const receiveFeatureFlagsSuccess = ({ commit }, response) => + commit(types.RECEIVE_FEATURE_FLAGS_SUCCESS, response); +export const receiveFeatureFlagsError = ({ commit }) => commit(types.RECEIVE_FEATURE_FLAGS_ERROR); + +export const fetchUserLists = ({ state, dispatch }) => { + dispatch('requestUserLists'); + + return Api.fetchFeatureFlagUserLists(state.projectId, state.options.page) + .then(({ data, headers }) => dispatch('receiveUserListsSuccess', { data, headers })) + .catch(() => dispatch('receiveUserListsError')); +}; + +export const requestUserLists = ({ commit }) => commit(types.REQUEST_USER_LISTS); +export const receiveUserListsSuccess = ({ commit }, response) => + commit(types.RECEIVE_USER_LISTS_SUCCESS, response); +export const receiveUserListsError = ({ commit }) => commit(types.RECEIVE_USER_LISTS_ERROR); + +export const toggleFeatureFlag = ({ dispatch }, flag) => { + dispatch('updateFeatureFlag', flag); + + axios + .put(flag.update_path, { + operations_feature_flag: flag, + }) + .then(response => dispatch('receiveUpdateFeatureFlagSuccess', response.data)) + .catch(() => dispatch('receiveUpdateFeatureFlagError', flag.id)); +}; + +export const updateFeatureFlag = ({ commit }, flag) => commit(types.UPDATE_FEATURE_FLAG, flag); + +export const receiveUpdateFeatureFlagSuccess = ({ commit }, data) => + commit(types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS, data); +export const receiveUpdateFeatureFlagError = ({ commit }, id) => + commit(types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, id); + +export const deleteUserList = ({ state, dispatch }, list) => { + dispatch('requestDeleteUserList', list); + + return Api.deleteFeatureFlagUserList(state.projectId, list.iid) + .then(() => dispatch('fetchUserLists')) + .catch(error => + dispatch('receiveDeleteUserListError', { + list, + error: error?.response?.data ?? error, + }), + ); +}; + +export const requestDeleteUserList = ({ commit }, list) => + commit(types.REQUEST_DELETE_USER_LIST, list); + +export const receiveDeleteUserListError = ({ commit }, { error, list }) => { + commit(types.RECEIVE_DELETE_USER_LIST_ERROR, { error, list }); +}; + +export const rotateInstanceId = ({ state, dispatch }) => { + dispatch('requestRotateInstanceId'); + + axios + .post(state.rotateEndpoint) + .then(({ data = {}, headers }) => dispatch('receiveRotateInstanceIdSuccess', { data, headers })) + .catch(() => dispatch('receiveRotateInstanceIdError')); +}; + +export const requestRotateInstanceId = ({ commit }) => commit(types.REQUEST_ROTATE_INSTANCE_ID); +export const receiveRotateInstanceIdSuccess = ({ commit }, response) => + commit(types.RECEIVE_ROTATE_INSTANCE_ID_SUCCESS, response); +export const receiveRotateInstanceIdError = ({ commit }) => + commit(types.RECEIVE_ROTATE_INSTANCE_ID_ERROR); + +export const clearAlert = ({ commit }, index) => { + commit(types.RECEIVE_CLEAR_ALERT, index); +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/index/index.js b/app/assets/javascripts/feature_flags/store/modules/index/index.js new file mode 100644 index 00000000000..665bb29a17e --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/index/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +export default { + namespaced: true, + actions, + mutations, + state: state(), +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/index/mutation_types.js b/app/assets/javascripts/feature_flags/store/modules/index/mutation_types.js new file mode 100644 index 00000000000..4a4bd13c945 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/index/mutation_types.js @@ -0,0 +1,26 @@ +export const SET_FEATURE_FLAGS_ENDPOINT = 'SET_FEATURE_FLAGS_ENDPOINT'; +export const SET_FEATURE_FLAGS_OPTIONS = 'SET_FEATURE_FLAGS_OPTIONS'; +export const SET_INSTANCE_ID_ENDPOINT = 'SET_INSTANCE_ID_ENDPOINT'; +export const SET_INSTANCE_ID = 'SET_INSTANCE_ID'; +export const SET_PROJECT_ID = 'SET_PROJECT_ID'; + +export const REQUEST_FEATURE_FLAGS = 'REQUEST_FEATURE_FLAGS'; +export const RECEIVE_FEATURE_FLAGS_SUCCESS = 'RECEIVE_FEATURE_FLAGS_SUCCESS'; +export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR'; + +export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS'; +export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS'; +export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR'; + +export const REQUEST_DELETE_USER_LIST = 'REQUEST_DELETE_USER_LIST'; +export const RECEIVE_DELETE_USER_LIST_ERROR = 'RECEIVE_DELETE_USER_LIST_ERROR'; + +export const UPDATE_FEATURE_FLAG = 'UPDATE_FEATURE_FLAG'; +export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS'; +export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR'; + +export const REQUEST_ROTATE_INSTANCE_ID = 'REQUEST_ROTATE_INSTANCE_ID'; +export const RECEIVE_ROTATE_INSTANCE_ID_SUCCESS = 'RECEIVE_ROTATE_INSTANCE_ID_SUCCESS'; +export const RECEIVE_ROTATE_INSTANCE_ID_ERROR = 'RECEIVE_ROTATE_INSTANCE_ID_ERROR'; + +export const RECEIVE_CLEAR_ALERT = 'RECEIVE_CLEAR_ALERT'; diff --git a/app/assets/javascripts/feature_flags/store/modules/index/mutations.js b/app/assets/javascripts/feature_flags/store/modules/index/mutations.js new file mode 100644 index 00000000000..948786a3533 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/index/mutations.js @@ -0,0 +1,125 @@ +import Vue from 'vue'; +import * as types from './mutation_types'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../../constants'; +import { mapToScopesViewModel } from '../helpers'; + +const mapFlag = flag => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) }); + +const updateFlag = (state, flag) => { + const index = state[FEATURE_FLAG_SCOPE].findIndex(({ id }) => id === flag.id); + Vue.set(state[FEATURE_FLAG_SCOPE], index, flag); +}; + +const createPaginationInfo = (state, headers) => { + let paginationInfo; + if (Object.keys(headers).length) { + const normalizedHeaders = normalizeHeaders(headers); + paginationInfo = parseIntPagination(normalizedHeaders); + } else { + paginationInfo = headers; + } + return paginationInfo; +}; + +export default { + [types.SET_FEATURE_FLAGS_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.SET_FEATURE_FLAGS_OPTIONS](state, options = {}) { + state.options = options; + }, + [types.SET_INSTANCE_ID_ENDPOINT](state, endpoint) { + state.rotateEndpoint = endpoint; + }, + [types.SET_INSTANCE_ID](state, instance) { + state.instanceId = instance; + }, + [types.SET_PROJECT_ID](state, project) { + state.projectId = project; + }, + [types.REQUEST_FEATURE_FLAGS](state) { + state.isLoading = true; + }, + [types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) { + state.isLoading = false; + state.hasError = false; + state[FEATURE_FLAG_SCOPE] = (response.data.feature_flags || []).map(mapFlag); + + const paginationInfo = createPaginationInfo(state, response.headers); + state.count = { + ...state.count, + [FEATURE_FLAG_SCOPE]: paginationInfo?.total ?? state[FEATURE_FLAG_SCOPE].length, + }; + state.pageInfo = { + ...state.pageInfo, + [FEATURE_FLAG_SCOPE]: paginationInfo, + }; + }, + [types.RECEIVE_FEATURE_FLAGS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, + [types.REQUEST_USER_LISTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_USER_LISTS_SUCCESS](state, response) { + state.isLoading = false; + state.hasError = false; + state[USER_LIST_SCOPE] = response.data || []; + + const paginationInfo = createPaginationInfo(state, response.headers); + state.count = { + ...state.count, + [USER_LIST_SCOPE]: paginationInfo?.total ?? state[USER_LIST_SCOPE].length, + }; + state.pageInfo = { + ...state.pageInfo, + [USER_LIST_SCOPE]: paginationInfo, + }; + }, + [types.RECEIVE_USER_LISTS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, + [types.REQUEST_ROTATE_INSTANCE_ID](state) { + state.isRotating = true; + state.hasRotateError = false; + }, + [types.RECEIVE_ROTATE_INSTANCE_ID_SUCCESS]( + state, + { + data: { token }, + }, + ) { + state.isRotating = false; + state.instanceId = token; + state.hasRotateError = false; + }, + [types.RECEIVE_ROTATE_INSTANCE_ID_ERROR](state) { + state.isRotating = false; + state.hasRotateError = true; + }, + [types.UPDATE_FEATURE_FLAG](state, flag) { + updateFlag(state, flag); + }, + [types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](state, data) { + updateFlag(state, mapFlag(data)); + }, + [types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](state, i) { + const flag = state[FEATURE_FLAG_SCOPE].find(({ id }) => i === id); + updateFlag(state, { ...flag, active: !flag.active }); + }, + [types.REQUEST_DELETE_USER_LIST](state, list) { + state.userLists = state.userLists.filter(l => l !== list); + }, + [types.RECEIVE_DELETE_USER_LIST_ERROR](state, { error, list }) { + state.isLoading = false; + state.hasError = false; + state.alerts = [].concat(error.message); + state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid); + }, + [types.RECEIVE_CLEAR_ALERT](state, index) { + state.alerts.splice(index, 1); + }, +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/index/state.js b/app/assets/javascripts/feature_flags/store/modules/index/state.js new file mode 100644 index 00000000000..443a12d485d --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/index/state.js @@ -0,0 +1,18 @@ +import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../../constants'; + +export default () => ({ + [FEATURE_FLAG_SCOPE]: [], + [USER_LIST_SCOPE]: [], + alerts: [], + count: {}, + pageInfo: { [FEATURE_FLAG_SCOPE]: {}, [USER_LIST_SCOPE]: {} }, + isLoading: true, + hasError: false, + endpoint: null, + rotateEndpoint: null, + instanceId: '', + isRotating: false, + hasRotateError: false, + options: {}, + projectId: '', +}); diff --git a/app/assets/javascripts/feature_flags/store/modules/new/actions.js b/app/assets/javascripts/feature_flags/store/modules/new/actions.js new file mode 100644 index 00000000000..d2159d55d53 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/new/actions.js @@ -0,0 +1,51 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { NEW_VERSION_FLAG } from '../../../constants'; +import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers'; + +/** + * Commits mutation to set the main endpoint + * @param {String} endpoint + */ +export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); + +/** + * Commits mutation to set the feature flag path. + * Used to redirect the user after form submission + * + * @param {String} path + */ +export const setPath = ({ commit }, path) => commit(types.SET_PATH, path); + +/** + * Handles the creation of a new feature flag. + * + * Will dispatch `requestCreateFeatureFlag` + * Serializes the params and makes a post request + * Dispatches an action acording to the request status. + * + * @param {Object} params + */ +export const createFeatureFlag = ({ state, dispatch }, params) => { + dispatch('requestCreateFeatureFlag'); + + return axios + .post( + state.endpoint, + params.version === NEW_VERSION_FLAG + ? mapStrategiesToRails(params) + : mapFromScopesViewModel(params), + ) + .then(() => { + dispatch('receiveCreateFeatureFlagSuccess'); + visitUrl(state.path); + }) + .catch(error => dispatch('receiveCreateFeatureFlagError', error.response.data)); +}; + +export const requestCreateFeatureFlag = ({ commit }) => commit(types.REQUEST_CREATE_FEATURE_FLAG); +export const receiveCreateFeatureFlagSuccess = ({ commit }) => + commit(types.RECEIVE_CREATE_FEATURE_FLAG_SUCCESS); +export const receiveCreateFeatureFlagError = ({ commit }, error) => + commit(types.RECEIVE_CREATE_FEATURE_FLAG_ERROR, error); diff --git a/app/assets/javascripts/feature_flags/store/modules/new/index.js b/app/assets/javascripts/feature_flags/store/modules/new/index.js new file mode 100644 index 00000000000..665bb29a17e --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/new/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +export default { + namespaced: true, + actions, + mutations, + state: state(), +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/new/mutation_types.js b/app/assets/javascripts/feature_flags/store/modules/new/mutation_types.js new file mode 100644 index 00000000000..317f3689dfd --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/new/mutation_types.js @@ -0,0 +1,6 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; +export const SET_PATH = 'SET_PATH'; + +export const REQUEST_CREATE_FEATURE_FLAG = 'REQUEST_CREATE_FEATURE_FLAG'; +export const RECEIVE_CREATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_CREATE_FEATURE_FLAG_SUCCESS'; +export const RECEIVE_CREATE_FEATURE_FLAG_ERROR = 'RECEIVE_CREATE_FEATURE_FLAG_ERROR'; diff --git a/app/assets/javascripts/feature_flags/store/modules/new/mutations.js b/app/assets/javascripts/feature_flags/store/modules/new/mutations.js new file mode 100644 index 00000000000..06e467c04f1 --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/new/mutations.js @@ -0,0 +1,21 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.SET_PATH](state, path) { + state.path = path; + }, + [types.REQUEST_CREATE_FEATURE_FLAG](state) { + state.isSendingRequest = true; + state.error = []; + }, + [types.RECEIVE_CREATE_FEATURE_FLAG_SUCCESS](state) { + state.isSendingRequest = false; + }, + [types.RECEIVE_CREATE_FEATURE_FLAG_ERROR](state, error) { + state.isSendingRequest = false; + state.error = error.message || []; + }, +}; diff --git a/app/assets/javascripts/feature_flags/store/modules/new/state.js b/app/assets/javascripts/feature_flags/store/modules/new/state.js new file mode 100644 index 00000000000..6f9263dbb2a --- /dev/null +++ b/app/assets/javascripts/feature_flags/store/modules/new/state.js @@ -0,0 +1,6 @@ +export default () => ({ + endpoint: null, + path: null, + isSendingRequest: false, + error: [], +}); |