Commit ca76d547 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Jose Ivan Vargas

Migrate to GlTabs for Feature Flags Page

This uses the tabs from GitLab UI!
parent a5c32b12
......@@ -42,10 +42,6 @@ export default {
},
props: {
helpPath: {
type: String,
required: true,
},
helpClientLibrariesPath: {
type: String,
required: true,
......@@ -80,7 +76,7 @@ export default {
required: true,
},
},
inject: ['projectName'],
inject: ['projectName', 'featureFlagsHelpPagePath'],
data() {
return {
enteredProjectName: '',
......@@ -149,7 +145,9 @@ export default {
</gl-link>
</template>
<template #docsLink="{ content }">
<gl-link :href="helpPath" target="_blank" data-testid="help-link">{{ content }}</gl-link>
<gl-link :href="featureFlagsHelpPagePath" target="_blank" data-testid="help-link">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>
......
<script>
import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTab } from '@gitlab/ui';
export default {
components: { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTab },
props: {
title: {
required: true,
type: String,
},
count: {
required: false,
type: Number,
default: null,
},
alerts: {
required: true,
type: Array,
},
isLoading: {
required: true,
type: Boolean,
},
loadingLabel: {
required: true,
type: String,
},
errorState: {
required: true,
type: Boolean,
},
errorTitle: {
required: true,
type: String,
},
emptyState: {
required: true,
type: Boolean,
},
emptyTitle: {
required: true,
type: String,
},
},
inject: ['errorStateSvgPath', 'featureFlagsHelpPagePath'],
computed: {
itemCount() {
return this.count ?? 0;
},
},
methods: {
clearAlert(index) {
this.$emit('dismissAlert', index);
},
onClick(event) {
return this.$emit('changeTab', event);
},
},
};
</script>
<template>
<gl-tab @click="onClick">
<template #title>
<span data-testid="feature-flags-tab-title">{{ title }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemCount }}</gl-badge>
</template>
<template>
<gl-alert
v-for="(message, index) in alerts"
:key="index"
data-testid="serverErrors"
variant="danger"
@dismiss="clearAlert(index)"
>
{{ message }}
</gl-alert>
<gl-loading-icon v-if="isLoading" :label="loadingLabel" size="md" class="gl-mt-4" />
<gl-empty-state
v-else-if="errorState"
:title="errorTitle"
:description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
:svg-path="errorStateSvgPath"
data-testid="error-state"
/>
<gl-empty-state
v-else-if="emptyState"
:title="emptyTitle"
:svg-path="errorStateSvgPath"
data-testid="empty-state"
>
<template #description>
{{
s__(
'FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.',
)
}}
<gl-link :href="featureFlagsHelpPagePath" target="_blank">
{{ s__('FeatureFlags|More information') }}
</gl-link>
</template>
</gl-empty-state>
<slot> </slot>
</template>
</gl-tab>
</template>
......@@ -16,6 +16,8 @@ export default () =>
provide() {
return {
projectName: this.dataset.projectName,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
errorStateSvgPath: this.dataset.errorStateSvgPath,
};
},
render(createElement) {
......@@ -23,8 +25,6 @@ export default () =>
props: {
endpoint: this.dataset.endpoint,
projectId: this.dataset.projectId,
errorStateSvgPath: this.dataset.errorStateSvgPath,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
featureFlagsClientLibrariesHelpPagePath: this.dataset
.featureFlagsClientLibrariesHelpPagePath,
featureFlagsClientExampleHelpPagePath: this.dataset.featureFlagsClientExampleHelpPagePath,
......
......@@ -5,10 +5,12 @@ import Callout from '~/vue_shared/components/callout.vue';
describe('Configure Feature Flags Modal', () => {
const mockEvent = { preventDefault: jest.fn() };
const projectName = 'fakeProjectName';
const provide = {
projectName: 'fakeProjectName',
featureFlagsHelpPagePath: '/help/path',
};
const propsData = {
helpPath: '/help/path',
helpClientLibrariesPath: '/help/path/#flags',
helpClientExamplePath: '/feature-flags#clientexample',
apiUrl: '/api/url',
......@@ -21,9 +23,7 @@ describe('Configure Feature Flags Modal', () => {
let wrapper;
const factory = (props = {}, { mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(Component, {
provide: {
projectName,
},
provide,
stubs: { GlSprintf },
propsData: {
...propsData,
......@@ -61,7 +61,7 @@ describe('Configure Feature Flags Modal', () => {
});
it('should clear the project name input after generating the token', async () => {
findProjectNameInput().vm.$emit('input', projectName);
findProjectNameInput().vm.$emit('input', provide.projectName);
findGlModal().vm.$emit('primary', mockEvent);
await wrapper.vm.$nextTick();
expect(findProjectNameInput().attributes('value')).toBe('');
......@@ -78,7 +78,9 @@ describe('Configure Feature Flags Modal', () => {
});
it('should have links to the documentation', () => {
expect(wrapper.find('[data-testid="help-link"]').attributes('href')).toBe(propsData.helpPath);
expect(wrapper.find('[data-testid="help-link"]').attributes('href')).toBe(
provide.featureFlagsHelpPagePath,
);
expect(wrapper.find('[data-testid="help-client-link"]').attributes('href')).toBe(
propsData.helpClientLibrariesPath,
);
......@@ -91,7 +93,9 @@ describe('Configure Feature Flags Modal', () => {
});
it('should display a message asking to fill the project name', () => {
expect(wrapper.find('[data-testid="prevent-accident-text"]').text()).toMatch(projectName);
expect(wrapper.find('[data-testid="prevent-accident-text"]').text()).toMatch(
provide.projectName,
);
});
it('should display the api URL in an input box', () => {
......@@ -110,7 +114,7 @@ describe('Configure Feature Flags Modal', () => {
beforeEach(factory);
it('should enable the primary action', async () => {
findProjectNameInput().vm.$emit('input', projectName);
findProjectNameInput().vm.$emit('input', provide.projectName);
await wrapper.vm.$nextTick();
const [{ disabled }] = findPrimaryAction().attributes;
expect(disabled).toBe(false);
......
......@@ -2,14 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import Api from 'ee/api';
import store from 'ee/feature_flags/store';
import { createStore } from 'ee/feature_flags/store';
import FeatureFlagsTab from 'ee/feature_flags/components/feature_flags_tab.vue';
import FeatureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue';
import FeatureFlagsTable from 'ee/feature_flags/components/feature_flags_table.vue';
import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue';
import ConfigureFeatureFlagsModal from 'ee/feature_flags/components/configure_feature_flags_modal.vue';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from 'ee/feature_flags/constants';
import { TEST_HOST } from 'spec/test_constants';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import axios from '~/lib/utils/axios_utils';
import { getRequestData, userList } from '../mock_data';
......@@ -18,8 +18,6 @@ describe('Feature flags', () => {
const mockData = {
endpoint: `${TEST_HOST}/endpoint.json`,
csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients',
featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example',
unleashApiUrl: `${TEST_HOST}/api/unleash`,
......@@ -33,12 +31,20 @@ describe('Feature flags', () => {
let wrapper;
let mock;
let store;
const factory = (propsData = mockData, fn = shallowMount) => {
store = createStore();
wrapper = fn(FeatureFlagsComponent, {
store,
propsData,
provide: {
projectName: 'fakeProjectName',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
},
stubs: {
FeatureFlagsTab,
},
});
};
......@@ -49,7 +55,6 @@ describe('Feature flags', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
jest.spyOn(store, 'dispatch');
jest.spyOn(Api, 'fetchFeatureFlagUserLists').mockResolvedValue({
data: [userList],
headers: {
......@@ -66,6 +71,7 @@ describe('Feature flags', () => {
afterEach(() => {
mock.restore();
wrapper.destroy();
wrapper = null;
});
describe('without permissions', () => {
......@@ -127,32 +133,30 @@ describe('Feature flags', () => {
describe('without feature flags', () => {
let emptyState;
beforeEach(done => {
mock
.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.replyOnce(
200,
{
feature_flags: [],
count: {
all: 0,
enabled: 0,
disabled: 0,
},
beforeEach(async () => {
mock.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } }).reply(
200,
{
feature_flags: [],
count: {
all: 0,
enabled: 0,
disabled: 0,
},
{},
);
},
{},
);
factory();
await wrapper.vm.$nextTick();
setImmediate(() => {
emptyState = wrapper.find(GlEmptyState);
done();
});
emptyState = wrapper.find(GlEmptyState);
});
it('should render the empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
it('should render the empty state', async () => {
await axios.waitForAll();
emptyState = wrapper.find(GlEmptyState);
expect(emptyState.exists()).toBe(true);
});
it('renders configure button', () => {
......@@ -189,6 +193,7 @@ describe('Feature flags', () => {
});
factory();
jest.spyOn(store, 'dispatch');
setImmediate(() => {
done();
});
......@@ -246,7 +251,7 @@ describe('Feature flags', () => {
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions');
wrapper.find(NavigationTabs).vm.$emit('onChangeTab', USER_LIST_SCOPE);
wrapper.find('[data-testid="user-lists-tab"]').vm.$emit('changeTab');
expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: USER_LIST_SCOPE,
......@@ -265,7 +270,7 @@ describe('Feature flags', () => {
});
});
beforeEach(() => {
wrapper.find(NavigationTabs).vm.$emit('onChangeTab', USER_LIST_SCOPE);
wrapper.find('[data-testid="user-lists-tab"]').vm.$emit('changeTab');
return wrapper.vm.$nextTick();
});
......
import { mount } from '@vue/test-utils';
import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import FeatureFlagsTab from 'ee/feature_flags/components/feature_flags_tab.vue';
const DEFAULT_PROPS = {
title: 'test',
count: 5,
alerts: ['an alert', 'another alert'],
isLoading: false,
loadingLabel: 'test loading',
errorState: false,
errorTitle: 'test title',
emptyState: true,
emptyTitle: 'test empty',
};
const DEFAULT_PROVIDE = {
errorStateSvgPath: '/error.svg',
featureFlagsHelpPagePath: '/help/page/path',
};
describe('ee/feature_flags/components/feature_flags_tab.vue', () => {
let wrapper;
const factory = (props = {}) =>
mount(
{
components: {
GlTabs,
FeatureFlagsTab,
},
render(h) {
return h(GlTabs, [
h(FeatureFlagsTab, { props: this.$attrs, on: this.$listeners }, this.$slots.default),
]);
},
},
{
propsData: {
...DEFAULT_PROPS,
...props,
},
provide: DEFAULT_PROVIDE,
slots: {
default: '<p data-testid="test-slot">testing</p>',
},
},
);
afterEach(() => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = null;
});
describe('alerts', () => {
let alerts;
beforeEach(() => {
wrapper = factory();
alerts = wrapper.findAll(GlAlert);
});
it('should show any alerts', () => {
expect(alerts).toHaveLength(DEFAULT_PROPS.alerts.length);
alerts.wrappers.forEach((alert, i) => expect(alert.text()).toBe(DEFAULT_PROPS.alerts[i]));
});
it('should emit a dismiss event for a dismissed alert', () => {
alerts.at(0).vm.$emit('dismiss');
expect(wrapper.find(FeatureFlagsTab).emitted('dismissAlert')).toEqual([[0]]);
});
});
describe('loading', () => {
beforeEach(() => {
wrapper = factory({ isLoading: true });
});
it('should show a loading icon and nothing else', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findAll(GlEmptyState)).toHaveLength(0);
});
});
describe('error', () => {
let emptyState;
beforeEach(() => {
wrapper = factory({ errorState: true });
emptyState = wrapper.find(GlEmptyState);
});
it('should show an error state if there has been an error', () => {
expect(emptyState.text()).toContain(DEFAULT_PROPS.errorTitle);
expect(emptyState.text()).toContain(
'Try again in a few moments or contact your support team.',
);
expect(emptyState.props('svgPath')).toBe(DEFAULT_PROVIDE.errorStateSvgPath);
});
});
describe('empty', () => {
let emptyState;
let emptyStateLink;
beforeEach(() => {
wrapper = factory({ emptyState: true });
emptyState = wrapper.find(GlEmptyState);
emptyStateLink = emptyState.find(GlLink);
});
it('should show an empty state if it is empty', () => {
expect(emptyState.text()).toContain(DEFAULT_PROPS.emptyTitle);
expect(emptyState.text()).toContain(
'Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.',
);
expect(emptyState.props('svgPath')).toBe(DEFAULT_PROVIDE.errorStateSvgPath);
expect(emptyStateLink.attributes('href')).toBe(DEFAULT_PROVIDE.featureFlagsHelpPagePath);
expect(emptyStateLink.text()).toBe('More information');
});
});
describe('slot', () => {
let slot;
beforeEach(async () => {
wrapper = factory();
await wrapper.vm.$nextTick();
slot = wrapper.find('[data-testid="test-slot"]');
});
it('should display the passed slot', () => {
expect(slot.exists()).toBe(true);
expect(slot.text()).toBe('testing');
});
});
describe('count', () => {
it('should display a count if there is one', async () => {
wrapper = factory();
await wrapper.vm.$nextTick();
expect(wrapper.find(GlBadge).text()).toBe(DEFAULT_PROPS.count.toString());
});
it('should display 0 if there is no count', async () => {
wrapper = factory({ count: undefined });
await wrapper.vm.$nextTick();
expect(wrapper.find(GlBadge).text()).toBe('0');
});
});
describe('title', () => {
it('should show the title', async () => {
wrapper = factory();
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="feature-flags-tab-title"]').text()).toBe(
DEFAULT_PROPS.title,
);
});
});
});
......@@ -10966,6 +10966,9 @@ msgstr ""
msgid "FeatureFlags|Loading feature flags"
msgstr ""
msgid "FeatureFlags|Loading user lists"
msgstr ""
msgid "FeatureFlags|More information"
msgstr ""
......@@ -10984,7 +10987,7 @@ msgstr ""
msgid "FeatureFlags|New feature flag"
msgstr ""
msgid "FeatureFlags|New list"
msgid "FeatureFlags|New user list"
msgstr ""
msgid "FeatureFlags|Percent of users"
......@@ -11023,6 +11026,9 @@ msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr ""
msgid "FeatureFlags|There was an error fetching the user lists."
msgstr ""
msgid "FeatureFlags|There was an error retrieving user lists"
msgstr ""
......@@ -11038,6 +11044,9 @@ msgstr ""
msgid "FeatureFlags|User List"
msgstr ""
msgid "FeatureFlags|User Lists"
msgstr ""
msgid "FeatureFlag|List"
msgstr ""
......@@ -15114,9 +15123,6 @@ msgstr ""
msgid "List your Bitbucket Server repositories"
msgstr ""
msgid "Lists"
msgstr ""
msgid "Live preview"
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