Commit 6a9f8023 authored by Andrew Fontaine's avatar Andrew Fontaine

Display the IID of a Feature Flag

This allows the flag to be referenced in other portions of the app
(coming soon), such as issues, MRs and epics.
parent 51d5357b
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { createNamespacedHelpers } from 'vuex'; import { createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import store from '../store/index'; import store from '../store/index';
import FeatureFlagForm from './form.vue'; import FeatureFlagForm from './form.vue';
...@@ -13,6 +14,7 @@ export default { ...@@ -13,6 +14,7 @@ export default {
GlLoadingIcon, GlLoadingIcon,
FeatureFlagForm, FeatureFlagForm,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -28,9 +30,14 @@ export default { ...@@ -28,9 +30,14 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['error', 'name', 'description', 'scopes', 'isLoading', 'hasError']), ...mapState(['error', 'name', 'description', 'scopes', 'isLoading', 'hasError', 'iid']),
title() { title() {
return sprintf(s__('Edit %{name}'), { name: this.name }); return this.hasFeatureFlagsIID
? `^${this.iid} ${this.name}`
: sprintf(s__('Edit %{name}'), { name: this.name });
},
hasFeatureFlagsIID() {
return this.glFeatures.featureFlagIID && this.iid;
}, },
}, },
created() { created() {
......
...@@ -3,6 +3,7 @@ import _ from 'underscore'; ...@@ -3,6 +3,7 @@ import _ from 'underscore';
import { GlButton, GlLink, GlTooltipDirective, GlModalDirective, GlModal } from '@gitlab/ui'; import { GlButton, GlLink, GlTooltipDirective, GlModalDirective, GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT } from '../constants'; import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT } from '../constants';
export default { export default {
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
GlModalDirective, GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
csrfToken: { csrfToken: {
type: String, type: String,
...@@ -34,7 +36,10 @@ export default { ...@@ -34,7 +36,10 @@ export default {
}, },
computed: { computed: {
permissions() { permissions() {
return gon && gon.features && gon.features.featureFlagPermissions; return this.glFeatures.featureFlagPermissions;
},
hasIIDs() {
return this.glFeatures.featureFlagIID;
}, },
modalTitle() { modalTitle() {
return sprintf( return sprintf(
...@@ -95,19 +100,26 @@ export default { ...@@ -95,19 +100,26 @@ export default {
<template> <template>
<div class="table-holder js-feature-flag-table"> <div class="table-holder js-feature-flag-table">
<div class="gl-responsive-table-row table-row-header" role="row"> <div class="gl-responsive-table-row table-row-header" role="row">
<div v-if="hasIIDs" class="table-section section-10">
{{ s__('FeatureFlags|ID') }}
</div>
<div class="table-section section-10" role="columnheader"> <div class="table-section section-10" role="columnheader">
{{ s__('FeatureFlags|Status') }} {{ s__('FeatureFlags|Status') }}
</div> </div>
<div class="table-section section-20" role="columnheader"> <div class="table-section section-20" role="columnheader">
{{ s__('FeatureFlags|Feature Flag') }} {{ s__('FeatureFlags|Feature Flag') }}
</div> </div>
<div class="table-section section-50" role="columnheader"> <div class="table-section section-40" role="columnheader">
{{ s__('FeatureFlags|Environment Specs') }} {{ s__('FeatureFlags|Environment Specs') }}
</div> </div>
</div> </div>
<template v-for="featureFlag in featureFlags"> <template v-for="featureFlag in featureFlags">
<div :key="featureFlag.id" class="gl-responsive-table-row" role="row"> <div :key="featureFlag.id" class="gl-responsive-table-row" role="row">
<div v-if="hasIIDs" 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">^{{ featureFlag.iid }}</div>
</div>
<div class="table-section section-10" role="gridcell"> <div class="table-section section-10" role="gridcell">
<div class="table-mobile-header" role="rowheader">{{ s__('FeatureFlags|Status') }}</div> <div class="table-mobile-header" role="rowheader">{{ s__('FeatureFlags|Status') }}</div>
<div class="table-mobile-content js-feature-flag-status"> <div class="table-mobile-content js-feature-flag-status">
...@@ -130,7 +142,7 @@ export default { ...@@ -130,7 +142,7 @@ export default {
</div> </div>
</div> </div>
<div class="table-section section-50" role="gridcell"> <div class="table-section section-40" role="gridcell">
<div class="table-mobile-header" role="rowheader"> <div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Environment Specs') }} {{ s__('FeatureFlags|Environment Specs') }}
</div> </div>
......
...@@ -17,6 +17,7 @@ export default { ...@@ -17,6 +17,7 @@ export default {
state.name = response.name; state.name = response.name;
state.description = response.description; state.description = response.description;
state.iid = response.iid;
state.scopes = mapToScopesViewModel(response.scopes); state.scopes = mapToScopesViewModel(response.scopes);
}, },
[types.RECEIVE_FEATURE_FLAG_ERROR](state) { [types.RECEIVE_FEATURE_FLAG_ERROR](state) {
......
...@@ -9,4 +9,5 @@ export default () => ({ ...@@ -9,4 +9,5 @@ export default () => ({
scopes: [], scopes: [],
isLoading: false, isLoading: false,
hasError: false, hasError: false,
iid: null,
}); });
...@@ -44,6 +44,7 @@ describe('Feature flags Edit Module Mutations', () => { ...@@ -44,6 +44,7 @@ describe('Feature flags Edit Module Mutations', () => {
name: '*', name: '*',
description: 'All environments', description: 'All environments',
scopes: [{ id: 1 }], scopes: [{ id: 1 }],
iid: 5,
}; };
beforeEach(() => { beforeEach(() => {
...@@ -69,6 +70,10 @@ describe('Feature flags Edit Module Mutations', () => { ...@@ -69,6 +70,10 @@ describe('Feature flags Edit Module Mutations', () => {
it('should set scope with the provided one', () => { it('should set scope with the provided one', () => {
expect(stateCopy.scope).toEqual(data.scope); expect(stateCopy.scope).toEqual(data.scope);
}); });
it('should set the iid to the provided one', () => {
expect(stateCopy.iid).toEqual(data.iid);
});
}); });
describe('RECEIVE_FEATURE_FLAG_ERROR', () => { describe('RECEIVE_FEATURE_FLAG_ERROR', () => {
......
...@@ -28,6 +28,11 @@ describe('Edit feature flag form', () => { ...@@ -28,6 +28,11 @@ describe('Edit feature flag form', () => {
path: '/feature_flags', path: '/feature_flags',
environmentsEndpoint: 'environments.json', environmentsEndpoint: 'environments.json',
}, },
provide: {
glFeatures: {
featureFlagIID: true,
},
},
store, store,
sync: false, sync: false,
}); });
...@@ -38,6 +43,7 @@ describe('Edit feature flag form', () => { ...@@ -38,6 +43,7 @@ describe('Edit feature flag form', () => {
mock.onGet(`${TEST_HOST}/feature_flags.json'`).replyOnce(200, { mock.onGet(`${TEST_HOST}/feature_flags.json'`).replyOnce(200, {
id: 21, id: 21,
iid: 5,
active: false, active: false,
created_at: '2019-01-17T17:27:39.778Z', created_at: '2019-01-17T17:27:39.778Z',
updated_at: '2019-01-17T17:27:39.778Z', updated_at: '2019-01-17T17:27:39.778Z',
...@@ -64,6 +70,14 @@ describe('Edit feature flag form', () => { ...@@ -64,6 +70,14 @@ describe('Edit feature flag form', () => {
mock.restore(); mock.restore();
}); });
it('should display the iid', done => {
setTimeout(() => {
expect(wrapper.find('h3').text()).toContain('^5');
done();
});
});
describe('with error', () => { describe('with error', () => {
it('should render the error', done => { it('should render the error', done => {
setTimeout(() => { setTimeout(() => {
...@@ -81,7 +95,7 @@ describe('Edit feature flag form', () => { ...@@ -81,7 +95,7 @@ describe('Edit feature flag form', () => {
describe('without error', () => { describe('without error', () => {
it('renders form title', done => { it('renders form title', done => {
setTimeout(() => { setTimeout(() => {
expect(wrapper.text()).toContain('Edit feature_flag'); expect(wrapper.text()).toContain('^5 feature_flag');
done(); done();
}, 0); }, 0);
}); });
......
import Vue from 'vue';
import featureFlagsTableComponent from 'ee/feature_flags/components/feature_flags_table.vue'; import featureFlagsTableComponent from 'ee/feature_flags/components/feature_flags_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper'; import { trimText } from 'spec/helpers/text_helper';
import { import {
ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_ALL_USERS,
...@@ -8,22 +7,28 @@ import { ...@@ -8,22 +7,28 @@ import {
DEFAULT_PERCENT_ROLLOUT, DEFAULT_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants'; } from 'ee/feature_flags/constants';
const localVue = createLocalVue();
describe('Feature flag table', () => { describe('Feature flag table', () => {
let Component; let Component;
let vm; let wrapper;
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
describe('with an active scope and a standard rollout strategy', () => { describe('with an active scope and a standard rollout strategy', () => {
beforeEach(() => { beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent); Component = localVue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, { wrapper = shallowMount(Component, {
localVue,
provide: { glFeatures: { featureFlagIID: true } },
propsData: {
featureFlags: [ featureFlags: [
{ {
id: 1, id: 1,
iid: 1,
active: true, active: true,
name: 'flag name', name: 'flag name',
description: 'flag description', description: 'flag description',
...@@ -44,60 +49,65 @@ describe('Feature flag table', () => { ...@@ -44,60 +49,65 @@ describe('Feature flag table', () => {
}, },
], ],
csrfToken: 'fakeToken', csrfToken: 'fakeToken',
},
}); });
}); });
it('Should render a table', () => { it('Should render a table', () => {
expect(vm.$el.getAttribute('class')).toContain('table-holder'); expect(wrapper.classes('table-holder')).toBe(true);
}); });
it('Should render rows', () => { it('Should render rows', () => {
expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBeNull(); expect(wrapper.find('.gl-responsive-table-row').exists()).toBe(true);
});
it('should render an ID column', () => {
expect(wrapper.find('.js-feature-flag-id').exists()).toBe(true);
expect(trimText(wrapper.find('.js-feature-flag-id').text())).toEqual('^1');
}); });
it('Should render a status column', () => { it('Should render a status column', () => {
expect(vm.$el.querySelector('.js-feature-flag-status')).not.toBeNull(); expect(wrapper.find('.js-feature-flag-status').exists()).toBe(true);
expect(trimText(vm.$el.querySelector('.js-feature-flag-status').textContent)).toEqual( expect(trimText(wrapper.find('.js-feature-flag-status').text())).toEqual('Active');
'Active',
);
}); });
it('Should render a feature flag column', () => { it('Should render a feature flag column', () => {
expect(vm.$el.querySelector('.js-feature-flag-title')).not.toBeNull(); expect(wrapper.find('.js-feature-flag-title').exists()).toBe(true);
expect(trimText(vm.$el.querySelector('.feature-flag-name').textContent)).toEqual('flag name'); expect(trimText(wrapper.find('.feature-flag-name').text())).toEqual('flag name');
expect(trimText(vm.$el.querySelector('.feature-flag-description').textContent)).toEqual(
expect(trimText(wrapper.find('.feature-flag-description').text())).toEqual(
'flag description', 'flag description',
); );
}); });
it('should render an environments specs column', () => { it('should render an environments specs column', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(envColumn).toBeDefined(); expect(envColumn).toBeDefined();
expect(trimText(envColumn.textContent)).toBe('scope'); expect(trimText(envColumn.text())).toBe('scope');
}); });
it('should render an environments specs badge with active class', () => { it('should render an environments specs badge with active class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(trimText(envColumn.querySelector('.badge-active').textContent)).toBe('scope'); expect(trimText(envColumn.find('.badge-active').text())).toBe('scope');
}); });
it('should render an actions column', () => { it('should render an actions column', () => {
expect(vm.$el.querySelector('.table-action-buttons')).not.toBeNull(); expect(wrapper.find('.table-action-buttons').exists()).toBe(true);
expect(vm.$el.querySelector('.js-feature-flag-delete-button')).not.toBeNull(); expect(wrapper.find('.js-feature-flag-delete-button').exists()).toBe(true);
expect(vm.$el.querySelector('.js-feature-flag-edit-button')).not.toBeNull(); expect(wrapper.find('.js-feature-flag-edit-button').exists()).toBe(true);
expect(vm.$el.querySelector('.js-feature-flag-edit-button').getAttribute('href')).toEqual( expect(wrapper.find('.js-feature-flag-edit-button').attributes('href')).toEqual('edit/path');
'edit/path',
);
}); });
}); });
describe('with an active scope and a percentage rollout strategy', () => { describe('with an active scope and a percentage rollout strategy', () => {
beforeEach(() => { beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent); Component = localVue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, { wrapper = shallowMount(Component, {
localVue,
propsData: {
featureFlags: [ featureFlags: [
{ {
id: 1, id: 1,
...@@ -121,21 +131,24 @@ describe('Feature flag table', () => { ...@@ -121,21 +131,24 @@ describe('Feature flag table', () => {
}, },
], ],
csrfToken: 'fakeToken', csrfToken: 'fakeToken',
},
}); });
}); });
it('should render an environments specs badge with percentage', () => { it('should render an environments specs badge with percentage', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(trimText(envColumn.querySelector('.badge').textContent)).toBe('scope: 54%'); expect(trimText(envColumn.find('.badge').text())).toBe('scope: 54%');
}); });
}); });
describe('with an inactive scope', () => { describe('with an inactive scope', () => {
beforeEach(() => { beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent); Component = localVue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, { wrapper = shallowMount(Component, {
localVue,
propsData: {
featureFlags: [ featureFlags: [
{ {
id: 1, id: 1,
...@@ -159,13 +172,14 @@ describe('Feature flag table', () => { ...@@ -159,13 +172,14 @@ describe('Feature flag table', () => {
}, },
], ],
csrfToken: 'fakeToken', csrfToken: 'fakeToken',
},
}); });
}); });
it('should render an environments specs badge with inactive class', () => { it('should render an environments specs badge with inactive class', () => {
const envColumn = vm.$el.querySelector('.js-feature-flag-environments'); const envColumn = wrapper.find('.js-feature-flag-environments');
expect(trimText(envColumn.querySelector('.badge-inactive').textContent)).toBe('scope'); expect(trimText(envColumn.find('.badge-inactive').text())).toBe('scope');
}); });
}); });
}); });
...@@ -7080,6 +7080,9 @@ msgstr "" ...@@ -7080,6 +7080,9 @@ msgstr ""
msgid "FeatureFlags|Get started with feature flags" msgid "FeatureFlags|Get started with feature flags"
msgstr "" msgstr ""
msgid "FeatureFlags|ID"
msgstr ""
msgid "FeatureFlags|Inactive" msgid "FeatureFlags|Inactive"
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