Commit 3675e151 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '8240-add-percent-rollout-strategy-to-feature-flags-ui' into 'master'

Update feature flag UI to support percentage rollout

See merge request gitlab-org/gitlab-ee!14538
parents 0e84283b 4e6cc417
......@@ -435,6 +435,7 @@ img.emoji {
/** COMMON SIZING CLASSES **/
.w-0 { width: 0; }
.w-8em { width: 8em; }
.w-3rem { width: 3rem; }
.h-12em { height: 12em; }
......
......@@ -3,6 +3,7 @@ import _ from 'underscore';
import { GlButton, GlLink, GlTooltipDirective, GlModalDirective, GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT } from '../constants';
export default {
components: {
......@@ -61,12 +62,22 @@ export default {
scopeTooltipText(scope) {
return !scope.active
? sprintf(s__('FeatureFlags|Inactive flag for %{scope}'), {
scope: scope.environment_scope,
scope: scope.environmentScope,
})
: '';
},
scopeName(name) {
return name === '*' ? s__('FeatureFlags|* (All environments)') : name;
badgeText(scope) {
const displayName =
scope.environmentScope === '*'
? s__('FeatureFlags|* (All environments)')
: scope.environmentScope;
const displayPercentage =
scope.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT
? `: ${scope.rolloutPercentage}%`
: '';
return `${displayName}${displayPercentage}`;
},
canDeleteFlag(flag) {
return !this.permissions || (flag.scopes || []).every(scope => scope.can_update);
......@@ -131,8 +142,11 @@ export default {
:key="scope.id"
v-gl-tooltip.hover="scopeTooltipText(scope)"
class="badge append-right-8 prepend-top-2"
:class="{ 'badge-active': scope.active, 'badge-inactive': !scope.active }"
>{{ scopeName(scope.environment_scope) }}</span
:class="{
'badge-active': scope.active,
'badge-inactive': !scope.active,
}"
>{{ badgeText(scope) }}</span
>
</div>
</div>
......
......@@ -2,6 +2,7 @@
import { createNamespacedHelpers } from 'vuex';
import store from '../store/index';
import FeatureFlagForm from './form.vue';
import { createNewEnvironmentScope } from '../store/modules/helpers';
const { mapState, mapActions } = createNamespacedHelpers('new');
......@@ -28,10 +29,10 @@ export default {
...mapState(['error']),
scopes() {
return [
{
environment_scope: '*',
createNewEnvironmentScope({
environmentScope: '*',
active: true,
},
}),
];
},
},
......
export const ROLLOUT_STRATEGY_ALL_USERS = 'default';
export const ROLLOUT_STRATEGY_PERCENT_ROLLOUT = 'gradualRolloutUserId';
export const PERCENT_ROLLOUT_GROUP_ID = 'default';
export const DEFAULT_PERCENT_ROLLOUT = '100';
export const ALL_ENVIRONMENTS_NAME = '*';
export const INTERNAL_ID_PREFIX = 'internal_';
......@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { parseFeatureFlagsParams } from '../helpers';
import { mapFromScopesViewModel } from '../helpers';
/**
* Commits mutation to set the main endpoint
......@@ -32,7 +32,7 @@ export const updateFeatureFlag = ({ state, dispatch }, params) => {
dispatch('requestUpdateFeatureFlag');
axios
.put(state.endpoint, parseFeatureFlagsParams(params))
.put(state.endpoint, mapFromScopesViewModel(params))
.then(() => {
dispatch('receiveUpdateFeatureFlagSuccess');
visitUrl(state.path);
......
import * as types from './mutation_types';
import { mapToScopesViewModel } from '../helpers';
export default {
[types.SET_ENDPOINT](state, endpoint) {
......@@ -16,9 +17,7 @@ export default {
state.name = response.name;
state.description = response.description;
// When there aren't scopes BE sends `null`
state.scopes = response.scopes || [];
state.scopes = mapToScopesViewModel(response.scopes);
},
[types.RECEIVE_FEATURE_FLAG_ERROR](state) {
state.isLoading = false;
......
......@@ -6,7 +6,7 @@ export default () => ({
name: null,
description: null,
scopes: null,
scopes: [],
isLoading: false,
hasError: false,
});
import _ from 'underscore';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
INTERNAL_ID_PREFIX,
DEFAULT_PERCENT_ROLLOUT,
PERCENT_ROLLOUT_GROUP_ID,
} from '../../constants';
export const internalKeyID = 'internal_';
export const parseFeatureFlagsParams = params => ({
operations_feature_flag: {
name: params.name,
description: params.description,
scopes_attributes: params.scopes.map(scope => {
const scopeCopy = Object.assign({}, scope);
if (_.isString(scopeCopy.id) && scopeCopy.id.indexOf(internalKeyID) !== -1) {
delete scopeCopy.id;
}
return scopeCopy;
}),
},
});
/**
* 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 [strategy] = s.strategies || [];
const rolloutStrategy = strategy ? strategy.name : ROLLOUT_STRATEGY_ALL_USERS;
let rolloutPercentage = DEFAULT_PERCENT_ROLLOUT;
if (strategy && strategy.parameters && strategy.parameters.percentage) {
rolloutPercentage = strategy.parameters.percentage;
}
return {
id: s.id,
environmentScope: s.environment_scope,
active: Boolean(s.active),
canUpdate: Boolean(s.can_update),
protected: Boolean(s.protected),
rolloutStrategy,
rolloutPercentage,
// eslint-disable-next-line no-underscore-dangle
shouldBeDestroyed: Boolean(s._destroy),
};
});
/**
* 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;
}
// Strip out any internal IDs
const id = _.isString(s.id) && s.id.startsWith(INTERNAL_ID_PREFIX) ? undefined : s.id;
return {
id,
environment_scope: s.environmentScope,
active: s.active,
can_update: s.canUpdate,
protected: s.protected,
_destroy: s.shouldBeDestroyed,
strategies: [
{
name: s.rolloutStrategy,
parameters,
},
],
};
});
return {
operations_feature_flag: {
name: params.name,
description: params.description,
scopes_attributes: scopes,
},
};
};
/**
* 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 = {}) => {
const defaultScope = {
environmentScope: '',
active: false,
id: _.uniqueId(INTERNAL_ID_PREFIX),
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
};
const newScope = {
...defaultScope,
...overrides,
};
if (gon && gon.features && gon.features.featureFlagPermissions) {
newScope.canUpdate = true;
newScope.protected = false;
}
return newScope;
};
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { mapToScopesViewModel } from '../helpers';
export default {
[types.SET_FEATURE_FLAGS_ENDPOINT](state, endpoint) {
......@@ -20,8 +21,11 @@ export default {
[types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) {
state.isLoading = false;
state.hasError = false;
state.featureFlags = response.data.feature_flags;
state.count = response.data.count;
state.featureFlags = (response.data.feature_flags || []).map(f => ({
...f,
scopes: mapToScopesViewModel(f.scopes || []),
}));
let paginationInfo;
if (Object.keys(response.headers).length) {
......
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { parseFeatureFlagsParams } from '../helpers';
import { mapFromScopesViewModel } from '../helpers';
/**
* Commits mutation to set the main endpoint
......@@ -30,7 +30,7 @@ export const createFeatureFlag = ({ state, dispatch }, params) => {
dispatch('requestCreateFeatureFlag');
axios
.post(state.endpoint, parseFeatureFlagsParams(params))
.post(state.endpoint, mapFromScopesViewModel(params))
.then(() => {
dispatch('receiveCreateFeatureFlagSuccess');
visitUrl(state.path);
......
.feature-flags-form {
input.rollout-percentage {
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
-moz-appearance: textfield;
}
}
---
title: Add percentage rollout support to feature flag UI
merge_request: 14538
author:
type: added
import _ from 'underscore';
import { parseFeatureFlagsParams, internalKeyID } from 'ee/feature_flags/store/modules/helpers';
import {
mapToScopesViewModel,
mapFromScopesViewModel,
createNewEnvironmentScope,
} from 'ee/feature_flags/store/modules/helpers';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
PERCENT_ROLLOUT_GROUP_ID,
INTERNAL_ID_PREFIX,
DEFAULT_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants';
describe('feature flags helpers spec', () => {
describe('parseFeatureFlagsParams', () => {
describe('with internalKeyId', () => {
it('removes id', () => {
const scopes = [
{
active: true,
created_at: '2019-01-17T17:22:07.625Z',
environment_scope: '*',
id: 2,
updated_at: '2019-01-17T17:22:07.625Z',
},
{
active: true,
created_at: '2019-03-11T11:18:42.709Z',
environment_scope: 'review',
id: 29,
updated_at: '2019-03-11T11:18:42.709Z',
},
describe('mapToScopesViewModel', () => {
it('converts the data object from the Rails API into something more usable by Vue', () => {
const input = [
{
id: 3,
environment_scope: 'environment_scope',
active: true,
can_update: true,
protected: true,
strategies: [
{
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: {
percentage: '56',
},
},
],
_destroy: true,
},
];
const expected = [
{
id: 3,
environmentScope: 'environment_scope',
active: true,
canUpdate: true,
protected: true,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '56',
shouldBeDestroyed: true,
},
];
const actual = mapToScopesViewModel(input);
expect(actual).toEqual(expected);
});
it('returns Boolean properties even when their Rails counterparts were not provided (are `undefined`)', () => {
const input = [
{
id: 3,
environment_scope: 'environment_scope',
},
];
const [result] = mapToScopesViewModel(input);
expect(result).toEqual(
expect.objectContaining({
active: false,
canUpdate: false,
protected: false,
shouldBeDestroyed: false,
}),
);
});
it('returns an empty array if null or undefined is provided as a parameter', () => {
expect(mapToScopesViewModel(null)).toEqual([]);
expect(mapToScopesViewModel(undefined)).toEqual([]);
});
});
describe('mapFromScopesViewModel', () => {
it('converts the object emitted from the Vue component into an object than is in the right format to be submitted to the Rails API', () => {
const input = {
name: 'name',
description: 'description',
scopes: [
{
id: 4,
environmentScope: 'environmentScope',
active: true,
created_at: '2019-03-11T11:18:42.709Z',
environment_scope: 'review',
id: _.uniqueId(internalKeyID),
updated_at: '2019-03-11T11:18:42.709Z',
canUpdate: true,
protected: true,
shouldBeDestroyed: true,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '48',
},
];
],
};
const expected = {
operations_feature_flag: {
name: 'name',
description: 'description',
scopes_attributes: [
{
id: 4,
environment_scope: 'environmentScope',
active: true,
can_update: true,
protected: true,
_destroy: true,
strategies: [
{
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
percentage: '48',
},
},
],
},
],
},
};
const actual = mapFromScopesViewModel(input);
expect(actual).toEqual(expected);
});
it('should strip out internal IDs', () => {
const input = {
scopes: [{ id: 3 }, { id: _.uniqueId(INTERNAL_ID_PREFIX) }],
};
const result = mapFromScopesViewModel(input);
const [realId, internalId] = result.operations_feature_flag.scopes_attributes;
expect(realId.id).toBe(3);
expect(internalId.id).toBeUndefined();
});
it('returns scopes_attributes as [] if param.scopes is null or undefined', () => {
let {
operations_feature_flag: { scopes_attributes: actualScopes },
} = mapFromScopesViewModel({ scopes: null });
expect(actualScopes).toEqual([]);
({
operations_feature_flag: { scopes_attributes: actualScopes },
} = mapFromScopesViewModel({ scopes: undefined }));
expect(actualScopes).toEqual([]);
});
});
describe('createNewEnvironmentScope', () => {
it('should return a new environment scope object populated with the default options', () => {
const expected = {
environmentScope: '',
active: false,
id: expect.stringContaining(INTERNAL_ID_PREFIX),
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
};
const actual = createNewEnvironmentScope();
expect(actual).toEqual(expected);
});
it('should return a new environment scope object with overrides applied', () => {
const overrides = {
environmentScope: 'environmentScope',
active: true,
};
const expected = {
environmentScope: 'environmentScope',
active: true,
id: expect.stringContaining(INTERNAL_ID_PREFIX),
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
};
const parsedScopes = parseFeatureFlagsParams({
name: 'review',
scopes,
description: 'feature flag',
});
const actual = createNewEnvironmentScope(overrides);
expect(parsedScopes.operations_feature_flag.scopes_attributes[2].id).toEqual(undefined);
});
expect(actual).toEqual(expected);
});
});
});
import Vue from 'vue';
import featureFlagsTableComponent from 'ee/feature_flags/components/feature_flags_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { featureFlag } from '../mock_data';
import { trimText } from 'spec/helpers/text_helper';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
DEFAULT_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants';
describe('Feature Flag table', () => {
let Component;
let vm;
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
});
afterEach(() => {
vm.$destroy();
});
it('Should render a table', () => {
vm = mountComponent(Component, {
featureFlags: [featureFlag],
csrfToken: 'fakeToken',
describe('with an active scope and a standard rollout strategy', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
});
});
expect(vm.$el.getAttribute('class')).toContain('table-holder');
});
it('Should render a table', () => {
expect(vm.$el.getAttribute('class')).toContain('table-holder');
});
it('Should render rows', () => {
expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBeNull();
});
it('Should render rows', () => {
expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBeNull();
});
it('Should render a status column', () => {
const status = featureFlag.active ? 'Active' : 'Inactive';
it('Should render a status column', () => {
expect(vm.$el.querySelector('.js-feature-flag-status')).not.toBeNull();
expect(trimText(vm.$el.querySelector('.js-feature-flag-status').textContent)).toEqual(
'Active',
);
});
expect(vm.$el.querySelector('.js-feature-flag-status')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-status').textContent.trim()).toEqual(status);
});
it('Should render a feature flag column', () => {
expect(vm.$el.querySelector('.js-feature-flag-title')).not.toBeNull();
expect(trimText(vm.$el.querySelector('.feature-flag-name').textContent)).toEqual('flag name');
expect(trimText(vm.$el.querySelector('.feature-flag-description').textContent)).toEqual(
'flag description',
);
});
it('Should render a feature flag column', () => {
expect(vm.$el.querySelector('.js-feature-flag-title')).not.toBeNull();
expect(vm.$el.querySelector('.feature-flag-name').textContent.trim()).toEqual(featureFlag.name);
expect(vm.$el.querySelector('.feature-flag-description').textContent.trim()).toEqual(
featureFlag.description,
);
});
it('should render an environments specs column', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
it('should render a environments specs column', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
expect(envColumn).toBeDefined();
expect(trimText(envColumn.textContent)).toBe('scope');
});
expect(envColumn).not.toBeNull();
expect(envColumn.textContent.trim()).toContain(featureFlag.scopes[0].environment_scope);
expect(envColumn.textContent.trim()).toContain(featureFlag.scopes[1].environment_scope);
});
it('should render an environments specs badge with active class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
it('should render a environments specs badge with inactive class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
expect(trimText(envColumn.querySelector('.badge-active').textContent)).toBe('scope');
});
expect(envColumn.querySelector('.badge-inactive').textContent.trim()).toContain(
featureFlag.scopes[1].environment_scope,
);
it('should render an actions column', () => {
expect(vm.$el.querySelector('.table-action-buttons')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-delete-button')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-edit-button')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-edit-button').getAttribute('href')).toEqual(
'edit/path',
);
});
});
it('should render a environments specs badge with active class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
describe('with an active scope and a percentage rollout strategy', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '54',
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
});
});
it('should render an environments specs badge with percentage', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
expect(envColumn.querySelector('.badge-active').textContent.trim()).toContain(
featureFlag.scopes[0].environment_scope,
);
expect(trimText(envColumn.querySelector('.badge').textContent)).toBe('scope: 54%');
});
});
it('Should render an actions column', () => {
expect(vm.$el.querySelector('.table-action-buttons')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-delete-button')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-edit-button')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-edit-button').getAttribute('href')).toEqual(
featureFlag.edit_path,
);
describe('with an inactive scope', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: false,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
});
});
it('should render an environments specs badge with inactive class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
expect(trimText(envColumn.querySelector('.badge-inactive').textContent)).toBe('scope');
});
});
});
......@@ -4,6 +4,7 @@ import { createLocalVue, mount } from '@vue/test-utils';
import Form from 'ee/feature_flags/components/form.vue';
import newModule from 'ee/feature_flags/store/modules/new';
import NewFeatureFlag from 'ee/feature_flags/components/new_feature_flag.vue';
import { ROLLOUT_STRATEGY_ALL_USERS, DEFAULT_PERCENT_ROLLOUT } from 'ee/feature_flags/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -60,8 +61,11 @@ describe('New feature flag form', () => {
it('should render default * row', () => {
expect(wrapper.vm.scopes).toEqual([
{
environment_scope: '*',
id: jasmine.any(String),
environmentScope: '*',
active: true,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
},
]);
......
export const featureFlagsList = [
{
id: 1,
active: true,
created_at: '2018-12-12T22:07:31.401Z',
updated_at: '2018-12-12T22:07:31.401Z',
name: 'test flag',
description: 'flag for tests',
destroy_path: 'feature_flags/1',
edit_path: 'feature_flags/1/edit',
},
];
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants';
export const featureFlag = {
id: 1,
......@@ -25,55 +17,69 @@ export const featureFlag = {
id: 1,
active: true,
environment_scope: '*',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
},
{
id: 2,
active: false,
environment_scope: 'production',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
},
],
};
export const getRequestData = {
feature_flags: [
{
id: 3,
active: true,
active: false,
environment_scope: 'review/*',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
name: 'ci_live_trace',
description: 'For the new live trace architecture',
edit_path: '/root/per-environment-feature-flags/-/feature_flags/3/edit',
destroy_path: '/root/per-environment-feature-flags/-/feature_flags/3',
scopes: [
{
id: 1,
active: true,
environment_scope: '*',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
},
strategies: [
{
id: 2,
active: false,
environment_scope: 'production',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
},
{
id: 4,
active: true,
environment_scope: 'development',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
id: 3,
active: false,
environment_scope: 'review/*',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: {
percentage: '86',
},
},
],
},
],
};
export const getRequestData = {
feature_flags: [featureFlag],
count: {
all: 1,
disabled: 1,
......
......@@ -65,22 +65,14 @@ describe('Feature flags Edit Module actions', () => {
describe('success', () => {
it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', done => {
mock
.onPut(mockedState.endpoint, {
operations_feature_flag: {
name: 'feature_flag',
description: 'feature flag',
scopes_attributes: [{ environment_scope: '*', active: true }],
},
})
.replyOnce(200);
mock.onPut(mockedState.endpoint).replyOnce(200);
testAction(
updateFeatureFlag,
{
name: 'feature_flag',
description: 'feature flag',
scopes: [{ environment_scope: '*', active: true }],
scopes: [{ environmentScope: '*', active: true }],
},
mockedState,
[],
......@@ -99,15 +91,7 @@ describe('Feature flags Edit Module actions', () => {
describe('error', () => {
it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError ', done => {
mock
.onPut(`${TEST_HOST}/endpoint.json`, {
operations_feature_flag: {
name: 'feature_flag',
description: 'feature flag',
scopes_attributes: [{ environment_scope: '*', active: true }],
},
})
.replyOnce(500, { message: [] });
mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] });
testAction(
updateFeatureFlag,
......
......@@ -2,6 +2,7 @@ import state from 'ee/feature_flags/store/modules/index/state';
import mutations from 'ee/feature_flags/store/modules/index/mutations';
import * as types from 'ee/feature_flags/store/modules/index/mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { mapToScopesViewModel } from 'ee/feature_flags/store/modules/helpers';
import { getRequestData, rotateData } from '../../mock_data';
describe('Feature flags store Mutations', () => {
......@@ -73,8 +74,13 @@ describe('Feature flags store Mutations', () => {
expect(stateCopy.hasError).toEqual(false);
});
it('should set featureFlags with the given data', () => {
expect(stateCopy.featureFlags).toEqual(getRequestData.feature_flags);
it('should set featureFlags with the transformed data', () => {
const expected = getRequestData.feature_flags.map(f => ({
...f,
scopes: mapToScopesViewModel(f.scopes || []),
}));
expect(stateCopy.featureFlags).toEqual(expected);
});
it('should set count with the given data', () => {
......
......@@ -12,6 +12,11 @@ import state from 'ee/feature_flags/store/modules/new/state';
import * as types from 'ee/feature_flags/store/modules/new/mutation_types';
import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants';
import { mapFromScopesViewModel } from 'ee/feature_flags/store/modules/helpers';
describe('Feature flags New Module Actions', () => {
let mockedState;
......@@ -49,6 +54,23 @@ describe('Feature flags New Module Actions', () => {
describe('createFeatureFlag', () => {
let mock;
const actionParams = {
name: 'name',
description: 'description',
scopes: [
{
id: 1,
environmentScope: 'environmentScope',
active: true,
canUpdate: true,
protected: true,
shouldBeDestroyed: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
},
],
};
beforeEach(() => {
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
......@@ -61,23 +83,13 @@ describe('Feature flags New Module Actions', () => {
describe('success', () => {
it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', done => {
mock
.onPost(`${TEST_HOST}/endpoint.json`, {
operations_feature_flag: {
name: 'feature_flag',
description: 'feature flag',
scopes_attributes: [{ environment_scope: '*', active: true }],
},
})
.replyOnce(200);
const convertedActionParams = mapFromScopesViewModel(actionParams);
mock.onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams).replyOnce(200);
testAction(
createFeatureFlag,
{
name: 'feature_flag',
description: 'feature flag',
scopes: [{ environment_scope: '*', active: true }],
},
actionParams,
mockedState,
[],
[
......@@ -95,23 +107,15 @@ describe('Feature flags New Module Actions', () => {
describe('error', () => {
it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', done => {
const convertedActionParams = mapFromScopesViewModel(actionParams);
mock
.onPost(`${TEST_HOST}/endpoint.json`, {
operations_feature_flag: {
name: 'feature_flag',
description: 'feature flag',
scopes_attributes: [{ environment_scope: '*', active: true }],
},
})
.onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams)
.replyOnce(500, { message: [] });
testAction(
createFeatureFlag,
{
name: 'feature_flag',
description: 'feature flag',
scopes: [{ environment_scope: '*', active: true }],
},
actionParams,
mockedState,
[],
[
......
......@@ -45,7 +45,7 @@ module FeatureFlagHelpers
end
def within_delete
within '.table-section:nth-child(3)' do
within '.table-section:nth-child(4)' do
yield
end
end
......
......@@ -6024,6 +6024,9 @@ msgstr ""
msgid "FeatureFlags|Active"
msgstr ""
msgid "FeatureFlags|All users"
msgstr ""
msgid "FeatureFlags|Configure"
msgstr ""
......@@ -6096,9 +6099,24 @@ msgstr ""
msgid "FeatureFlags|New Feature Flag"
msgstr ""
msgid "FeatureFlags|Percent rollout (logged in users)"
msgstr ""
msgid "FeatureFlags|Percent rollout must be a whole number between 0 and 100"
msgstr ""
msgid "FeatureFlags|Protected"
msgstr ""
msgid "FeatureFlags|Remove"
msgstr ""
msgid "FeatureFlags|Rollout Percentage"
msgstr ""
msgid "FeatureFlags|Rollout Strategy"
msgstr ""
msgid "FeatureFlags|Status"
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