Commit 78cba0fd authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'as-email-column' into 'master'

Add email column to "Seats in use" table

See merge request gitlab-org/gitlab!47432
parents b850a3cd 483b51cb
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlTable, GlAvatarLabeled, GlAvatarLink, GlPagination, GlLoadingIcon } from '@gitlab/ui'; import {
GlTable,
GlAvatarLabeled,
GlAvatarLink,
GlPagination,
GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { parseInt } from 'lodash'; import { parseInt } from 'lodash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
const AVATAR_SIZE = 32; const AVATAR_SIZE = 32;
export default { export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: { components: {
GlTable, GlTable,
GlAvatarLabeled, GlAvatarLabeled,
...@@ -16,26 +26,12 @@ export default { ...@@ -16,26 +26,12 @@ export default {
}, },
data() { data() {
return { return {
fields: ['user'], fields: ['user', 'email'],
}; };
}, },
computed: { computed: {
...mapState([ ...mapState(['isLoading', 'page', 'perPage', 'total', 'namespaceId', 'namespaceName']),
'members', ...mapGetters(['tableItems']),
'isLoading',
'page',
'perPage',
'total',
'namespaceId',
'namespaceName',
]),
items() {
return this.members.map(({ name, username, avatar_url, web_url }) => {
const formattedUserName = `@${username}`;
return { user: { name, username: formattedUserName, avatar_url, web_url } };
});
},
headingText() { headingText() {
return sprintf(s__('Billing|Users occupying seats in %{namespaceName} Group (%{total})'), { return sprintf(s__('Billing|Users occupying seats in %{namespaceName} Group (%{total})'), {
total: this.total, total: this.total,
...@@ -70,6 +66,9 @@ export default { ...@@ -70,6 +66,9 @@ export default {
}, },
}, },
avatarSize: AVATAR_SIZE, avatarSize: AVATAR_SIZE,
emailNotVisibleTooltipText: s__(
'Billing|An email address is only visible for users managed through Group Managed Accounts.',
),
}; };
</script> </script>
...@@ -78,22 +77,37 @@ export default { ...@@ -78,22 +77,37 @@ export default {
<h4 data-testid="heading">{{ headingText }}</h4> <h4 data-testid="heading">{{ headingText }}</h4>
<p>{{ subHeadingText }}</p> <p>{{ subHeadingText }}</p>
<gl-table <gl-table
data-testid="seats-table"
class="seats-table" class="seats-table"
:items="items" :items="tableItems"
:fields="fields" :fields="fields"
:busy="isLoading" :busy="isLoading"
:show-empty="true" :show-empty="true"
data-testid="table"
> >
<template #cell(user)="data"> <template #cell(user)="data">
<gl-avatar-link target="blank" :href="data.value.web_url" :alt="data.value.name"> <div class="gl-display-flex">
<gl-avatar-labeled <gl-avatar-link target="blank" :href="data.value.web_url" :alt="data.value.name">
:src="data.value.avatar_url" <gl-avatar-labeled
:size="$options.avatarSize" :src="data.value.avatar_url"
:label="data.value.name" :size="$options.avatarSize"
:sub-label="data.value.username" :label="data.value.name"
/> :sub-label="data.value.username"
</gl-avatar-link> />
</gl-avatar-link>
</div>
</template>
<template #cell(email)="data">
<div data-testid="email">
<span v-if="data.value" class="gl-text-gray-900">{{ data.value }}</span>
<span
v-else
v-gl-tooltip
:title="$options.emailNotVisibleTooltipText"
class="gl-font-style-italic"
>{{ s__('Billing|Private') }}</span
>
</div>
</template> </template>
<template #empty> <template #empty>
......
export const tableItems = state => {
if (state.members.length) {
return state.members.map(({ name, username, avatar_url, web_url, email }) => {
const formattedUserName = `@${username}`;
return { user: { name, username: formattedUserName, avatar_url, web_url }, email };
});
}
return [];
};
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
export default (initState = {}) => ({ export default (initState = {}) => ({
actions, actions,
mutations, mutations,
getters,
state: state(initState), state: state(initState),
}); });
...@@ -120,7 +120,6 @@ ...@@ -120,7 +120,6 @@
tr { tr {
th, th,
td { td {
@include gl-display-flex;
@include gl-border-b-solid; @include gl-border-b-solid;
@include gl-border-b-1; @include gl-border-b-1;
@include gl-p-5; @include gl-p-5;
......
...@@ -70,20 +70,23 @@ export const mockDataSeats = { ...@@ -70,20 +70,23 @@ export const mockDataSeats = {
{ {
name: 'Administrator', name: 'Administrator',
username: 'root', username: 'root',
avatar_url: 'path/to/img', avatar_url: 'path/to/img_administrator',
web_url: 'path/to/user', web_url: 'path/to/administrator',
email: 'administrator@email.com',
}, },
{ {
name: 'Agustin Walker', name: 'Agustin Walker',
username: 'lester.orn', username: 'lester.orn',
avatar_url: 'path/to/img', avatar_url: 'path/to/img_agustin_walker',
web_url: 'path/to/user', web_url: 'path/to/agustin_walker',
email: 'agustin_walker@email.com',
}, },
{ {
name: 'Joella Miller', name: 'Joella Miller',
username: 'era', username: 'era',
avatar_url: 'path/to/img', avatar_url: 'path/to/img_joella_miller',
web_url: 'path/to/user', web_url: 'path/to/joella_miller',
email: null,
}, },
], ],
headers: { headers: {
...@@ -93,29 +96,32 @@ export const mockDataSeats = { ...@@ -93,29 +96,32 @@ export const mockDataSeats = {
}, },
}; };
export const seatsTableItems = [ export const mockTableItems = [
{ {
email: 'administrator@email.com',
user: { user: {
avatar_url: 'path/to/img_administrator',
name: 'Administrator', name: 'Administrator',
username: '@root', username: '@root',
avatar_url: 'path/to/img', web_url: 'path/to/administrator',
web_url: 'path/to/user',
}, },
}, },
{ {
email: 'agustin_walker@email.com',
user: { user: {
avatar_url: 'path/to/img_agustin_walker',
name: 'Agustin Walker', name: 'Agustin Walker',
username: '@lester.orn', username: '@lester.orn',
avatar_url: 'path/to/img', web_url: 'path/to/agustin_walker',
web_url: 'path/to/user',
}, },
}, },
{ {
email: null,
user: { user: {
avatar_url: 'path/to/img_joella_miller',
name: 'Joella Miller', name: 'Joella Miller',
username: '@era', username: '@era',
avatar_url: 'path/to/img', web_url: 'path/to/joella_miller',
web_url: 'path/to/user',
}, },
}, },
]; ];
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Subscription Seats renders table content renders the correct data 1`] = `
Array [
Object {
"email": "administrator@email.com",
"tooltip": undefined,
"user": Object {
"avatarLabeled": Object {
"size": "32",
"src": "path/to/img_administrator",
"text": "Administrator @root",
},
"avatarLink": Object {
"alt": "Administrator",
"href": "path/to/administrator",
},
},
},
Object {
"email": "agustin_walker@email.com",
"tooltip": undefined,
"user": Object {
"avatarLabeled": Object {
"size": "32",
"src": "path/to/img_agustin_walker",
"text": "Agustin Walker @lester.orn",
},
"avatarLink": Object {
"alt": "Agustin Walker",
"href": "path/to/agustin_walker",
},
},
},
Object {
"email": "Private",
"tooltip": "An email address is only visible for users managed through Group Managed Accounts.",
"user": Object {
"avatarLabeled": Object {
"size": "32",
"src": "path/to/img_joella_miller",
"text": "Joella Miller @era",
},
"avatarLink": Object {
"alt": "Joella Miller",
"href": "path/to/joella_miller",
},
},
},
]
`;
import { GlPagination } from '@gitlab/ui'; import { GlPagination, GlTable, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import SubscriptionSeats from 'ee/billings/seat_usage/components/subscription_seats.vue'; import SubscriptionSeats from 'ee/billings/seat_usage/components/subscription_seats.vue';
import { mockDataSeats, seatsTableItems } from 'ee_jest/billings/mock_data'; import { mockDataSeats, mockTableItems } from 'ee_jest/billings/mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -17,82 +17,156 @@ const providedFields = { ...@@ -17,82 +17,156 @@ const providedFields = {
namespaceId: '1000', namespaceId: '1000',
}; };
const fakeStore = ({ initialState }) => const fakeStore = ({ initialState, initialGetters }) =>
new Vuex.Store({ new Vuex.Store({
actions: actionSpies, actions: actionSpies,
getters: {
tableItems: () => mockTableItems,
...initialGetters,
},
state: { state: {
isLoading: false, isLoading: false,
hasError: false, hasError: false,
namespaceId: null,
members: [...mockDataSeats.data],
total: 300,
page: 1,
perPage: 5,
...providedFields, ...providedFields,
...initialState, ...initialState,
}, },
}); });
const createComponent = (initialState = {}) => {
return shallowMount(SubscriptionSeats, {
store: fakeStore({ initialState }),
localVue,
stubs: {
GlTable: { template: '<div></div>', props: { items: Array, fields: Array, busy: Boolean } },
},
});
};
describe('Subscription Seats', () => { describe('Subscription Seats', () => {
let wrapper; let wrapper;
const findTable = () => wrapper.find('[data-testid="seats-table"]'); const createComponent = ({
const findHeading = () => wrapper.find('[data-testid="heading"]'); initialState = {},
mountFn = shallowMount,
initialGetters = {},
} = {}) => {
return mountFn(SubscriptionSeats, {
store: fakeStore({ initialState, initialGetters }),
localVue,
});
};
const findTable = () => wrapper.find(GlTable);
const findPageHeading = () => wrapper.find('[data-testid="heading"]');
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
beforeEach(() => { const serializeUser = rowWrapper => {
wrapper = createComponent({ const avatarLink = rowWrapper.find(GlAvatarLink);
namespaceId: null, const avatarLabeled = rowWrapper.find(GlAvatarLabeled);
members: [...mockDataSeats.data],
total: 300, return {
page: 1, avatarLink: {
perPage: 5, href: avatarLink.attributes('href'),
alt: avatarLink.attributes('alt'),
},
avatarLabeled: {
src: avatarLabeled.attributes('src'),
size: avatarLabeled.attributes('size'),
text: avatarLabeled.text(),
},
};
};
const serializeTableRow = rowWrapper => {
const emailWrapper = rowWrapper.find('[data-testid="email"]');
return {
user: serializeUser(rowWrapper),
email: emailWrapper.text(),
tooltip: emailWrapper.find('span').attributes('title'),
};
};
const findSerializedTable = tableWrapper => {
return tableWrapper.findAll('tbody tr').wrappers.map(serializeTableRow);
};
describe('actions', () => {
beforeEach(() => {
wrapper = createComponent();
}); });
});
it('correct actions are called on create', () => { afterEach(() => {
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledWith(expect.any(Object), 1); wrapper.destroy();
}); wrapper = null;
});
describe('heading text', () => { it('correct actions are called on create', () => {
it('contains the group name and total seats number', () => { expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledWith(expect.any(Object), 1);
expect(findHeading().text()).toMatch(providedFields.namespaceName);
expect(findHeading().text()).toMatch('300');
}); });
}); });
describe('table', () => { describe('renders', () => {
it('is rendered and passed correct values', () => { beforeEach(() => {
expect(findTable().props('fields')).toEqual(['user']); wrapper = createComponent({
expect(findTable().props('busy')).toBe(false); mountFn: mount,
expect(findTable().props('items')).toEqual(seatsTableItems); initialGetters: {
tableItems: () => mockTableItems,
},
});
}); });
});
describe('pagination', () => { afterEach(() => {
it('is rendered and passed correct values', () => { wrapper.destroy();
wrapper = null;
});
describe('heading text', () => {
it('contains the group name and total seats number', () => {
expect(findPageHeading().text()).toMatch(providedFields.namespaceName);
expect(findPageHeading().text()).toMatch('300');
});
});
describe('table content', () => {
it('renders the correct data', () => {
const serializedTable = findSerializedTable(wrapper.find(GlTable));
expect(serializedTable).toMatchSnapshot();
});
});
it('pagination is rendered and passed correct values', () => {
expect(findPagination().vm.value).toBe(1); expect(findPagination().vm.value).toBe(1);
expect(findPagination().props('perPage')).toBe(5); expect(findPagination().props('perPage')).toBe(5);
expect(findPagination().props('totalItems')).toBe(300); expect(findPagination().props('totalItems')).toBe(300);
}); });
});
describe('pagination', () => {
it.each([null, NaN, undefined, 'a string', false])( it.each([null, NaN, undefined, 'a string', false])(
'will not render given %s for currentPage', 'will not render given %s for currentPage',
value => { value => {
wrapper = createComponent({ wrapper = createComponent({
namespaceId: null, initialState: {
members: [...mockDataSeats.data], page: value,
total: 300, },
page: value,
perPage: 5,
}); });
expect(findPagination().exists()).toBe(false); expect(findPagination().exists()).toBe(false);
wrapper.destroy();
wrapper = null;
}, },
); );
}); });
describe('is loading', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { isLoading: true } });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays table in loading state', () => {
expect(findTable().attributes('busy')).toBe('true');
});
});
}); });
import State from 'ee/billings/seat_usage/store/state';
import * as getters from 'ee/billings/seat_usage/store/getters';
import { mockDataSeats, mockTableItems } from 'ee_jest/billings/mock_data';
describe('Seat usage table getters', () => {
let state;
beforeEach(() => {
state = State();
});
describe('Table items', () => {
it('should return expected value if data is provided', () => {
state.members = [...mockDataSeats.data];
expect(getters.tableItems(state)).toEqual(mockTableItems);
});
it('should return an empty array if data is not provided', () => {
state.members = [];
expect(getters.tableItems(state)).toEqual([]);
});
});
});
...@@ -4387,12 +4387,18 @@ msgstr "" ...@@ -4387,12 +4387,18 @@ msgstr ""
msgid "BillingPlan|Upgrade" msgid "BillingPlan|Upgrade"
msgstr "" msgstr ""
msgid "Billing|An email address is only visible for users managed through Group Managed Accounts."
msgstr ""
msgid "Billing|An error occurred while loading billable members list" msgid "Billing|An error occurred while loading billable members list"
msgstr "" msgstr ""
msgid "Billing|No users to display." msgid "Billing|No users to display."
msgstr "" msgstr ""
msgid "Billing|Private"
msgstr ""
msgid "Billing|Updated live" msgid "Billing|Updated live"
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