summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/feature_flags
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/feature_flags')
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue121
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue13
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy.vue155
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy_parameters.vue3
-rw-r--r--app/assets/javascripts/feature_flags/constants.js5
-rw-r--r--app/assets/javascripts/feature_flags/utils.js18
7 files changed, 242 insertions, 75 deletions
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
new file mode 100644
index 00000000000..020a0d43096
--- /dev/null
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -0,0 +1,121 @@
+<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>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
index b13bd86e900..ec97e8b1350 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
@@ -49,7 +49,7 @@ export default {
:state="hasUserLists"
:invalid-feedback="$options.translations.rolloutUserListNoListError"
:label="$options.translations.rolloutUserListLabel"
- :description="$options.translations.rolloutUserListDescription"
+ :description="hasUserLists ? $options.translations.rolloutUserListDescription : ''"
>
<template #default="{ inputId }">
<gl-form-select
diff --git a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
index 9311589c364..d262769c891 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
@@ -15,7 +15,7 @@ export default {
type: Object,
},
},
- translations: {
+ i18n: {
rolloutPercentageDescription: __('Enter a whole number between 0 and 100'),
rolloutPercentageInvalid: s__(
'FeatureFlags|Percent rollout must be a whole number between 0 and 100',
@@ -24,10 +24,11 @@ export default {
},
computed: {
isValid() {
- return Number(this.percentage) >= 0 && Number(this.percentage) <= 100;
+ const percentageNum = Number(this.percentage);
+ return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
},
percentage() {
- return this.strategy?.parameters?.percentage ?? '';
+ return this.strategy?.parameters?.percentage ?? '100';
},
},
methods: {
@@ -44,9 +45,9 @@ export default {
</script>
<template>
<parameter-form-group
- :label="$options.translations.rolloutPercentageLabel"
- :description="$options.translations.rolloutPercentageDescription"
- :invalid-feedback="$options.translations.rolloutPercentageInvalid"
+ :label="$options.i18n.rolloutPercentageLabel"
+ :description="isValid ? $options.i18n.rolloutPercentageDescription : ''"
+ :invalid-feedback="$options.i18n.rolloutPercentageInvalid"
:state="isValid"
>
<template #default="{ inputId }">
diff --git a/app/assets/javascripts/feature_flags/components/strategy.vue b/app/assets/javascripts/feature_flags/components/strategy.vue
index c83e2c897e3..ae559a4c9e3 100644
--- a/app/assets/javascripts/feature_flags/components/strategy.vue
+++ b/app/assets/javascripts/feature_flags/components/strategy.vue
@@ -1,15 +1,20 @@
<script>
import Vue from 'vue';
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 { 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 StrategyParameters from './strategy_parameters.vue';
export default {
components: {
+ GlAlert,
GlButton,
GlFormGroup,
GlFormSelect,
@@ -51,13 +56,13 @@ export default {
i18n: {
allEnvironments: __('All environments'),
environmentsLabel: __('Environments'),
- rolloutUserListLabel: s__('FeatureFlag|List'),
- rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
- rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
- strategyTypeDescription: __('Select strategy activation method.'),
+ strategyTypeDescription: __('Select strategy activation method'),
strategyTypeLabel: s__('FeatureFlag|Type'),
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 {
filteredEnvironments() {
return this.environments.filter(e => !e.shouldBeDestroyed);
},
+ isPercentUserRollout() {
+ return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
+ },
},
methods: {
addEnvironment(environment) {
@@ -121,73 +129,84 @@ export default {
};
</script>
<template>
- <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="mr-5">
- <gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
- <p class="gl-display-inline-block ">{{ $options.i18n.strategyTypeDescription }}</p>
- <gl-link :href="strategyTypeDocsPagePath" target="_blank">
- <gl-icon name="question" />
- </gl-link>
- <gl-form-select
- :id="strategyTypeId"
- :value="formStrategy.name"
- :options="$options.strategies"
- @change="onStrategyTypeChange"
+ <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-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
+ <div class="mr-5">
+ <gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
+ <template #description>
+ {{ $options.i18n.strategyTypeDescription }}
+ <gl-link :href="strategyTypeDocsPagePath" target="_blank">
+ <gl-icon name="question" />
+ </gl-link>
+ </template>
+ <gl-form-select
+ :id="strategyTypeId"
+ :value="formStrategy.name"
+ :options="$options.strategies"
+ @change="onStrategyTypeChange"
+ />
+ </gl-form-group>
+ </div>
+
+ <div data-testid="strategy">
+ <strategy-parameters
+ :strategy="strategy"
+ :user-lists="userLists"
+ @change="onStrategyChange"
/>
- </gl-form-group>
- </div>
+ </div>
- <div data-testid="strategy">
- <strategy-parameters
- :strategy="strategy"
- :user-lists="userLists"
- @change="onStrategyChange"
- />
+ <div
+ class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
+ >
+ <gl-button
+ data-testid="delete-strategy-button"
+ variant="danger"
+ icon="remove"
+ @click="$emit('delete')"
+ />
+ </div>
</div>
- <div
- class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
- >
- <gl-button
- data-testid="delete-strategy-button"
- variant="danger"
- icon="remove"
- @click="$emit('delete')"
- />
- </div>
- </div>
- <label class="gl-display-block" :for="environmentsDropdownId">{{
- $options.i18n.environmentsLabel
- }}</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 gl-md-flex-direction-row align-items-start gl-md-align-items-center"
- >
- <new-environments-dropdown
- :id="environmentsDropdownId"
- :endpoint="endpoint"
- class="gl-mr-3"
- @add="addEnvironment"
- />
- <span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
- {{ $options.i18n.allEnvironments }}
- </span>
- <div v-else class="gl-display-flex gl-align-items-center">
- <gl-token
- v-for="environment in filteredEnvironments"
- :key="environment.id"
- class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
- @close="removeScope(environment)"
- >
- {{ environment.environmentScope }}
- </gl-token>
+ <label class="gl-display-block" :for="environmentsDropdownId">{{
+ $options.i18n.environmentsLabel
+ }}</label>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
+ >
+ <new-environments-dropdown
+ :id="environmentsDropdownId"
+ :endpoint="endpoint"
+ class="gl-mr-3"
+ @add="addEnvironment"
+ />
+ <span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
+ {{ $options.i18n.allEnvironments }}
+ </span>
+ <div v-else class="gl-display-flex gl-align-items-center">
+ <gl-token
+ v-for="environment in filteredEnvironments"
+ :key="environment.id"
+ class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
+ @close="removeScope(environment)"
+ >
+ {{ environment.environmentScope }}
+ </gl-token>
+ </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>
</template>
diff --git a/app/assets/javascripts/feature_flags/components/strategy_parameters.vue b/app/assets/javascripts/feature_flags/components/strategy_parameters.vue
index 6953095daff..b6e06880315 100644
--- a/app/assets/javascripts/feature_flags/components/strategy_parameters.vue
+++ b/app/assets/javascripts/feature_flags/components/strategy_parameters.vue
@@ -1,18 +1,21 @@
<script>
import {
ROLLOUT_STRATEGY_ALL_USERS,
+ ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '../constants';
import Default from './strategies/default.vue';
+import FlexibleRollout from './strategies/flexible_rollout.vue';
import PercentRollout from './strategies/percent_rollout.vue';
import UsersWithId from './strategies/users_with_id.vue';
import GitlabUserList from './strategies/gitlab_user_list.vue';
const STRATEGIES = Object.freeze({
[ROLLOUT_STRATEGY_ALL_USERS]: Default,
+ [ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: FlexibleRollout,
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: PercentRollout,
[ROLLOUT_STRATEGY_USER_ID]: UsersWithId,
[ROLLOUT_STRATEGY_GITLAB_USER_LIST]: GitlabUserList,
diff --git a/app/assets/javascripts/feature_flags/constants.js b/app/assets/javascripts/feature_flags/constants.js
index 79bd6d8fe43..4843eca149a 100644
--- a/app/assets/javascripts/feature_flags/constants.js
+++ b/app/assets/javascripts/feature_flags/constants.js
@@ -3,6 +3,7 @@ import { s__ } from '~/locale';
export const ROLLOUT_STRATEGY_ALL_USERS = 'default';
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_GITLAB_USER_LIST = 'gitlabUserList';
@@ -35,6 +36,10 @@ export const STRATEGY_SELECTIONS = [
text: s__('FeatureFlags|All users'),
},
{
+ value: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
+ text: s__('FeatureFlags|Percent rollout'),
+ },
+ {
value: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
text: s__('FeatureFlags|Percent of users'),
},
diff --git a/app/assets/javascripts/feature_flags/utils.js b/app/assets/javascripts/feature_flags/utils.js
index ccb6ac17792..24c570657e6 100644
--- a/app/assets/javascripts/feature_flags/utils.js
+++ b/app/assets/javascripts/feature_flags/utils.js
@@ -2,6 +2,7 @@ import { s__, n__, sprintf } from '~/locale';
import {
ALL_ENVIRONMENTS_NAME,
ROLLOUT_STRATEGY_ALL_USERS,
+ ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
@@ -12,6 +13,23 @@ const badgeTextByType = {
name: s__('FeatureFlags|All Users'),
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]: {
name: s__('FeatureFlags|Percent of users'),
parameters: ({ parameters: { percentage } }) => `${percentage}%`,