2020-10-01 21:08:36 +00:00
|
|
|
<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';
|
2020-10-05 18:08:51 +00:00
|
|
|
import { labelForStrategy } from '../utils';
|
2020-10-01 21:08:36 +00:00
|
|
|
|
|
|
|
export default {
|
|
|
|
components: {
|
|
|
|
GlBadge,
|
|
|
|
GlButton,
|
|
|
|
GlIcon,
|
|
|
|
GlModal,
|
|
|
|
GlToggle,
|
|
|
|
},
|
|
|
|
directives: {
|
|
|
|
GlTooltip: GlTooltipDirective,
|
|
|
|
},
|
|
|
|
mixins: [glFeatureFlagMixin()],
|
|
|
|
props: {
|
|
|
|
featureFlags: {
|
|
|
|
type: Array,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
2020-10-15 09:08:41 +00:00
|
|
|
inject: ['csrfToken'],
|
2020-10-01 21:08:36 +00:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
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) {
|
2020-12-03 21:09:35 +00:00
|
|
|
return flag.version !== NEW_VERSION_FLAG;
|
2020-10-01 21:08:36 +00:00
|
|
|
},
|
|
|
|
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) {
|
2020-12-23 21:10:24 +00:00
|
|
|
return !this.permissions || (flag.scopes || []).every((scope) => scope.can_update);
|
2020-10-01 21:08:36 +00:00
|
|
|
},
|
|
|
|
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>
|