diff options
Diffstat (limited to 'app/assets/javascripts/feature_flags/components/feature_flags_table.vue')
-rw-r--r-- | app/assets/javascripts/feature_flags/components/feature_flags_table.vue | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue new file mode 100644 index 00000000000..54d038606f4 --- /dev/null +++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue @@ -0,0 +1,271 @@ +<script> +import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle, GlIcon } from '@gitlab/ui'; +import { sprintf, s__ } 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 { + components: { + GlBadge, + GlButton, + GlIcon, + GlModal, + GlToggle, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [glFeatureFlagMixin()], + props: { + featureFlags: { + type: Array, + required: true, + }, + }, + inject: ['csrfToken'], + data() { + return { + deleteFeatureFlagUrl: null, + deleteFeatureFlagName: null, + }; + }, + translations: { + legacyFlagAlert: s__('FeatureFlags|Flag becomes read only soon'), + legacyFlagReadOnlyAlert: s__('FeatureFlags|Flag is read-only'), + }, + computed: { + permissions() { + return this.glFeatures.featureFlagPermissions; + }, + isNewVersionFlagsEnabled() { + return this.glFeatures.featureFlagsNewVersion; + }, + isLegacyReadOnlyFlagsEnabled() { + return ( + this.glFeatures.featureFlagsLegacyReadOnly && + !this.glFeatures.featureFlagsLegacyReadOnlyOverride + ); + }, + modalTitle() { + return sprintf(s__('FeatureFlags|Delete %{name}?'), { + name: this.deleteFeatureFlagName, + }); + }, + deleteModalMessage() { + return sprintf(s__('FeatureFlags|Feature flag %{name} will be removed. Are you sure?'), { + name: this.deleteFeatureFlagName, + }); + }, + modalId() { + return 'delete-feature-flag'; + }, + legacyFlagToolTipText() { + const { legacyFlagReadOnlyAlert, legacyFlagAlert } = this.$options.translations; + + return this.isLegacyReadOnlyFlagsEnabled ? legacyFlagReadOnlyAlert : legacyFlagAlert; + }, + }, + methods: { + isLegacyFlag(flag) { + return !this.isNewVersionFlagsEnabled || flag.version !== NEW_VERSION_FLAG; + }, + statusToggleDisabled(flag) { + return this.isLegacyReadOnlyFlagsEnabled && flag.version === LEGACY_FLAG; + }, + scopeTooltipText(scope) { + return !scope.active + ? sprintf(s__('FeatureFlags|Inactive flag for %{scope}'), { + scope: scope.environmentScope, + }) + : ''; + }, + 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); + }, + featureFlagIidText(featureFlag) { + return featureFlag.iid ? `^${featureFlag.iid}` : ''; + }, + canDeleteFlag(flag) { + return !this.permissions || (flag.scopes || []).every(scope => scope.can_update); + }, + setDeleteModalData(featureFlag) { + this.deleteFeatureFlagUrl = featureFlag.destroy_path; + this.deleteFeatureFlagName = featureFlag.name; + + this.$refs[this.modalId].show(); + }, + onSubmit() { + this.$refs.form.submit(); + }, + toggleFeatureFlag(flag) { + this.$emit('toggle-flag', { + ...flag, + active: !flag.active, + }); + }, + }, +}; +</script> +<template> + <div class="table-holder js-feature-flag-table"> + <div class="gl-responsive-table-row table-row-header" role="row"> + <div class="table-section section-10"> + {{ s__('FeatureFlags|ID') }} + </div> + <div class="table-section section-10" role="columnheader"> + {{ s__('FeatureFlags|Status') }} + </div> + <div class="table-section section-20" role="columnheader"> + {{ s__('FeatureFlags|Feature Flag') }} + </div> + <div class="table-section section-40" role="columnheader"> + {{ s__('FeatureFlags|Environment Specs') }} + </div> + </div> + + <template v-for="featureFlag in featureFlags"> + <div :key="featureFlag.id" class="gl-responsive-table-row" role="row"> + <div class="table-section section-10" role="gridcell"> + <div class="table-mobile-header" role="rowheader">{{ s__('FeatureFlags|ID') }}</div> + <div class="table-mobile-content js-feature-flag-id"> + {{ featureFlagIidText(featureFlag) }} + </div> + </div> + <div class="table-section section-10" role="gridcell"> + <div class="table-mobile-header" role="rowheader">{{ s__('FeatureFlags|Status') }}</div> + <div class="table-mobile-content"> + <gl-toggle + v-if="featureFlag.update_path" + :value="featureFlag.active" + :disabled="statusToggleDisabled(featureFlag)" + data-testid="feature-flag-status-toggle" + data-track-event="click_button" + data-track-label="feature_flag_toggle" + @change="toggleFeatureFlag(featureFlag)" + /> + <gl-badge + v-else-if="featureFlag.active" + variant="success" + data-testid="feature-flag-status-badge" + > + {{ s__('FeatureFlags|Active') }} + </gl-badge> + <gl-badge v-else variant="danger">{{ s__('FeatureFlags|Inactive') }}</gl-badge> + </div> + </div> + + <div class="table-section section-20" role="gridcell"> + <div class="table-mobile-header" role="rowheader"> + {{ s__('FeatureFlags|Feature Flag') }} + </div> + <div class="table-mobile-content d-flex flex-column js-feature-flag-title"> + <div class="gl-display-flex gl-align-items-center"> + <div class="feature-flag-name text-monospace text-truncate"> + {{ featureFlag.name }} + </div> + <gl-icon + v-if="isLegacyFlag(featureFlag)" + v-gl-tooltip.hover="legacyFlagToolTipText" + class="gl-ml-3" + name="information-o" + /> + </div> + <div class="feature-flag-description text-secondary text-truncate"> + {{ featureFlag.description }} + </div> + </div> + </div> + + <div class="table-section section-40" role="gridcell"> + <div class="table-mobile-header" role="rowheader"> + {{ s__('FeatureFlags|Environment Specs') }} + </div> + <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" + > + {{ strategyBadgeText(strategy) }} + </gl-badge> + </template> + </div> + </div> + + <div class="table-section section-20 table-button-footer" role="gridcell"> + <div class="table-action-buttons btn-group"> + <template v-if="featureFlag.edit_path"> + <gl-button + v-gl-tooltip.hover.bottom="__('Edit')" + class="js-feature-flag-edit-button" + icon="pencil" + :href="featureFlag.edit_path" + /> + </template> + <template v-if="featureFlag.destroy_path"> + <gl-button + v-gl-tooltip.hover.bottom="__('Delete')" + class="js-feature-flag-delete-button" + variant="danger" + icon="remove" + :disabled="!canDeleteFlag(featureFlag)" + @click="setDeleteModalData(featureFlag)" + /> + </template> + </div> + </div> + </div> + </template> + + <gl-modal + :ref="modalId" + :title="modalTitle" + :ok-title="s__('FeatureFlags|Delete feature flag')" + :modal-id="modalId" + title-tag="h4" + ok-variant="danger" + category="primary" + @ok="onSubmit" + > + {{ deleteModalMessage }} + <form ref="form" :action="deleteFeatureFlagUrl" method="post" class="js-requires-input"> + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + </form> + </gl-modal> + </div> +</template> |