Commit 84a7fefd authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Nicolò Maria Mezzopera

Display Strategy Information for New Feature Flags

New Version Feature Flags now show information on the strategies that
they are configured with.
parent 46b8c840
<script> <script>
import { escape } from 'lodash'; import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle, GlIcon } from '@gitlab/ui';
import { GlButton, GlTooltipDirective, GlModal, GlToggle, GlIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT, NEW_VERSION_FLAG } from '../constants'; import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT, NEW_VERSION_FLAG } from '../constants';
import labelForStrategy from '../utils';
export default { export default {
components: { components: {
GlBadge,
GlButton, GlButton,
GlIcon, GlIcon,
GlModal, GlModal,
...@@ -43,22 +44,14 @@ export default { ...@@ -43,22 +44,14 @@ export default {
return this.glFeatures.featureFlagsNewVersion; return this.glFeatures.featureFlagsNewVersion;
}, },
modalTitle() { modalTitle() {
return sprintf( return sprintf(s__('FeatureFlags|Delete %{name}?'), {
s__('FeatureFlags|Delete %{name}?'), name: this.deleteFeatureFlagName,
{ });
name: escape(this.deleteFeatureFlagName),
},
false,
);
}, },
deleteModalMessage() { deleteModalMessage() {
return sprintf( return sprintf(s__('FeatureFlags|Feature flag %{name} will be removed. Are you sure?'), {
s__('FeatureFlags|Feature flag %{name} will be removed. Are you sure?'), name: this.deleteFeatureFlagName,
{ });
name: escape(this.deleteFeatureFlagName),
},
false,
);
}, },
modalId() { modalId() {
return 'delete-feature-flag'; return 'delete-feature-flag';
...@@ -66,7 +59,7 @@ export default { ...@@ -66,7 +59,7 @@ export default {
}, },
methods: { methods: {
isLegacyFlag(flag) { isLegacyFlag(flag) {
return this.isNewVersionFlagsEnabled && flag.version !== NEW_VERSION_FLAG; return !this.isNewVersionFlagsEnabled || flag.version !== NEW_VERSION_FLAG;
}, },
scopeTooltipText(scope) { scopeTooltipText(scope) {
return !scope.active return !scope.active
...@@ -88,6 +81,12 @@ export default { ...@@ -88,6 +81,12 @@ export default {
return `${displayName}${displayPercentage}`; return `${displayName}${displayPercentage}`;
}, },
badgeVariant(scope) {
return scope.active ? 'info' : 'muted';
},
strategyBadgeText(strategy) {
return labelForStrategy(strategy);
},
featureFlagIidText(featureFlag) { featureFlagIidText(featureFlag) {
return featureFlag.iid ? `^${featureFlag.iid}` : ''; return featureFlag.iid ? `^${featureFlag.iid}` : '';
}, },
...@@ -145,10 +144,10 @@ export default { ...@@ -145,10 +144,10 @@ export default {
:value="featureFlag.active" :value="featureFlag.active"
@change="toggleFeatureFlag(featureFlag)" @change="toggleFeatureFlag(featureFlag)"
/> />
<span v-else-if="featureFlag.active" class="badge badge-success"> <gl-badge v-else-if="featureFlag.active" variant="success">
{{ s__('FeatureFlags|Active') }} {{ s__('FeatureFlags|Active') }}
</span> </gl-badge>
<span v-else class="badge badge-danger">{{ s__('FeatureFlags|Inactive') }}</span> <gl-badge v-else variant="danger">{{ s__('FeatureFlags|Inactive') }}</gl-badge>
</div> </div>
</div> </div>
...@@ -181,17 +180,29 @@ export default { ...@@ -181,17 +180,29 @@ export default {
<div <div
class="table-mobile-content d-flex flex-wrap justify-content-end justify-content-md-start js-feature-flag-environments" class="table-mobile-content d-flex flex-wrap justify-content-end justify-content-md-start js-feature-flag-environments"
> >
<span <template v-if="isLegacyFlag(featureFlag)">
v-for="scope in featureFlag.scopes" <gl-badge
:key="scope.id" v-for="scope in featureFlag.scopes"
v-gl-tooltip.hover="scopeTooltipText(scope)" :key="scope.id"
class="badge gl-mr-3 gl-mt-2" v-gl-tooltip.hover="scopeTooltipText(scope)"
:class="{ :variant="badgeVariant(scope)"
'badge-active': scope.active, :data-qa-selector="`feature-flag-scope-${badgeVariant(scope)}-badge`"
'badge-inactive': !scope.active, class="gl-mr-3 gl-mt-2"
}" >
>{{ badgeText(scope) }}</span {{ 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> </div>
......
import { s__, n__, sprintf } from '~/locale';
import {
ALL_ENVIRONMENTS_NAME,
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from './constants';
const badgeTextByType = {
[ROLLOUT_STRATEGY_ALL_USERS]: {
name: s__('FeatureFlags|All Users'),
parameters: null,
},
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: {
name: s__('FeatureFlags|Percent of users'),
parameters: ({ parameters: { percentage } }) => `${percentage}%`,
},
[ROLLOUT_STRATEGY_USER_ID]: {
name: s__('FeatureFlags|User IDs'),
parameters: ({ parameters: { userIds } }) =>
sprintf(n__('FeatureFlags|%d user', 'FeatureFlags|%d users', userIds.split(',').length)),
},
[ROLLOUT_STRATEGY_GITLAB_USER_LIST]: {
name: s__('FeatureFlags|User List'),
parameters: ({ user_list: { name } }) => name,
},
};
const scopeName = ({ environment_scope: scope }) =>
scope === ALL_ENVIRONMENTS_NAME ? s__('FeatureFlags|All Environments') : scope;
export default strategy => {
const { name, parameters } = badgeTextByType[strategy.name];
if (parameters) {
return sprintf('%{name} - %{parameters}: %{scopes}', {
name,
parameters: parameters(strategy),
scopes: strategy.scopes.map(scopeName).join(', '),
});
}
return sprintf('%{name}: %{scopes}', {
name,
scopes: strategy.scopes.map(scopeName).join(', '),
});
};
---
title: Display Strategy Information for New Feature Flags
merge_request: 38227
author:
type: added
...@@ -87,8 +87,7 @@ RSpec.describe 'User creates feature flag', :js do ...@@ -87,8 +87,7 @@ RSpec.describe 'User creates feature flag', :js do
expect(page).to have_css('.js-feature-flag-status button.is-checked') expect(page).to have_css('.js-feature-flag-status button.is-checked')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active')
end end
end end
end end
...@@ -118,8 +117,7 @@ RSpec.describe 'User creates feature flag', :js do ...@@ -118,8 +117,7 @@ RSpec.describe 'User creates feature flag', :js do
expect(page).to have_css('.js-feature-flag-status button.is-checked') expect(page).to have_css('.js-feature-flag-status button.is-checked')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive')
end end
end end
end end
...@@ -150,10 +148,8 @@ RSpec.describe 'User creates feature flag', :js do ...@@ -150,10 +148,8 @@ RSpec.describe 'User creates feature flag', :js do
expect(page).to have_css('.js-feature-flag-status button.is-checked') expect(page).to have_css('.js-feature-flag-status button.is-checked')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-active')
end end
end end
end end
...@@ -182,10 +178,8 @@ RSpec.describe 'User creates feature flag', :js do ...@@ -182,10 +178,8 @@ RSpec.describe 'User creates feature flag', :js do
expect(page).to have_css('.js-feature-flag-status button.is-checked') expect(page).to have_css('.js-feature-flag-status button.is-checked')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end end
end end
end end
......
...@@ -34,10 +34,8 @@ RSpec.describe 'User sees feature flag list', :js do ...@@ -34,10 +34,8 @@ RSpec.describe 'User sees feature flag list', :js do
expect(page).to have_css('.js-feature-flag-status button:not(.is-checked)') expect(page).to have_css('.js-feature-flag-status button:not(.is-checked)')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-active')
end end
end end
end end
...@@ -49,8 +47,7 @@ RSpec.describe 'User sees feature flag list', :js do ...@@ -49,8 +47,7 @@ RSpec.describe 'User sees feature flag list', :js do
expect(page).to have_css('.js-feature-flag-status button:not(.is-checked)') expect(page).to have_css('.js-feature-flag-status button:not(.is-checked)')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-inactive')
end end
end end
end end
...@@ -62,10 +59,8 @@ RSpec.describe 'User sees feature flag list', :js do ...@@ -62,10 +59,8 @@ RSpec.describe 'User sees feature flag list', :js do
expect(page).to have_css('.js-feature-flag-status button.is-checked') expect(page).to have_css('.js-feature-flag-status button.is-checked')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end end
end end
end end
......
...@@ -107,10 +107,8 @@ RSpec.describe 'User updates feature flag', :js do ...@@ -107,10 +107,8 @@ RSpec.describe 'User updates feature flag', :js do
expect(page).to have_css('.js-feature-flag-status button.is-checked') expect(page).to have_css('.js-feature-flag-status button.is-checked')
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*') expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')).to have_content('review/*')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end end
end end
end end
...@@ -140,8 +138,7 @@ RSpec.describe 'User updates feature flag', :js do ...@@ -140,8 +138,7 @@ RSpec.describe 'User updates feature flag', :js do
it 'shows the newly created scope' do it 'shows the newly created scope' do
within_feature_flag_row(1) do within_feature_flag_row(1) do
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page.find('.badge:nth-child(3)')).to have_content('production') expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(3)')).to have_content('production')
expect(page.find('.badge:nth-child(3)')['class']).to include('badge-inactive')
end end
end end
end end
...@@ -168,8 +165,8 @@ RSpec.describe 'User updates feature flag', :js do ...@@ -168,8 +165,8 @@ RSpec.describe 'User updates feature flag', :js do
it 'shows the updated feature flag' do it 'shows the updated feature flag' do
within_feature_flag_row(1) do within_feature_flag_row(1) do
within_feature_flag_scopes do within_feature_flag_scopes do
expect(page).to have_css('.badge:nth-child(1)') expect(page).to have_css('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')
expect(page).not_to have_css('.badge:nth-child(2)') expect(page).not_to have_css('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(2)')
end end
end end
end end
......
import FeatureFlagsTable from 'ee/feature_flags/components/feature_flags_table.vue'; import FeatureFlagsTable from 'ee/feature_flags/components/feature_flags_table.vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlToggle } from '@gitlab/ui'; import { GlToggle, GlBadge } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
NEW_VERSION_FLAG,
LEGACY_FLAG,
DEFAULT_PERCENT_ROLLOUT, DEFAULT_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants'; } from 'ee/feature_flags/constants';
...@@ -18,6 +22,7 @@ const getDefaultProps = () => ({ ...@@ -18,6 +22,7 @@ const getDefaultProps = () => ({
description: 'flag description', description: 'flag description',
destroy_path: 'destroy/path', destroy_path: 'destroy/path',
edit_path: 'edit/path', edit_path: 'edit/path',
version: LEGACY_FLAG,
scopes: [ scopes: [
{ {
id: 1, id: 1,
...@@ -97,7 +102,7 @@ describe('Feature flag table', () => { ...@@ -97,7 +102,7 @@ describe('Feature flag table', () => {
it('should render an environments specs badge with active class', () => { it('should render an environments specs badge with active class', () => {
const envColumn = wrapper.find('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(trimText(envColumn.find('.badge-active').text())).toBe('scope'); expect(trimText(envColumn.find(GlBadge).text())).toBe('scope');
}); });
it('should render an actions column', () => { it('should render an actions column', () => {
...@@ -142,7 +147,7 @@ describe('Feature flag table', () => { ...@@ -142,7 +147,7 @@ describe('Feature flag table', () => {
it('should render an environments specs badge with percentage', () => { it('should render an environments specs badge with percentage', () => {
const envColumn = wrapper.find('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(trimText(envColumn.find('.badge').text())).toBe('scope: 54%'); expect(trimText(envColumn.find(GlBadge).text())).toBe('scope: 54%');
}); });
}); });
...@@ -155,7 +160,82 @@ describe('Feature flag table', () => { ...@@ -155,7 +160,82 @@ describe('Feature flag table', () => {
it('should render an environments specs badge with inactive class', () => { it('should render an environments specs badge with inactive class', () => {
const envColumn = wrapper.find('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(trimText(envColumn.find('.badge-inactive').text())).toBe('scope'); expect(trimText(envColumn.find(GlBadge).text())).toBe('scope');
});
});
describe('with a new version flag', () => {
let badges;
beforeEach(() => {
const newVersionProps = {
...props,
featureFlags: [
{
id: 1,
iid: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
version: NEW_VERSION_FLAG,
scopes: [],
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
scopes: [{ environment_scope: '*' }],
},
{
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: { percentage: '50' },
scopes: [{ environment_scope: 'production' }, { environment_scope: 'staging' }],
},
{
name: ROLLOUT_STRATEGY_USER_ID,
parameters: { userIds: '1,2,3,4' },
scopes: [{ environment_scope: 'review/*' }],
},
{
name: ROLLOUT_STRATEGY_GITLAB_USER_LIST,
parameters: {},
user_list: { name: 'test list' },
scopes: [{ environment_scope: '*' }],
},
],
},
],
};
createWrapper(newVersionProps, { provide: { glFeatures: { featureFlagsNewVersion: true } } });
badges = wrapper.findAll('[data-testid="strategy-badge"]');
});
it('shows All Environments if the environment scope is *', () => {
expect(badges.at(0).text()).toContain('All Environments');
});
it('shows the environment scope if another is set', () => {
expect(badges.at(1).text()).toContain('production');
expect(badges.at(1).text()).toContain('staging');
expect(badges.at(2).text()).toContain('review/*');
});
it('shows All Users for the default strategy', () => {
expect(badges.at(0).text()).toContain('All Users');
});
it('shows the percent for a percent rollout', () => {
expect(badges.at(1).text()).toContain('Percent of users - 50%');
});
it('shows the number of users for users with ID', () => {
expect(badges.at(2).text()).toContain('User IDs - 4 users');
});
it('shows the name of a user list for user list', () => {
expect(badges.at(3).text()).toContain('User List - test list');
}); });
}); });
......
...@@ -10260,6 +10260,11 @@ msgstr "" ...@@ -10260,6 +10260,11 @@ msgstr ""
msgid "Feature flag was successfully removed." msgid "Feature flag was successfully removed."
msgstr "" msgstr ""
msgid "FeatureFlags|%d user"
msgid_plural "FeatureFlags|%d users"
msgstr[0] ""
msgstr[1] ""
msgid "FeatureFlags|* (All Environments)" msgid "FeatureFlags|* (All Environments)"
msgstr "" msgstr ""
...@@ -10275,6 +10280,12 @@ msgstr "" ...@@ -10275,6 +10280,12 @@ msgstr ""
msgid "FeatureFlags|Add strategy" msgid "FeatureFlags|Add strategy"
msgstr "" msgstr ""
msgid "FeatureFlags|All Environments"
msgstr ""
msgid "FeatureFlags|All Users"
msgstr ""
msgid "FeatureFlags|All users" msgid "FeatureFlags|All users"
msgstr "" msgstr ""
...@@ -10389,6 +10400,9 @@ msgstr "" ...@@ -10389,6 +10400,9 @@ msgstr ""
msgid "FeatureFlags|New list" msgid "FeatureFlags|New list"
msgstr "" msgstr ""
msgid "FeatureFlags|Percent of users"
msgstr ""
msgid "FeatureFlags|Percent rollout (logged in users)" msgid "FeatureFlags|Percent rollout (logged in users)"
msgstr "" msgstr ""
...@@ -10431,6 +10445,9 @@ msgstr "" ...@@ -10431,6 +10445,9 @@ msgstr ""
msgid "FeatureFlags|User IDs" msgid "FeatureFlags|User IDs"
msgstr "" msgstr ""
msgid "FeatureFlags|User List"
msgstr ""
msgid "FeatureFlag|Delete strategy" msgid "FeatureFlag|Delete strategy"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment