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 { ...@@ -42,10 +42,6 @@ export default {
}, },
props: { props: {
helpPath: {
type: String,
required: true,
},
helpClientLibrariesPath: { helpClientLibrariesPath: {
type: String, type: String,
required: true, required: true,
...@@ -80,7 +76,7 @@ export default { ...@@ -80,7 +76,7 @@ export default {
required: true, required: true,
}, },
}, },
inject: ['projectName'], inject: ['projectName', 'featureFlagsHelpPagePath'],
data() { data() {
return { return {
enteredProjectName: '', enteredProjectName: '',
...@@ -149,7 +145,9 @@ export default { ...@@ -149,7 +145,9 @@ export default {
</gl-link> </gl-link>
</template> </template>
<template #docsLink="{ content }"> <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> </template>
</gl-sprintf> </gl-sprintf>
</p> </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 () => ...@@ -16,6 +16,8 @@ export default () =>
provide() { provide() {
return { return {
projectName: this.dataset.projectName, projectName: this.dataset.projectName,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
errorStateSvgPath: this.dataset.errorStateSvgPath,
}; };
}, },
render(createElement) { render(createElement) {
...@@ -23,8 +25,6 @@ export default () => ...@@ -23,8 +25,6 @@ export default () =>
props: { props: {
endpoint: this.dataset.endpoint, endpoint: this.dataset.endpoint,
projectId: this.dataset.projectId, projectId: this.dataset.projectId,
errorStateSvgPath: this.dataset.errorStateSvgPath,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
featureFlagsClientLibrariesHelpPagePath: this.dataset featureFlagsClientLibrariesHelpPagePath: this.dataset
.featureFlagsClientLibrariesHelpPagePath, .featureFlagsClientLibrariesHelpPagePath,
featureFlagsClientExampleHelpPagePath: this.dataset.featureFlagsClientExampleHelpPagePath, featureFlagsClientExampleHelpPagePath: this.dataset.featureFlagsClientExampleHelpPagePath,
......
...@@ -5,10 +5,12 @@ import Callout from '~/vue_shared/components/callout.vue'; ...@@ -5,10 +5,12 @@ import Callout from '~/vue_shared/components/callout.vue';
describe('Configure Feature Flags Modal', () => { describe('Configure Feature Flags Modal', () => {
const mockEvent = { preventDefault: jest.fn() }; const mockEvent = { preventDefault: jest.fn() };
const projectName = 'fakeProjectName'; const provide = {
projectName: 'fakeProjectName',
featureFlagsHelpPagePath: '/help/path',
};
const propsData = { const propsData = {
helpPath: '/help/path',
helpClientLibrariesPath: '/help/path/#flags', helpClientLibrariesPath: '/help/path/#flags',
helpClientExamplePath: '/feature-flags#clientexample', helpClientExamplePath: '/feature-flags#clientexample',
apiUrl: '/api/url', apiUrl: '/api/url',
...@@ -21,9 +23,7 @@ describe('Configure Feature Flags Modal', () => { ...@@ -21,9 +23,7 @@ describe('Configure Feature Flags Modal', () => {
let wrapper; let wrapper;
const factory = (props = {}, { mountFn = shallowMount, ...options } = {}) => { const factory = (props = {}, { mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(Component, { wrapper = mountFn(Component, {
provide: { provide,
projectName,
},
stubs: { GlSprintf }, stubs: { GlSprintf },
propsData: { propsData: {
...propsData, ...propsData,
...@@ -61,7 +61,7 @@ describe('Configure Feature Flags Modal', () => { ...@@ -61,7 +61,7 @@ describe('Configure Feature Flags Modal', () => {
}); });
it('should clear the project name input after generating the token', async () => { 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); findGlModal().vm.$emit('primary', mockEvent);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findProjectNameInput().attributes('value')).toBe(''); expect(findProjectNameInput().attributes('value')).toBe('');
...@@ -78,7 +78,9 @@ describe('Configure Feature Flags Modal', () => { ...@@ -78,7 +78,9 @@ describe('Configure Feature Flags Modal', () => {
}); });
it('should have links to the documentation', () => { 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( expect(wrapper.find('[data-testid="help-client-link"]').attributes('href')).toBe(
propsData.helpClientLibrariesPath, propsData.helpClientLibrariesPath,
); );
...@@ -91,7 +93,9 @@ describe('Configure Feature Flags Modal', () => { ...@@ -91,7 +93,9 @@ describe('Configure Feature Flags Modal', () => {
}); });
it('should display a message asking to fill the project name', () => { 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', () => { it('should display the api URL in an input box', () => {
...@@ -110,7 +114,7 @@ describe('Configure Feature Flags Modal', () => { ...@@ -110,7 +114,7 @@ describe('Configure Feature Flags Modal', () => {
beforeEach(factory); beforeEach(factory);
it('should enable the primary action', async () => { it('should enable the primary action', async () => {
findProjectNameInput().vm.$emit('input', projectName); findProjectNameInput().vm.$emit('input', provide.projectName);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
const [{ disabled }] = findPrimaryAction().attributes; const [{ disabled }] = findPrimaryAction().attributes;
expect(disabled).toBe(false); expect(disabled).toBe(false);
......
...@@ -2,14 +2,14 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,14 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import Api from 'ee/api'; 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 FeatureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue';
import FeatureFlagsTable from 'ee/feature_flags/components/feature_flags_table.vue'; import FeatureFlagsTable from 'ee/feature_flags/components/feature_flags_table.vue';
import UserListsTable from 'ee/feature_flags/components/user_lists_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 ConfigureFeatureFlagsModal from 'ee/feature_flags/components/configure_feature_flags_modal.vue';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from 'ee/feature_flags/constants'; import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from 'ee/feature_flags/constants';
import { TEST_HOST } from 'spec/test_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 TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { getRequestData, userList } from '../mock_data'; import { getRequestData, userList } from '../mock_data';
...@@ -18,8 +18,6 @@ describe('Feature flags', () => { ...@@ -18,8 +18,6 @@ describe('Feature flags', () => {
const mockData = { const mockData = {
endpoint: `${TEST_HOST}/endpoint.json`, endpoint: `${TEST_HOST}/endpoint.json`,
csrfToken: 'testToken', csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients', featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients',
featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example', featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example',
unleashApiUrl: `${TEST_HOST}/api/unleash`, unleashApiUrl: `${TEST_HOST}/api/unleash`,
...@@ -33,12 +31,20 @@ describe('Feature flags', () => { ...@@ -33,12 +31,20 @@ describe('Feature flags', () => {
let wrapper; let wrapper;
let mock; let mock;
let store;
const factory = (propsData = mockData, fn = shallowMount) => { const factory = (propsData = mockData, fn = shallowMount) => {
store = createStore();
wrapper = fn(FeatureFlagsComponent, { wrapper = fn(FeatureFlagsComponent, {
store,
propsData, propsData,
provide: { provide: {
projectName: 'fakeProjectName', projectName: 'fakeProjectName',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
},
stubs: {
FeatureFlagsTab,
}, },
}); });
}; };
...@@ -49,7 +55,6 @@ describe('Feature flags', () => { ...@@ -49,7 +55,6 @@ describe('Feature flags', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
jest.spyOn(store, 'dispatch');
jest.spyOn(Api, 'fetchFeatureFlagUserLists').mockResolvedValue({ jest.spyOn(Api, 'fetchFeatureFlagUserLists').mockResolvedValue({
data: [userList], data: [userList],
headers: { headers: {
...@@ -66,6 +71,7 @@ describe('Feature flags', () => { ...@@ -66,6 +71,7 @@ describe('Feature flags', () => {
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('without permissions', () => { describe('without permissions', () => {
...@@ -127,32 +133,30 @@ describe('Feature flags', () => { ...@@ -127,32 +133,30 @@ describe('Feature flags', () => {
describe('without feature flags', () => { describe('without feature flags', () => {
let emptyState; let emptyState;
beforeEach(done => { beforeEach(async () => {
mock mock.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } }).reply(
.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } }) 200,
.replyOnce( {
200, feature_flags: [],
{ count: {
feature_flags: [], all: 0,
count: { enabled: 0,
all: 0, disabled: 0,
enabled: 0,
disabled: 0,
},
}, },
{}, },
); {},
);
factory(); factory();
await wrapper.vm.$nextTick();
setImmediate(() => { emptyState = wrapper.find(GlEmptyState);
emptyState = wrapper.find(GlEmptyState);
done();
});
}); });
it('should render the empty state', () => { it('should render the empty state', async () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true); await axios.waitForAll();
emptyState = wrapper.find(GlEmptyState);
expect(emptyState.exists()).toBe(true);
}); });
it('renders configure button', () => { it('renders configure button', () => {
...@@ -189,6 +193,7 @@ describe('Feature flags', () => { ...@@ -189,6 +193,7 @@ describe('Feature flags', () => {
}); });
factory(); factory();
jest.spyOn(store, 'dispatch');
setImmediate(() => { setImmediate(() => {
done(); done();
}); });
...@@ -246,7 +251,7 @@ describe('Feature flags', () => { ...@@ -246,7 +251,7 @@ describe('Feature flags', () => {
it('should make an API request when using tabs', () => { it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions'); 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({ expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: USER_LIST_SCOPE, scope: USER_LIST_SCOPE,
...@@ -265,7 +270,7 @@ describe('Feature flags', () => { ...@@ -265,7 +270,7 @@ describe('Feature flags', () => {
}); });
}); });
beforeEach(() => { beforeEach(() => {
wrapper.find(NavigationTabs).vm.$emit('onChangeTab', USER_LIST_SCOPE); wrapper.find('[data-testid="user-lists-tab"]').vm.$emit('changeTab');
return wrapper.vm.$nextTick(); 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 "" ...@@ -10966,6 +10966,9 @@ msgstr ""
msgid "FeatureFlags|Loading feature flags" msgid "FeatureFlags|Loading feature flags"
msgstr "" msgstr ""
msgid "FeatureFlags|Loading user lists"
msgstr ""
msgid "FeatureFlags|More information" msgid "FeatureFlags|More information"
msgstr "" msgstr ""
...@@ -10984,7 +10987,7 @@ msgstr "" ...@@ -10984,7 +10987,7 @@ msgstr ""
msgid "FeatureFlags|New feature flag" msgid "FeatureFlags|New feature flag"
msgstr "" msgstr ""
msgid "FeatureFlags|New list" msgid "FeatureFlags|New user list"
msgstr "" msgstr ""
msgid "FeatureFlags|Percent of users" msgid "FeatureFlags|Percent of users"
...@@ -11023,6 +11026,9 @@ msgstr "" ...@@ -11023,6 +11026,9 @@ msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags." msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr "" msgstr ""
msgid "FeatureFlags|There was an error fetching the user lists."
msgstr ""
msgid "FeatureFlags|There was an error retrieving user lists" msgid "FeatureFlags|There was an error retrieving user lists"
msgstr "" msgstr ""
...@@ -11038,6 +11044,9 @@ msgstr "" ...@@ -11038,6 +11044,9 @@ msgstr ""
msgid "FeatureFlags|User List" msgid "FeatureFlags|User List"
msgstr "" msgstr ""
msgid "FeatureFlags|User Lists"
msgstr ""
msgid "FeatureFlag|List" msgid "FeatureFlag|List"
msgstr "" msgstr ""
...@@ -15114,9 +15123,6 @@ msgstr "" ...@@ -15114,9 +15123,6 @@ msgstr ""
msgid "List your Bitbucket Server repositories" msgid "List your Bitbucket Server repositories"
msgstr "" msgstr ""
msgid "Lists"
msgstr ""
msgid "Live preview" msgid "Live preview"
msgstr "" 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