Commit eb8d8da8 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '7731-feature-flags-table-view-filter-tabs' into 'master'

Resolve "Feature Flags - Table View filter tabs"

Closes #7731

See merge request gitlab-org/gitlab-ee!8821
parents 2b249629 3d969a96
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import { GlButton, GlModal, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlButton,
GlModal,
Icon,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
props: {
deleteFeatureFlagUrl: {
type: String,
required: true,
},
featureFlagName: {
type: String,
required: true,
},
modalId: {
type: String,
required: true,
},
csrfToken: {
type: String,
required: true,
},
},
computed: {
message() {
return sprintf(
s__('FeatureFlags|Feature flag %{name} will be removed. Are you sure?'),
{
name: _.escape(this.featureFlagName),
},
false,
);
},
title() {
return sprintf(
s__('FeatureFlags|Delete %{name}?'),
{
name: _.escape(this.featureFlagName),
},
false,
);
},
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
},
};
</script>
<template>
<div class="d-inline-block">
<gl-button
v-gl-tooltip.hover.bottom="__('Delete')"
v-gl-modal="modalId"
class="js-feature-flag-delete-button"
variant="danger"
>
<icon name="remove" :size="16" />
</gl-button>
<gl-modal
:title="title"
:ok-title="s__('FeatureFlags|Delete feature flag')"
:modal-id="modalId"
title-tag="h4"
ok-variant="danger"
@ok="onSubmit"
>
{{ message }}
<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>
<script>
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import FeatureFlagsTable from './feature_flags_table.vue';
import store from '../store';
import { __ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/table_pagination.vue';
import {
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
export default {
store,
components: {
FeatureFlagsTable,
NavigationTabs,
TablePagination,
GlEmptyState,
GlLoadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
csrfToken: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
featureFlagsHelpPagePath: {
type: String,
required: true,
},
},
data() {
return {
scope: getParameterByName('scope') || this.$options.scopes.all,
page: getParameterByName('page') || '1',
};
},
scopes: {
all: 'all',
enabled: 'enabled',
disabled: 'disabled',
},
computed: {
...mapState(['featureFlags', 'count', 'pageInfo', 'isLoading', 'hasError', 'options']),
shouldRenderTabs() {
/* Do not show tabs until after the first request to get the count */
return this.count.all !== undefined;
},
shouldRenderPagination() {
return (
!this.isLoading &&
!this.hasError &&
this.featureFlags.length &&
this.pageInfo.total > this.pageInfo.perPage
);
},
shouldShowEmptyState() {
return !this.isLoading && !this.hasError && this.featureFlags.length === 0;
},
shouldRenderTable() {
return !this.isLoading && this.featureFlags.length > 0 && !this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
tabs() {
const { scopes } = this.$options;
return [
{
name: __('All'),
scope: scopes.all,
count: this.count.all,
isActive: this.scope === scopes.all,
},
{
name: __('Enabled'),
scope: scopes.enabled,
count: this.count.enabled,
isActive: this.scope === scopes.enabled,
},
{
name: __('Disabled'),
scope: scopes.disabled,
count: this.count.disabled,
isActive: this.scope === scopes.disabled,
},
];
},
},
created() {
this.setFeatureFlagsEndpoint(this.endpoint);
this.setFeatureFlagsOptions({ scope: this.scope, page: this.page });
this.fetchFeatureFlags();
},
methods: {
...mapActions(['setFeatureFlagsEndpoint', 'setFeatureFlagsOptions', 'fetchFeatureFlags']),
onChangeTab(scope) {
this.scope = scope;
this.updateFeatureFlagOptions({
scope,
page: '1',
});
},
onChangePage(page) {
this.updateFeatureFlagOptions({
scope: this.scope,
/* URLS parameters are strings, we need to parse to match types */
page: Number(page).toString(),
});
},
updateFeatureFlagOptions(parameters) {
const queryString = Object.keys(parameters)
.map(parameter => {
const value = parameters[parameter];
return `${parameter}=${encodeURIComponent(value)}`;
})
.join('&');
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.setFeatureFlagsOptions(parameters);
this.fetchFeatureFlags();
},
},
};
</script>
<template>
<div>
<div v-if="shouldRenderTabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<navigation-tabs :tabs="tabs" scope="featureflags" @onChangeTab="onChangeTab" />
</div>
<gl-loading-icon
v-if="isLoading"
:label="s__('Pipelines|Loading Pipelines')"
:size="3"
class="prepend-top-20"
/>
<template v-else-if="shouldRenderErrorState">
<gl-empty-state
:title="s__(`FeatureFlags|There was an error fetching the feature flags.`)"
:description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
:svg-path="errorStateSvgPath"
/>
</template>
<template v-else-if="shouldShowEmptyState">
<gl-empty-state
class="js-feature-flags-empty-state"
:title="s__(`FeatureFlags|Get started with feature flags`)"
:description="
s__(
`FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.`,
)
"
:svg-path="errorStateSvgPath"
:primary-button-link="featureFlagsHelpPagePath"
:primary-button-text="s__(`FeatureFlags|More Information`)"
/>
</template>
<template v-else-if="shouldRenderTable">
<feature-flags-table :csrf-token="csrfToken" :feature-flags="featureFlags" />
</template>
<table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" />
</div>
</template>
<script>
import { GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
import DeleteFeatureFlag from './delete_feature_flag.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
DeleteFeatureFlag,
GlButton,
GlLink,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
csrfToken: {
type: String,
required: true,
},
featureFlags: {
type: Array,
required: true,
},
},
};
</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" role="columnheader">
{{ s__('FeatureFlags|Status') }}
</div>
<div class="table-section section-50" role="columnheader">
{{ s__('FeatureFlags|Feature flag') }}
</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|Status') }}</div>
<div class="table-mobile-content js-feature-flag-status">
<template v-if="featureFlag.active">
<span class="badge badge-success">{{ s__('FeatureFlags|Active') }}</span>
</template>
<template v-else>
<span class="badge badge-danger">{{ s__('FeatureFlags|Inactive') }}</span>
</template>
</div>
</div>
<div class="table-section section-50" 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="feature-flag-name text-monospace text-truncate">{{ featureFlag.name }}</div>
<div class="feature-flag-description text-secondary text-truncate">
{{ featureFlag.description }}
</div>
</div>
</div>
<div class="table-section section-40 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"
:href="featureFlag.edit_path"
variant="outline-primary"
>
<icon name="pencil" :size="16" />
</gl-button>
</template>
<template v-if="featureFlag.destroy_path">
<delete-feature-flag
:delete-feature-flag-url="featureFlag.destroy_path"
:feature-flag-name="featureFlag.name"
:modal-id="`delete-feature-flag-${featureFlag.id}`"
:csrf-token="csrfToken"
/>
</template>
</div>
</div>
</div>
</template>
</div>
</template>
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
export const requestFeatureFlags = ({ commit }) => commit(types.REQUEST_FEATURE_FLAGS);
export const receiveFeatureFlagsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAGS_SUCCESS, response);
export const receiveFeatureFlagsError = ({ commit }, error) =>
commit(types.RECEIVE_FEATURE_FLAGS_ERROR, error);
export const fetchFeatureFlags = ({ state, dispatch }) => {
dispatch('requestFeatureFlags');
axios
.get(state.endpoint, {
params: state.options,
})
.then(response => dispatch('receiveFeatureFlagsSuccess', response))
.catch(error => dispatch('receiveFeatureFlagsError', error));
};
export const setFeatureFlagsEndpoint = ({ commit }, endpoint) =>
commit(types.SET_FEATURE_FLAGS_ENDPOINT, endpoint);
export const setFeatureFlagsOptions = ({ commit }, options) =>
commit(types.SET_FEATURE_FLAGS_OPTIONS, options);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
actions,
mutations,
state,
});
export default createStore();
export const SET_FEATURE_FLAGS_ENDPOINT = 'SET_FEATURE_FLAGS_ENDPOINT';
export const SET_FEATURE_FLAGS_OPTIONS = 'SET_FEATURE_FLAGS_OPTIONS';
export const REQUEST_FEATURE_FLAGS = 'REQUEST_FEATURE_FLAGS';
export const RECEIVE_FEATURE_FLAGS_SUCCESS = 'RECEIVE_FEATURE_FLAGS_SUCCESS';
export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
export default {
[types.SET_FEATURE_FLAGS_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
[types.SET_FEATURE_FLAGS_OPTIONS](state, options = {}) {
state.options = options;
},
[types.REQUEST_FEATURE_FLAGS](state) {
state.isLoading = true;
},
[types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) {
state.isLoading = false;
state.featureFlags = response.data.feature_flags;
state.count = response.data.count;
let paginationInfo;
if (Object.keys(response.headers).length) {
const normalizedHeaders = normalizeHeaders(response.headers);
paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = response.headers;
}
state.pageInfo = paginationInfo;
},
[types.RECEIVE_FEATURE_FLAGS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
};
export default () => ({
featureFlags: [],
count: {},
pageInfo: {},
isLoading: true,
hasError: false,
endpoint: null,
options: {},
});
import Vue from 'vue';
import FeatureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue';
import csrf from '~/lib/utils/csrf';
document.addEventListener(
'DOMContentLoaded',
() =>
new Vue({
el: '#feature-flags-vue',
components: {
FeatureFlagsComponent,
},
data() {
return {
dataset: document.querySelector(this.$options.el).dataset,
};
},
render(createElement) {
return createElement('feature-flags-component', {
props: {
endpoint: this.dataset.endpoint,
errorStateSvgPath: this.dataset.errorStateSvgPath,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
csrfToken: csrf.token,
},
});
},
}),
);
...@@ -2,9 +2,22 @@ ...@@ -2,9 +2,22 @@
= render 'configure_feature_flags_modal' = render 'configure_feature_flags_modal'
- if @feature_flags.empty? - if Feature.enabled?(:operations_feature_flag_index_tab, default_enabled: true)
= render 'empty_state' %h3.page-title.with-button
= _('Feature Flags')
.pull-right
= render 'configure_feature_flags_button'
= render 'new_feature_flag_button'
%div
#feature-flags-vue{ data: { endpoint: project_feature_flags_path(@project, format: :json),
"error-state-svg-path" => image_path('illustrations/feature_flag.svg'),
"feature-flags-help-page-path" => help_page_path("user/project/operations/feature_flags") } }
- else - else
- if @feature_flags.empty?
= render 'empty_state'
- else
%h3.page-title.with-button %h3.page-title.with-button
= _('Feature Flags') = _('Feature Flags')
......
title: Allow to filter Feature Flags
merge_request: 8821
author:
type: added
...@@ -26,7 +26,6 @@ describe Projects::FeatureFlagsController do ...@@ -26,7 +26,6 @@ describe Projects::FeatureFlagsController do
it 'shows an empty state with buttons' do it 'shows an empty state with buttons' do
expect(response).to be_ok expect(response).to be_ok
expect(response).to render_template('_empty_state')
expect(response).to render_template('_configure_feature_flags_button') expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button') expect(response).to render_template('_new_feature_flag_button')
end end
...@@ -41,7 +40,6 @@ describe Projects::FeatureFlagsController do ...@@ -41,7 +40,6 @@ describe Projects::FeatureFlagsController do
it 'shows an list of feature flags with buttons' do it 'shows an list of feature flags with buttons' do
expect(response).to be_ok expect(response).to be_ok
expect(response).to render_template('_table')
expect(response).to render_template('_configure_feature_flags_button') expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button') expect(response).to render_template('_new_feature_flag_button')
end end
......
...@@ -59,6 +59,7 @@ describe 'Feature Flags', :js do ...@@ -59,6 +59,7 @@ describe 'Feature Flags', :js do
add_feature_flag('feature-flag-to-edit', 'with some description', false) add_feature_flag('feature-flag-to-edit', 'with some description', false)
end end
shared_examples_for 'correct edit behavior' do
context 'and input is valid' do context 'and input is valid' do
it 'updates the feature flag' do it 'updates the feature flag' do
name = 'new-name' name = 'new-name'
...@@ -84,11 +85,23 @@ describe 'Feature Flags', :js do ...@@ -84,11 +85,23 @@ describe 'Feature Flags', :js do
end end
end end
it_behaves_like 'correct edit behavior'
context 'when operations_feature_flag_index_tab feature flag is disabled' do
before do
stub_feature_flags(operations_feature_flag_index_tab: false)
end
it_behaves_like 'correct edit behavior'
end
end
context 'when deleting a feature flag' do context 'when deleting a feature flag' do
before do before do
add_feature_flag('feature-flag-to-delete', 'with some description', false) add_feature_flag('feature-flag-to-delete', 'with some description', false)
end end
shared_examples_for 'correct delete behavior' do
context 'and no feature flags are left' do context 'and no feature flags are left' do
it 'shows empty state' do it 'shows empty state' do
visit(project_feature_flags_path(project)) visit(project_feature_flags_path(project))
...@@ -122,6 +135,75 @@ describe 'Feature Flags', :js do ...@@ -122,6 +135,75 @@ describe 'Feature Flags', :js do
end end
end end
it_behaves_like 'correct delete behavior'
context 'when operations_feature_flag_index_tab feature flag is disabled' do
before do
stub_feature_flags(operations_feature_flag_index_tab: false)
end
it_behaves_like 'correct delete behavior'
end
end
context 'when user sees empty index page' do
before do
visit(project_feature_flags_path(project))
end
shared_examples_for 'correct empty index behavior' do
it 'shows empty state' do
expect(page).to have_content('Get started with feature flags')
expect(page).to have_link('New Feature Flag')
expect(page).to have_button('Configure')
end
end
it_behaves_like 'correct empty index behavior'
context 'when operations_feature_flag_index_tab feature flag is disabled' do
before do
stub_feature_flags(operations_feature_flag_index_tab: false)
end
it_behaves_like 'correct empty index behavior'
end
end
context 'when user sees index page' do
let!(:feature_flag_enabled) { create(:operations_feature_flag, project: project, active: true) }
let!(:feature_flag_disabled) { create(:operations_feature_flag, project: project, active: false) }
before do
visit(project_feature_flags_path(project))
end
context 'when user sees all tab' do
it 'shows all feature flags' do
expect(page).to have_content(feature_flag_enabled.name)
expect(page).to have_content(feature_flag_disabled.name)
end
end
context 'when user sees enabled tab' do
it 'shows only active feature flags' do
find('.js-featureflags-tab-enabled').click
expect(page).to have_content(feature_flag_enabled.name)
expect(page).not_to have_content(feature_flag_disabled.name)
end
end
context 'when user sees disabled tab' do
it 'shows only inactive feature flags' do
find('.js-featureflags-tab-disabled').click
expect(page).not_to have_content(feature_flag_enabled.name)
expect(page).to have_content(feature_flag_disabled.name)
end
end
end
private private
def add_feature_flag(name, description, status) def add_feature_flag(name, description, status)
...@@ -140,7 +222,13 @@ describe 'Feature Flags', :js do ...@@ -140,7 +222,13 @@ describe 'Feature Flags', :js do
end end
def delete_feature_flag(name, confirm = true) def delete_feature_flag(name, confirm = true)
delete_button = find('.gl-responsive-table-row', text: name).find('.btn-danger[title="Delete"]') delete_button =
if Feature.enabled?(:operations_feature_flag_index_tab)
find('.gl-responsive-table-row', text: name).find('.js-feature-flag-delete-button')
else
find('.gl-responsive-table-row', text: name).find('.btn-danger[title="Delete"]')
end
delete_button.click delete_button.click
within '.modal' do within '.modal' do
...@@ -154,7 +242,14 @@ describe 'Feature Flags', :js do ...@@ -154,7 +242,14 @@ describe 'Feature Flags', :js do
def edit_feature_flag(old_name, new_name, new_description, new_status) def edit_feature_flag(old_name, new_name, new_description, new_status)
visit(project_feature_flags_path(project)) visit(project_feature_flags_path(project))
edit_button = find('.gl-responsive-table-row', text: old_name).find('.btn-default[title="Edit"]')
edit_button =
if Feature.enabled?(:operations_feature_flag_index_tab)
find('.gl-responsive-table-row', text: old_name).find('.js-feature-flag-edit-button')
else
find('.gl-responsive-table-row', text: old_name).find('.btn-default[title="Edit"]')
end
edit_button.click edit_button.click
fill_in 'Name', with: new_name fill_in 'Name', with: new_name
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import featureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue';
import { createStore } from 'ee/feature_flags/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { featureFlag } from './mock_data';
describe('Feature Flags', () => {
const mockData = {
endpoint: 'feature_flags.json',
csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
};
let store;
let FeatureFlagsComponent;
let component;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
FeatureFlagsComponent = Vue.extend(featureFlagsComponent);
});
afterEach(() => {
component.$destroy();
mock.restore();
});
describe('successful request', () => {
describe('without feature flags', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, {
feature_flags: [],
count: {
all: 0,
enabled: 0,
disabled: 0,
},
});
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props: mockData,
});
setTimeout(() => {
done();
}, 0);
});
it('should render the empty state', () => {
expect(component.$el.querySelectorAll('.js-feature-flags-empty-state')).not.toBeNull();
});
});
describe('with paginated feature flags', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(
200,
{
feature_flags: [featureFlag],
count: {
all: 37,
enabled: 5,
disabled: 32,
},
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
store = createStore();
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props: mockData,
});
setTimeout(() => {
done();
}, 0);
});
it('should render a table with feature flags', () => {
expect(component.$el.querySelectorAll('.js-feature-flag-table')).not.toBeNull();
expect(component.$el.querySelector('.feature-flag-name').textContent.trim()).toEqual(
featureFlag.name,
);
expect(component.$el.querySelector('.feature-flag-description').textContent.trim()).toEqual(
featureFlag.description,
);
});
describe('pagination', () => {
it('should render pagination', () => {
expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
});
it('should make an API request when page is clicked', done => {
spyOn(component, 'updateFeatureFlagOptions');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(component.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: 'all',
page: '2',
});
done();
}, 0);
});
it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateFeatureFlagOptions');
component.$el.querySelector('.js-featureflags-tab-enabled').click();
expect(component.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: 'enabled',
page: '1',
});
done();
}, 0);
});
});
});
});
describe('unsuccessful request', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {});
store = createStore();
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props: mockData,
});
setTimeout(() => {
done();
}, 0);
});
it('should render error state', () => {
expect(component.$el.querySelector('.empty-state').textContent.trim()).toContain(
'There was an error fetching the feature flags. Try again in a few moments or contact your support team.',
);
});
});
});
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 { featureFlag } from './mock_data';
describe('Feature Flag table', () => {
let Component;
let vm;
beforeEach(() => {
Component = Vue.extend(featureFlagsTableComponent);
});
afterEach(() => {
vm.$destroy();
});
it('Should render a table', () => {
vm = mountComponent(Component, {
featureFlags: [featureFlag],
csrfToken: 'fakeToken',
});
expect(vm.$el.getAttribute('class')).toContain('table-holder');
});
it('Should render rows', () => {
expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBeNull();
});
it('Should render a status column', () => {
const status = featureFlag.active ? 'Active' : 'Inactive';
expect(vm.$el.querySelector('.js-feature-flag-status')).not.toBeNull();
expect(vm.$el.querySelector('.js-feature-flag-status').textContent).toEqual(status);
});
it('Should render a feature flag column', () => {
expect(vm.$el.querySelector('.js-feature-flag-title')).not.toBeNull();
expect(vm.$el.querySelector('.feature-flag-name').textContent.trim()).toEqual(featureFlag.name);
expect(vm.$el.querySelector('.feature-flag-description').textContent.trim()).toEqual(
featureFlag.description,
);
});
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(
featureFlag.edit_path,
);
});
});
export const featureFlagsList = [
{
id: 1,
active: true,
created_at: '2018-12-12T22:07:31.401Z',
updated_at: '2018-12-12T22:07:31.401Z',
name: 'test flag',
description: 'flag for tests',
destroy_path: 'feature_flags/1',
edit_path: 'feature_flags/1/edit',
},
];
export const featureFlag = {
id: 1,
active: true,
created_at: '2018-12-12T22:07:31.401Z',
updated_at: '2018-12-12T22:07:31.401Z',
name: 'test flag',
description: 'flag for tests',
destroy_path: 'feature_flags/1',
edit_path: 'feature_flags/1/edit',
};
...@@ -3695,6 +3695,12 @@ msgstr "" ...@@ -3695,6 +3695,12 @@ msgstr ""
msgid "FeatureFlags|Delete %{feature_flag_name}?" msgid "FeatureFlags|Delete %{feature_flag_name}?"
msgstr "" msgstr ""
msgid "FeatureFlags|Delete %{name}?"
msgstr ""
msgid "FeatureFlags|Delete feature flag"
msgstr ""
msgid "FeatureFlags|Description" msgid "FeatureFlags|Description"
msgstr "" msgstr ""
...@@ -3713,6 +3719,9 @@ msgstr "" ...@@ -3713,6 +3719,9 @@ msgstr ""
msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?" msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?"
msgstr "" msgstr ""
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?"
msgstr ""
msgid "FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality." msgid "FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality."
msgstr "" msgstr ""
...@@ -3728,6 +3737,9 @@ msgstr "" ...@@ -3728,6 +3737,9 @@ msgstr ""
msgid "FeatureFlags|Instance ID" msgid "FeatureFlags|Instance ID"
msgstr "" msgstr ""
msgid "FeatureFlags|More Information"
msgstr ""
msgid "FeatureFlags|More information" msgid "FeatureFlags|More information"
msgstr "" msgstr ""
...@@ -3746,6 +3758,12 @@ msgstr "" ...@@ -3746,6 +3758,12 @@ msgstr ""
msgid "FeatureFlags|Status" msgid "FeatureFlags|Status"
msgstr "" msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr ""
msgid "FeatureFlags|Try again in a few moments or contact your support team."
msgstr ""
msgid "Feb" msgid "Feb"
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