Commit 251ffe30 authored by Amy Troschinetz's avatar Amy Troschinetz

Support flexible rollout strategy in the UX

**doc/operations/feature_flags.md:**

Adds documentation for this new feature.

**app/assets/javascripts/feature_flags/constants.js:**

Adds flexible rollout strategy.

**app/assets/javascripts/feature_flags/utils.js:**

Adds flexible rollout strategy.

**app/assets/javascripts/feature_flags/components/strategy.vue:**

- Removes some cruft.
- Adds a Tip to suggest using flexible rollout instead of users rollout.
- Moves form description elements to the correct location.

**app/assets/javascripts/feature_flags/components/
strategy_parameters.vue:**

Adds flexible rollout strategy.

**app/assets/javascripts/feature_flags/components/strategies/
flexible_rollout.vue:**

Implementation of the new flexible rollout strategy.

**app/assets/javascripts/feature_flags/components/strategies/
gitlab_user_list.vue:**

Don't show description when there's an error.

**app/assets/javascripts/feature_flags/components/strategies/
percent_rollout.vue:**

Default to 100%.

**spec/frontend/feature_flags/mock_data.js:**

Adds flexible rollout strategy.

**spec/frontend/feature_flags/components/strategy_spec.js:**

Adds flexible rollout strategy.

**spec/frontend/feature_flags/components/strategies/
flexible_rollout_spec.js:**

Spec tests for the new flexible rollout strategy.

**locale/gitlab.pot:**

I18n updates.

**changelogs/unreleased/feature-flags-flexible-rollout-ux.yml:**

