Commit aa69348a authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'display-user-lists-fe' into 'master'

Display Saved User Lists by Feature Flags

See merge request gitlab-org/gitlab!34294
parents f08b4a7e 56c07911
......@@ -308,10 +308,10 @@ export default {
return axios.put(`${url}/${node.id}`, node);
},
fetchFeatureFlagUserLists(id) {
fetchFeatureFlagUserLists(id, page) {
const url = Api.buildUrl(this.featureFlagUserLists).replace(':id', id);
return axios.get(url);
return axios.get(url, { params: { page } });
},
createFeatureFlagUserList(id, list) {
......
......@@ -8,7 +8,9 @@ import {
GlModalDirective,
GlLink,
} from '@gitlab/ui';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
import FeatureFlagsTable from './feature_flags_table.vue';
import UserListsTable from './user_lists_table.vue';
import store from '../store';
import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
......@@ -27,6 +29,7 @@ export default {
store,
components: {
FeatureFlagsTable,
UserListsTable,
NavigationTabs,
TablePagination,
GlEmptyState,
......@@ -43,6 +46,10 @@ export default {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
},
csrfToken: {
type: String,
required: true,
......@@ -84,18 +91,18 @@ export default {
},
data() {
return {
scope: getParameterByName('scope') || this.$options.scopes.all,
scope: getParameterByName('scope') || this.$options.scopes.featureFlags,
page: getParameterByName('page') || '1',
};
},
scopes: {
all: 'all',
enabled: 'enabled',
disabled: 'disabled',
[FEATURE_FLAG_SCOPE]: FEATURE_FLAG_SCOPE,
[USER_LIST_SCOPE]: USER_LIST_SCOPE,
},
computed: {
...mapState([
'featureFlags',
FEATURE_FLAG_SCOPE,
USER_LIST_SCOPE,
'count',
'pageInfo',
'isLoading',
......@@ -108,23 +115,23 @@ export default {
canUserRotateToken() {
return this.rotateInstanceIdPath !== '';
},
currentlyDisplayedData() {
return this.dataForScope(this.scope);
},
shouldRenderTabs() {
/* Do not show tabs until after the first request to get the count */
return this.count.all !== undefined;
return this.count[this.scope] !== undefined;
},
shouldRenderPagination() {
return (
!this.isLoading &&
!this.hasError &&
this.featureFlags.length &&
this.pageInfo.total > this.pageInfo.perPage
this.currentlyDisplayedData.length > 0 &&
this.pageInfo[this.scope].total > this.pageInfo[this.scope].perPage
);
},
shouldShowEmptyState() {
return !this.isLoading && !this.hasError && this.featureFlags.length === 0;
},
shouldRenderTable() {
return !this.isLoading && this.featureFlags.length > 0 && !this.hasError;
return !this.isLoading && !this.hasError && this.currentlyDisplayedData.length === 0;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
......@@ -134,22 +141,16 @@ export default {
return [
{
name: __('All'),
scope: scopes.all,
count: this.count.all,
isActive: this.scope === scopes.all,
name: __('Feature Flags'),
scope: scopes[FEATURE_FLAG_SCOPE],
count: this.count[FEATURE_FLAG_SCOPE],
isActive: this.scope === scopes[FEATURE_FLAG_SCOPE],
},
{
name: __('Enabled'),
scope: scopes.enabled,
count: this.count.enabled,
isActive: this.scope === scopes.enabled,
},
{
name: __('Disabled'),
scope: scopes.disabled,
count: this.count.disabled,
isActive: this.scope === scopes.disabled,
name: __('Lists'),
scope: scopes[USER_LIST_SCOPE],
count: this.count[USER_LIST_SCOPE],
isActive: this.scope === scopes[USER_LIST_SCOPE],
},
];
},
......@@ -157,18 +158,15 @@ export default {
return !isEmpty(this.newFeatureFlagPath);
},
emptyStateTitle() {
if (this.scope === this.$options.scopes.disabled) {
return s__(`FeatureFlags|There are no inactive feature flags`);
} else if (this.scope === this.$options.scopes.enabled) {
return s__(`FeatureFlags|There are no active feature flags`);
}
return s__(`FeatureFlags|Get started with feature flags`);
},
},
created() {
this.setFeatureFlagsEndpoint(this.endpoint);
this.setFeatureFlagsOptions({ scope: this.scope, page: this.page });
this.setProjectId(this.projectId);
this.fetchFeatureFlags();
this.fetchUserLists();
this.setInstanceId(this.unleashApiInstanceId);
this.setInstanceIdEndpoint(this.rotateInstanceIdPath);
},
......@@ -177,8 +175,10 @@ export default {
'setFeatureFlagsEndpoint',
'setFeatureFlagsOptions',
'fetchFeatureFlags',
'fetchUserLists',
'setInstanceIdEndpoint',
'setInstanceId',
'setProjectId',
'rotateInstanceId',
'toggleFeatureFlag',
]),
......@@ -206,7 +206,22 @@ export default {
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.setFeatureFlagsOptions(parameters);
this.fetchFeatureFlags();
if (this.scope === this.$options.scopes.featureFlags) {
this.fetchFeatureFlags();
} else {
this.fetchUserLists();
}
},
shouldRenderTable(scope) {
return (
!this.isLoading &&
this.dataForScope(scope).length > 0 &&
!this.hasError &&
this.scope === scope
);
},
dataForScope(scope) {
return this[scope];
},
},
};
......@@ -284,12 +299,21 @@ export default {
</gl-empty-state>
<feature-flags-table
v-else-if="shouldRenderTable"
v-else-if="shouldRenderTable($options.scopes.featureFlags)"
:csrf-token="csrfToken"
:feature-flags="featureFlags"
@toggle-flag="toggleFeatureFlag"
/>
<table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" />
<user-lists-table
v-else-if="shouldRenderTable($options.scopes.userLists)"
:user-lists="userLists"
/>
<table-pagination
v-if="shouldRenderPagination"
:change="onChangePage"
:page-info="pageInfo[scope]"
/>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { GlTooltipDirective } from '@gitlab/ui';
export default {
directives: { GlTooltip: GlTooltipDirective },
mixins: [timeagoMixin],
props: {
userLists: {
type: Array,
required: true,
},
},
translations: {
createdTimeagoLabel: s__('created %{timeago}'),
},
methods: {
createdTimeago(list) {
return sprintf(this.$options.translations.createdTimeagoLabel, {
timeago: this.timeFormatted(list.created_at),
});
},
displayList(list) {
return list.user_xids.replace(/,/g, ', ');
},
},
};
</script>
<template>
<div>
<div
v-for="list in userLists"
:key="list.id"
data-testid="ffUserList"
class="gl-border-b-solid gl-border-gray-100 gl-border-b-1 gl-w-full gl-py-4 gl-display-flex gl-justify-content-space-between"
>
<div class="gl-display-flex gl-flex-direction-column gl-overflow-hidden gl-flex-grow-1">
<span data-testid="ffUserListName" class="gl-font-weight-bold gl-mb-2">{{
list.name
}}</span>
<span
v-gl-tooltip
:title="tooltipTitle(list.created_at)"
data-testid="ffUserListTimestamp"
class="gl-text-gray-500 gl-mb-2"
>
{{ createdTimeago(list) }}
</span>
<span data-testid="ffUserListIds" class="gl-str-truncated">{{ displayList(list) }}</span>
</div>
</div>
</div>
</template>
......@@ -23,3 +23,6 @@ export const LEGACY_FLAG = 'legacy_flag';
export const NEW_FLAG_ALERT = s__(
'FeatureFlags|Feature Flags will look different in the next milestone. No action is needed, but you may notice the functionality was changed to improve the workflow.',
);
export const FEATURE_FLAG_SCOPE = 'featureFlags';
export const USER_LIST_SCOPE = 'userLists';
......@@ -17,6 +17,7 @@ export default () =>
return createElement('feature-flags-component', {
props: {
endpoint: this.dataset.endpoint,
projectId: this.dataset.projectId,
errorStateSvgPath: this.dataset.errorStateSvgPath,
featureFlagsHelpPagePath: this.dataset.featureFlagsHelpPagePath,
featureFlagsAnchoredHelpPagePath: this.dataset.featureFlagsAnchoredHelpPagePath,
......
import * as types from './mutation_types';
import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils';
export const setFeatureFlagsEndpoint = ({ commit }, endpoint) =>
......@@ -10,6 +11,8 @@ export const setFeatureFlagsOptions = ({ commit }, options) =>
export const setInstanceIdEndpoint = ({ commit }, endpoint) =>
commit(types.SET_INSTANCE_ID_ENDPOINT, endpoint);
export const setProjectId = ({ commit }, endpoint) => commit(types.SET_PROJECT_ID, endpoint);
export const setInstanceId = ({ commit }, instanceId) => commit(types.SET_INSTANCE_ID, instanceId);
export const fetchFeatureFlags = ({ state, dispatch }) => {
......@@ -33,6 +36,19 @@ export const receiveFeatureFlagsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAGS_SUCCESS, response);
export const receiveFeatureFlagsError = ({ commit }) => commit(types.RECEIVE_FEATURE_FLAGS_ERROR);
export const fetchUserLists = ({ state, dispatch }) => {
dispatch('requestUserLists');
return Api.fetchFeatureFlagUserLists(state.projectId, state.options.page)
.then(({ data, headers }) => dispatch('receiveUserListsSuccess', { data, headers }))
.catch(() => dispatch('receiveUserListsError'));
};
export const requestUserLists = ({ commit }) => commit(types.REQUEST_USER_LISTS);
export const receiveUserListsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_USER_LISTS_SUCCESS, response);
export const receiveUserListsError = ({ commit }) => commit(types.RECEIVE_USER_LISTS_ERROR);
export const toggleFeatureFlag = ({ dispatch }, flag) => {
dispatch('updateFeatureFlag', flag);
......
......@@ -2,11 +2,15 @@ export const SET_FEATURE_FLAGS_ENDPOINT = 'SET_FEATURE_FLAGS_ENDPOINT';
export const SET_FEATURE_FLAGS_OPTIONS = 'SET_FEATURE_FLAGS_OPTIONS';
export const SET_INSTANCE_ID_ENDPOINT = 'SET_INSTANCE_ID_ENDPOINT';
export const SET_INSTANCE_ID = 'SET_INSTANCE_ID';
export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const REQUEST_FEATURE_FLAGS = 'REQUEST_FEATURE_FLAGS';
export const RECEIVE_FEATURE_FLAGS_SUCCESS = 'RECEIVE_FEATURE_FLAGS_SUCCESS';
export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR';
export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS';
export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS';
export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR';
export const UPDATE_FEATURE_FLAG = 'UPDATE_FEATURE_FLAG';
export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS';
export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR';
......
import Vue from 'vue';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../../constants';
import { mapToScopesViewModel } from '../helpers';
const mapFlag = flag => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) });
const updateFlag = (state, flag) => {
const i = state.featureFlags.findIndex(({ id }) => id === flag.id);
const staleFlag = state.featureFlags.find(({ id }) => id === flag.id);
Vue.set(state.featureFlags, i, flag);
const index = state[FEATURE_FLAG_SCOPE].findIndex(({ id }) => id === flag.id);
Vue.set(state[FEATURE_FLAG_SCOPE], index, flag);
};
if (staleFlag.active !== flag.active) {
const change = flag.active ? 1 : -1;
Vue.set(state.count, 'enabled', state.count.enabled + change);
Vue.set(state.count, 'disabled', state.count.disabled - change);
const createPaginationInfo = (state, headers) => {
let paginationInfo;
if (Object.keys(headers).length) {
const normalizedHeaders = normalizeHeaders(headers);
paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = headers;
}
return paginationInfo;
};
export default {
......@@ -30,28 +35,53 @@ export default {
[types.SET_INSTANCE_ID](state, instance) {
state.instanceId = instance;
},
[types.SET_PROJECT_ID](state, project) {
state.projectId = project;
},
[types.REQUEST_FEATURE_FLAGS](state) {
state.isLoading = true;
},
[types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) {
state.isLoading = false;
state.hasError = false;
state.count = response.data.count;
state.featureFlags = (response.data.feature_flags || []).map(mapFlag);
state[FEATURE_FLAG_SCOPE] = (response.data.feature_flags || []).map(mapFlag);
let paginationInfo;
if (Object.keys(response.headers).length) {
const normalizedHeaders = normalizeHeaders(response.headers);
paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = response.headers;
}
state.pageInfo = paginationInfo;
const paginationInfo = createPaginationInfo(state, response.headers);
state.count = {
...state.count,
[FEATURE_FLAG_SCOPE]: paginationInfo?.total ?? state[FEATURE_FLAG_SCOPE].length,
};
state.pageInfo = {
...state.pageInfo,
[FEATURE_FLAG_SCOPE]: paginationInfo,
};
},
[types.RECEIVE_FEATURE_FLAGS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.REQUEST_USER_LISTS](state) {
state.isLoading = true;
},
[types.RECEIVE_USER_LISTS_SUCCESS](state, response) {
state.isLoading = false;
state.hasError = false;
state[USER_LIST_SCOPE] = response.data || [];
const paginationInfo = createPaginationInfo(state, response.headers);
state.count = {
...state.count,
[USER_LIST_SCOPE]: paginationInfo?.total ?? state[USER_LIST_SCOPE].length,
};
state.pageInfo = {
...state.pageInfo,
[USER_LIST_SCOPE]: paginationInfo,
};
},
[types.RECEIVE_USER_LISTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.REQUEST_ROTATE_INSTANCE_ID](state) {
state.isRotating = true;
state.hasRotateError = false;
......@@ -77,7 +107,7 @@ export default {
updateFlag(state, mapFlag(data));
},
[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](state, i) {
const flag = state.featureFlags.find(({ id }) => i === id);
const flag = state[FEATURE_FLAG_SCOPE].find(({ id }) => i === id);
updateFlag(state, { ...flag, active: !flag.active });
},
};
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../../constants';
export default () => ({
featureFlags: [],
[FEATURE_FLAG_SCOPE]: [],
[USER_LIST_SCOPE]: [],
count: {},
pageInfo: {},
pageInfo: { [FEATURE_FLAG_SCOPE]: {}, [USER_LIST_SCOPE]: {} },
isLoading: true,
hasError: false,
endpoint: null,
......@@ -10,4 +13,5 @@ export default () => ({
isRotating: false,
hasRotateError: false,
options: {},
projectId: '',
});
- page_title s_('FeatureFlags|Feature Flags')
#feature-flags-vue{ data: { endpoint: project_feature_flags_path(@project, format: :json),
"project-id" => @project.id,
"error-state-svg-path" => image_path('illustrations/feature_flag.svg'),
"feature-flags-help-page-path" => help_page_path("user/project/operations/feature_flags"),
"feature-flags-anchored-help-page-path" => help_page_path("user/project/operations/feature_flags", anchor: "client-libraries"),
......
---
title: Display Saved User Lists by Feature Flags
merge_request: 34294
author:
type: added
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 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 } from '../mock_data';
import { getRequestData, userList } from '../mock_data';
describe('Feature flags', () => {
const mockData = {
......@@ -23,6 +26,7 @@ describe('Feature flags', () => {
canUserConfigure: true,
canUserRotateToken: true,
newFeatureFlagPath: 'feature-flags/new',
projectId: '8',
};
let wrapper;
......@@ -40,6 +44,17 @@ describe('Feature flags', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
jest.spyOn(store, 'dispatch');
jest.spyOn(Api, 'fetchFeatureFlagUserLists').mockResolvedValue({
data: [userList],
headers: {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '8',
'X-Prev-Page': '',
'X-TOTAL': '40',
'X-Total-Pages': '5',
},
});
});
afterEach(() => {
......@@ -58,11 +73,12 @@ describe('Feature flags', () => {
featureFlagsAnchoredHelpPagePath: '/help/feature-flags#unleash-clients',
unleashApiUrl: `${TEST_HOST}/api/unleash`,
unleashApiInstanceId: 'oP6sCNRqtRHmpy1gw2-F',
projectId: '8',
};
beforeEach(done => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: 'all', page: '1' } })
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.reply(200, getRequestData, {});
factory(propsData);
......@@ -84,7 +100,7 @@ describe('Feature flags', () => {
describe('loading state', () => {
it('renders a loading icon', () => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: 'all', page: '1' } })
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.replyOnce(200, getRequestData, {});
factory();
......@@ -101,18 +117,20 @@ describe('Feature flags', () => {
let emptyState;
beforeEach(done => {
mock.onGet(mockData.endpoint, { params: { scope: 'all', page: '1' } }).replyOnce(
200,
{
feature_flags: [],
count: {
all: 0,
enabled: 0,
disabled: 0,
mock
.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.replyOnce(
200,
{
feature_flags: [],
count: {
all: 0,
enabled: 0,
disabled: 0,
},
},
},
{},
);
{},
);
factory();
......@@ -134,37 +152,17 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(true);
});
describe('in all tab', () => {
describe('in feature flags tab', () => {
it('renders generic title', () => {
expect(emptyState.props('title')).toEqual('Get started with feature flags');
});
});
describe('in disabled tab', () => {
it('renders disabled title', () => {
wrapper.setData({ scope: 'disabled' });
return wrapper.vm.$nextTick(() => {
expect(emptyState.props('title')).toEqual('There are no inactive feature flags');
});
});
});
describe('in enabled tab', () => {
it('renders enabled title', () => {
wrapper.setData({ scope: 'enabled' });
wrapper.vm.$nextTick(() => {
expect(emptyState.props('title')).toEqual('There are no active feature flags');
});
});
});
});
describe('with paginated feature flags', () => {
beforeEach(done => {
mock
.onGet(mockData.endpoint, { params: { scope: 'all', page: '1' } })
.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.replyOnce(200, getRequestData, {
'x-next-page': '2',
'x-page': '1',
......@@ -183,7 +181,7 @@ describe('Feature flags', () => {
it('should render a table with feature flags', () => {
const table = wrapper.find(FeatureFlagsTable);
expect(table.exists()).toBe(true);
expect(table.props('featureFlags')).toEqual(
expect(table.props(FEATURE_FLAG_SCOPE)).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: getRequestData.feature_flags[0].name,
......@@ -196,7 +194,7 @@ describe('Feature flags', () => {
it('should toggle a flag when receiving the toggle-flag event', () => {
const table = wrapper.find(FeatureFlagsTable);
const [flag] = table.props('featureFlags');
const [flag] = table.props(FEATURE_FLAG_SCOPE);
table.vm.$emit('toggle-flag', flag);
expect(store.dispatch).toHaveBeenCalledWith('index/toggleFeatureFlag', flag);
......@@ -220,27 +218,52 @@ describe('Feature flags', () => {
wrapper.find(TablePagination).vm.change(4);
expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: 'all',
scope: FEATURE_FLAG_SCOPE,
page: '4',
});
});
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions');
wrapper.find(NavigationTabs).vm.$emit('onChangeTab', 'enabled');
wrapper.find(NavigationTabs).vm.$emit('onChangeTab', USER_LIST_SCOPE);
expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: 'enabled',
scope: USER_LIST_SCOPE,
page: '1',
});
});
});
});
describe('in user lists tab', () => {
beforeEach(done => {
factory();
setImmediate(() => {
done();
});
});
beforeEach(() => {
wrapper.find(NavigationTabs).vm.$emit('onChangeTab', USER_LIST_SCOPE);
return wrapper.vm.$nextTick();
});
it('should display the user list table', () => {
expect(wrapper.contains(UserListsTable)).toBe(true);
});
it('should set the user lists to display', () => {
expect(wrapper.find(UserListsTable).props('userLists')).toEqual([userList]);
});
});
});
describe('unsuccessful request', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint, { params: { scope: 'all', page: '1' } }).replyOnce(500, {});
mock
.onGet(mockData.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.replyOnce(500, {});
Api.fetchFeatureFlagUserLists.mockRejectedValueOnce();
factory();
......@@ -269,7 +292,7 @@ describe('Feature flags', () => {
describe('rotate instance id', () => {
beforeEach(done => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: 'all', page: '1' } })
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.reply(200, getRequestData, {});
factory();
......
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue';
import { userList } from '../mock_data';
jest.mock('timeago.js', () => ({
format: jest.fn().mockReturnValue('2 weeks ago'),
register: jest.fn(),
}));
describe('User Lists Table', () => {
let wrapper;
let userLists;
beforeEach(() => {
userLists = new Array(5).fill(userList).map((x, i) => ({ ...x, id: i }));
wrapper = mount(UserListsTable, {
propsData: { userLists },
});
});
afterEach(() => {
wrapper.destroy();
});
it('should display the details of a user list', () => {
expect(wrapper.find('[data-testid="ffUserListName"]').text()).toBe(userList.name);
expect(wrapper.find('[data-testid="ffUserListIds"]').text()).toBe(
userList.user_xids.replace(/,/g, ', '),
);
expect(wrapper.find('[data-testid="ffUserListTimestamp"]').text()).toBe('created 2 weeks ago');
expect(timeago.format).toHaveBeenCalledWith(userList.created_at);
});
it('should set the title for a tooltip on the created stamp', () => {
expect(wrapper.find('[data-testid="ffUserListTimestamp"]').attributes('title')).toBe(
'Feb 4, 2020 8:13am GMT+0000',
);
});
it('should display a user list entry per user list', () => {
const lists = wrapper.findAll('[data-testid="ffUserList"]');
expect(lists).toHaveLength(5);
lists.wrappers.forEach(list => {
expect(list.contains('[data-testid="ffUserListName"]')).toBe(true);
expect(list.contains('[data-testid="ffUserListIds"]')).toBe(true);
expect(list.contains('[data-testid="ffUserListTimestamp"]')).toBe(true);
});
});
});
......@@ -16,6 +16,10 @@ import {
updateFeatureFlag,
receiveUpdateFeatureFlagSuccess,
receiveUpdateFeatureFlagError,
requestUserLists,
receiveUserListsSuccess,
receiveUserListsError,
fetchUserLists,
} from 'ee/feature_flags/store/modules/index/actions';
import { mapToScopesViewModel } from 'ee/feature_flags/store/modules/helpers';
import state from 'ee/feature_flags/store/modules/index/state';
......@@ -23,7 +27,10 @@ import * as types from 'ee/feature_flags/store/modules/index/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import { getRequestData, rotateData, featureFlag } from '../../mock_data';
import Api from 'ee/api';
import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
jest.mock('ee/api.js');
describe('Feature flags actions', () => {
let mockedState;
......@@ -186,6 +193,99 @@ describe('Feature flags actions', () => {
});
});
describe('fetchUserLists', () => {
beforeEach(() => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} });
});
describe('success', () => {
it('dispatches requestUserLists and receiveUserListsSuccess ', done => {
testAction(
fetchUserLists,
null,
mockedState,
[],
[
{
type: 'requestUserLists',
},
{
payload: { data: [userList], headers: {} },
type: 'receiveUserListsSuccess',
},
],
done,
);
});
});
describe('error', () => {
it('dispatches requestUserLists and receiveUserListsError ', done => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
testAction(
fetchUserLists,
null,
mockedState,
[],
[
{
type: 'requestUserLists',
},
{
type: 'receiveUserListsError',
},
],
done,
);
});
});
});
describe('requestUserLists', () => {
it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', done => {
testAction(
requestUserLists,
null,
mockedState,
[{ type: types.REQUEST_USER_LISTS }],
[],
done,
);
});
});
describe('receiveUserListsSuccess', () => {
it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', done => {
testAction(
receiveUserListsSuccess,
{ data: [userList], headers: {} },
mockedState,
[
{
type: types.RECEIVE_USER_LISTS_SUCCESS,
payload: { data: [userList], headers: {} },
},
],
[],
done,
);
});
});
describe('receiveUserListsError', () => {
it('should commit RECEIVE_USER_LISTS_ERROR mutation', done => {
testAction(
receiveUserListsError,
null,
mockedState,
[{ type: types.RECEIVE_USER_LISTS_ERROR }],
[],
done,
);
});
});
describe('rotateInstanceId', () => {
let mock;
......
......@@ -3,7 +3,7 @@ import mutations from 'ee/feature_flags/store/modules/index/mutations';
import * as types from 'ee/feature_flags/store/modules/index/mutation_types';
import { mapToScopesViewModel } from 'ee/feature_flags/store/modules/helpers';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getRequestData, rotateData, featureFlag } from '../../mock_data';
import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
describe('Feature flags store Mutations', () => {
let stateCopy;
......@@ -84,11 +84,13 @@ describe('Feature flags store Mutations', () => {
});
it('should set count with the given data', () => {
expect(stateCopy.count).toEqual(getRequestData.count);
expect(stateCopy.count.featureFlags).toEqual(37);
});
it('should set pagination', () => {
expect(stateCopy.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers)));
expect(stateCopy.pageInfo.featureFlags).toEqual(
parseIntPagination(normalizeHeaders(headers)),
);
});
});
......@@ -106,6 +108,58 @@ describe('Feature flags store Mutations', () => {
});
});
describe('REQUEST_USER_LISTS', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_USER_LISTS](stateCopy);
expect(stateCopy.isLoading).toBe(true);
});
});
describe('RECIEVE_USER_LISTS_SUCCESS', () => {
const headers = {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '5',
};
beforeEach(() => {
mutations[types.RECEIVE_USER_LISTS_SUCCESS](stateCopy, { data: [userList], headers });
});
it('sets isLoading to false', () => {
expect(stateCopy.isLoading).toBe(false);
});
it('sets userLists to the received userLists', () => {
expect(stateCopy.userLists).toEqual([userList]);
});
it('sets pagination info for user lits', () => {
expect(stateCopy.pageInfo.userLists).toEqual(parseIntPagination(normalizeHeaders(headers)));
});
it('sets the count for user lists', () => {
expect(stateCopy.count.userLists).toBe(parseInt(headers['X-TOTAL'], 10));
});
});
describe('RECEIVE_USER_LISTS_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_USER_LISTS_ERROR](stateCopy);
});
it('should set isLoading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to true', () => {
expect(stateCopy.hasError).toEqual(true);
});
});
describe('REQUEST_ROTATE_INSTANCE_ID', () => {
beforeEach(() => {
mutations[types.REQUEST_ROTATE_INSTANCE_ID](stateCopy);
......@@ -158,7 +212,7 @@ describe('Feature flags store Mutations', () => {
...flag,
scopes: mapToScopesViewModel(flag.scopes || []),
}));
stateCopy.count = { enabled: 1, disabled: 0 };
stateCopy.count = { featureFlags: 1, userLists: 0 };
mutations[types.UPDATE_FEATURE_FLAG](stateCopy, {
...featureFlag,
......@@ -176,12 +230,6 @@ describe('Feature flags store Mutations', () => {
},
]);
});
it('should update the enabled count', () => {
expect(stateCopy.count.enabled).toBe(0);
});
it('should update the disabled count', () => {
expect(stateCopy.count.disabled).toBe(1);
});
});
describe('RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS', () => {
......@@ -191,7 +239,7 @@ describe('Feature flags store Mutations', () => {
...flagState,
scopes: mapToScopesViewModel(flag.scopes || []),
}));
stateCopy.count = stateCount;
stateCopy.count.featureFlags = stateCount;
mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](stateCopy, {
...featureFlag,
......@@ -210,32 +258,6 @@ describe('Feature flags store Mutations', () => {
},
]);
});
it('updates the count data', () => {
runUpdate({ all: 1, enabled: 1, disabled: 0 }, { active: true }, { active: false });
expect(stateCopy.count).toEqual({ all: 1, enabled: 0, disabled: 1 });
});
describe('when count data does not match up with the number of flags in state', () => {
it('updates the count data when the flag changes to inactive', () => {
runUpdate({ all: 4, enabled: 1, disabled: 3 }, { active: true }, { active: false });
expect(stateCopy.count).toEqual({ all: 4, enabled: 0, disabled: 4 });
});
it('updates the count data when the flag changes to active', () => {
runUpdate({ all: 4, enabled: 1, disabled: 3 }, { active: false }, { active: true });
expect(stateCopy.count).toEqual({ all: 4, enabled: 2, disabled: 2 });
});
it('retains the count data when flag.active does not change', () => {
runUpdate({ all: 4, enabled: 1, disabled: 3 }, { active: true }, { active: true });
expect(stateCopy.count).toEqual({ all: 4, enabled: 1, disabled: 3 });
});
});
});
describe('RECEIVE_UPDATE_FEATURE_FLAG_ERROR', () => {
......@@ -258,11 +280,5 @@ describe('Feature flags store Mutations', () => {
},
]);
});
it('should update the enabled count', () => {
expect(stateCopy.count.enabled).toBe(0);
});
it('should update the disabled count', () => {
expect(stateCopy.count.disabled).toBe(1);
});
});
});
......@@ -70,8 +70,7 @@ module FeatureFlagHelpers
def expect_user_to_see_feature_flags_index_page
expect(page).to have_css('h3.page-title', text: 'Feature Flags')
expect(page).to have_text('All')
expect(page).to have_text('Enabled')
expect(page).to have_text('Disabled')
expect(page).to have_text('Feature Flags')
expect(page).to have_text('Lists')
end
end
......@@ -9695,12 +9695,6 @@ msgstr ""
msgid "FeatureFlags|Target environments"
msgstr ""
msgid "FeatureFlags|There are no active feature flags"
msgstr ""
msgid "FeatureFlags|There are no inactive feature flags"
msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr ""
......@@ -13289,6 +13283,9 @@ msgstr ""
msgid "List your Bitbucket Server repositories"
msgstr ""
msgid "Lists"
msgstr ""
msgid "Live preview"
msgstr ""
......@@ -26602,6 +26599,9 @@ msgstr ""
msgid "created %{timeAgo}"
msgstr ""
msgid "created %{timeago}"
msgstr ""
msgid "customize"
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