Commit f485cc07 authored by Mark Florian's avatar Mark Florian

Merge branch 'feature-flag-user-list-strategy-fe' into 'master'

Add User List as a Choosable Strategy

See merge request gitlab-org/gitlab!32976
parents 5b65045d 1b96ee31
...@@ -308,43 +308,36 @@ export default { ...@@ -308,43 +308,36 @@ export default {
return axios.put(`${url}/${node.id}`, node); return axios.put(`${url}/${node.id}`, node);
}, },
fetchFeatureFlagUserLists(version, id) { fetchFeatureFlagUserLists(id) {
const url = Api.buildUrl(this.featureFlagUserLists) const url = Api.buildUrl(this.featureFlagUserLists).replace(':id', id);
.replace(':version', version)
.replace(':id', id);
return axios.get(url); return axios.get(url);
}, },
createFeatureFlagUserList(version, id, list) { createFeatureFlagUserList(id, list) {
const url = Api.buildUrl(this.featureFlagUserLists) const url = Api.buildUrl(this.featureFlagUserLists).replace(':id', id);
.replace(':version', version)
.replace(':id', id);
return axios.post(url, list); return axios.post(url, list);
}, },
fetchFeatureFlagUserList(version, id, listIid) { fetchFeatureFlagUserList(id, listIid) {
const url = Api.buildUrl(this.featureFlagUserList) const url = Api.buildUrl(this.featureFlagUserList)
.replace(':version', version)
.replace(':id', id) .replace(':id', id)
.replace(':list_iid', listIid); .replace(':list_iid', listIid);
return axios.get(url); return axios.get(url);
}, },
updateFeatureFlagUserList(version, id, list) { updateFeatureFlagUserList(id, list) {
const url = Api.buildUrl(this.featureFlagUserList) const url = Api.buildUrl(this.featureFlagUserList)
.replace(':version', version)
.replace(':id', id) .replace(':id', id)
.replace(':list_iid', list.iid); .replace(':list_iid', list.iid);
return axios.put(url, list); return axios.put(url, list);
}, },
deleteFeatureFlagUserList(version, id, listIid) { deleteFeatureFlagUserList(id, listIid) {
const url = Api.buildUrl(this.featureFlagUserList) const url = Api.buildUrl(this.featureFlagUserList)
.replace(':version', version)
.replace(':id', id) .replace(':id', id)
.replace(':list_iid', listIid); .replace(':list_iid', listIid);
......
...@@ -31,6 +31,10 @@ export default { ...@@ -31,6 +31,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: String,
required: true,
},
}, },
translations: { translations: {
legacyFlagAlert: s__( legacyFlagAlert: s__(
...@@ -101,6 +105,7 @@ export default { ...@@ -101,6 +105,7 @@ export default {
<feature-flag-form <feature-flag-form
:name="name" :name="name"
:description="description" :description="description"
:project-id="projectId"
:scopes="scopes" :scopes="scopes"
:strategies="strategies" :strategies="strategies"
:cancel-path="path" :cancel-path="path"
......
...@@ -10,7 +10,9 @@ import { ...@@ -10,7 +10,9 @@ import {
GlFormCheckbox, GlFormCheckbox,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Api from 'ee/api';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import flash, { FLASH_TYPES } from '~/flash';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ToggleButton from '~/vue_shared/components/toggle_button.vue'; import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -60,6 +62,10 @@ export default { ...@@ -60,6 +62,10 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
projectId: {
type: String,
required: true,
},
scopes: { scopes: {
type: Array, type: Array,
required: false, required: false,
...@@ -118,6 +124,7 @@ export default { ...@@ -118,6 +124,7 @@ export default {
formStrategies: cloneDeep(this.strategies), formStrategies: cloneDeep(this.strategies),
newScope: '', newScope: '',
userLists: [],
}; };
}, },
computed: { computed: {
...@@ -141,6 +148,17 @@ export default { ...@@ -141,6 +148,17 @@ export default {
return this.formStrategies.length > 1; return this.formStrategies.length > 1;
}, },
}, },
mounted() {
if (this.supportsStrategies) {
Api.fetchFeatureFlagUserLists(this.projectId)
.then(({ data }) => {
this.userLists = data;
})
.catch(() => {
flash(s__('FeatureFlags|There was an error retrieving user lists'), FLASH_TYPES.WARNING);
});
}
},
methods: { methods: {
addStrategy() { addStrategy() {
this.formStrategies.push({ name: '', parameters: {}, scopes: [] }); this.formStrategies.push({ name: '', parameters: {}, scopes: [] });
...@@ -252,13 +270,8 @@ export default { ...@@ -252,13 +270,8 @@ export default {
scope.rolloutUserIds.length > 0 && scope.rolloutUserIds.length > 0 &&
scope.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT; scope.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
}, },
onFormStrategyChange({ id, name, parameters, scopes }, index) { onFormStrategyChange(strategy, index) {
Object.assign(this.filteredStrategies[index], { Object.assign(this.filteredStrategies[index], strategy);
id,
name,
parameters,
scopes,
});
}, },
}, },
}; };
...@@ -313,6 +326,7 @@ export default { ...@@ -313,6 +326,7 @@ export default {
:index="index" :index="index"
:endpoint="environmentsEndpoint" :endpoint="environmentsEndpoint"
:can-delete="canDeleteStrategy" :can-delete="canDeleteStrategy"
:user-lists="userLists"
@change="onFormStrategyChange($event, index)" @change="onFormStrategyChange($event, index)"
@delete="deleteStrategy(strategy)" @delete="deleteStrategy(strategy)"
/> />
......
...@@ -30,6 +30,10 @@ export default { ...@@ -30,6 +30,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: String,
required: true,
},
}, },
translations: { translations: {
newFlagAlert: NEW_FLAG_ALERT, newFlagAlert: NEW_FLAG_ALERT,
...@@ -78,6 +82,7 @@ export default { ...@@ -78,6 +82,7 @@ export default {
</div> </div>
<feature-flag-form <feature-flag-form
:project-id="projectId"
:cancel-path="path" :cancel-path="path"
:submit-text="s__('FeatureFlags|Create feature flag')" :submit-text="s__('FeatureFlags|Create feature flag')"
:scopes="scopes" :scopes="scopes"
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '../constants'; } from '../constants';
import NewEnvironmentsDropdown from './new_environments_dropdown.vue'; import NewEnvironmentsDropdown from './new_environments_dropdown.vue';
...@@ -53,10 +54,16 @@ export default { ...@@ -53,10 +54,16 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
userLists: {
type: Array,
required: false,
default: () => [],
},
}, },
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
translations: { translations: {
allEnvironments: __('All environments'), allEnvironments: __('All environments'),
...@@ -69,6 +76,9 @@ export default { ...@@ -69,6 +76,9 @@ export default {
rolloutPercentageLabel: s__('FeatureFlag|Percentage'), rolloutPercentageLabel: s__('FeatureFlag|Percentage'),
rolloutUserIdsDescription: __('Enter one or more user ID separated by commas'), rolloutUserIdsDescription: __('Enter one or more user ID separated by commas'),
rolloutUserIdsLabel: s__('FeatureFlag|User IDs'), rolloutUserIdsLabel: s__('FeatureFlag|User IDs'),
rolloutUserListLabel: s__('FeatureFlag|List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
strategyTypeDescription: __('Select strategy activation method'), strategyTypeDescription: __('Select strategy activation method'),
strategyTypeLabel: s__('FeatureFlag|Type'), strategyTypeLabel: s__('FeatureFlag|Type'),
}, },
...@@ -83,6 +93,8 @@ export default { ...@@ -83,6 +93,8 @@ export default {
: '', : '',
formUserIds: formUserIds:
this.strategy.name === ROLLOUT_STRATEGY_USER_ID ? this.strategy.parameters.userIds : '', this.strategy.name === ROLLOUT_STRATEGY_USER_ID ? this.strategy.parameters.userIds : '',
formUserListId:
this.strategy.name === ROLLOUT_STRATEGY_GITLAB_USER_LIST ? this.strategy.userListId : '',
strategies: [ strategies: [
{ {
value: ROLLOUT_STRATEGY_ALL_USERS, value: ROLLOUT_STRATEGY_ALL_USERS,
...@@ -96,6 +108,10 @@ export default { ...@@ -96,6 +108,10 @@ export default {
value: ROLLOUT_STRATEGY_USER_ID, value: ROLLOUT_STRATEGY_USER_ID,
text: __('User IDs'), text: __('User IDs'),
}, },
{
value: ROLLOUT_STRATEGY_GITLAB_USER_LIST,
text: __('List'),
},
], ],
}; };
}, },
...@@ -109,6 +125,9 @@ export default { ...@@ -109,6 +125,9 @@ export default {
strategyUserIdsId() { strategyUserIdsId() {
return `strategy-user-ids-${this.index}`; return `strategy-user-ids-${this.index}`;
}, },
strategyUserListId() {
return `strategy-user-list-${this.index}`;
},
environmentsDropdownId() { environmentsDropdownId() {
return `environments-dropdown-${this.index}`; return `environments-dropdown-${this.index}`;
}, },
...@@ -118,6 +137,9 @@ export default { ...@@ -118,6 +137,9 @@ export default {
isUserWithId() { isUserWithId() {
return this.isStrategyType(ROLLOUT_STRATEGY_USER_ID); return this.isStrategyType(ROLLOUT_STRATEGY_USER_ID);
}, },
isUserList() {
return this.isStrategyType(ROLLOUT_STRATEGY_GITLAB_USER_LIST);
},
appliesToAllEnvironments() { appliesToAllEnvironments() {
return ( return (
this.filteredEnvironments.length === 0 || this.filteredEnvironments.length === 0 ||
...@@ -128,6 +150,12 @@ export default { ...@@ -128,6 +150,12 @@ export default {
filteredEnvironments() { filteredEnvironments() {
return this.environments.filter(e => !e.shouldBeDestroyed); return this.environments.filter(e => !e.shouldBeDestroyed);
}, },
userListOptions() {
return this.userLists.map(({ name, id }) => ({ value: id, text: name }));
},
hasUserLists() {
return this.userListOptions.length > 0;
},
}, },
methods: { methods: {
addEnvironment(environment) { addEnvironment(environment) {
...@@ -140,6 +168,10 @@ export default { ...@@ -140,6 +168,10 @@ export default {
}, },
onStrategyChange() { onStrategyChange() {
const parameters = {}; const parameters = {};
const strategy = {
...this.formStrategy,
scopes: this.environments,
};
switch (this.formStrategy.name) { switch (this.formStrategy.name) {
case ROLLOUT_STRATEGY_PERCENT_ROLLOUT: case ROLLOUT_STRATEGY_PERCENT_ROLLOUT:
parameters.percentage = this.formPercentage; parameters.percentage = this.formPercentage;
...@@ -148,13 +180,15 @@ export default { ...@@ -148,13 +180,15 @@ export default {
case ROLLOUT_STRATEGY_USER_ID: case ROLLOUT_STRATEGY_USER_ID:
parameters.userIds = this.formUserIds; parameters.userIds = this.formUserIds;
break; break;
case ROLLOUT_STRATEGY_GITLAB_USER_LIST:
strategy.userListId = this.formUserListId;
break;
default: default:
break; break;
} }
this.$emit('change', { this.$emit('change', {
...this.formStrategy, ...strategy,
parameters, parameters,
scopes: this.environments,
}); });
}, },
removeScope(environment) { removeScope(environment) {
...@@ -189,7 +223,7 @@ export default { ...@@ -189,7 +223,7 @@ export default {
</gl-form-group> </gl-form-group>
</div> </div>
<div> <div data-testid="strategy">
<gl-form-group <gl-form-group
v-if="isPercentRollout" v-if="isPercentRollout"
:label="$options.translations.rolloutPercentageLabel" :label="$options.translations.rolloutPercentageLabel"
...@@ -221,6 +255,21 @@ export default { ...@@ -221,6 +255,21 @@ export default {
@input="onStrategyChange" @input="onStrategyChange"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group
v-if="isUserList"
:state="hasUserLists"
:invalid-feedback="$options.translations.rolloutUserListNoListError"
:label="$options.translations.rolloutUserListLabel"
:description="$options.translations.rolloutUserListDescription"
:label-for="strategyUserListId"
>
<gl-form-select
:id="strategyUserListId"
v-model="formUserListId"
:options="userListOptions"
@change="onStrategyChange"
/>
</gl-form-group>
</div> </div>
<div class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 ml-auto"> <div class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 ml-auto">
......
...@@ -4,6 +4,7 @@ import { s__ } from '~/locale'; ...@@ -4,6 +4,7 @@ import { s__ } from '~/locale';
export const ROLLOUT_STRATEGY_ALL_USERS = 'default'; export const ROLLOUT_STRATEGY_ALL_USERS = 'default';
export const ROLLOUT_STRATEGY_PERCENT_ROLLOUT = 'gradualRolloutUserId'; export const ROLLOUT_STRATEGY_PERCENT_ROLLOUT = 'gradualRolloutUserId';
export const ROLLOUT_STRATEGY_USER_ID = 'userWithId'; export const ROLLOUT_STRATEGY_USER_ID = 'userWithId';
export const ROLLOUT_STRATEGY_GITLAB_USER_LIST = 'gitlabUserList';
export const PERCENT_ROLLOUT_GROUP_ID = 'default'; export const PERCENT_ROLLOUT_GROUP_ID = 'default';
......
...@@ -15,6 +15,7 @@ export default () => { ...@@ -15,6 +15,7 @@ export default () => {
endpoint: el.dataset.endpoint, endpoint: el.dataset.endpoint,
path: el.dataset.featureFlagsPath, path: el.dataset.featureFlagsPath,
environmentsEndpoint: el.dataset.environmentsEndpoint, environmentsEndpoint: el.dataset.environmentsEndpoint,
projectId: el.dataset.projectId,
}, },
}); });
}, },
......
...@@ -15,6 +15,7 @@ export default () => { ...@@ -15,6 +15,7 @@ export default () => {
endpoint: el.dataset.endpoint, endpoint: el.dataset.endpoint,
path: el.dataset.featureFlagsPath, path: el.dataset.featureFlagsPath,
environmentsEndpoint: el.dataset.environmentsEndpoint, environmentsEndpoint: el.dataset.environmentsEndpoint,
projectId: el.dataset.projectId,
}, },
}); });
}, },
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
INTERNAL_ID_PREFIX, INTERNAL_ID_PREFIX,
DEFAULT_PERCENT_ROLLOUT, DEFAULT_PERCENT_ROLLOUT,
PERCENT_ROLLOUT_GROUP_ID, PERCENT_ROLLOUT_GROUP_ID,
...@@ -173,6 +174,7 @@ export const mapStrategiesToViewModel = strategiesFromRails => ...@@ -173,6 +174,7 @@ export const mapStrategiesToViewModel = strategiesFromRails =>
id: s.id, id: s.id,
name: s.name, name: s.name,
parameters: mapStrategiesParametersToViewModel(s.parameters), parameters: mapStrategiesParametersToViewModel(s.parameters),
userListId: s.user_list?.id,
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
shouldBeDestroyed: Boolean(s._destroy), shouldBeDestroyed: Boolean(s._destroy),
scopes: mapStrategyScopesToView(s.scopes), scopes: mapStrategyScopesToView(s.scopes),
...@@ -185,18 +187,27 @@ const mapStrategiesParametersToRails = params => { ...@@ -185,18 +187,27 @@ const mapStrategiesParametersToRails = params => {
return params; 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 => ({ export const mapStrategiesToRails = params => ({
operations_feature_flag: { operations_feature_flag: {
name: params.name, name: params.name,
description: params.description, description: params.description,
version: params.version, version: params.version,
active: params.active, active: params.active,
strategies_attributes: (params.strategies || []).map(s => ({ strategies_attributes: (params.strategies || []).map(mapStrategyToRails),
id: s.id,
name: s.name,
parameters: mapStrategiesParametersToRails(s.parameters),
_destroy: s.shouldBeDestroyed,
scopes_attributes: mapStrategyScopesToRails(s.scopes || []),
})),
}, },
}); });
...@@ -2,4 +2,7 @@ ...@@ -2,4 +2,7 @@
- breadcrumb_title @feature_flag.name - breadcrumb_title @feature_flag.name
- page_title s_('FeatureFlags|Edit Feature Flag') - page_title s_('FeatureFlags|Edit Feature Flag')
#js-edit-feature-flag{ data: { endpoint: project_feature_flag_path(@project, @feature_flag), feature_flags_path: project_feature_flags_path(@project), environments_endpoint: search_project_environments_path(@project, format: :json)} } #js-edit-feature-flag{ data: { endpoint: project_feature_flag_path(@project, @feature_flag),
project_id: @project.id,
feature_flags_path: project_feature_flags_path(@project),
environments_endpoint: search_project_environments_path(@project, format: :json)} }
...@@ -3,4 +3,7 @@ ...@@ -3,4 +3,7 @@
- breadcrumb_title s_('FeatureFlags|New') - breadcrumb_title s_('FeatureFlags|New')
- page_title s_('FeatureFlags|New Feature Flag') - page_title s_('FeatureFlags|New Feature Flag')
#js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json), feature_flags_path: project_feature_flags_path(@project), environments_endpoint: search_project_environments_path(@project, format: :json) } } #js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json),
feature_flags_path: project_feature_flags_path(@project),
environments_endpoint: search_project_environments_path(@project, format: :json),
project_id: @project.id } }
...@@ -788,7 +788,7 @@ describe('Api', () => { ...@@ -788,7 +788,7 @@ describe('Api', () => {
it('GETs the right url', () => { it('GETs the right url', () => {
mock.onGet(expectedUrl).replyOnce(200, []); mock.onGet(expectedUrl).replyOnce(200, []);
return Api.fetchFeatureFlagUserLists(dummyApiVersion, projectId).then(({ data }) => { return Api.fetchFeatureFlagUserLists(projectId).then(({ data }) => {
expect(data).toEqual([]); expect(data).toEqual([]);
}); });
}); });
...@@ -802,11 +802,9 @@ describe('Api', () => { ...@@ -802,11 +802,9 @@ describe('Api', () => {
}; };
mock.onPost(expectedUrl, mockUserListData).replyOnce(200, mockUserList); mock.onPost(expectedUrl, mockUserListData).replyOnce(200, mockUserList);
return Api.createFeatureFlagUserList(dummyApiVersion, projectId, mockUserListData).then( return Api.createFeatureFlagUserList(projectId, mockUserListData).then(({ data }) => {
({ data }) => {
expect(data).toEqual(mockUserList); expect(data).toEqual(mockUserList);
}, });
);
}); });
}); });
...@@ -814,7 +812,7 @@ describe('Api', () => { ...@@ -814,7 +812,7 @@ describe('Api', () => {
it('GETs the right url', () => { it('GETs the right url', () => {
mock.onGet(`${expectedUrl}/1`).replyOnce(200, mockUserList); mock.onGet(`${expectedUrl}/1`).replyOnce(200, mockUserList);
return Api.fetchFeatureFlagUserList(dummyApiVersion, projectId, 1).then(({ data }) => { return Api.fetchFeatureFlagUserList(projectId, 1).then(({ data }) => {
expect(data).toEqual(mockUserList); expect(data).toEqual(mockUserList);
}); });
}); });
...@@ -824,7 +822,7 @@ describe('Api', () => { ...@@ -824,7 +822,7 @@ describe('Api', () => {
it('PUTs the right url', () => { it('PUTs the right url', () => {
mock.onPut(`${expectedUrl}/1`).replyOnce(200, { ...mockUserList, user_xids: '5' }); mock.onPut(`${expectedUrl}/1`).replyOnce(200, { ...mockUserList, user_xids: '5' });
return Api.updateFeatureFlagUserList(dummyApiVersion, projectId, { return Api.updateFeatureFlagUserList(projectId, {
...mockUserList, ...mockUserList,
user_xids: '5', user_xids: '5',
}).then(({ data }) => { }).then(({ data }) => {
...@@ -837,7 +835,7 @@ describe('Api', () => { ...@@ -837,7 +835,7 @@ describe('Api', () => {
it('DELETEs the right url', () => { it('DELETEs the right url', () => {
mock.onDelete(`${expectedUrl}/1`).replyOnce(200, 'deleted'); mock.onDelete(`${expectedUrl}/1`).replyOnce(200, 'deleted');
return Api.deleteFeatureFlagUserList(dummyApiVersion, projectId, 1).then(({ data }) => { return Api.deleteFeatureFlagUserList(projectId, 1).then(({ data }) => {
expect(data).toBe('deleted'); expect(data).toBe('deleted');
}); });
}); });
......
...@@ -33,6 +33,7 @@ describe('Edit feature flag form', () => { ...@@ -33,6 +33,7 @@ describe('Edit feature flag form', () => {
endpoint: `${TEST_HOST}/feature_flags.json`, endpoint: `${TEST_HOST}/feature_flags.json`,
path: '/feature_flags', path: '/feature_flags',
environmentsEndpoint: 'environments.json', environmentsEndpoint: 'environments.json',
projectId: '8',
}, },
store, store,
provide: { provide: {
......
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlFormTextarea, GlFormCheckbox, GlDeprecatedButton } from '@gitlab/ui'; import { GlFormTextarea, GlFormCheckbox, GlDeprecatedButton } from '@gitlab/ui';
import Api from 'ee/api';
import Form from 'ee/feature_flags/components/form.vue'; import Form from 'ee/feature_flags/components/form.vue';
import EnvironmentsDropdown from 'ee/feature_flags/components/environments_dropdown.vue'; import EnvironmentsDropdown from 'ee/feature_flags/components/environments_dropdown.vue';
import Strategy from 'ee/feature_flags/components/strategy.vue'; import Strategy from 'ee/feature_flags/components/strategy.vue';
...@@ -13,7 +14,9 @@ import { ...@@ -13,7 +14,9 @@ import {
NEW_VERSION_FLAG, NEW_VERSION_FLAG,
} from 'ee/feature_flags/constants'; } from 'ee/feature_flags/constants';
import ToggleButton from '~/vue_shared/components/toggle_button.vue'; import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import { featureFlag } from '../mock_data'; import { featureFlag, userList } from '../mock_data';
jest.mock('ee/api.js');
describe('feature flag form', () => { describe('feature flag form', () => {
let wrapper; let wrapper;
...@@ -21,6 +24,7 @@ describe('feature flag form', () => { ...@@ -21,6 +24,7 @@ describe('feature flag form', () => {
cancelPath: 'feature_flags', cancelPath: 'feature_flags',
submitText: 'Create', submitText: 'Create',
environmentsEndpoint: '/environments.json', environmentsEndpoint: '/environments.json',
projectId: '1',
}; };
const factory = (props = {}) => { const factory = (props = {}) => {
...@@ -35,6 +39,10 @@ describe('feature flag form', () => { ...@@ -35,6 +39,10 @@ describe('feature flag form', () => {
}); });
}; };
beforeEach(() => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [] });
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -388,6 +396,7 @@ describe('feature flag form', () => { ...@@ -388,6 +396,7 @@ describe('feature flag form', () => {
describe('with strategies', () => { describe('with strategies', () => {
beforeEach(() => { beforeEach(() => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList] });
factory({ factory({
...requiredProps, ...requiredProps,
name: featureFlag.name, name: featureFlag.name,
...@@ -409,6 +418,12 @@ describe('feature flag form', () => { ...@@ -409,6 +418,12 @@ describe('feature flag form', () => {
}); });
}); });
it('should request the user lists on mount', () => {
return wrapper.vm.$nextTick(() => {
expect(Api.fetchFeatureFlagUserLists).toHaveBeenCalledWith('1');
});
});
it('should show the strategy component', () => { it('should show the strategy component', () => {
const strategy = wrapper.find(Strategy); const strategy = wrapper.find(Strategy);
expect(strategy.exists()).toBe(true); expect(strategy.exists()).toBe(true);
...@@ -440,5 +455,9 @@ describe('feature flag form', () => { ...@@ -440,5 +455,9 @@ describe('feature flag form', () => {
expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy); expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy);
}); });
}); });
it('should provide the user lists to the strategy', () => {
expect(wrapper.find(Strategy).props('userLists')).toEqual([userList]);
});
}); });
}); });
...@@ -25,6 +25,7 @@ describe('New feature flag form', () => { ...@@ -25,6 +25,7 @@ describe('New feature flag form', () => {
endpoint: 'feature_flags.json', endpoint: 'feature_flags.json',
path: '/feature_flags', path: '/feature_flags',
environmentsEndpoint: 'environments.json', environmentsEndpoint: 'environments.json',
projectId: '8',
}, },
store, store,
}); });
...@@ -72,4 +73,8 @@ describe('New feature flag form', () => { ...@@ -72,4 +73,8 @@ describe('New feature flag form', () => {
it('should alert users that feature flags are changing soon', () => { it('should alert users that feature flags are changing soon', () => {
expect(wrapper.find(GlAlert).text()).toBe(NEW_FLAG_ALERT); expect(wrapper.find(GlAlert).text()).toBe(NEW_FLAG_ALERT);
}); });
it('should pass in the project ID', () => {
expect(wrapper.find(Form).props('projectId')).toBe('8');
});
}); });
...@@ -5,13 +5,18 @@ import { ...@@ -5,13 +5,18 @@ import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from 'ee/feature_flags/constants'; } from 'ee/feature_flags/constants';
import Strategy from 'ee/feature_flags/components/strategy.vue'; import Strategy from 'ee/feature_flags/components/strategy.vue';
import NewEnvironmentsDropdown from 'ee/feature_flags/components/new_environments_dropdown.vue'; import NewEnvironmentsDropdown from 'ee/feature_flags/components/new_environments_dropdown.vue';
import { userList } from '../mock_data';
describe('Feature flags strategy', () => { describe('Feature flags strategy', () => {
let wrapper; let wrapper;
const findStrategy = () => wrapper.find('[data-testid="strategy"]');
const factory = ( const factory = (
opts = { opts = {
propsData: { propsData: {
...@@ -19,6 +24,7 @@ describe('Feature flags strategy', () => { ...@@ -19,6 +24,7 @@ describe('Feature flags strategy', () => {
index: 0, index: 0,
endpoint: '', endpoint: '',
canDelete: true, canDelete: true,
userLists: [userList],
}, },
}, },
) => { ) => {
...@@ -37,11 +43,11 @@ describe('Feature flags strategy', () => { ...@@ -37,11 +43,11 @@ describe('Feature flags strategy', () => {
}); });
describe.each` describe.each`
name | parameter | value | input name | parameter | value | newValue | input
${ROLLOUT_STRATEGY_ALL_USERS} | ${null} | ${null} | ${null} ${ROLLOUT_STRATEGY_ALL_USERS} | ${null} | ${null} | ${null} | ${null}
${ROLLOUT_STRATEGY_PERCENT_ROLLOUT} | ${'percentage'} | ${'50'} | ${GlFormInput} ${ROLLOUT_STRATEGY_PERCENT_ROLLOUT} | ${'percentage'} | ${'50'} | ${'20'} | ${GlFormInput}
${ROLLOUT_STRATEGY_USER_ID} | ${'userIds'} | ${'1,2'} | ${GlFormTextarea} ${ROLLOUT_STRATEGY_USER_ID} | ${'userIds'} | ${'1,2'} | ${'1,2,3'} | ${GlFormTextarea}
`('with strategy $name', ({ name, parameter, value, input }) => { `('with strategy $name', ({ name, parameter, value, newValue, input }) => {
let propsData; let propsData;
let strategy; let strategy;
beforeEach(() => { beforeEach(() => {
...@@ -57,27 +63,74 @@ describe('Feature flags strategy', () => { ...@@ -57,27 +63,74 @@ describe('Feature flags strategy', () => {
it('should set the select to match the strategy name', () => { it('should set the select to match the strategy name', () => {
expect(wrapper.find(GlFormSelect).attributes('value')).toBe(name); expect(wrapper.find(GlFormSelect).attributes('value')).toBe(name);
}); });
it('should not show inputs for other paramters', () => {
[GlFormTextarea, GlFormInput] it('should not show inputs for other parameters', () => {
[GlFormTextarea, GlFormInput, GlFormSelect]
.filter(component => component !== input) .filter(component => component !== input)
.map(component => wrapper.findAll(component)) .map(component => findStrategy().findAll(component))
.forEach(inputWrapper => expect(inputWrapper).toHaveLength(0)); .forEach(inputWrapper => expect(inputWrapper).toHaveLength(0));
}); });
if (parameter !== null) { if (parameter !== null) {
it(`should show the input for ${parameter} with the correct value`, () => { it(`should show the input for ${parameter} with the correct value`, () => {
const inputWrapper = wrapper.find(input); const inputWrapper = findStrategy().find(input);
expect(inputWrapper.exists()).toBe(true); expect(inputWrapper.exists()).toBe(true);
expect(inputWrapper.attributes('value')).toBe(value); expect(inputWrapper.attributes('value')).toBe(value);
}); });
it(`should emit a change event when altering ${parameter}`, () => { it(`should emit a change event when altering ${parameter}`, () => {
const inputWrapper = wrapper.find(input); const inputWrapper = findStrategy().find(input);
inputWrapper.vm.$emit('input', ''); inputWrapper.vm.$emit('input', newValue);
expect(wrapper.emitted('change')).toEqual([ expect(wrapper.emitted('change')).toEqual([
[{ name, parameters: expect.objectContaining({ [parameter]: '' }), scopes: [] }], [{ name, parameters: expect.objectContaining({ [parameter]: newValue }), scopes: [] }],
]); ]);
}); });
} }
}); });
describe('with strategy gitlabUserList', () => {
let propsData;
let strategy;
beforeEach(() => {
strategy = { name: ROLLOUT_STRATEGY_GITLAB_USER_LIST, userListId: '2', parameters: {} };
propsData = { strategy, index: 0, endpoint: '', canDelete: true, userLists: [userList] };
factory({ propsData });
});
it('should set the select to match the strategy name', () => {
expect(wrapper.find(GlFormSelect).attributes('value')).toBe(
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
);
});
it('should not show inputs for other parameters', () => {
expect(findStrategy().contains(GlFormTextarea)).toBe(false);
expect(findStrategy().contains(GlFormInput)).toBe(false);
});
it('should show the input for userListId with the correct value', () => {
const inputWrapper = findStrategy().find(GlFormSelect);
expect(inputWrapper.exists()).toBe(true);
expect(inputWrapper.attributes('value')).toBe('2');
});
it('should emit a change event when altering the userListId', () => {
const inputWrapper = findStrategy().find(GlFormSelect);
inputWrapper.vm.$emit('input', '3');
inputWrapper.vm.$emit('change', '3');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('change')).toEqual([
[
{
name: ROLLOUT_STRATEGY_GITLAB_USER_LIST,
userListId: '3',
scopes: [],
parameters: {},
},
],
]);
});
});
});
describe('with a strategy', () => { describe('with a strategy', () => {
describe('with scopes defined', () => { describe('with scopes defined', () => {
......
...@@ -89,3 +89,13 @@ export const getRequestData = { ...@@ -89,3 +89,13 @@ export const getRequestData = {
}; };
export const rotateData = { token: 'oP6sCNRqtRHmpy1gw2-F' }; export const rotateData = { token: 'oP6sCNRqtRHmpy1gw2-F' };
export const userList = {
name: 'test_users',
user_xids: 'user3,user4,user5',
id: 2,
iid: 2,
project_id: 1,
created_at: '2020-02-04T08:13:10.507Z',
updated_at: '2020-02-04T08:13:10.507Z',
};
...@@ -9623,6 +9623,9 @@ msgstr "" ...@@ -9623,6 +9623,9 @@ msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags." msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr "" msgstr ""
msgid "FeatureFlags|There was an error retrieving user lists"
msgstr ""
msgid "FeatureFlags|Try again in a few moments or contact your support team." msgid "FeatureFlags|Try again in a few moments or contact your support team."
msgstr "" msgstr ""
...@@ -9632,9 +9635,18 @@ msgstr "" ...@@ -9632,9 +9635,18 @@ msgstr ""
msgid "FeatureFlag|Delete strategy" msgid "FeatureFlag|Delete strategy"
msgstr "" msgstr ""
msgid "FeatureFlag|List"
msgstr ""
msgid "FeatureFlag|Percentage" msgid "FeatureFlag|Percentage"
msgstr "" msgstr ""
msgid "FeatureFlag|Select a user list"
msgstr ""
msgid "FeatureFlag|There are no configured user lists"
msgstr ""
msgid "FeatureFlag|Type" msgid "FeatureFlag|Type"
msgstr "" msgstr ""
......
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