Changelog.
parent f9c4b9d8
<script>
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import { __ } from '~/locale';
import { PERCENT_ROLLOUT_GROUP_ID } from '../../constants';
import ParameterFormGroup from './parameter_form_group.vue';
export default {
components: {
GlFormInput,
GlFormSelect,
ParameterFormGroup,
},
props: {
strategy: {
required: true,
type: Object,
},
},
i18n: {
percentageDescription: __('Enter a whole number between 0 and 100'),
percentageInvalid: __('Percent rollout must be a whole number between 0 and 100'),
percentageLabel: __('Percentage'),
stickinessDescription: __('Consistency guarantee method'),
stickinessLabel: __('Based on'),
},
stickinessOptions: [
{
value: 'DEFAULT',
text: __('Available ID'),
},
{
value: 'USERID',
text: __('User ID'),
},
{
value: 'SESSIONID',
text: __('Session ID'),
},
{
value: 'RANDOM',
text: __('Random'),
},
],
computed: {
isValid() {
const percentageNum = Number(this.percentage);
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
},
percentage() {
return this.strategy?.parameters?.rollout ?? '100';
},
stickiness() {
return this.strategy?.parameters?.stickiness ?? this.$options.stickinessOptions[0].value;
},
},
methods: {
onPercentageChange(value) {
this.$emit('change', {
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
rollout: value,
stickiness: this.stickiness,
},
});
},
onStickinessChange(value) {
this.$emit('change', {
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
rollout: this.percentage,
stickiness: value,
},
});
},
},
};
</script>
<template>
<div class="gl-display-flex">
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-percentage">
<parameter-form-group
:label="$options.i18n.percentageLabel"
:description="isValid ? $options.i18n.percentageDescription : ''"
:invalid-feedback="$options.i18n.percentageInvalid"
:state="isValid"
>
<template #default="{ inputId }">
<div class="gl-display-flex gl-align-items-center">
<gl-form-input
:id="inputId"
:value="percentage"
:state="isValid"
class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
</div>
</template>
</parameter-form-group>
</div>
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-stickiness">
<parameter-form-group
:label="$options.i18n.stickinessLabel"
:description="$options.i18n.stickinessDescription"
>
<template #default="{ inputId }">
<gl-form-select
:id="inputId"
:value="stickiness"
:options="$options.stickinessOptions"
@change="onStickinessChange"
/>
</template>
</parameter-form-group>
</div>
</div>
</template>
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
:state="hasUserLists" :state="hasUserLists"
:invalid-feedback="$options.translations.rolloutUserListNoListError" :invalid-feedback="$options.translations.rolloutUserListNoListError"
:label="$options.translations.rolloutUserListLabel" :label="$options.translations.rolloutUserListLabel"
:description="$options.translations.rolloutUserListDescription" :description="hasUserLists ? $options.translations.rolloutUserListDescription : ''"
> >
<template #default="{ inputId }"> <template #default="{ inputId }">
<gl-form-select <gl-form-select
......
...@@ -15,7 +15,7 @@ export default { ...@@ -15,7 +15,7 @@ export default {
type: Object, type: Object,
}, },
}, },
translations: { i18n: {
rolloutPercentageDescription: __('Enter a whole number between 0 and 100'), rolloutPercentageDescription: __('Enter a whole number between 0 and 100'),
rolloutPercentageInvalid: s__( rolloutPercentageInvalid: s__(
'FeatureFlags|Percent rollout must be a whole number between 0 and 100', 'FeatureFlags|Percent rollout must be a whole number between 0 and 100',
...@@ -24,10 +24,11 @@ export default { ...@@ -24,10 +24,11 @@ export default {
}, },
computed: { computed: {
isValid() { isValid() {
return Number(this.percentage) >= 0 && Number(this.percentage) <= 100; const percentageNum = Number(this.percentage);
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
}, },
percentage() { percentage() {
return this.strategy?.parameters?.percentage ?? ''; return this.strategy?.parameters?.percentage ?? '100';
}, },
}, },
methods: { methods: {
...@@ -44,9 +45,9 @@ export default { ...@@ -44,9 +45,9 @@ export default {
</script> </script>
<template> <template>
<parameter-form-group <parameter-form-group
:label="$options.translations.rolloutPercentageLabel" :label="$options.i18n.rolloutPercentageLabel"
:description="$options.translations.rolloutPercentageDescription" :description="isValid ? $options.i18n.rolloutPercentageDescription : ''"
:invalid-feedback="$options.translations.rolloutPercentageInvalid" :invalid-feedback="$options.i18n.rolloutPercentageInvalid"
:state="isValid" :state="isValid"
> >
<template #default="{ inputId }"> <template #default="{ inputId }">
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui'; import { GlAlert, GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { EMPTY_PARAMETERS, STRATEGY_SELECTIONS } from '../constants'; import {
EMPTY_PARAMETERS,
STRATEGY_SELECTIONS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
} from '../constants';
import NewEnvironmentsDropdown from './new_environments_dropdown.vue'; import NewEnvironmentsDropdown from './new_environments_dropdown.vue';
import StrategyParameters from './strategy_parameters.vue'; import StrategyParameters from './strategy_parameters.vue';
export default { export default {
components: { components: {
GlAlert,
GlButton, GlButton,
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
...@@ -51,13 +56,13 @@ export default { ...@@ -51,13 +56,13 @@ export default {
i18n: { i18n: {
allEnvironments: __('All environments'), allEnvironments: __('All environments'),
environmentsLabel: __('Environments'), environmentsLabel: __('Environments'),
rolloutUserListLabel: s__('FeatureFlag|List'), strategyTypeDescription: __('Select strategy activation method'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
strategyTypeDescription: __('Select strategy activation method.'),
strategyTypeLabel: s__('FeatureFlag|Type'), strategyTypeLabel: s__('FeatureFlag|Type'),
environmentsSelectDescription: s__( environmentsSelectDescription: s__(
'FeatureFlag|Select the environment scope for this feature flag.', 'FeatureFlag|Select the environment scope for this feature flag',
),
considerFlexibleRollout: s__(
'FeatureFlags|Consider using the more flexible "Percent rollout" strategy instead.',
), ),
}, },
...@@ -85,6 +90,9 @@ export default { ...@@ -85,6 +90,9 @@ export default {
filteredEnvironments() { filteredEnvironments() {
return this.environments.filter(e => !e.shouldBeDestroyed); return this.environments.filter(e => !e.shouldBeDestroyed);
}, },
isPercentUserRollout() {
return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
},
}, },
methods: { methods: {
addEnvironment(environment) { addEnvironment(environment) {
...@@ -121,14 +129,21 @@ export default { ...@@ -121,14 +129,21 @@ export default {
}; };
</script> </script>
<template> <template>
<div>
<gl-alert v-if="isPercentUserRollout" variant="tip" :dismissible="false">
{{ $options.i18n.considerFlexibleRollout }}
</gl-alert>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6"> <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap"> <div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
<div class="mr-5"> <div class="mr-5">
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId"> <gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
<p class="gl-display-inline-block ">{{ $options.i18n.strategyTypeDescription }}</p> <template #description>
{{ $options.i18n.strategyTypeDescription }}
<gl-link :href="strategyTypeDocsPagePath" target="_blank"> <gl-link :href="strategyTypeDocsPagePath" target="_blank">
<gl-icon name="question" /> <gl-icon name="question" />
</gl-link> </gl-link>
</template>
<gl-form-select <gl-form-select
:id="strategyTypeId" :id="strategyTypeId"
:value="formStrategy.name" :value="formStrategy.name"
...@@ -157,13 +172,10 @@ export default { ...@@ -157,13 +172,10 @@ export default {
/> />
</div> </div>
</div> </div>
<label class="gl-display-block" :for="environmentsDropdownId">{{ <label class="gl-display-block" :for="environmentsDropdownId">{{
$options.i18n.environmentsLabel $options.i18n.environmentsLabel
}}</label> }}</label>
<p class="gl-display-inline-block">{{ $options.i18n.environmentsSelectDescription }}</p>
<gl-link :href="environmentsScopeDocsPath" target="_blank">
<gl-icon name="question" />
</gl-link>
<div class="gl-display-flex gl-flex-direction-column"> <div class="gl-display-flex gl-flex-direction-column">
<div <div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center" class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
...@@ -189,5 +201,12 @@ export default { ...@@ -189,5 +201,12 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<span class="gl-display-inline-block gl-py-3">
{{ $options.i18n.environmentsSelectDescription }}
</span>
<gl-link :href="environmentsScopeDocsPath" target="_blank">
<gl-icon name="question" />
</gl-link>
</div>
</div> </div>
</template> </template>
<script> <script>
import { import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST, ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '../constants'; } from '../constants';
import Default from './strategies/default.vue'; import Default from './strategies/default.vue';
import FlexibleRollout from './strategies/flexible_rollout.vue';
import PercentRollout from './strategies/percent_rollout.vue'; import PercentRollout from './strategies/percent_rollout.vue';
import UsersWithId from './strategies/users_with_id.vue'; import UsersWithId from './strategies/users_with_id.vue';
import GitlabUserList from './strategies/gitlab_user_list.vue'; import GitlabUserList from './strategies/gitlab_user_list.vue';
const STRATEGIES = Object.freeze({ const STRATEGIES = Object.freeze({
[ROLLOUT_STRATEGY_ALL_USERS]: Default, [ROLLOUT_STRATEGY_ALL_USERS]: Default,
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: FlexibleRollout,
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: PercentRollout, [ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: PercentRollout,
[ROLLOUT_STRATEGY_USER_ID]: UsersWithId, [ROLLOUT_STRATEGY_USER_ID]: UsersWithId,
[ROLLOUT_STRATEGY_GITLAB_USER_LIST]: GitlabUserList, [ROLLOUT_STRATEGY_GITLAB_USER_LIST]: GitlabUserList,
......
...@@ -3,6 +3,7 @@ import { s__ } from '~/locale'; ...@@ -3,6 +3,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_FLEXIBLE_ROLLOUT = 'flexibleRollout';
export const ROLLOUT_STRATEGY_USER_ID = 'userWithId'; export const ROLLOUT_STRATEGY_USER_ID = 'userWithId';
export const ROLLOUT_STRATEGY_GITLAB_USER_LIST = 'gitlabUserList'; export const ROLLOUT_STRATEGY_GITLAB_USER_LIST = 'gitlabUserList';
...@@ -34,6 +35,10 @@ export const STRATEGY_SELECTIONS = [ ...@@ -34,6 +35,10 @@ export const STRATEGY_SELECTIONS = [
value: ROLLOUT_STRATEGY_ALL_USERS, value: ROLLOUT_STRATEGY_ALL_USERS,
text: s__('FeatureFlags|All users'), text: s__('FeatureFlags|All users'),
}, },
{
value: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
text: s__('FeatureFlags|Percent rollout'),
},
{ {
value: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, value: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
text: s__('FeatureFlags|Percent of users'), text: s__('FeatureFlags|Percent of users'),
......
...@@ -2,6 +2,7 @@ import { s__, n__, sprintf } from '~/locale'; ...@@ -2,6 +2,7 @@ import { s__, n__, sprintf } from '~/locale';
import { import {
ALL_ENVIRONMENTS_NAME, ALL_ENVIRONMENTS_NAME,
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST, ROLLOUT_STRATEGY_GITLAB_USER_LIST,
...@@ -12,6 +13,23 @@ const badgeTextByType = { ...@@ -12,6 +13,23 @@ const badgeTextByType = {
name: s__('FeatureFlags|All Users'), name: s__('FeatureFlags|All Users'),
parameters: null, parameters: null,
}, },
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: {
name: s__('FeatureFlags|Percent rollout'),
parameters: ({ parameters: { rollout, stickiness } }) => {
switch (stickiness) {
case 'USERID':
return sprintf(s__('FeatureFlags|%{percent} by user ID'), { percent: `${rollout}%` });
case 'SESSIONID':
return sprintf(s__('FeatureFlags|%{percent} by session ID'), { percent: `${rollout}%` });
case 'RANDOM':
return sprintf(s__('FeatureFlags|%{percent} randomly'), { percent: `${rollout}%` });
default:
return sprintf(s__('FeatureFlags|%{percent} by available ID'), {
percent: `${rollout}%`,
});
}
},
},
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: { [ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: {
name: s__('FeatureFlags|Percent of users'), name: s__('FeatureFlags|Percent of users'),
parameters: ({ parameters: { percentage } }) => `${percentage}%`, parameters: ({ parameters: { percentage } }) => `${percentage}%`,
......
---
title: Adds flexible rollout strategy UX and documentation
merge_request: 43611
author:
type: added
...@@ -87,12 +87,49 @@ and clicking **{pencil}** (edit). ...@@ -87,12 +87,49 @@ and clicking **{pencil}** (edit).
Enables the feature for all users. It uses the [`default`](https://unleash.github.io/docs/activation_strategy#default) Enables the feature for all users. It uses the [`default`](https://unleash.github.io/docs/activation_strategy#default)
Unleash activation strategy. Unleash activation strategy.
### Percent Rollout
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43340) in GitLab 13.5.
Enables the feature for a percentage of page views, with configurable consistency
of behavior. This consistency is also known as stickiness. It uses the
[`flexibleRollout`](https://unleash.github.io/docs/activation_strategy#flexiblerollout)
Unleash activation strategy.
You can configure the consistency to be based on:
- **User IDs**: Each user ID has a consistent behavior, ignoring session IDs.
- **Session IDs**: Each session ID has a consistent behavior, ignoring user IDs.
- **Random**: Consistent behavior is not guaranteed. The feature is enabled for the
selected percentage of page views randomly. User IDs and session IDs are ignored.
- **Available ID**: Consistent behavior is attempted based on the status of the user:
- If the user is logged in, make behavior consistent based on user ID.
- If the user is anonymous, make the behavior consistent based on the session ID.
- If there is no user ID or session ID, then the feature is enabled for the selected
percentage of page view randomly.
For example, set a value of 15% based on **Available ID** to enable the feature for 15% of page views. For
authenticated users this is based on their user ID. For anonymous users with a session ID it would be based on their
session ID instead as they do not have a user ID. Then if no session ID is provided, it falls back to random.
The rollout percentage can be from 0% to 100%.
Selecting a consistency based on User IDs functions the same as the [percent of Users](#percent-of-users) rollout.
CAUTION: **Caution:**
Selecting **Random** provides inconsistent application behavior for individual users.
### Percent of Users ### Percent of Users
Enables the feature for a percentage of authenticated users. It uses the Enables the feature for a percentage of authenticated users. It uses the
[`gradualRolloutUserId`](https://unleash.github.io/docs/activation_strategy#gradualrolloutuserid) [`gradualRolloutUserId`](https://unleash.github.io/docs/activation_strategy#gradualrolloutuserid)
Unleash activation strategy. Unleash activation strategy.
NOTE: **Note:**
[Percent rollout](#percent-rollout) with a consistency based on **User IDs** has the same
behavior. It is recommended to use percent rollout instead of percent of users as
it is more flexible.
For example, set a value of 15% to enable the feature for 15% of authenticated users. For example, set a value of 15% to enable the feature for 15% of authenticated users.
The rollout percentage can be from 0% to 100%. The rollout percentage can be from 0% to 100%.
......
...@@ -3897,6 +3897,9 @@ msgstr "" ...@@ -3897,6 +3897,9 @@ msgstr ""
msgid "Available" msgid "Available"
msgstr "" msgstr ""
msgid "Available ID"
msgstr ""
msgid "Available Runners: %{runners}" msgid "Available Runners: %{runners}"
msgstr "" msgstr ""
...@@ -4038,6 +4041,9 @@ msgstr "" ...@@ -4038,6 +4041,9 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo." msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "" msgstr ""
msgid "Based on"
msgstr ""
msgid "Be careful. Changing the project's namespace can have unintended side effects." msgid "Be careful. Changing the project's namespace can have unintended side effects."
msgstr "" msgstr ""
...@@ -6878,6 +6884,9 @@ msgstr "" ...@@ -6878,6 +6884,9 @@ msgstr ""
msgid "Connection timeout" msgid "Connection timeout"
msgstr "" msgstr ""
msgid "Consistency guarantee method"
msgstr ""
msgid "Contact sales to upgrade" msgid "Contact sales to upgrade"
msgstr "" msgstr ""
...@@ -11023,6 +11032,18 @@ msgid_plural "FeatureFlags|%d users" ...@@ -11023,6 +11032,18 @@ msgid_plural "FeatureFlags|%d users"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "FeatureFlags|%{percent} by available ID"
msgstr ""
msgid "FeatureFlags|%{percent} by session ID"
msgstr ""
msgid "FeatureFlags|%{percent} by user ID"
msgstr ""
msgid "FeatureFlags|%{percent} randomly"
msgstr ""
msgid "FeatureFlags|* (All Environments)" msgid "FeatureFlags|* (All Environments)"
msgstr "" msgstr ""
...@@ -11053,6 +11074,9 @@ msgstr "" ...@@ -11053,6 +11074,9 @@ msgstr ""
msgid "FeatureFlags|Configure feature flags" msgid "FeatureFlags|Configure feature flags"
msgstr "" msgstr ""
msgid "FeatureFlags|Consider using the more flexible \"Percent rollout\" strategy instead."
msgstr ""
msgid "FeatureFlags|Create feature flag" msgid "FeatureFlags|Create feature flag"
msgstr "" msgstr ""
...@@ -11170,6 +11194,9 @@ msgstr "" ...@@ -11170,6 +11194,9 @@ msgstr ""
msgid "FeatureFlags|Percent of users" msgid "FeatureFlags|Percent of users"
msgstr "" msgstr ""
msgid "FeatureFlags|Percent rollout"
msgstr ""
msgid "FeatureFlags|Percent rollout (logged in users)" msgid "FeatureFlags|Percent rollout (logged in users)"
msgstr "" msgstr ""
...@@ -11233,7 +11260,7 @@ msgstr "" ...@@ -11233,7 +11260,7 @@ msgstr ""
msgid "FeatureFlag|Select a user list" msgid "FeatureFlag|Select a user list"
msgstr "" msgstr ""
msgid "FeatureFlag|Select the environment scope for this feature flag." msgid "FeatureFlag|Select the environment scope for this feature flag"
msgstr "" msgstr ""
msgid "FeatureFlag|There are no configured user lists" msgid "FeatureFlag|There are no configured user lists"
...@@ -18752,6 +18779,9 @@ msgstr "" ...@@ -18752,6 +18779,9 @@ msgstr ""
msgid "People without permission will never get a notification." msgid "People without permission will never get a notification."
msgstr "" msgstr ""
msgid "Percent rollout must be a whole number between 0 and 100"
msgstr ""
msgid "Percentage" msgid "Percentage"
msgstr "" msgstr ""
...@@ -21233,6 +21263,9 @@ msgstr "" ...@@ -21233,6 +21263,9 @@ msgstr ""
msgid "Rake Tasks Help" msgid "Rake Tasks Help"
msgstr "" msgstr ""
msgid "Random"
msgstr ""
msgid "Raw blob request rate limit per minute" msgid "Raw blob request rate limit per minute"
msgstr "" msgstr ""
...@@ -23299,7 +23332,7 @@ msgstr "" ...@@ -23299,7 +23332,7 @@ msgstr ""
msgid "Select status" msgid "Select status"
msgstr "" msgstr ""
msgid "Select strategy activation method." msgid "Select strategy activation method"
msgstr "" msgstr ""
msgid "Select subscription" msgid "Select subscription"
...@@ -23521,6 +23554,9 @@ msgstr "" ...@@ -23521,6 +23554,9 @@ msgstr ""
msgid "Service URL" msgid "Service URL"
msgstr "" msgstr ""
msgid "Session ID"
msgstr ""
msgid "Session duration (minutes)" msgid "Session duration (minutes)"
msgstr "" msgstr ""
...@@ -28165,6 +28201,9 @@ msgstr "" ...@@ -28165,6 +28201,9 @@ msgstr ""
msgid "User %{username} was successfully removed." msgid "User %{username} was successfully removed."
msgstr "" msgstr ""
msgid "User ID"
msgstr ""
msgid "User OAuth applications" msgid "User OAuth applications"
msgstr "" msgstr ""
......
import { mount } from '@vue/test-utils';
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import FlexibleRollout from '~/feature_flags/components/strategies/flexible_rollout.vue';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
import { PERCENT_ROLLOUT_GROUP_ID } from '~/feature_flags/constants';
import { flexibleRolloutStrategy } from '../../mock_data';
const DEFAULT_PROPS = {
strategy: flexibleRolloutStrategy,
};
describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
let wrapper;
let percentageFormGroup;
let percentageInput;
let stickinessFormGroup;
let stickinessSelect;
const factory = (props = {}) =>
mount(FlexibleRollout, { propsData: { ...DEFAULT_PROPS, ...props } });
afterEach(() => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = null;
});
describe('with valid percentage', () => {
beforeEach(() => {
wrapper = factory();
percentageFormGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
.find(ParameterFormGroup);
percentageInput = percentageFormGroup.find(GlFormInput);
stickinessFormGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-stickiness"]')
.find(ParameterFormGroup);
stickinessSelect = stickinessFormGroup.find(GlFormSelect);
});
it('displays the current percentage value', () => {
expect(percentageInput.element.value).toBe(flexibleRolloutStrategy.parameters.rollout);
});
it('displays the current stickiness value', () => {
expect(stickinessSelect.element.value).toBe(flexibleRolloutStrategy.parameters.stickiness);
});
it('emits a change when the percentage value changes', async () => {
percentageInput.setValue('75');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('change')).toEqual([
[
{
parameters: {
rollout: '75',
groupId: PERCENT_ROLLOUT_GROUP_ID,
stickiness: flexibleRolloutStrategy.parameters.stickiness,
},
},
],
]);
});
it('emits a change when the stickiness value changes', async () => {
stickinessSelect.setValue('USERID');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('change')).toEqual([
[
{
parameters: {
rollout: flexibleRolloutStrategy.parameters.rollout,
groupId: PERCENT_ROLLOUT_GROUP_ID,
stickiness: 'USERID',
},
},
],
]);
});
it('does not show errors', () => {
expect(percentageFormGroup.attributes('state')).toBe('true');
});
});
describe('with percentage that is out of range', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { rollout: '101' } } });
});
it('shows errors', () => {
const formGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
.find(ParameterFormGroup);
expect(formGroup.attributes('state')).toBeUndefined();
});
});
describe('with percentage that is not a whole number', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { rollout: '3.14' } } });
});
it('shows errors', () => {
const formGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
.find(ParameterFormGroup);
expect(formGroup.attributes('state')).toBeUndefined();
});
});
});
...@@ -62,4 +62,17 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { ...@@ -62,4 +62,17 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
expect(formGroup.attributes('state')).toBeUndefined(); expect(formGroup.attributes('state')).toBeUndefined();
}); });
}); });
describe('with percentage that is not a whole number', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { percentage: '3.14' } } });
input = wrapper.find(GlFormInput);
formGroup = wrapper.find(ParameterFormGroup);
});
it('shows errors', () => {
expect(formGroup.attributes('state')).toBeUndefined();
});
});
}); });
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { last } from 'lodash'; import { last } from 'lodash';
import { GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui'; import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
import { import {
PERCENT_ROLLOUT_GROUP_ID, PERCENT_ROLLOUT_GROUP_ID,
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST, ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '~/feature_flags/constants'; } from '~/feature_flags/constants';
...@@ -66,6 +67,7 @@ describe('Feature flags strategy', () => { ...@@ -66,6 +67,7 @@ describe('Feature flags strategy', () => {
name name
${ROLLOUT_STRATEGY_ALL_USERS} ${ROLLOUT_STRATEGY_ALL_USERS}
${ROLLOUT_STRATEGY_PERCENT_ROLLOUT} ${ROLLOUT_STRATEGY_PERCENT_ROLLOUT}
${ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT}
${ROLLOUT_STRATEGY_USER_ID} ${ROLLOUT_STRATEGY_USER_ID}
${ROLLOUT_STRATEGY_GITLAB_USER_LIST} ${ROLLOUT_STRATEGY_GITLAB_USER_LIST}
`('with strategy $name', ({ name }) => { `('with strategy $name', ({ name }) => {
...@@ -91,6 +93,26 @@ describe('Feature flags strategy', () => { ...@@ -91,6 +93,26 @@ describe('Feature flags strategy', () => {
}); });
}); });
describe('with the gradualRolloutByUserId strategy', () => {
let strategy;
beforeEach(() => {
strategy = {
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: { percentage: '50', groupId: 'default' },
scopes: [{ environmentScope: 'production' }],
};
const propsData = { strategy, index: 0, endpoint: '' };
factory({ propsData, provide });
});
it('shows an alert asking users to consider using flexibleRollout instead', () => {
expect(wrapper.find(GlAlert).text()).toContain(
'Consider using the more flexible "Percent rollout" strategy instead.',
);
});
});
describe('with a strategy', () => { describe('with a strategy', () => {
describe('with a single environment scope defined', () => { describe('with a single environment scope defined', () => {
let strategy; let strategy;
......
import { import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_GITLAB_USER_LIST, ROLLOUT_STRATEGY_GITLAB_USER_LIST,
ROLLOUT_STRATEGY_USER_ID, ROLLOUT_STRATEGY_USER_ID,
} from '~/feature_flags/constants'; } from '~/feature_flags/constants';
...@@ -78,6 +79,24 @@ export const featureFlag = { ...@@ -78,6 +79,24 @@ export const featureFlag = {
}, },
], ],
}, },
{
id: 5,
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_FLEXIBLE_ROLLOUT,
parameters: {
rollout: '42',
stickiness: 'DEFAULT',
},
},
],
},
], ],
}; };
...@@ -117,6 +136,12 @@ export const percentRolloutStrategy = { ...@@ -117,6 +136,12 @@ export const percentRolloutStrategy = {
scopes: [], scopes: [],
}; };
export const flexibleRolloutStrategy = {
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
parameters: { rollout: '50', groupId: 'default', stickiness: 'DEFAULT' },
scopes: [],
};
export const usersWithIdStrategy = { export const usersWithIdStrategy = {
name: ROLLOUT_STRATEGY_USER_ID, name: ROLLOUT_STRATEGY_USER_ID,
parameters: { userIds: '1,2,3' }, parameters: { userIds: '1,2,3' },
......
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