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>
import { mapActions, mapState } from 'vuex';
import { GlTable, GlAvatarLabeled, GlAvatarLink, GlPagination, GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlTable,
GlAvatarLabeled,
GlAvatarLink,
GlPagination,
GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { parseInt } from 'lodash';
import { s__, sprintf } from '~/locale';
const AVATAR_SIZE = 32;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlTable,
GlAvatarLabeled,
......@@ -16,26 +26,12 @@ export default {
},
data() {
return {
fields: ['user'],
fields: ['user', 'email'],
};
},
computed: {
...mapState([
'members',
'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 } };
});
},
...mapState(['isLoading', 'page', 'perPage', 'total', 'namespaceId', 'namespaceName']),
...mapGetters(['tableItems']),
headingText() {
return sprintf(s__('Billing|Users occupying seats in %{namespaceName} Group (%{total})'), {
total: this.total,
......@@ -70,6 +66,9 @@ export default {
},
},
avatarSize: AVATAR_SIZE,
emailNotVisibleTooltipText: s__(
'Billing|An email address is only visible for users managed through Group Managed Accounts.',
),
};
</script>
......@@ -78,22 +77,37 @@ export default {
<h4 data-testid="heading">{{ headingText }}</h4>
<p>{{ subHeadingText }}</p>
<gl-table
data-testid="seats-table"
class="seats-table"
:items="items"
:items="tableItems"
:fields="fields"
:busy="isLoading"
:show-empty="true"
data-testid="table"
>
<template #cell(user)="data">
<gl-avatar-link target="blank" :href="data.value.web_url" :alt="data.value.name">
<gl-avatar-labeled
:src="data.value.avatar_url"
:size="$options.avatarSize"
:label="data.value.name"
:sub-label="data.value.username"
/>
</gl-avatar-link>
<div class="gl-display-flex">
<gl-avatar-link target="blank" :href="data.value.web_url" :alt="data.value.name">
<gl-avatar-labeled
:src="data.value.avatar_url"
:size="$options.avatarSize"
:label="data.value.name"
:sub-label="data.value.username"
/>
</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 #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 getters from './getters';
import mutations from './mutations';
import state from './state';
export default (initState = {}) => ({
actions,
mutations,
getters,
state: state(initState),
});
......@@ -120,7 +120,6 @@
tr {
th,
td {
@include gl-display-flex;
@include gl-border-b-solid;
@include gl-border-b-1;
@include gl-p-5;
......
......@@ -70,20 +70,23 @@ export const mockDataSeats = {
{
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
avatar_url: 'path/to/img_administrator',
web_url: 'path/to/administrator',
email: 'administrator@email.com',
},
{
name: 'Agustin Walker',
username: 'lester.orn',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
avatar_url: 'path/to/img_agustin_walker',
web_url: 'path/to/agustin_walker',
email: 'agustin_walker@email.com',
},
{
name: 'Joella Miller',
username: 'era',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
avatar_url: 'path/to/img_joella_miller',
web_url: 'path/to/joella_miller',
email: null,
},
],
headers: {
......@@ -93,29 +96,32 @@ export const mockDataSeats = {
},
};
export const seatsTableItems = [
export const mockTableItems = [
{
email: 'administrator@email.com',
user: {
avatar_url: 'path/to/img_administrator',
name: 'Administrator',
username: '@root',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
web_url: 'path/to/administrator',
},
},
{
email: 'agustin_walker@email.com',
user: {
avatar_url: 'path/to/img_agustin_walker',
name: 'Agustin Walker',
username: '@lester.orn',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
web_url: 'path/to/agustin_walker',
},
},
{
email: null,
user: {
avatar_url: 'path/to/img_joella_miller',
name: 'Joella Miller',
username: '@era',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
web_url: 'path/to/joella_miller',
},
},
];
// 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 { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlTable, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
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();
localVue.use(Vuex);
......@@ -17,82 +17,156 @@ const providedFields = {
namespaceId: '1000',
};
const fakeStore = ({ initialState }) =>
const fakeStore = ({ initialState, initialGetters }) =>
new Vuex.Store({
actions: actionSpies,
getters: {
tableItems: () => mockTableItems,
...initialGetters,
},
state: {
isLoading: false,
hasError: false,
namespaceId: null,
members: [...mockDataSeats.data],
total: 300,
page: 1,
perPage: 5,
...providedFields,
...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', () => {
let wrapper;
const findTable = () => wrapper.find('[data-testid="seats-table"]');
const findHeading = () => wrapper.find('[data-testid="heading"]');
const createComponent = ({
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);
beforeEach(() => {
wrapper = createComponent({
namespaceId: null,
members: [...mockDataSeats.data],
total: 300,
page: 1,
perPage: 5,
const serializeUser = rowWrapper => {
const avatarLink = rowWrapper.find(GlAvatarLink);
const avatarLabeled = rowWrapper.find(GlAvatarLabeled);
return {
avatarLink: {
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', () => {
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledWith(expect.any(Object), 1);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('heading text', () => {
it('contains the group name and total seats number', () => {
expect(findHeading().text()).toMatch(providedFields.namespaceName);
expect(findHeading().text()).toMatch('300');
it('correct actions are called on create', () => {
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledWith(expect.any(Object), 1);
});
});
describe('table', () => {
it('is rendered and passed correct values', () => {
expect(findTable().props('fields')).toEqual(['user']);
expect(findTable().props('busy')).toBe(false);
expect(findTable().props('items')).toEqual(seatsTableItems);
describe('renders', () => {
beforeEach(() => {
wrapper = createComponent({
mountFn: mount,
initialGetters: {
tableItems: () => mockTableItems,
},
});
});
});
describe('pagination', () => {
it('is rendered and passed correct values', () => {
afterEach(() => {
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().props('perPage')).toBe(5);
expect(findPagination().props('totalItems')).toBe(300);
});
});
describe('pagination', () => {
it.each([null, NaN, undefined, 'a string', false])(
'will not render given %s for currentPage',
value => {
wrapper = createComponent({
namespaceId: null,
members: [...mockDataSeats.data],
total: 300,
page: value,
perPage: 5,
initialState: {
page: value,
},
});
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 ""
msgid "BillingPlan|Upgrade"
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"
msgstr ""
msgid "Billing|No users to display."
msgstr ""
msgid "Billing|Private"
msgstr ""
msgid "Billing|Updated live"
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