Commit e8b780a3 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'feature-flags-migrate-to-gl-tabs' into 'master'

Migrate to GlTabs for Feature Flags Page

See merge request gitlab-org/gitlab!42371
parents 380d09a8 ca76d547
......@@ -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