diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:55:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:55:51 +0300 |
commit | e8d2c2579383897a1dd7f9debd359abe8ae8373d (patch) | |
tree | c42be41678c2586d49a75cabce89322082698334 /app/assets/javascripts/feature_flags | |
parent | fc845b37ec3a90aaa719975f607740c22ba6a113 (diff) |
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/feature_flags')
14 files changed, 50 insertions, 721 deletions
diff --git a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue index 77e40039b43..d86e13ce722 100644 --- a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue +++ b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue @@ -196,6 +196,7 @@ export default { /> <gl-loading-icon v-if="isRotating" + size="sm" class="gl-absolute gl-align-self-center gl-right-5 gl-mr-7" /> diff --git a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue index e7f4b51c964..dde021b67be 100644 --- a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue +++ b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue @@ -1,10 +1,8 @@ <script> import { GlAlert, GlLoadingIcon, GlToggle } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; -import axios from '~/lib/utils/axios_utils'; import { sprintf, s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { LEGACY_FLAG } from '../constants'; import FeatureFlagForm from './form.vue'; export default { @@ -15,59 +13,29 @@ export default { FeatureFlagForm, }, mixins: [glFeatureFlagMixin()], - inject: { - showUserCallout: {}, - userCalloutId: { - default: '', - }, - userCalloutsPath: { - default: '', - }, - }, - data() { - return { - userShouldSeeNewFlagAlert: this.showUserCallout, - }; - }, - translations: { - legacyReadOnlyFlagAlert: s__( - 'FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag.', - ), - }, computed: { ...mapState([ 'path', 'error', 'name', 'description', - 'scopes', 'strategies', 'isLoading', 'hasError', 'iid', 'active', - 'version', ]), title() { return this.iid ? `^${this.iid} ${this.name}` : sprintf(s__('Edit %{name}'), { name: this.name }); }, - deprecated() { - return this.version === LEGACY_FLAG; - }, }, created() { return this.fetchFeatureFlag(); }, methods: { ...mapActions(['updateFeatureFlag', 'fetchFeatureFlag', 'toggleActive']), - dismissNewVersionFlagAlert() { - this.userShouldSeeNewFlagAlert = false; - axios.post(this.userCalloutsPath, { - feature_name: this.userCalloutId, - }); - }, }, }; </script> @@ -76,9 +44,6 @@ export default { <gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-7" /> <template v-else-if="!isLoading && !hasError"> - <gl-alert v-if="deprecated" variant="warning" :dismissible="false" class="gl-my-5">{{ - $options.translations.legacyReadOnlyFlagAlert - }}</gl-alert> <div class="gl-display-flex gl-align-items-center gl-mb-4 gl-mt-4"> <gl-toggle :value="active" @@ -100,12 +65,10 @@ export default { <feature-flag-form :name="name" :description="description" - :scopes="scopes" :strategies="strategies" :cancel-path="path" :submit-text="__('Save changes')" :active="active" - :version="version" @handleSubmit="(data) => updateFeatureFlag(data)" /> </template> diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue index d08e8d2b3a1..53909dcf42e 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue @@ -3,11 +3,8 @@ import { GlAlert, GlBadge, GlButton, GlModalDirective, GlSprintf } from '@gitlab import { isEmpty } from 'lodash'; import { mapState, mapActions } from 'vuex'; -import { - buildUrlWithCurrentLocation, - getParameterByName, - historyPushState, -} from '~/lib/utils/common_utils'; +import { buildUrlWithCurrentLocation, historyPushState } from '~/lib/utils/common_utils'; +import { getParameterByName } from '~/lib/utils/url_utility'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue'; import EmptyState from './empty_state.vue'; diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue index 9220077af71..cfd838bf5a1 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue @@ -1,8 +1,7 @@ <script> -import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle, GlIcon } from '@gitlab/ui'; +import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT, NEW_VERSION_FLAG, LEGACY_FLAG } from '../constants'; import { labelForStrategy } from '../utils'; export default { @@ -14,7 +13,6 @@ export default { components: { GlBadge, GlButton, - GlIcon, GlModal, GlToggle, }, @@ -35,13 +33,7 @@ export default { deleteFeatureFlagName: null, }; }, - translations: { - legacyFlagReadOnlyAlert: s__('FeatureFlags|Flag is read-only'), - }, computed: { - permissions() { - return this.glFeatures.featureFlagPermissions; - }, modalTitle() { return sprintf(s__('FeatureFlags|Delete %{name}?'), { name: this.deleteFeatureFlagName, @@ -57,12 +49,6 @@ export default { }, }, methods: { - isLegacyFlag(flag) { - return flag.version !== NEW_VERSION_FLAG; - }, - statusToggleDisabled(flag) { - return flag.version === LEGACY_FLAG; - }, scopeTooltipText(scope) { return !scope.active ? sprintf(s__('FeatureFlags|Inactive flag for %{scope}'), { @@ -70,22 +56,6 @@ export default { }) : ''; }, - 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}`; - }, - badgeVariant(scope) { - return scope.active ? 'info' : 'muted'; - }, strategyBadgeText(strategy) { return labelForStrategy(strategy); }, @@ -142,7 +112,6 @@ export default { <gl-toggle v-if="featureFlag.update_path" :value="featureFlag.active" - :disabled="statusToggleDisabled(featureFlag)" :label="$options.i18n.toggleLabel" label-position="hidden" data-testid="feature-flag-status-toggle" @@ -169,12 +138,6 @@ export default { <div class="feature-flag-name text-monospace text-truncate"> {{ featureFlag.name }} </div> - <gl-icon - v-if="isLegacyFlag(featureFlag)" - v-gl-tooltip.hover="$options.translations.legacyFlagReadOnlyAlert" - class="gl-ml-3" - name="information-o" - /> </div> <div class="feature-flag-description text-secondary text-truncate"> {{ featureFlag.description }} @@ -189,27 +152,14 @@ export default { <div class="table-mobile-content d-flex flex-wrap justify-content-end justify-content-md-start js-feature-flag-environments" > - <template v-if="isLegacyFlag(featureFlag)"> - <gl-badge - v-for="scope in featureFlag.scopes" - :key="scope.id" - v-gl-tooltip.hover="scopeTooltipText(scope)" - :variant="badgeVariant(scope)" - :data-qa-selector="`feature-flag-scope-${badgeVariant(scope)}-badge`" - class="gl-mr-3 gl-mt-2" - >{{ badgeText(scope) }}</gl-badge - > - </template> - <template v-else> - <gl-badge - v-for="strategy in featureFlag.strategies" - :key="strategy.id" - data-testid="strategy-badge" - variant="info" - class="gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left gl-px-5" - >{{ strategyBadgeText(strategy) }}</gl-badge - > - </template> + <gl-badge + v-for="strategy in featureFlag.strategies" + :key="strategy.id" + data-testid="strategy-badge" + variant="info" + class="gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left gl-px-5" + >{{ strategyBadgeText(strategy) }}</gl-badge + > </div> </div> diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue index 67ddceaf080..f7ad2c1f106 100644 --- a/app/assets/javascripts/feature_flags/components/form.vue +++ b/app/assets/javascripts/feature_flags/components/form.vue @@ -1,16 +1,6 @@ <script> -import { - GlButton, - GlBadge, - GlTooltip, - GlTooltipDirective, - GlFormTextarea, - GlFormCheckbox, - GlSprintf, - GlIcon, - GlToggle, -} from '@gitlab/ui'; -import { memoize, isString, cloneDeep, isNumber, uniqueId } from 'lodash'; +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'; @@ -20,12 +10,8 @@ import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_USER_ID, ALL_ENVIRONMENTS_NAME, - INTERNAL_ID_PREFIX, NEW_VERSION_FLAG, - LEGACY_FLAG, } from '../constants'; -import { createNewEnvironmentScope } from '../store/helpers'; -import EnvironmentsDropdown from './environments_dropdown.vue'; import Strategy from './strategy.vue'; export default { @@ -35,20 +21,9 @@ export default { }, components: { GlButton, - GlBadge, - GlFormTextarea, - GlFormCheckbox, - GlTooltip, - GlSprintf, - GlIcon, - GlToggle, - EnvironmentsDropdown, Strategy, RelatedIssuesRoot, }, - directives: { - GlTooltip: GlTooltipDirective, - }, mixins: [featureFlagsMixin()], inject: { featureFlagIssuesEndpoint: { @@ -71,11 +46,6 @@ export default { required: false, default: '', }, - scopes: { - type: Array, - required: false, - default: () => [], - }, cancelPath: { type: String, required: true, @@ -89,11 +59,6 @@ export default { required: false, default: () => [], }, - version: { - type: String, - required: false, - default: LEGACY_FLAG, - }, }, translations: { allEnvironmentsText: s__('FeatureFlags|* (All Environments)'), @@ -120,35 +85,18 @@ export default { formName: this.name, formDescription: this.description, - // operate on a clone to avoid mutating props - formScopes: this.scopes.map((s) => ({ ...s })), formStrategies: cloneDeep(this.strategies), newScope: '', }; }, computed: { - filteredScopes() { - return this.formScopes.filter((scope) => !scope.shouldBeDestroyed); - }, filteredStrategies() { return this.formStrategies.filter((s) => !s.shouldBeDestroyed); }, - canUpdateFlag() { - return !this.permissionsFlag || (this.formScopes || []).every((scope) => scope.canUpdate); - }, - permissionsFlag() { - return this.glFeatures.featureFlagPermissions; - }, - supportsStrategies() { - return this.version === NEW_VERSION_FLAG; - }, showRelatedIssues() { return this.featureFlagIssuesEndpoint.length > 0; }, - readOnly() { - return this.version === LEGACY_FLAG; - }, }, methods: { keyFor(strategy) { @@ -174,37 +122,6 @@ export default { isAllEnvironment(name) { return name === ALL_ENVIRONMENTS_NAME; }, - - /** - * When the user clicks the remove button we delete the scope - * - * 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 {Object} scope - */ - removeScope(scope) { - if (isString(scope.id) && scope.id.startsWith(INTERNAL_ID_PREFIX)) { - this.formScopes = this.formScopes.filter((s) => s !== scope); - } else { - Vue.set(scope, 'shouldBeDestroyed', true); - } - }, - - /** - * Creates a new scope and adds it to the list of scopes - * - * @param overrides An object whose properties will - * be used override the default scope options - */ - createNewScope(overrides) { - this.formScopes.push(createNewEnvironmentScope(overrides, this.permissionsFlag)); - this.newScope = ''; - }, - /** * When the user clicks the submit button * it triggers an event with the form data @@ -214,61 +131,16 @@ export default { name: this.formName, description: this.formDescription, active: this.active, - version: this.version, + version: NEW_VERSION_FLAG, + strategies: this.formStrategies, }; - if (this.version === LEGACY_FLAG) { - flag.scopes = this.formScopes; - } else { - flag.strategies = this.formStrategies; - } - this.$emit('handleSubmit', flag); }, - canUpdateScope(scope) { - 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}`; - }, - rolloutUserId(index) { - return `rollout-user-id-${index}`; - }, - - shouldDisplayIncludeUserIds(scope) { - return ![ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_USER_ID].includes( - scope.rolloutStrategy, - ); - }, - shouldDisplayUserIds(scope) { - return scope.rolloutStrategy === ROLLOUT_STRATEGY_USER_ID || scope.shouldIncludeUserIds; - }, - onStrategyChange(index) { - const scope = this.filteredScopes[index]; - scope.shouldIncludeUserIds = - scope.rolloutUserIds.length > 0 && - scope.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT; - }, onFormStrategyChange(strategy, index) { Object.assign(this.filteredStrategies[index], strategy); }, @@ -281,12 +153,7 @@ export default { <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" - :disabled="!canUpdateFlag" - class="form-control" - /> + <input id="feature-flag-name" v-model="formName" class="form-control" /> </div> </div> @@ -298,7 +165,6 @@ export default { <textarea id="feature-flag-description" v-model="formDescription" - :disabled="!canUpdateFlag" class="form-control" rows="4" ></textarea> @@ -312,277 +178,35 @@ export default { :show-categorized-issues="false" /> - <template v-if="supportsStrategies"> - <div class="row"> - <div class="col-md-12"> - <h4>{{ s__('FeatureFlags|Strategies') }}</h4> - <div class="flex align-items-baseline justify-content-between"> - <p class="mr-3">{{ $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="flex justify-content-center border-top py-4 w-100"> - <span>{{ $options.translations.noStrategiesText }}</span> - </div> - </template> - - <div v-else class="row"> - <div class="form-group col-md-12"> - <h4>{{ s__('FeatureFlags|Target environments') }}</h4> - <gl-sprintf :message="$options.translations.helpText"> - <template #code="{ content }"> - <code>{{ content }}</code> - </template> - <template #bold="{ content }"> - <b>{{ content }}</b> - </template> - </gl-sprintf> - - <div class="js-scopes-table gl-mt-3"> - <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-30" role="columnheader"> - {{ s__('FeatureFlags|Environment Spec') }} - </div> - <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 - v-for="(scope, index) in filteredScopes" - :key="scope.id" - ref="scopeRow" - class="gl-responsive-table-row" - role="row" - > - <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 gl-display-flex gl-align-items-center gl-justify-content-start" - > - <p v-if="isAllEnvironment(scope.environmentScope)" class="js-scope-all pl-3"> - {{ $options.translations.allEnvironmentsText }} - </p> - - <environments-dropdown - v-else - class="col-12" - :value="scope.environmentScope" - :disabled="!canUpdateScope(scope) || scope.environmentScope !== ''" - @selectEnvironment="(env) => (scope.environmentScope = env)" - @createClicked="(env) => (scope.environmentScope = env)" - @clearInput="(env) => (scope.environmentScope = '')" - /> - - <gl-badge v-if="permissionsFlag && scope.protected" variant="success"> - {{ s__('FeatureFlags|Protected') }} - </gl-badge> - </div> - </div> - - <div class="table-section section-20 text-center" role="gridcell"> - <div class="table-mobile-header" role="rowheader"> - {{ $options.i18n.statusLabel }} - </div> - <div class="table-mobile-content gl-display-flex gl-justify-content-center"> - <gl-toggle - :value="scope.active" - :disabled="!active || !canUpdateScope(scope)" - :label="$options.i18n.statusLabel" - label-position="hidden" - @change="(status) => (scope.active = status)" - /> - </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="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" - @change="onStrategyChange(index)" - > - <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> - <option :value="$options.ROLLOUT_STRATEGY_USER_ID"> - {{ s__('FeatureFlags|User IDs') }} - </option> - </select> - <gl-icon - name="chevron-down" - class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500" - :size="16" - /> - </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="gl-w-9"> - <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 an integer number between 0 and 100', - ) - }} - </gl-tooltip> - <span class="ml-1">%</span> - </div> - <div class="d-flex flex-column align-items-start mt-2 w-100"> - <gl-form-checkbox - v-if="shouldDisplayIncludeUserIds(scope)" - v-model="scope.shouldIncludeUserIds" - >{{ s__('FeatureFlags|Include additional user IDs') }}</gl-form-checkbox - > - <template v-if="shouldDisplayUserIds(scope)"> - <label :for="rolloutUserId(index)" class="mb-2"> - {{ s__('FeatureFlags|User IDs') }} - </label> - <gl-form-textarea - :id="rolloutUserId(index)" - v-model="scope.rolloutUserIds" - class="w-100" - /> - </template> - </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"> - <gl-button - v-if="!isAllEnvironment(scope.environmentScope) && canUpdateScope(scope)" - v-gl-tooltip - :title="$options.i18n.removeLabel" - :aria-label="$options.i18n.removeLabel" - class="js-delete-scope btn-transparent pr-3 pl-3" - icon="clear" - data-testid="feature-flag-delete" - @click="removeScope(scope)" - /> - </div> - </div> - </div> - - <div class="gl-responsive-table-row" role="row" data-testid="add-new-scope"> - <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"> - <environments-dropdown - class="js-new-scope-name col-12" - :value="newScope" - @selectEnvironment="(env) => createNewScope({ environmentScope: env })" - @createClicked="(env) => createNewScope({ environmentScope: env })" - /> - </div> - </div> - - <div class="table-section section-20 text-center" role="gridcell"> - <div class="table-mobile-header" role="rowheader"> - {{ $options.i18n.statusLabel }} - </div> - <div class="table-mobile-content gl-display-flex gl-justify-content-center"> - <gl-toggle - :disabled="!active" - :label="$options.i18n.statusLabel" - label-position="hidden" - :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> - <gl-icon - name="chevron-down" - class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500" - :size="16" - /> - </div> - </div> - </div> - </div> + <div class="row"> + <div class="col-md-12"> + <h4>{{ s__('FeatureFlags|Strategies') }}</h4> + <div class="flex align-items-baseline justify-content-between"> + <p class="mr-3">{{ $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="flex justify-content-center border-top py-4 w-100"> + <span>{{ $options.translations.noStrategiesText }}</span> + </div> </fieldset> <div class="form-actions"> <gl-button ref="submitButton" - :disabled="readOnly" type="button" variant="confirm" class="js-ff-submit col-xs-12" diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue index c59e3178b09..5575c6567b5 100644 --- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue +++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue @@ -80,7 +80,7 @@ export default { @focus="fetchEnvironments" @keyup="fetchEnvironments" /> - <gl-loading-icon v-if="isLoading" /> + <gl-loading-icon v-if="isLoading" size="sm" /> <gl-dropdown-item v-for="environment in results" v-else-if="results.length" diff --git a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue index 19be57f9d27..865c1e677cd 100644 --- a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue +++ b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue @@ -1,10 +1,8 @@ <script> import { GlAlert } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; -import axios from '~/lib/utils/axios_utils'; import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { NEW_VERSION_FLAG, ROLLOUT_STRATEGY_ALL_USERS } from '../constants'; -import { createNewEnvironmentScope } from '../store/helpers'; +import { ROLLOUT_STRATEGY_ALL_USERS } from '../constants'; import FeatureFlagForm from './form.vue'; export default { @@ -13,48 +11,14 @@ export default { GlAlert, }, mixins: [featureFlagsMixin()], - inject: { - showUserCallout: {}, - userCalloutId: { - default: '', - }, - userCalloutsPath: { - default: '', - }, - }, - data() { - return { - userShouldSeeNewFlagAlert: this.showUserCallout, - }; - }, computed: { ...mapState(['error', 'path']), - scopes() { - return [ - createNewEnvironmentScope( - { - environmentScope: '*', - active: true, - }, - this.glFeatures.featureFlagsPermissions, - ), - ]; - }, - version() { - return NEW_VERSION_FLAG; - }, strategies() { return [{ name: ROLLOUT_STRATEGY_ALL_USERS, parameters: {}, scopes: [] }]; }, }, methods: { ...mapActions(['createFeatureFlag']), - dismissNewVersionFlagAlert() { - this.userShouldSeeNewFlagAlert = false; - axios.post(this.userCalloutsPath, { - feature_name: this.userCalloutId, - }); - }, }, }; </script> @@ -69,9 +33,7 @@ export default { <feature-flag-form :cancel-path="path" :submit-text="s__('FeatureFlags|Create feature flag')" - :scopes="scopes" :strategies="strategies" - :version="version" @handleSubmit="(data) => createFeatureFlag(data)" /> </div> 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 45fc37da747..9dbffe75f6b 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 @@ -76,7 +76,7 @@ export default { @focus="fetchUserLists" @keyup="fetchUserLists" /> - <gl-loading-icon v-if="isLoading" /> + <gl-loading-icon v-if="isLoading" size="sm" /> <gl-dropdown-item v-for="list in userLists" :key="list.id" diff --git a/app/assets/javascripts/feature_flags/edit.js b/app/assets/javascripts/feature_flags/edit.js index 010674592f8..98dee7c7e97 100644 --- a/app/assets/javascripts/feature_flags/edit.js +++ b/app/assets/javascripts/feature_flags/edit.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import { parseBoolean } from '~/lib/utils/common_utils'; import EditFeatureFlag from './components/edit_feature_flag.vue'; import createStore from './store/edit'; @@ -16,9 +15,6 @@ export default () => { environmentsEndpoint, projectId, featureFlagIssuesEndpoint, - userCalloutsPath, - userCalloutId, - showUserCallout, } = el.dataset; return new Vue({ @@ -30,9 +26,6 @@ export default () => { environmentsEndpoint, projectId, featureFlagIssuesEndpoint, - userCalloutsPath, - userCalloutId, - showUserCallout: parseBoolean(showUserCallout), }, render(createElement) { return createElement(EditFeatureFlag); diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js index 54c7e8c4453..8656479190a 100644 --- a/app/assets/javascripts/feature_flags/store/edit/actions.js +++ b/app/assets/javascripts/feature_flags/store/edit/actions.js @@ -2,8 +2,7 @@ import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; -import { NEW_VERSION_FLAG } from '../../constants'; -import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers'; +import { mapStrategiesToRails } from '../helpers'; import * as types from './mutation_types'; /** @@ -19,12 +18,7 @@ export const updateFeatureFlag = ({ state, dispatch }, params) => { dispatch('requestUpdateFeatureFlag'); axios - .put( - state.endpoint, - params.version === NEW_VERSION_FLAG - ? mapStrategiesToRails(params) - : mapFromScopesViewModel(params), - ) + .put(state.endpoint, mapStrategiesToRails(params)) .then(() => { dispatch('receiveUpdateFeatureFlagSuccess'); visitUrl(state.path); diff --git a/app/assets/javascripts/feature_flags/store/edit/mutations.js b/app/assets/javascripts/feature_flags/store/edit/mutations.js index 0a610f4b395..3882cb2dfff 100644 --- a/app/assets/javascripts/feature_flags/store/edit/mutations.js +++ b/app/assets/javascripts/feature_flags/store/edit/mutations.js @@ -1,5 +1,5 @@ import { LEGACY_FLAG } from '../../constants'; -import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers'; +import { mapStrategiesToViewModel } from '../helpers'; import * as types from './mutation_types'; export default { @@ -14,7 +14,6 @@ export default { state.description = response.description; state.iid = response.iid; state.active = response.active; - state.scopes = mapToScopesViewModel(response.scopes); state.strategies = mapStrategiesToViewModel(response.strategies); state.version = response.version || LEGACY_FLAG; }, diff --git a/app/assets/javascripts/feature_flags/store/helpers.js b/app/assets/javascripts/feature_flags/store/helpers.js index 2fa20e25f4e..300709f2771 100644 --- a/app/assets/javascripts/feature_flags/store/helpers.js +++ b/app/assets/javascripts/feature_flags/store/helpers.js @@ -1,149 +1,4 @@ -import { isEmpty, uniqueId, isString } from 'lodash'; -import { - ROLLOUT_STRATEGY_ALL_USERS, - ROLLOUT_STRATEGY_PERCENT_ROLLOUT, - ROLLOUT_STRATEGY_USER_ID, - ROLLOUT_STRATEGY_GITLAB_USER_LIST, - INTERNAL_ID_PREFIX, - DEFAULT_PERCENT_ROLLOUT, - PERCENT_ROLLOUT_GROUP_ID, - fetchPercentageParams, - fetchUserIdParams, - LEGACY_FLAG, -} from '../constants'; - -/** - * 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 percentStrategy = (s.strategies || []).find( - (strat) => strat.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT, - ); - - const rolloutPercentage = fetchPercentageParams(percentStrategy) || DEFAULT_PERCENT_ROLLOUT; - - const userStrategy = (s.strategies || []).find( - (strat) => strat.name === ROLLOUT_STRATEGY_USER_ID, - ); - - const rolloutStrategy = - (percentStrategy && percentStrategy.name) || - (userStrategy && userStrategy.name) || - ROLLOUT_STRATEGY_ALL_USERS; - - const rolloutUserIds = (fetchUserIdParams(userStrategy) || '') - .split(',') - .filter((id) => id) - .join(', '); - - return { - id: s.id, - environmentScope: s.environment_scope, - active: Boolean(s.active), - canUpdate: Boolean(s.can_update), - protected: Boolean(s.protected), - rolloutStrategy, - rolloutPercentage, - rolloutUserIds, - - // eslint-disable-next-line no-underscore-dangle - shouldBeDestroyed: Boolean(s._destroy), - shouldIncludeUserIds: rolloutUserIds.length > 0 && percentStrategy !== null, - }; - }); -/** - * 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; - } else if (s.rolloutStrategy === ROLLOUT_STRATEGY_USER_ID) { - parameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ','); - } - - const userIdParameters = {}; - - if (s.shouldIncludeUserIds && s.rolloutStrategy !== ROLLOUT_STRATEGY_USER_ID) { - userIdParameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ','); - } - - // Strip out any internal IDs - const id = isString(s.id) && s.id.startsWith(INTERNAL_ID_PREFIX) ? undefined : s.id; - - const strategies = [ - { - name: s.rolloutStrategy, - parameters, - }, - ]; - - if (!isEmpty(userIdParameters)) { - strategies.push({ name: ROLLOUT_STRATEGY_USER_ID, parameters: userIdParameters }); - } - - return { - id, - environment_scope: s.environmentScope, - active: s.active, - can_update: s.canUpdate, - protected: s.protected, - _destroy: s.shouldBeDestroyed, - strategies, - }; - }); - - const model = { - operations_feature_flag: { - name: params.name, - description: params.description, - active: params.active, - scopes_attributes: scopes, - version: LEGACY_FLAG, - }, - }; - - return model; -}; - -/** - * 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 = {}, featureFlagPermissions = false) => { - const defaultScope = { - environmentScope: '', - active: false, - id: uniqueId(INTERNAL_ID_PREFIX), - rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, - rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, - rolloutUserIds: '', - }; - - const newScope = { - ...defaultScope, - ...overrides, - }; - - if (featureFlagPermissions) { - newScope.canUpdate = true; - newScope.protected = false; - } - - return newScope; -}; +import { ROLLOUT_STRATEGY_GITLAB_USER_LIST, NEW_VERSION_FLAG } from '../constants'; const mapStrategyScopesToRails = (scopes) => scopes.length === 0 @@ -206,8 +61,8 @@ export const mapStrategiesToRails = (params) => ({ operations_feature_flag: { name: params.name, description: params.description, - version: params.version, active: params.active, strategies_attributes: (params.strategies || []).map(mapStrategyToRails), + version: NEW_VERSION_FLAG, }, }); diff --git a/app/assets/javascripts/feature_flags/store/index/mutations.js b/app/assets/javascripts/feature_flags/store/index/mutations.js index 54e48a4b80c..7e08440c299 100644 --- a/app/assets/javascripts/feature_flags/store/index/mutations.js +++ b/app/assets/javascripts/feature_flags/store/index/mutations.js @@ -1,10 +1,7 @@ import Vue from 'vue'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; -import { mapToScopesViewModel } from '../helpers'; import * as types from './mutation_types'; -const mapFlag = (flag) => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) }); - const updateFlag = (state, flag) => { const index = state.featureFlags.findIndex(({ id }) => id === flag.id); Vue.set(state.featureFlags, index, flag); @@ -31,7 +28,7 @@ export default { [types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) { state.isLoading = false; state.hasError = false; - state.featureFlags = (response.data.feature_flags || []).map(mapFlag); + state.featureFlags = response.data.feature_flags || []; const paginationInfo = createPaginationInfo(response.headers); state.count = paginationInfo?.total ?? state.featureFlags.length; @@ -58,7 +55,7 @@ export default { updateFlag(state, flag); }, [types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](state, data) { - updateFlag(state, mapFlag(data)); + updateFlag(state, data); }, [types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](state, i) { const flag = state.featureFlags.find(({ id }) => i === id); diff --git a/app/assets/javascripts/feature_flags/store/new/actions.js b/app/assets/javascripts/feature_flags/store/new/actions.js index d0a1c77a69e..dc3f7a21cdb 100644 --- a/app/assets/javascripts/feature_flags/store/new/actions.js +++ b/app/assets/javascripts/feature_flags/store/new/actions.js @@ -1,7 +1,6 @@ import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; -import { NEW_VERSION_FLAG } from '../../constants'; -import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers'; +import { mapStrategiesToRails } from '../helpers'; import * as types from './mutation_types'; /** @@ -17,12 +16,7 @@ export const createFeatureFlag = ({ state, dispatch }, params) => { dispatch('requestCreateFeatureFlag'); return axios - .post( - state.endpoint, - params.version === NEW_VERSION_FLAG - ? mapStrategiesToRails(params) - : mapFromScopesViewModel(params), - ) + .post(state.endpoint, mapStrategiesToRails(params)) .then(() => { dispatch('receiveCreateFeatureFlagSuccess'); visitUrl(state.path); |