Commit 41884e50 authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes

Moves index page of feature flags completely to Vue

parent a0463bae
......@@ -11,7 +11,7 @@ export default {
Icon,
},
directives: {
GlModal: GlModalDirective,
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
props: {
......@@ -64,7 +64,7 @@ export default {
<div class="d-inline-block">
<gl-button
v-gl-tooltip.hover.bottom="__('Delete')"
v-gl-modal="modalId"
v-gl-modal-directive="modalId"
class="js-feature-flag-delete-button"
variant="danger"
>
......
<script>
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import _ from 'underscore';
import { GlEmptyState, GlLoadingIcon, GlButton } from '@gitlab/ui';
import FeatureFlagsTable from './feature_flags_table.vue';
import store from '../store';
import { __ } from '~/locale';
......@@ -20,6 +21,7 @@ export default {
TablePagination,
GlEmptyState,
GlLoadingIcon,
GlButton,
},
props: {
endpoint: {
......@@ -38,6 +40,15 @@ export default {
type: String,
required: true,
},
canUserConfigure: {
type: Boolean,
required: true,
},
newFeatureFlagPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -97,6 +108,9 @@ export default {
},
];
},
hasNewPath() {
return !_.isEmpty(this.newFeatureFlagPath);
},
},
created() {
this.setFeatureFlagsEndpoint(this.endpoint);
......@@ -136,6 +150,29 @@ export default {
</script>
<template>
<div>
<h3 class="page-title with-button">
{{ s__('FeatureFlags|Feature Flags') }}
<div class="pull-right">
<button
v-if="canUserConfigure"
type="button"
class="js-ff-configure append-right-8 btn-inverted btn btn-primary"
data-toggle="modal"
data-target="#configure-feature-flags-modal"
>
{{ s__('FeatureFlags|Configure') }}
</button>
<gl-button
v-if="hasNewPath"
:href="newFeatureFlagPath"
variant="success"
class="js-ff-new"
>{{ s__('FeatureFlags|New Feature Flag') }}</gl-button
>
</div>
</h3>
<div v-if="shouldRenderTabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<navigation-tabs :tabs="tabs" scope="featureflags" @onChangeTab="onChangeTab" />
</div>
......@@ -147,32 +184,32 @@ export default {
class="js-loading-state 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>
<gl-empty-state
v-else-if="shouldRenderErrorState"
: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 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>
<gl-empty-state
v-else-if="shouldShowEmptyState"
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 v-else-if="shouldRenderTable">
<feature-flags-table :csrf-token="csrfToken" :feature-flags="featureFlags" />
</template>
<feature-flags-table
v-else-if="shouldRenderTable"
:csrf-token="csrfToken"
:feature-flags="featureFlags"
/>
<table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" />
</div>
......
......@@ -20,6 +20,8 @@ export default () =>
errorStateSvgPath: this.dataset.errorStateSvgPath,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
csrfToken: csrf.token,
canUserConfigure: this.dataset.canUserAdminFeatureFlag,
newFeatureFlagPath: this.dataset.newFeatureFlagPath,
},
});
},
......
- if can?(current_user, :admin_feature_flag, @project)
%button.btn.btn-primary.btn-inverted.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }>
= s_('FeatureFlags|Configure')
- if can?(current_user, :destroy_feature_flag, @project)
.modal{ id: "delete-feature-flag-modal-#{feature_flag.id}",
tabindex: -1,
role: 'dialog' }
.modal-dialog{ role: 'document' }
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
- truncated_feature_flag_name = capture do
%span.text-truncate.prepend-left-4.append-right-4= feature_flag.name
= s_('FeatureFlags|Delete %{feature_flag_name}?').html_safe % { feature_flag_name: truncated_feature_flag_name }
%button.close{ type: 'button', data: { dismiss: 'modal' }, aria: { label: _('Close') } }
%span{ "aria-hidden": true } &times;
.modal-body
%p
- monospace_feature_flag_name = capture do
%span.text-monospace= feature_flag.name
= s_('FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?').html_safe % { feature_flag_name: monospace_feature_flag_name }
.modal-footer
%button{ type: 'button', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
= button_to 'Delete',
project_feature_flag_path(@project, feature_flag),
title: 'Delete',
method: :delete,
class: 'btn btn-remove'
.row.empty-state
.col-12
.svg-content
= image_tag 'illustrations/feature_flag.svg'
.col-12
.text-content
%h4.text-center= s_('FeatureFlags|Get started with feature flags')
%p
= s_('FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.')
= link_to 'More information', help_page_path("user/project/operations/feature_flags")
.text-center
= render 'new_feature_flag_button'
= render 'configure_feature_flags_button'
- if can?(current_user, :create_feature_flag, @project)
= link_to new_project_feature_flag_path(@project), class: 'btn btn-success' do
= s_('FeatureFlags|New Feature Flag')
.table-holder.border-top
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'columnheader' }= s_('FeatureFlags|Status')
.table-section.section-50{ role: 'columnheader' }= s_('FeatureFlags|Feature flag')
- @feature_flags.each do |feature_flag|
= render 'delete_feature_flag_modal', { feature_flag: feature_flag }
.gl-responsive-table-row{ role: 'row' }
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: "rowheader" }= s_('FeatureFlags|Status')
.table-mobile-content
- if feature_flag.active?
%span.badge.badge-success
= s_('FeatureFlags|Active')
- else
%span.badge.badge-danger
= s_('FeatureFlags|Inactive')
.table-section.section-50{ role: 'gridcell' }
.table-mobile-header{ role: "rowheader" }= s_('FeatureFlags|Feature Flag')
.table-mobile-content.d-flex.flex-column
.text-monospace.text-truncate= feature_flag.name
.text-secondary.text-truncate= feature_flag.description
.table-section.section-40.table-button-footer{ role: 'gridcell' }
.table-action-buttons.btn-group
- if can?(current_user, :update_feature_flag, @project)
= link_to edit_project_feature_flag_path(@project, feature_flag),
class: 'btn btn-default has-tooltip',
type: 'button',
title: _('Edit') do
= sprite_icon('pencil', size: 16)
- if can?(current_user, :destroy_feature_flag, @project)
%button.btn.btn-danger.has-tooltip{ type: 'button',
data: { toggle: 'modal',
target: "#delete-feature-flag-modal-#{feature_flag.id}" },
title: _('Delete') }
= sprite_icon('remove', size: 16)
= paginate @feature_flags, theme: "gitlab"
- page_title _('Feature Flags')
- page_title s_('FeatureFlags|Feature Flags')
= render 'configure_feature_flags_modal'
- if Feature.enabled?(:operations_feature_flag_index_tab, default_enabled: true)
%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
- 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'
#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"),
"can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
"new-feature-flag-path" => can?(current_user, :create_feature_flag, @project) ? new_project_feature_flag_path(@project): nil } }
......@@ -27,10 +27,8 @@ describe Projects::FeatureFlagsController do
subject
end
it 'shows an empty state with buttons' do
it 'renders page' do
expect(response).to be_ok
expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button')
end
end
......@@ -41,10 +39,8 @@ describe Projects::FeatureFlagsController do
subject
end
it 'shows an list of feature flags with buttons' do
expect(response).to be_ok
expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button')
it 'renders page' do
expect(response).to have_gitlab_http_status(:ok)
end
end
......
......@@ -86,14 +86,6 @@ describe 'Feature Flags', :js do
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
......@@ -136,14 +128,6 @@ describe 'Feature Flags', :js do
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
......@@ -160,14 +144,6 @@ describe 'Feature Flags', :js do
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
......@@ -182,6 +158,8 @@ describe 'Feature Flags', :js 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)
expect(page).to have_link('New Feature Flag')
expect(page).to have_button('Configure')
end
end
......@@ -222,12 +200,7 @@ describe 'Feature Flags', :js do
end
def delete_feature_flag(name, confirm = true)
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 = find('.gl-responsive-table-row', text: name).find('.js-feature-flag-delete-button')
delete_button.click
......@@ -243,12 +216,7 @@ 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 =
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 = find('.gl-responsive-table-row', text: old_name).find('.js-feature-flag-edit-button')
edit_button.click
......
......@@ -12,6 +12,8 @@ describe('Feature Flags', () => {
csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
canUserConfigure: true,
newFeatureFlagPath: 'feature-flags/new',
};
let store;
......@@ -30,6 +32,43 @@ describe('Feature Flags', () => {
mock.restore();
});
describe('without permissions', () => {
const props = {
endpoint: 'feature_flags.json',
csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
canUserConfigure: false,
};
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, {
feature_flags: [],
count: {
all: 0,
enabled: 0,
disabled: 0,
},
});
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props,
});
setTimeout(() => {
done();
}, 0);
});
it('does not render configure button', () => {
expect(component.$el.querySelector('.js-ff-configure')).toBeNull();
});
it('does not render new feature flag button', () => {
expect(component.$el.querySelector('.js-ff-new')).toBeNull();
});
});
describe('loading state', () => {
it('renders a loading icon', done => {
mock.onGet(mockData.endpoint).reply(200, {
......@@ -84,6 +123,14 @@ describe('Feature Flags', () => {
it('should render the empty state', () => {
expect(component.$el.querySelectorAll('.js-feature-flags-empty-state')).not.toBeNull();
});
it('renders configure button', () => {
expect(component.$el.querySelector('.js-ff-configure')).not.toBeNull();
});
it('renders new feature flag button', () => {
expect(component.$el.querySelector('.js-ff-new')).not.toBeNull();
});
});
describe('with paginated feature flags', () => {
......@@ -131,6 +178,14 @@ describe('Feature Flags', () => {
);
});
it('renders configure button', () => {
expect(component.$el.querySelector('.js-ff-configure')).not.toBeNull();
});
it('renders new feature flag button', () => {
expect(component.$el.querySelector('.js-ff-new')).not.toBeNull();
});
describe('pagination', () => {
it('should render pagination', () => {
expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
......@@ -185,5 +240,13 @@ describe('Feature Flags', () => {
'There was an error fetching the feature flags. Try again in a few moments or contact your support team.',
);
});
it('renders configure button', () => {
expect(component.$el.querySelector('.js-ff-configure')).not.toBeNull();
});
it('renders new feature flag button', () => {
expect(component.$el.querySelector('.js-ff-new')).not.toBeNull();
});
});
});
......@@ -3749,9 +3749,6 @@ msgstr ""
msgid "FeatureFlags|Create feature flag"
msgstr ""
msgid "FeatureFlags|Delete %{feature_flag_name}?"
msgstr ""
msgid "FeatureFlags|Delete %{name}?"
msgstr ""
......@@ -3770,10 +3767,10 @@ msgstr ""
msgid "FeatureFlags|Feature Flag"
msgstr ""
msgid "FeatureFlags|Feature flag"
msgid "FeatureFlags|Feature Flags"
msgstr ""
msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?"
msgid "FeatureFlags|Feature flag"
msgstr ""
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?"
......
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