gitlab-org--gitlab-foss/app/assets/javascripts/feature_flags/components/form.vue

228 lines
6.4 KiB
Vue

<script>
import { GlButton } from '@gitlab/ui';
import { memoize, cloneDeep, isNumber, uniqueId } from 'lodash';
import Vue from 'vue';
import { s__ } from '~/locale';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ALL_ENVIRONMENTS_NAME,
NEW_VERSION_FLAG,
} from '../constants';
import Strategy from './strategy.vue';
export default {
i18n: {
removeLabel: s__('FeatureFlags|Remove'),
statusLabel: s__('FeatureFlags|Status'),
},
components: {
GlButton,
Strategy,
RelatedIssuesRoot,
},
mixins: [featureFlagsMixin()],
inject: {
featureFlagIssuesEndpoint: {
default: '',
},
},
props: {
active: {
type: Boolean,
required: false,
default: true,
},
name: {
type: String,
required: false,
default: '',
},
description: {
type: String,
required: false,
default: '',
},
cancelPath: {
type: String,
required: true,
},
submitText: {
type: String,
required: true,
},
strategies: {
type: Array,
required: false,
default: () => [],
},
},
translations: {
allEnvironmentsText: s__('FeatureFlags|* (All Environments)'),
helpText: 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}.',
),
newHelpText: s__(
'FeatureFlags|Enable features for specific users and environments by configuring feature flag strategies.',
),
noStrategiesText: s__('FeatureFlags|Feature Flag has no strategies'),
},
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
// Matches numbers 0 through 100
rolloutPercentageRegex: /^[0-9]$|^[1-9][0-9]$|^100$/,
data() {
return {
formName: this.name,
formDescription: this.description,
formStrategies: cloneDeep(this.strategies),
newScope: '',
};
},
computed: {
filteredStrategies() {
return this.formStrategies.filter((s) => !s.shouldBeDestroyed);
},
showRelatedIssues() {
return Boolean(this.featureFlagIssuesEndpoint);
},
},
methods: {
keyFor(strategy) {
if (strategy.id) {
return strategy.id;
}
return uniqueId('strategy_');
},
addStrategy() {
this.formStrategies.push({ name: ROLLOUT_STRATEGY_ALL_USERS, parameters: {}, scopes: [] });
},
deleteStrategy(s) {
if (isNumber(s.id)) {
Vue.set(s, 'shouldBeDestroyed', true);
} else {
this.formStrategies = this.formStrategies.filter((strategy) => strategy !== s);
}
},
isAllEnvironment(name) {
return name === ALL_ENVIRONMENTS_NAME;
},
/**
* When the user clicks the submit button
* it triggers an event with the form data
*/
handleSubmit() {
const flag = {
name: this.formName,
description: this.formDescription,
active: this.active,
version: NEW_VERSION_FLAG,
strategies: this.formStrategies,
};
this.$emit('handleSubmit', flag);
},
isRolloutPercentageInvalid: memoize(function isRolloutPercentageInvalid(percentage) {
return !this.$options.rolloutPercentageRegex.test(percentage);
}),
onFormStrategyChange(strategy, index) {
const currentUserListId = this.filteredStrategies[index]?.userList?.id;
const newUserListId = strategy?.userList?.id;
Object.assign(this.filteredStrategies[index], strategy);
if (currentUserListId !== newUserListId) {
this.formStrategies = [...this.formStrategies];
}
},
},
};
</script>
<template>
<form class="feature-flags-form">
<fieldset>
<div class="row">
<div class="form-group col-md-4">
<label for="feature-flag-name" class="label-bold">{{ s__('FeatureFlags|Name') }} *</label>
<input id="feature-flag-name" v-model="formName" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4">
<label for="feature-flag-description" class="label-bold">
{{ s__('FeatureFlags|Description') }}
</label>
<textarea
id="feature-flag-description"
v-model="formDescription"
class="form-control"
rows="4"
></textarea>
</div>
</div>
<related-issues-root
v-if="showRelatedIssues"
:endpoint="featureFlagIssuesEndpoint"
:can-admin="true"
:show-categorized-issues="false"
/>
<div class="row">
<div class="col-md-12">
<h4>{{ s__('FeatureFlags|Strategies') }}</h4>
<div class="gl-display-flex gl-align-items-baseline gl-justify-content-space-between">
<p class="gl-mr-5">{{ $options.translations.newHelpText }}</p>
<gl-button variant="confirm" category="secondary" @click="addStrategy">
{{ s__('FeatureFlags|Add strategy') }}
</gl-button>
</div>
</div>
</div>
<div v-if="filteredStrategies.length > 0" data-testid="feature-flag-strategies">
<strategy
v-for="(strategy, index) in filteredStrategies"
:key="keyFor(strategy)"
:strategy="strategy"
:index="index"
@change="onFormStrategyChange($event, index)"
@delete="deleteStrategy(strategy)"
/>
</div>
<div v-else class="gl-display-flex gl-justify-content-center gl-border-t gl-py-6 w-100">
<span>{{ $options.translations.noStrategiesText }}</span>
</div>
</fieldset>
<div class="gl-mr-6">
<gl-button
ref="submitButton"
type="button"
variant="confirm"
class="js-ff-submit gl-mr-2"
@click="handleSubmit"
>{{ submitText }}</gl-button
>
<gl-button :href="cancelPath" class="js-ff-cancel">
{{ __('Cancel') }}
</gl-button>
</div>
</form>
</template>