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>
......
<script>
import Vue from 'vue';
import _ from 'underscore';
import { GlButton, GlBadge } from '@gitlab/ui';
import { GlButton, GlBadge, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import EnvironmentsDropdown from './environments_dropdown.vue';
import { internalKeyID } from '../store/modules/helpers';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ALL_ENVIRONMENTS_NAME,
INTERNAL_ID_PREFIX,
} from '../constants';
import { createNewEnvironmentScope } from '../store/modules/helpers';
export default {
components: {
GlButton,
GlBadge,
GlTooltip,
ToggleButton,
Icon,
EnvironmentsDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
name: {
type: String,
......@@ -44,15 +55,9 @@ export default {
required: true,
},
},
data() {
return {
formName: this.name,
formDescription: this.description,
formScopes: this.scopes || [],
newScope: '',
};
},
allEnvironmentsText: s__('FeatureFlags|* (All Environments)'),
helpText: sprintf(
s__(
'FeatureFlags|Feature Flag behavior is built up by creating a set of rules to define the status of target environments. A default wildcard rule %{codeStart}*%{codeEnd} for %{boldStart}All Environments%{boldEnd} is set, and you are able to add as many rules as you need by choosing environment specs below. You can toggle the behavior for each of your rules to set them %{boldStart}Active%{boldEnd} or %{boldStart}Inactive%{boldEnd}.',
......@@ -65,16 +70,30 @@ export default {
},
false,
),
allEnvironments: s__('FeatureFlags|* (All Environments)'),
all: '*',
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
// Matches numbers 0 through 100
rolloutPercentageRegex: /^[0-9]$|^[1-9][0-9]$|^100$/,
data() {
return {
formName: this.name,
formDescription: this.description,
// operate on a clone to avoid mutating props
formScopes: this.scopes.map(s => ({ ...s })),
newScope: '',
};
},
computed: {
filteredScopes() {
// eslint-disable-next-line no-underscore-dangle
return this.formScopes.filter(scope => !scope._destroy);
return this.formScopes.filter(scope => !scope.shouldBeDestroyed);
},
canUpdateFlag() {
return !this.permissionsFlag || (this.scopes || []).every(scope => scope.can_update);
return !this.permissionsFlag || (this.formScopes || []).every(scope => scope.canUpdate);
},
permissionsFlag() {
return gon && gon.features && gon.features.featureFlagPermissions;
......@@ -82,88 +101,39 @@ export default {
},
methods: {
isAllEnvironment(name) {
return name === this.$options.all;
},
/**
* When the user updates the status of
* an existing scope we toggle the status for
* the `formScopes`
*
* @param {Object} scope
* @param {Number} index
* @param {Boolean} status
*/
onUpdateScopeStatus(scope, status) {
const index = this.formScopes.findIndex(el => el.id === scope.id);
this.formScopes.splice(index, 1, Object.assign({}, scope, { active: status }));
},
/**
* When the user selects or creates a new scope in the environemnts dropdoown
* we update the selected value.
*
* @param {String} name
* @param {Object} scope
* @param {Number} index
*/
updateScope(name, scope) {
const index = this.formScopes.findIndex(el => el.id === scope.id);
this.formScopes.splice(index, 1, Object.assign({}, scope, { environment_scope: name }));
return name === ALL_ENVIRONMENTS_NAME;
},
/**
* When the user clicks the toggle button in the new row,
* we automatically add it as a new scope
*
* @param {Boolean} value the toggle value
*/
onChangeNewScopeStatus(value) {
const newScope = {
active: value,
environment_scope: this.newScope,
id: _.uniqueId(internalKeyID),
};
if (this.permissionsFlag) {
newScope.can_update = true;
newScope.protected = false;
}
this.formScopes.push(newScope);
this.newScope = '';
},
/**
* When the user clicks the remove button we delete the scope
*
* If the scope has an ID, we need to add the `_destroy` flag
* otherwise we can just remove it.
* Backend needs the destroy flag only in the PUT request.
* If the scope has an ID, we need to add the `shouldBeDestroyed` flag.
* If the scope does *not* have an ID, we can just remove it.
*
* This flag will be used when submitting the data to the backend
* to determine which records to delete (via a "_destroy" property).
*
* @param {Number} index
* @param {Object} scope
*/
removeScope(scope) {
const index = this.formScopes.findIndex(el => el.id === scope.id);
if (_.isString(scope.id) && scope.id.indexOf(internalKeyID) !== -1) {
this.formScopes.splice(index, 1);
if (_.isString(scope.id) && scope.id.startsWith(INTERNAL_ID_PREFIX)) {
this.formScopes = this.formScopes.filter(s => s !== scope);
} else {
this.formScopes.splice(index, 1, Object.assign({}, scope, { _destroy: true }));
Vue.set(scope, 'shouldBeDestroyed', true);
}
},
/**
* When the user selects a value or creates a new value in the environments
* dropdown in the creation row, we push a new entry with the selected value.
* Creates a new scope and adds it to the list of scopes
*
* @param {String}
* @param overrides An object whose properties will
* be used override the default scope options
*/
createNewEnvironment(name) {
this.formScopes.push({
environment_scope: name,
active: false,
id: _.uniqueId(internalKeyID),
});
createNewScope(overrides) {
this.formScopes.push(createNewEnvironmentScope(overrides));
this.newScope = '';
},
/**
* When the user clicks the submit button
* it triggers an event with the form data
......@@ -177,13 +147,35 @@ export default {
},
canUpdateScope(scope) {
return !this.permissionsFlag || scope.can_update;
return !this.permissionsFlag || scope.canUpdate;
},
isRolloutPercentageInvalid: _.memoize(function isRolloutPercentageInvalid(percentage) {
return !this.$options.rolloutPercentageRegex.test(percentage);
}),
/**
* Generates a unique ID for the strategy based on the v-for index
*
* @param index The index of the strategy
*/
rolloutStrategyId(index) {
return `rollout-strategy-${index}`;
},
/**
* Generates a unique ID for the percentage based on the v-for index
*
* @param index The index of the percentage
*/
rolloutPercentageId(index) {
return `rollout-percentage-${index}`;
},
},
};
</script>
<template>
<form>
<form class="feature-flags-form">
<fieldset>
<div class="row">
<div class="form-group col-md-4">
......@@ -219,12 +211,15 @@ export default {
<div class="js-scopes-table prepend-top-default">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-60" role="columnheader">
<div class="table-section section-30" role="columnheader">
{{ s__('FeatureFlags|Environment Spec') }}
</div>
<div class="table-section section-20" role="columnheader">
<div class="table-section section-20 text-center" role="columnheader">
{{ s__('FeatureFlags|Status') }}
</div>
<div class="table-section section-40" role="columnheader">
{{ s__('FeatureFlags|Rollout Strategy') }}
</div>
</div>
<div
......@@ -233,26 +228,26 @@ export default {
class="gl-responsive-table-row"
role="row"
>
<div class="table-section section-60" role="gridcell">
<div class="table-section section-30" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Environment Spec') }}
</div>
<div
class="table-mobile-content js-feature-flag-status d-flex align-items-center justify-content-start"
>
<p v-if="isAllEnvironment(scope.environment_scope)" class="js-scope-all pl-3">
{{ $options.allEnvironments }}
<p v-if="isAllEnvironment(scope.environmentScope)" class="js-scope-all pl-3">
{{ $options.allEnvironmentsText }}
</p>
<environments-dropdown
v-else
class="col-md-6"
:value="scope.environment_scope"
class="col-12"
:value="scope.environmentScope"
:endpoint="environmentsEndpoint"
:disabled="!canUpdateScope(scope)"
@selectEnvironment="env => updateScope(env, scope, index)"
@createClicked="env => updateScope(env, scope, index)"
@clearInput="updateScope('', scope, index)"
@selectEnvironment="env => (scope.environmentScope = env)"
@createClicked="env => (scope.environmentScope = env)"
@clearInput="env => (scope.environmentScope = '')"
/>
<gl-badge v-if="permissionsFlag && scope.protected" variant="success">{{
......@@ -261,7 +256,7 @@ export default {
</div>
</div>
<div class="table-section section-20" role="gridcell">
<div class="table-section section-20 text-center" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
</div>
......@@ -269,19 +264,81 @@ export default {
<toggle-button
:value="scope.active"
:disabled-input="!canUpdateScope(scope)"
@change="status => onUpdateScopeStatus(scope, status)"
@change="status => (scope.active = status)"
/>
</div>
</div>
<div class="table-section section-20" role="gridcell">
<div class="table-section section-40" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
{{ s__('FeatureFlags|Rollout Strategy') }}
</div>
<div class="table-mobile-content js-rollout-strategy form-inline">
<label class="sr-only" :for="rolloutStrategyId(index)">
{{ s__('FeatureFlags|Rollout Strategy') }}
</label>
<div class="select-wrapper col-12 col-md-8 p-0">
<select
:id="rolloutStrategyId(index)"
v-model="scope.rolloutStrategy"
:disabled="!scope.active"
class="form-control select-control w-100 js-rollout-strategy"
>
<option :value="$options.ROLLOUT_STRATEGY_ALL_USERS">{{
s__('FeatureFlags|All users')
}}</option>
<option :value="$options.ROLLOUT_STRATEGY_PERCENT_ROLLOUT">{{
s__('FeatureFlags|Percent rollout (logged in users)')
}}</option>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
</div>
<div
v-if="scope.rolloutStrategy === $options.ROLLOUT_STRATEGY_PERCENT_ROLLOUT"
class="d-flex-center mt-2 mt-md-0 ml-md-2"
>
<label class="sr-only" :for="rolloutPercentageId(index)">
{{ s__('FeatureFlags|Rollout Percentage') }}
</label>
<div class="w-3rem">
<input
:id="rolloutPercentageId(index)"
v-model="scope.rolloutPercentage"
:disabled="!scope.active"
:class="{
'is-invalid': isRolloutPercentageInvalid(scope.rolloutPercentage),
}"
type="number"
min="0"
max="100"
:pattern="$options.rolloutPercentageRegex.source"
class="rollout-percentage js-rollout-percentage form-control text-right w-100"
/>
</div>
<gl-tooltip
v-if="isRolloutPercentageInvalid(scope.rolloutPercentage)"
:target="rolloutPercentageId(index)"
>
{{
s__('FeatureFlags|Percent rollout must be a whole number between 0 and 100')
}}
</gl-tooltip>
<span class="ml-1">%</span>
</div>
</div>
</div>
<div class="table-section section-10 text-right" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Remove') }}
</div>
<div class="table-mobile-content js-feature-flag-delete">
<gl-button
v-if="!isAllEnvironment(scope.environment_scope) && canUpdateScope(scope)"
class="js-delete-scope btn-transparent"
v-if="!isAllEnvironment(scope.environmentScope) && canUpdateScope(scope)"
v-gl-tooltip
:title="s__('FeatureFlags|Remove')"
class="js-delete-scope btn-transparent pr-3 pl-3"
@click="removeScope(scope)"
>
<icon name="clear" />
......@@ -291,27 +348,48 @@ export default {
</div>
<div class="js-add-new-scope gl-responsive-table-row" role="row">
<div class="table-section section-60" role="gridcell">
<div class="table-section section-30" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Environment Spec') }}
</div>
<div class="table-mobile-content js-feature-flag-status">
<environments-dropdown
class="js-new-scope-name col-md-6"
class="js-new-scope-name col-12"
:endpoint="environmentsEndpoint"
:value="newScope"
@selectEnvironment="env => createNewEnvironment(env)"
@createClicked="env => createNewEnvironment(env)"
@selectEnvironment="env => createNewScope({ environmentScope: env })"
@createClicked="env => createNewScope({ environmentScope: env })"
/>
</div>
</div>
<div class="table-section section-20" role="gridcell">
<div class="table-section section-20 text-center" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
</div>
<div class="table-mobile-content js-feature-flag-status">
<toggle-button :value="false" @change="onChangeNewScopeStatus" />
<toggle-button :value="false" @change="createNewScope({ active: true })" />
</div>
</div>
<div class="table-section section-40" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Rollout Strategy') }}
</div>
<div class="table-mobile-content js-rollout-strategy form-inline">
<label class="sr-only" for="new-rollout-strategy-placeholder">
{{ s__('FeatureFlags|Rollout Strategy') }}
</label>
<div class="select-wrapper col-12 col-md-8 p-0">
<select
id="new-rollout-strategy-placeholder"
disabled
class="form-control select-control w-100"
>
<option>{{ s__('FeatureFlags|All users') }}</option>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
</div>
</div>
</div>
</div>
......@@ -322,6 +400,7 @@ export default {
<div class="form-actions">
<gl-button
ref="submitButton"
type="button"
variant="success"
class="js-ff-submit col-xs-12"
......
......@@ -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_';
/**
* 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 || [];
export const parseFeatureFlagsParams = params => ({
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: params.scopes.map(scope => {
const scopeCopy = Object.assign({}, scope);
if (_.isString(scopeCopy.id) && scopeCopy.id.indexOf(internalKeyID) !== -1) {
delete scopeCopy.id;
}
return scopeCopy;
}),
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 = [
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,
created_at: '2019-01-17T17:22:07.625Z',
environment_scope: '*',
id: 2,
updated_at: '2019-01-17T17:22:07.625Z',
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,
created_at: '2019-03-11T11:18:42.709Z',
environment_scope: 'review',
id: 29,
updated_at: '2019-03-11T11:18:42.709Z',
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 = [
{
active: true,
created_at: '2019-03-11T11:18:42.709Z',
environment_scope: 'review',
id: _.uniqueId(internalKeyID),
updated_at: '2019-03-11T11:18:42.709Z',
id: 3,
environment_scope: 'environment_scope',
},
];
const parsedScopes = parseFeatureFlagsParams({
name: 'review',
scopes,
description: 'feature flag',
const [result] = mapToScopesViewModel(input);
expect(result).toEqual(
expect.objectContaining({
active: false,
canUpdate: false,
protected: false,
shouldBeDestroyed: false,
}),
);
});
expect(parsedScopes.operations_feature_flag.scopes_attributes[2].id).toEqual(undefined);
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,
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 actual = createNewEnvironmentScope(overrides);
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', () => {
describe('with an active scope and a standard rollout strategy', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [featureFlag],
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',
});
});
it('Should render a table', () => {
expect(vm.$el.getAttribute('class')).toContain('table-holder');
});
......@@ -29,50 +56,116 @@ describe('Feature Flag table', () => {
});
it('Should render a status column', () => {
const status = featureFlag.active ? 'Active' : 'Inactive';
expect(vm.$el.querySelector('.js-feature-flag-status')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-status').textContent.trim()).toEqual(status);
expect(trimText(vm.$el.querySelector('.js-feature-flag-status').textContent)).toEqual(
'Active',
);
});
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,
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 environments specs column', () => {
it('should render an environments specs column', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
expect(envColumn).not.toBeNull();
expect(envColumn.textContent.trim()).toContain(featureFlag.scopes[0].environment_scope);
expect(envColumn.textContent.trim()).toContain(featureFlag.scopes[1].environment_scope);
expect(envColumn).toBeDefined();
expect(trimText(envColumn.textContent)).toBe('scope');
});
it('should render a environments specs badge with inactive class', () => {
it('should render an environments specs badge with active class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments');
expect(envColumn.querySelector('.badge-inactive').textContent.trim()).toContain(
featureFlag.scopes[1].environment_scope,
);
});
it('should render a environments specs badge with active class', () => {
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-active').textContent)).toBe('scope');
});
it('Should render an actions column', () => {
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,
'edit/path',
);
});
});
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(trimText(envColumn.querySelector('.badge').textContent)).toBe('scope: 54%');
});
});
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');
});
});
});
......@@ -3,7 +3,13 @@ import { createLocalVue, mount } from '@vue/test-utils';
import Form from 'ee/feature_flags/components/form.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import EnvironmentsDropdown from 'ee/feature_flags/components/environments_dropdown.vue';
import { internalKeyID } from 'ee/feature_flags/store/modules/helpers';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
INTERNAL_ID_PREFIX,
DEFAULT_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants';
import { featureFlag } from '../mock_data';
describe('feature flag form', () => {
let wrapper;
......@@ -76,7 +82,7 @@ describe('feature flag form', () => {
expect(wrapper.vm.formScopes.length).toEqual(1);
expect(wrapper.vm.formScopes[0].active).toEqual(true);
expect(wrapper.vm.formScopes[0].environment_scope).toEqual('');
expect(wrapper.vm.formScopes[0].environmentScope).toEqual('');
expect(wrapper.vm.newScope).toEqual('');
});
......@@ -89,29 +95,26 @@ describe('feature flag form', () => {
beforeEach(() => {
factory({
...requiredProps,
name: 'feature_flag_1',
description: 'this is a feature flag',
name: featureFlag.name,
description: featureFlag.description,
scopes: [
{
environment_scope: 'production',
active: false,
can_update: true,
protected: true,
id: 2,
},
{
environment_scope: 'review',
id: 1,
active: true,
can_update: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
id: 4,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '54',
},
{
environment_scope: 'staging',
id: 2,
active: true,
can_update: false,
environmentScope: 'scope',
canUpdate: false,
protected: true,
id: 5,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '54',
},
],
});
......@@ -129,33 +132,9 @@ describe('feature flag form', () => {
describe('update scope', () => {
describe('on click on toggle', () => {
it('should update the scope', () => {
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(wrapper.vm.formScopes).toEqual([
{
active: true,
environment_scope: 'production',
id: 2,
can_update: true,
protected: true,
},
{
active: true,
environment_scope: 'review',
id: 4,
can_update: true,
protected: false,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
]);
wrapper.find(ToggleButton).vm.$emit('change', false);
expect(wrapper.vm.newScope).toEqual('');
expect(_.first(wrapper.vm.formScopes).active).toBe(false);
});
});
});
......@@ -165,50 +144,12 @@ describe('feature flag form', () => {
wrapper.find('.js-delete-scope').trigger('click');
});
it('should add `_destroy` key the clicked scope', () => {
expect(wrapper.vm.formScopes).toEqual([
{
environment_scope: 'production',
active: false,
_destroy: true,
id: 2,
can_update: true,
protected: true,
},
{
active: true,
environment_scope: 'review',
id: 4,
can_update: true,
protected: false,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
]);
it('should add `shouldBeDestroyed` key the clicked scope', () => {
expect(_.first(wrapper.vm.formScopes).shouldBeDestroyed).toBe(true);
});
it('should not render deleted scopes', () => {
expect(wrapper.vm.filteredScopes).toEqual([
{
active: true,
environment_scope: 'review',
id: 4,
can_update: true,
protected: false,
},
{
environment_scope: 'staging',
active: true,
can_update: false,
protected: true,
id: 5,
},
]);
expect(wrapper.vm.filteredScopes).toEqual([jasmine.objectContaining({ id: 2 })]);
});
});
......@@ -220,11 +161,17 @@ describe('feature flag form', () => {
description: 'this is a feature flag',
scopes: [
{
environment_scope: 'new_scope',
environmentScope: 'new_scope',
active: false,
id: _.uniqueId(internalKeyID),
can_update: true,
id: _.uniqueId(INTERNAL_ID_PREFIX),
canUpdate: true,
protected: false,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
},
],
});
......@@ -243,8 +190,11 @@ describe('feature flag form', () => {
description: 'this is a feature flag',
scopes: [
{
environment_scope: '*',
environmentScope: '*',
active: false,
canUpdate: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
},
],
});
......@@ -269,7 +219,7 @@ describe('feature flag form', () => {
});
it('should have the scope that cannot be updated be disabled', () => {
const row = wrapper.findAll('.gl-responsive-table-row').wrappers[3];
const row = wrapper.findAll('.gl-responsive-table-row').at(2);
expect(row.find(EnvironmentsDropdown).vm.disabled).toBe(true);
expect(row.find(ToggleButton).vm.disabledInput).toBe(true);
......@@ -279,23 +229,37 @@ describe('feature flag form', () => {
});
describe('on submit', () => {
beforeEach(() => {
const selectFirstRolloutStrategyOption = dropdownIndex => {
wrapper
.findAll('select.js-rollout-strategy')
.at(dropdownIndex)
.findAll('option')
.at(1)
.setSelected();
};
beforeEach(done => {
factory({
...requiredProps,
name: 'feature_flag_1',
description: 'this is a feature flag',
scopes: [
{
environment_scope: 'production',
can_update: true,
id: 1,
environmentScope: 'production',
canUpdate: true,
protected: true,
active: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
},
],
});
wrapper.vm.$nextTick(done, done.fail);
});
it('should emit handleSubmit with the updated data', () => {
it('should emit handleSubmit with the updated data', done => {
wrapper.find('#feature-flag-name').setValue('feature_flag_2');
wrapper
......@@ -308,19 +272,62 @@ describe('feature flag form', () => {
.find(ToggleButton)
.vm.$emit('change', true);
wrapper.vm.handleSubmit();
wrapper.find(ToggleButton).vm.$emit('change', true);
wrapper.vm
.$nextTick()
.then(() => {
selectFirstRolloutStrategyOption(0);
selectFirstRolloutStrategyOption(2);
return wrapper.vm.$nextTick();
})
.then(() => {
wrapper.find('.js-rollout-percentage').setValue('55');
return wrapper.vm.$nextTick();
})
.then(() => {
wrapper.find({ ref: 'submitButton' }).trigger('click');
const data = wrapper.emitted().handleSubmit[0][0];
expect(data.name).toEqual('feature_flag_2');
expect(data.description).toEqual('this is a feature flag');
expect(data.scopes.length).toEqual(3);
expect(data.scopes[0]).toEqual({
active: false,
environment_scope: 'production',
can_update: true,
expect(data.scopes).toEqual([
{
id: 1,
active: true,
environmentScope: 'production',
canUpdate: true,
protected: true,
});
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '55',
},
{
id: jasmine.any(String),
active: false,
environmentScope: 'review',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
},
{
id: jasmine.any(String),
active: true,
environmentScope: '',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
},
]);
})
.then(done)
.catch(done.fail);
});
});
});
......
......@@ -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: [
strategies: [
{
id: 1,
active: true,
environment_scope: '*',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
{
id: 2,
active: false,
environment_scope: 'production',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
],
},
{
id: 3,
active: false,
environment_scope: 'review/*',
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: [
{
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