Commit f6327d33 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch 'feature-flags-show-strategy-on-table' into 'master'

Display Strategy Information for New Feature Flags

See merge request gitlab-org/gitlab!38227
parents b1be46e2 84a7fefd
<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');
}); });
}); });
......
...@@ -10269,6 +10269,11 @@ msgstr "" ...@@ -10269,6 +10269,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 ""
...@@ -10284,6 +10289,12 @@ msgstr "" ...@@ -10284,6 +10289,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 ""
...@@ -10398,6 +10409,9 @@ msgstr "" ...@@ -10398,6 +10409,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 ""
...@@ -10440,6 +10454,9 @@ msgstr "" ...@@ -10440,6 +10454,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