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 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import store from '../store/index';
import FeatureFlagForm from './form.vue';
......@@ -13,6 +14,7 @@ export default {
GlLoadingIcon,
FeatureFlagForm,
},
mixins: [glFeatureFlagMixin()],
props: {
endpoint: {
type: String,
......@@ -28,9 +30,14 @@ export default {
},
},
computed: {
...mapState(['error', 'name', 'description', 'scopes', 'isLoading', 'hasError']),
...mapState(['error', 'name', 'description', 'scopes', 'isLoading', 'hasError', 'iid']),
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() {
......
......@@ -3,6 +3,7 @@ import _ from 'underscore';
import { GlButton, GlLink, GlTooltipDirective, GlModalDirective, GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
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';
export default {
......@@ -16,6 +17,7 @@ export default {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
csrfToken: {
type: String,
......@@ -34,7 +36,10 @@ export default {
},
computed: {
permissions() {
return gon && gon.features && gon.features.featureFlagPermissions;
return this.glFeatures.featureFlagPermissions;
},
hasIIDs() {
return this.glFeatures.featureFlagIID;
},
modalTitle() {
return sprintf(
......@@ -95,19 +100,26 @@ export default {
<template>
<div class="table-holder js-feature-flag-table">
<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">
{{ s__('FeatureFlags|Status') }}
</div>
<div class="table-section section-20" role="columnheader">
{{ s__('FeatureFlags|Feature Flag') }}
</div>
<div class="table-section section-50" role="columnheader">
<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 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-mobile-header" role="rowheader">{{ s__('FeatureFlags|Status') }}</div>
<div class="table-mobile-content js-feature-flag-status">
......@@ -130,7 +142,7 @@ export default {
</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">
{{ s__('FeatureFlags|Environment Specs') }}
</div>
......
......@@ -17,6 +17,7 @@ export default {
state.name = response.name;
state.description = response.description;
state.iid = response.iid;
state.scopes = mapToScopesViewModel(response.scopes);
},
[types.RECEIVE_FEATURE_FLAG_ERROR](state) {
......
......@@ -9,4 +9,5 @@ export default () => ({
scopes: [],
isLoading: false,
hasError: false,
iid: null,
});
......@@ -44,6 +44,7 @@ describe('Feature flags Edit Module Mutations', () => {
name: '*',
description: 'All environments',
scopes: [{ id: 1 }],
iid: 5,
};
beforeEach(() => {
......@@ -69,6 +70,10 @@ describe('Feature flags Edit Module Mutations', () => {
it('should set scope with the provided one', () => {
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', () => {
......
......@@ -28,6 +28,11 @@ describe('Edit feature flag form', () => {
path: '/feature_flags',
environmentsEndpoint: 'environments.json',
},
provide: {
glFeatures: {
featureFlagIID: true,
},
},
store,
sync: false,
});
......@@ -38,6 +43,7 @@ describe('Edit feature flag form', () => {
mock.onGet(`${TEST_HOST}/feature_flags.json'`).replyOnce(200, {
id: 21,
iid: 5,
active: false,
created_at: '2019-01-17T17:27:39.778Z',
updated_at: '2019-01-17T17:27:39.778Z',
......@@ -64,6 +70,14 @@ describe('Edit feature flag form', () => {
mock.restore();
});
it('should display the iid', done => {
setTimeout(() => {
expect(wrapper.find('h3').text()).toContain('^5');
done();
});
});
describe('with error', () => {
it('should render the error', done => {
setTimeout(() => {
......@@ -81,7 +95,7 @@ describe('Edit feature flag form', () => {
describe('without error', () => {
it('renders form title', done => {
setTimeout(() => {
expect(wrapper.text()).toContain('Edit feature_flag');
expect(wrapper.text()).toContain('^5 feature_flag');
done();
}, 0);
});
......
import Vue from '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 {
ROLLOUT_STRATEGY_ALL_USERS,
......@@ -8,164 +7,179 @@ import {
DEFAULT_PERCENT_ROLLOUT,
} from 'ee/feature_flags/constants';
const localVue = createLocalVue();
describe('Feature flag table', () => {
let Component;
let vm;
let wrapper;
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
describe('with an active scope and a standard rollout strategy', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
Component = localVue.extend(featureFlagsTableComponent);
wrapper = shallowMount(Component, {
localVue,
provide: { glFeatures: { featureFlagIID: true } },
propsData: {
featureFlags: [
{
id: 1,
iid: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
},
});
});
it('Should render a table', () => {
expect(vm.$el.getAttribute('class')).toContain('table-holder');
expect(wrapper.classes('table-holder')).toBe(true);
});
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', () => {
expect(vm.$el.querySelector('.js-feature-flag-status')).not.toBeNull();
expect(trimText(vm.$el.querySelector('.js-feature-flag-status').textContent)).toEqual(
'Active',
);
expect(wrapper.find('.js-feature-flag-status').exists()).toBe(true);
expect(trimText(wrapper.find('.js-feature-flag-status').text())).toEqual('Active');
});
it('Should render a feature flag column', () => {
expect(vm.$el.querySelector('.js-feature-flag-title')).not.toBeNull();
expect(trimText(vm.$el.querySelector('.feature-flag-name').textContent)).toEqual('flag name');
expect(trimText(vm.$el.querySelector('.feature-flag-description').textContent)).toEqual(
expect(wrapper.find('.js-feature-flag-title').exists()).toBe(true);
expect(trimText(wrapper.find('.feature-flag-name').text())).toEqual('flag name');
expect(trimText(wrapper.find('.feature-flag-description').text())).toEqual(
'flag description',
);
});
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(trimText(envColumn.textContent)).toBe('scope');
expect(trimText(envColumn.text())).toBe('scope');
});
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', () => {
expect(vm.$el.querySelector('.table-action-buttons')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-delete-button')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-edit-button')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-edit-button').getAttribute('href')).toEqual(
'edit/path',
);
expect(wrapper.find('.table-action-buttons').exists()).toBe(true);
expect(wrapper.find('.js-feature-flag-delete-button').exists()).toBe(true);
expect(wrapper.find('.js-feature-flag-edit-button').exists()).toBe(true);
expect(wrapper.find('.js-feature-flag-edit-button').attributes('href')).toEqual('edit/path');
});
});
describe('with an active scope and a percentage rollout strategy', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '54',
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
Component = localVue.extend(featureFlagsTableComponent);
wrapper = shallowMount(Component, {
localVue,
propsData: {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: true,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
rolloutPercentage: '54',
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
},
});
});
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', () => {
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
vm = mountComponent(Component, {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: false,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
Component = localVue.extend(featureFlagsTableComponent);
wrapper = shallowMount(Component, {
localVue,
propsData: {
featureFlags: [
{
id: 1,
active: true,
name: 'flag name',
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
scopes: [
{
id: 1,
active: false,
environmentScope: 'scope',
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
shouldBeDestroyed: false,
},
],
},
],
csrfToken: 'fakeToken',
},
});
});
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 ""
msgid "FeatureFlags|Get started with feature flags"
msgstr ""
msgid "FeatureFlags|ID"
msgstr ""
msgid "FeatureFlags|Inactive"
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