Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
483b51cb
Commit
483b51cb
authored
Nov 27, 2020
by
Andrei Stoicescu
Committed by
Natalia Tepluhina
Nov 27, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add email column to seats table
- add data - add styles
parent
40f82832
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
275 additions
and
87 deletions
+275
-87
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seats.vue
...pts/billings/seat_usage/components/subscription_seats.vue
+43
-29
ee/app/assets/javascripts/billings/seat_usage/store/getters.js
...p/assets/javascripts/billings/seat_usage/store/getters.js
+11
-0
ee/app/assets/javascripts/billings/seat_usage/store/index.js
ee/app/assets/javascripts/billings/seat_usage/store/index.js
+2
-0
ee/app/assets/stylesheets/pages/billings.scss
ee/app/assets/stylesheets/pages/billings.scss
+0
-1
ee/spec/frontend/billings/mock_data.js
ee/spec/frontend/billings/mock_data.js
+19
-13
ee/spec/frontend/billings/seat_usage/components/__snapshots__/subscription_seats_spec.js.snap
.../components/__snapshots__/subscription_seats_spec.js.snap
+51
-0
ee/spec/frontend/billings/seat_usage/components/subscription_seats_spec.js
...billings/seat_usage/components/subscription_seats_spec.js
+118
-44
ee/spec/frontend/billings/seat_usage/store/getters_spec.js
ee/spec/frontend/billings/seat_usage/store/getters_spec.js
+25
-0
locale/gitlab.pot
locale/gitlab.pot
+6
-0
No files found.
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seats.vue
View file @
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=
"
i
tems"
:items=
"
tableI
tems"
: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
>
...
...
ee/app/assets/javascripts/billings/seat_usage/store/getters.js
0 → 100644
View file @
483b51cb
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
[];
};
ee/app/assets/javascripts/billings/seat_usage/store/index.js
View file @
483b51cb
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
),
});
});
ee/app/assets/stylesheets/pages/billings.scss
View file @
483b51cb
...
@@ -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
;
...
...
ee/spec/frontend/billings/mock_data.js
View file @
483b51cb
...
@@ -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
seats
TableItems
=
[
export
const
mock
TableItems
=
[
{
{
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
'
,
},
},
},
},
];
];
ee/spec/frontend/billings/seat_usage/components/__snapshots__/subscription_seats_spec.js.snap
0 → 100644
View file @
483b51cb
// 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",
},
},
},
]
`;
ee/spec/frontend/billings/seat_usage/components/subscription_seats_spec.js
View file @
483b51cb
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
,
seats
TableItems
}
from
'
ee_jest/billings/mock_data
'
;
import
{
mockDataSeats
,
mock
TableItems
}
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
'
);
});
});
});
});
ee/spec/frontend/billings/seat_usage/store/getters_spec.js
0 → 100644
View file @
483b51cb
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
([]);
});
});
});
locale/gitlab.pot
View file @
483b51cb
...
@@ -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 ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment