diff options
Diffstat (limited to 'app/assets/javascripts/feature_flags')
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}%`, |