Commit 56c07911 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Kushal Pandya

Display Saved User Lists by Feature Flags

User Lists can only be created via the API, but this allows users to
view the saved lists and delete them if they are no longer needed.
parent 60ba024c
......@@ -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
......@@ -9683,12 +9683,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 ""
......@@ -13274,6 +13268,9 @@ msgstr ""
msgid "List your Bitbucket Server repositories"
msgstr ""
msgid "Lists"
msgstr ""
msgid "Live preview"
msgstr ""
......@@ -26584,6 +26581,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