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,7 @@
= render 'configure_feature_flags_modal'
- if @feature_flags.empty?
= render 'empty_state'
- else
- if Feature.enabled?(:operations_feature_flag_index_tab, default_enabled: true)
%h3.page-title.with-button
= _('Feature Flags')
......@@ -12,4 +10,19 @@
= render 'configure_feature_flags_button'
= render 'new_feature_flag_button'
= render 'table'
%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
- if @feature_flags.empty?
= render 'empty_state'
- else
%h3.page-title.with-button
= _('Feature Flags')
.pull-right
= render 'configure_feature_flags_button'
= render 'new_feature_flag_button'
= render 'table'
title: Allow to filter Feature Flags
merge_request: 8821
author:
type: added
......@@ -26,7 +26,6 @@ describe Projects::FeatureFlagsController do
it 'shows an empty state with buttons' do
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('_new_feature_flag_button')
end
......@@ -41,7 +40,6 @@ describe Projects::FeatureFlagsController do
it 'shows an list of feature flags with buttons' do
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('_new_feature_flag_button')
end
......
......@@ -59,29 +59,41 @@ describe 'Feature Flags', :js do
add_feature_flag('feature-flag-to-edit', 'with some description', false)
end
context 'and input is valid' do
it 'updates the feature flag' do
name = 'new-name'
description = 'new description'
shared_examples_for 'correct edit behavior' do
context 'and input is valid' do
it 'updates the feature flag' do
name = 'new-name'
description = 'new description'
edit_feature_flag('feature-flag-to-edit', name, description, true)
edit_feature_flag('feature-flag-to-edit', name, description, true)
expect_feature_flag(name, description, true)
expect(page).to have_selector '.flash-container', text: 'successfully updated'
expect_feature_flag(name, description, true)
expect(page).to have_selector '.flash-container', text: 'successfully updated'
end
end
end
context 'and input is invalid' do
where(:name, :description, :error_message, &invalid_input_table)
context 'and input is invalid' do
where(:name, :description, :error_message, &invalid_input_table)
with_them do
it 'displays an error message' do
edit_feature_flag('feature-flag-to-edit', name, description, false)
with_them do
it 'displays an error message' do
edit_feature_flag('feature-flag-to-edit', name, description, false)
expect(page).to have_selector '.alert-danger', text: error_message
expect(page).to have_selector '.alert-danger', text: error_message
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
......@@ -89,36 +101,106 @@ describe 'Feature Flags', :js do
add_feature_flag('feature-flag-to-delete', 'with some description', false)
end
context 'and no feature flags are left' do
it 'shows empty state' do
shared_examples_for 'correct delete behavior' do
context 'and no feature flags are left' do
it 'shows empty state' do
visit(project_feature_flags_path(project))
delete_feature_flag('feature-flag-to-delete')
expect_empty_state
end
end
context 'and there is a feature flag left' do
before do
add_feature_flag('another-feature-flag', '', true)
end
it 'shows feature flag table without deleted feature flag' do
visit(project_feature_flags_path(project))
delete_feature_flag('feature-flag-to-delete')
expect_feature_flag('another-feature-flag', '', true)
end
end
it 'does not delete if modal is cancelled' do
visit(project_feature_flags_path(project))
delete_feature_flag('feature-flag-to-delete')
delete_feature_flag('feature-flag-to-delete', false)
expect_empty_state
expect_feature_flag('feature-flag-to-delete', 'with some description', false)
end
end
context 'and there is a feature flag left' do
it_behaves_like 'correct delete behavior'
context 'when operations_feature_flag_index_tab feature flag is disabled' do
before do
add_feature_flag('another-feature-flag', '', true)
stub_feature_flags(operations_feature_flag_index_tab: false)
end
it 'shows feature flag table without deleted feature flag' do
visit(project_feature_flags_path(project))
it_behaves_like 'correct delete behavior'
end
end
delete_feature_flag('feature-flag-to-delete')
context 'when user sees empty index page' do
before do
visit(project_feature_flags_path(project))
end
expect_feature_flag('another-feature-flag', '', true)
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 'does not delete if modal is cancelled' do
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
delete_feature_flag('feature-flag-to-delete', false)
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
expect_feature_flag('feature-flag-to-delete', 'with some description', false)
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
......@@ -140,7 +222,13 @@ describe 'Feature Flags', :js do
end
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
within '.modal' do
......@@ -154,7 +242,14 @@ describe 'Feature Flags', :js do
def edit_feature_flag(old_name, new_name, new_description, new_status)
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
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 ""
msgid "FeatureFlags|Delete %{feature_flag_name}?"
msgstr ""
msgid "FeatureFlags|Delete %{name}?"
msgstr ""
msgid "FeatureFlags|Delete feature flag"
msgstr ""
msgid "FeatureFlags|Description"
msgstr ""
......@@ -3713,6 +3719,9 @@ msgstr ""
msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?"
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."
msgstr ""
......@@ -3728,6 +3737,9 @@ msgstr ""
msgid "FeatureFlags|Instance ID"
msgstr ""
msgid "FeatureFlags|More Information"
msgstr ""
msgid "FeatureFlags|More information"
msgstr ""
......@@ -3746,6 +3758,12 @@ msgstr ""
msgid "FeatureFlags|Status"
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"
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