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 { ...@@ -11,7 +11,7 @@ export default {
Icon, Icon,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
...@@ -64,7 +64,7 @@ export default { ...@@ -64,7 +64,7 @@ export default {
<div class="d-inline-block"> <div class="d-inline-block">
<gl-button <gl-button
v-gl-tooltip.hover.bottom="__('Delete')" v-gl-tooltip.hover.bottom="__('Delete')"
v-gl-modal="modalId" v-gl-modal-directive="modalId"
class="js-feature-flag-delete-button" class="js-feature-flag-delete-button"
variant="danger" variant="danger"
> >
......
<script> <script>
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import _ from 'underscore';
import { GlEmptyState, GlLoadingIcon, GlButton } from '@gitlab/ui';
import FeatureFlagsTable from './feature_flags_table.vue'; import FeatureFlagsTable from './feature_flags_table.vue';
import store from '../store'; import store from '../store';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -20,6 +21,7 @@ export default { ...@@ -20,6 +21,7 @@ export default {
TablePagination, TablePagination,
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlButton,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -38,6 +40,15 @@ export default { ...@@ -38,6 +40,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
canUserConfigure: {
type: Boolean,
required: true,
},
newFeatureFlagPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
...@@ -97,6 +108,9 @@ export default { ...@@ -97,6 +108,9 @@ export default {
}, },
]; ];
}, },
hasNewPath() {
return !_.isEmpty(this.newFeatureFlagPath);
},
}, },
created() { created() {
this.setFeatureFlagsEndpoint(this.endpoint); this.setFeatureFlagsEndpoint(this.endpoint);
...@@ -136,6 +150,29 @@ export default { ...@@ -136,6 +150,29 @@ export default {
</script> </script>
<template> <template>
<div> <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"> <div v-if="shouldRenderTabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<navigation-tabs :tabs="tabs" scope="featureflags" @onChangeTab="onChangeTab" /> <navigation-tabs :tabs="tabs" scope="featureflags" @onChangeTab="onChangeTab" />
</div> </div>
...@@ -147,32 +184,32 @@ export default { ...@@ -147,32 +184,32 @@ export default {
class="js-loading-state prepend-top-20" class="js-loading-state prepend-top-20"
/> />
<template v-else-if="shouldRenderErrorState"> <gl-empty-state
<gl-empty-state v-else-if="shouldRenderErrorState"
:title="s__(`FeatureFlags|There was an error fetching the feature flags.`)" :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.`)" :description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
:svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
/> />
</template>
<template v-else-if="shouldShowEmptyState"> <gl-empty-state
<gl-empty-state v-else-if="shouldShowEmptyState"
class="js-feature-flags-empty-state" class="js-feature-flags-empty-state"
:title="s__(`FeatureFlags|Get started with feature flags`)" :title="s__(`FeatureFlags|Get started with feature flags`)"
:description=" :description="
s__( s__(
`FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.`, `FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.`,
) )
" "
:svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
:primary-button-link="featureFlagsHelpPagePath" :primary-button-link="featureFlagsHelpPagePath"
:primary-button-text="s__(`FeatureFlags|More Information`)" :primary-button-text="s__(`FeatureFlags|More Information`)"
/> />
</template>
<template v-else-if="shouldRenderTable"> <feature-flags-table
<feature-flags-table :csrf-token="csrfToken" :feature-flags="featureFlags" /> v-else-if="shouldRenderTable"
</template> :csrf-token="csrfToken"
:feature-flags="featureFlags"
/>
<table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" /> <table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" />
</div> </div>
......
...@@ -20,6 +20,8 @@ export default () => ...@@ -20,6 +20,8 @@ export default () =>
errorStateSvgPath: this.dataset.errorStateSvgPath, errorStateSvgPath: this.dataset.errorStateSvgPath,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath, featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
csrfToken: csrf.token, 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' = render 'configure_feature_flags_modal'
- if Feature.enabled?(:operations_feature_flag_index_tab, default_enabled: true) #feature-flags-vue{ data: { endpoint: project_feature_flags_path(@project, format: :json),
%h3.page-title.with-button "error-state-svg-path" => image_path('illustrations/feature_flag.svg'),
= _('Feature Flags') "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),
.pull-right "new-feature-flag-path" => can?(current_user, :create_feature_flag, @project) ? new_project_feature_flag_path(@project): nil } }
= 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'
...@@ -27,10 +27,8 @@ describe Projects::FeatureFlagsController do ...@@ -27,10 +27,8 @@ describe Projects::FeatureFlagsController do
subject subject
end end
it 'shows an empty state with buttons' do it 'renders page' do
expect(response).to be_ok 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
end end
...@@ -41,10 +39,8 @@ describe Projects::FeatureFlagsController do ...@@ -41,10 +39,8 @@ describe Projects::FeatureFlagsController do
subject subject
end end
it 'shows an list of feature flags with buttons' do it 'renders page' do
expect(response).to be_ok expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button')
end end
end end
......
...@@ -86,14 +86,6 @@ describe 'Feature Flags', :js do ...@@ -86,14 +86,6 @@ describe 'Feature Flags', :js do
end end
it_behaves_like 'correct edit behavior' 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 end
context 'when deleting a feature flag' do context 'when deleting a feature flag' do
...@@ -136,14 +128,6 @@ describe 'Feature Flags', :js do ...@@ -136,14 +128,6 @@ describe 'Feature Flags', :js do
end end
it_behaves_like 'correct delete behavior' 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 end
context 'when user sees empty index page' do context 'when user sees empty index page' do
...@@ -160,14 +144,6 @@ describe 'Feature Flags', :js do ...@@ -160,14 +144,6 @@ describe 'Feature Flags', :js do
end end
it_behaves_like 'correct empty index behavior' 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 end
context 'when user sees index page' do context 'when user sees index page' do
...@@ -182,6 +158,8 @@ describe 'Feature Flags', :js do ...@@ -182,6 +158,8 @@ describe 'Feature Flags', :js do
it 'shows all feature flags' do it 'shows all feature flags' do
expect(page).to have_content(feature_flag_enabled.name) expect(page).to have_content(feature_flag_enabled.name)
expect(page).to have_content(feature_flag_disabled.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
end end
...@@ -222,12 +200,7 @@ describe 'Feature Flags', :js do ...@@ -222,12 +200,7 @@ describe 'Feature Flags', :js do
end end
def delete_feature_flag(name, confirm = true) def delete_feature_flag(name, confirm = true)
delete_button = delete_button = find('.gl-responsive-table-row', text: name).find('.js-feature-flag-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
...@@ -243,12 +216,7 @@ describe 'Feature Flags', :js do ...@@ -243,12 +216,7 @@ 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 = edit_button = find('.gl-responsive-table-row', text: old_name).find('.js-feature-flag-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
......
...@@ -12,6 +12,8 @@ describe('Feature Flags', () => { ...@@ -12,6 +12,8 @@ describe('Feature Flags', () => {
csrfToken: 'testToken', csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg', errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags', featureFlagsHelpPagePath: '/help/feature-flags',
canUserConfigure: true,
newFeatureFlagPath: 'feature-flags/new',
}; };
let store; let store;
...@@ -30,6 +32,43 @@ describe('Feature Flags', () => { ...@@ -30,6 +32,43 @@ describe('Feature Flags', () => {
mock.restore(); 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', () => { describe('loading state', () => {
it('renders a loading icon', done => { it('renders a loading icon', done => {
mock.onGet(mockData.endpoint).reply(200, { mock.onGet(mockData.endpoint).reply(200, {
...@@ -84,6 +123,14 @@ describe('Feature Flags', () => { ...@@ -84,6 +123,14 @@ describe('Feature Flags', () => {
it('should render the empty state', () => { it('should render the empty state', () => {
expect(component.$el.querySelectorAll('.js-feature-flags-empty-state')).not.toBeNull(); 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', () => { describe('with paginated feature flags', () => {
...@@ -131,6 +178,14 @@ describe('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', () => { describe('pagination', () => {
it('should render pagination', () => { it('should render pagination', () => {
expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5); expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
...@@ -185,5 +240,13 @@ describe('Feature Flags', () => { ...@@ -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.', '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 "" ...@@ -3749,9 +3749,6 @@ msgstr ""
msgid "FeatureFlags|Create feature flag" msgid "FeatureFlags|Create feature flag"
msgstr "" msgstr ""
msgid "FeatureFlags|Delete %{feature_flag_name}?"
msgstr ""
msgid "FeatureFlags|Delete %{name}?" msgid "FeatureFlags|Delete %{name}?"
msgstr "" msgstr ""
...@@ -3770,10 +3767,10 @@ msgstr "" ...@@ -3770,10 +3767,10 @@ msgstr ""
msgid "FeatureFlags|Feature Flag" msgid "FeatureFlags|Feature Flag"
msgstr "" msgstr ""
msgid "FeatureFlags|Feature flag" msgid "FeatureFlags|Feature Flags"
msgstr "" msgstr ""
msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?" msgid "FeatureFlags|Feature flag"
msgstr "" msgstr ""
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?" 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