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
825e8f8b
Commit
825e8f8b
authored
Apr 29, 2021
by
Vitaly Slobodin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add details table to seat usage table
parent
34fe7658
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
522 additions
and
17 deletions
+522
-17
ee/app/assets/javascripts/api.js
ee/app/assets/javascripts/api.js
+10
-0
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seat_details.vue
...lings/seat_usage/components/subscription_seat_details.vue
+63
-0
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seat_details_loader.vue
...eat_usage/components/subscription_seat_details_loader.vue
+55
-0
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seats.vue
...pts/billings/seat_usage/components/subscription_seats.vue
+30
-7
ee/app/assets/javascripts/billings/seat_usage/constants.js
ee/app/assets/javascripts/billings/seat_usage/constants.js
+7
-0
ee/app/assets/javascripts/billings/seat_usage/store/actions.js
...p/assets/javascripts/billings/seat_usage/store/actions.js
+27
-0
ee/app/assets/javascripts/billings/seat_usage/store/getters.js
...p/assets/javascripts/billings/seat_usage/store/getters.js
+4
-0
ee/app/assets/javascripts/billings/seat_usage/store/mutation_types.js
...s/javascripts/billings/seat_usage/store/mutation_types.js
+4
-0
ee/app/assets/javascripts/billings/seat_usage/store/mutations.js
...assets/javascripts/billings/seat_usage/store/mutations.js
+22
-0
ee/app/assets/javascripts/billings/seat_usage/store/state.js
ee/app/assets/javascripts/billings/seat_usage/store/state.js
+1
-0
ee/changelogs/unreleased/vs-billable-members-list-drilldown.yml
...ngelogs/unreleased/vs-billable-members-list-drilldown.yml
+5
-0
ee/spec/features/groups/billing/seat_usage_spec.rb
ee/spec/features/groups/billing/seat_usage_spec.rb
+22
-0
ee/spec/frontend/api_spec.js
ee/spec/frontend/api_spec.js
+18
-7
ee/spec/frontend/billings/mock_data.js
ee/spec/frontend/billings/mock_data.js
+11
-0
ee/spec/frontend/billings/seat_usage/components/subscription_seat_details_spec.js
...s/seat_usage/components/subscription_seat_details_spec.js
+56
-0
ee/spec/frontend/billings/seat_usage/store/actions_spec.js
ee/spec/frontend/billings/seat_usage/store/actions_spec.js
+110
-1
ee/spec/frontend/billings/seat_usage/store/getters_spec.js
ee/spec/frontend/billings/seat_usage/store/getters_spec.js
+26
-1
ee/spec/frontend/billings/seat_usage/store/mutations_spec.js
ee/spec/frontend/billings/seat_usage/store/mutations_spec.js
+42
-1
locale/gitlab.pot
locale/gitlab.pot
+9
-0
No files found.
ee/app/assets/javascripts/api.js
View file @
825e8f8b
...
...
@@ -50,6 +50,8 @@ export default {
issueMetricSingleImagePath
:
'
/api/:version/projects/:id/issues/:issue_iid/metric_images/:image_id
'
,
billableGroupMembersPath
:
'
/api/:version/groups/:id/billable_members
'
,
billableGroupMemberMembershipsPath
:
'
/api/:version/groups/:group_id/billable_members/:member_id/memberships
'
,
userSubscription
(
namespaceId
)
{
const
url
=
Api
.
buildUrl
(
this
.
subscriptionPath
).
replace
(
'
:id
'
,
encodeURIComponent
(
namespaceId
));
...
...
@@ -424,4 +426,12 @@ export default {
return
{
data
,
headers
};
});
},
fetchBillableGroupMemberMemberships
(
namespaceId
,
memberId
)
{
const
url
=
Api
.
buildUrl
(
this
.
billableGroupMemberMembershipsPath
)
.
replace
(
'
:group_id
'
,
namespaceId
)
.
replace
(
'
:member_id
'
,
memberId
);
return
axios
.
get
(
url
);
},
};
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seat_details.vue
0 → 100644
View file @
825e8f8b
<
script
>
import
{
GlTable
,
GlBadge
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
formatDate
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
DETAILS_FIELDS
}
from
'
../constants
'
;
import
SubscriptionSeatDetailsLoader
from
'
./subscription_seat_details_loader.vue
'
;
export
default
{
name
:
'
SubscriptionSeatDetails
'
,
components
:
{
GlBadge
,
GlTable
,
GlLink
,
SubscriptionSeatDetailsLoader
,
},
props
:
{
seatMemberId
:
{
type
:
Number
,
required
:
true
,
},
},
computed
:
{
...
mapGetters
([
'
membershipsById
'
]),
state
()
{
return
this
.
membershipsById
(
this
.
seatMemberId
);
},
items
()
{
return
this
.
state
.
items
;
},
isLoading
()
{
return
this
.
state
.
isLoading
;
},
},
created
()
{
this
.
fetchBillableMemberDetails
(
this
.
seatMemberId
);
},
methods
:
{
...
mapActions
([
'
fetchBillableMemberDetails
'
]),
formatDate
,
},
fields
:
DETAILS_FIELDS
,
};
</
script
>
<
template
>
<div
v-if=
"isLoading"
>
<subscription-seat-details-loader
/>
</div>
<gl-table
v-else
:fields=
"$options.fields"
:items=
"items"
data-testid=
"seat-usage-details"
>
<template
#cell(source_full_name)=
"
{ item }">
<gl-link
:href=
"item.source_members_url"
target=
"_blank"
>
{{
item
.
source_full_name
}}
</gl-link>
</
template
>
<
template
#cell(created_at)=
"{ item }"
>
<span>
{{
formatDate
(
item
.
created_at
,
'
yyyy-mm-dd
'
)
}}
</span>
</
template
>
<
template
#cell(expires_at)=
"{ item }"
>
<span>
{{
item
.
expires_at
?
formatDate
(
item
.
expires_at
,
'
yyyy-mm-dd
'
)
:
__
(
'
Never
'
)
}}
</span>
</
template
>
<
template
#cell(role)=
"{ item }"
>
<gl-badge>
{{
item
.
access_level
.
string_value
}}
</gl-badge>
</
template
>
</gl-table>
</template>
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seat_details_loader.vue
0 → 100644
View file @
825e8f8b
<
script
>
import
{
GlSkeletonLoader
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlSkeletonLoader
,
},
shapes
:
[
{
type
:
'
rect
'
,
width
:
'
100
'
,
height
:
'
10
'
,
x
:
'
20
'
,
y
:
'
20
'
},
{
type
:
'
rect
'
,
width
:
'
100
'
,
height
:
'
10
'
,
x
:
'
385
'
,
y
:
'
20
'
},
{
type
:
'
rect
'
,
width
:
'
100
'
,
height
:
'
10
'
,
x
:
'
760
'
,
y
:
'
20
'
},
{
type
:
'
rect
'
,
width
:
'
30
'
,
height
:
'
10
'
,
x
:
'
970
'
,
y
:
'
20
'
},
],
rowsToRender
:
{
mobile
:
1
,
desktop
:
5
,
},
};
</
script
>
<
template
>
<div>
<div
class=
"gl-flex-direction-column gl-sm-display-none"
data-testid=
"mobile-loader"
>
<gl-skeleton-loader
v-for=
"index in $options.rowsToRender.mobile"
:key=
"index"
:width=
"500"
:height=
"170"
preserve-aspect-ratio=
"xMinYMax meet"
>
<rect
width=
"500"
height=
"10"
x=
"0"
y=
"15"
rx=
"4"
/>
</gl-skeleton-loader>
</div>
<div
class=
"gl-display-none gl-sm-display-flex gl-flex-direction-column"
data-testid=
"desktop-loader"
>
<gl-skeleton-loader
v-for=
"index in $options.rowsToRender.desktop"
:key=
"index"
:width=
"1000"
:height=
"54"
preserve-aspect-ratio=
"xMinYMax meet"
>
<component
:is=
"r.type"
v-for=
"(r, rIndex) in $options.shapes"
:key=
"rIndex"
rx=
"4"
v-bind=
"r"
/>
</gl-skeleton-loader>
</div>
</div>
</
template
>
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seats.vue
View file @
825e8f8b
...
...
@@ -3,9 +3,11 @@ import {
GlAvatarLabeled
,
GlAvatarLink
,
GlBadge
,
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlModalDirective
,
GlIcon
,
GlPagination
,
GlSearchBoxByType
,
GlTable
,
...
...
@@ -21,6 +23,7 @@ import {
}
from
'
ee/billings/seat_usage/constants
'
;
import
{
s__
}
from
'
~/locale
'
;
import
RemoveBillableMemberModal
from
'
./remove_billable_member_modal.vue
'
;
import
SubscriptionSeatDetails
from
'
./subscription_seat_details.vue
'
;
export
default
{
directives
:
{
...
...
@@ -31,12 +34,15 @@ export default {
GlAvatarLabeled
,
GlAvatarLink
,
GlBadge
,
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlIcon
,
GlPagination
,
GlSearchBoxByType
,
GlTable
,
RemoveBillableMemberModal
,
SubscriptionSeatDetails
,
},
data
()
{
return
{
...
...
@@ -156,22 +162,35 @@ export default {
data-testid=
"table"
:empty-text=
"emptyText"
>
<template
#cell(user)=
"
data
"
>
<template
#cell(user)=
"
{ item, toggleDetails, detailsShowing }
">
<div
class=
"gl-display-flex"
>
<gl-avatar-link
target=
"blank"
:href=
"data.value.web_url"
:alt=
"data.value.name"
>
<gl-button
variant=
"link"
class=
"gl-mr-2"
:aria-label=
"s__('Billing|Toggle seat details')"
data-testid=
"toggle-seat-usage-details"
@
click=
"toggleDetails"
>
<gl-icon
:name=
"detailsShowing ? 'angle-down' : 'angle-right'"
class=
"text-secondary-900"
/>
</gl-button>
<gl-avatar-link
target=
"blank"
:href=
"item.user.web_url"
:alt=
"item.user.name"
>
<gl-avatar-labeled
:src=
"
data.value
.avatar_url"
:src=
"
item.user
.avatar_url"
:size=
"$options.avatarSize"
:label=
"
data.value
.name"
:sub-label=
"
data.value
.username"
:label=
"
item.user
.name"
:sub-label=
"
item.user
.username"
/>
</gl-avatar-link>
</div>
</
template
>
<
template
#cell(email)=
"
data
"
>
<
template
#cell(email)=
"
{ item }
"
>
<div
data-testid=
"email"
>
<span
v-if=
"
data.value"
class=
"gl-text-gray-900"
>
{{
data
.
value
}}
</span>
<span
v-if=
"
item.email"
class=
"gl-text-gray-900"
>
{{
item
.
email
}}
</span>
<span
v-else
v-gl-tooltip
...
...
@@ -199,6 +218,10 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</
template
>
<
template
#row-details=
"{ item }"
>
<subscription-seat-details
:seat-member-id=
"item.user.id"
/>
</
template
>
</gl-table>
<gl-pagination
...
...
ee/app/assets/javascripts/billings/seat_usage/constants.js
View file @
825e8f8b
...
...
@@ -28,6 +28,13 @@ export const FIELDS = [
},
];
export
const
DETAILS_FIELDS
=
[
{
key
:
'
source_full_name
'
,
label
:
s__
(
'
Billing|Direct memberships
'
),
thClass
:
thWidthClass
(
40
)
},
{
key
:
'
created_at
'
,
label
:
__
(
'
Access granted
'
),
thClass
:
thWidthClass
(
40
)
},
{
key
:
'
expires_at
'
,
label
:
__
(
'
Access expires
'
),
thClass
:
thWidthClass
(
40
)
},
{
key
:
'
role
'
,
label
:
__
(
'
Role
'
),
thClass
:
thWidthClass
(
40
)
},
];
export
const
REMOVE_BILLABLE_MEMBER_MODAL_ID
=
'
billable-member-remove-modal
'
;
export
const
REMOVE_BILLABLE_MEMBER_MODAL_CONTENT_TEXT_TEMPLATE
=
s__
(
`Billing|You are about to remove user %{username} from your subscription.
...
...
ee/app/assets/javascripts/billings/seat_usage/store/actions.js
View file @
825e8f8b
...
...
@@ -55,3 +55,30 @@ export const removeBillableMemberError = ({ commit }) => {
});
commit
(
types
.
REMOVE_BILLABLE_MEMBER_ERROR
);
};
export
const
fetchBillableMemberDetails
=
({
dispatch
,
commit
,
state
},
memberId
)
=>
{
if
(
state
.
userDetails
[
memberId
])
{
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
{
memberId
,
memberships
:
state
.
userDetails
[
memberId
].
items
,
});
return
Promise
.
resolve
();
}
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS
,
memberId
);
return
Api
.
fetchBillableGroupMemberMemberships
(
state
.
namespaceId
,
memberId
)
.
then
(({
data
})
=>
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
{
memberId
,
memberships
:
data
}),
)
.
catch
(()
=>
dispatch
(
'
fetchBillableMemberDetailsError
'
,
memberId
));
};
export
const
fetchBillableMemberDetailsError
=
({
commit
},
memberId
)
=>
{
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
,
memberId
);
createFlash
({
message
:
s__
(
'
Billing|An error occurred while getting a billable member details
'
),
});
};
ee/app/assets/javascripts/billings/seat_usage/store/getters.js
View file @
825e8f8b
...
...
@@ -20,3 +20,7 @@ export const tableItems = (state) => {
}
return
[];
};
export
const
membershipsById
=
(
state
)
=>
(
memberId
)
=>
{
return
state
.
userDetails
[
memberId
]
||
{
isLoading
:
true
,
items
:
[]
};
};
ee/app/assets/javascripts/billings/seat_usage/store/mutation_types.js
View file @
825e8f8b
...
...
@@ -9,3 +9,7 @@ export const REMOVE_BILLABLE_MEMBER = 'REMOVE_BILLABLE_MEMBER';
export
const
REMOVE_BILLABLE_MEMBER_SUCCESS
=
'
REMOVE_BILLABLE_MEMBER_SUCCESS
'
;
export
const
REMOVE_BILLABLE_MEMBER_ERROR
=
'
REMOVE_BILLABLE_MEMBER_ERROR
'
;
export
const
SET_BILLABLE_MEMBER_TO_REMOVE
=
'
SET_BILLABLE_MEMBER_TO_REMOVE
'
;
export
const
FETCH_BILLABLE_MEMBER_DETAILS
=
'
FETCH_BILLABLE_MEMBER_DETAILS
'
;
export
const
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
=
'
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
'
;
export
const
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
=
'
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
'
;
ee/app/assets/javascripts/billings/seat_usage/store/mutations.js
View file @
825e8f8b
import
Vue
from
'
vue
'
;
import
{
HEADER_TOTAL_ENTRIES
,
HEADER_PAGE_NUMBER
,
...
...
@@ -67,4 +68,25 @@ export default {
state
.
hasError
=
true
;
state
.
billableMemberToRemove
=
null
;
},
[
types
.
FETCH_BILLABLE_MEMBER_DETAILS
](
state
,
{
memberId
})
{
Vue
.
set
(
state
.
userDetails
,
memberId
,
{
isLoading
:
true
,
items
:
[],
});
},
[
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
](
state
,
{
memberId
,
memberships
})
{
Vue
.
set
(
state
.
userDetails
,
memberId
,
{
isLoading
:
false
,
items
:
memberships
,
});
},
[
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
](
state
,
{
memberId
})
{
Vue
.
set
(
state
.
userDetails
,
memberId
,
{
isLoading
:
false
,
items
:
[],
});
},
};
ee/app/assets/javascripts/billings/seat_usage/store/state.js
View file @
825e8f8b
...
...
@@ -8,4 +8,5 @@ export default ({ namespaceId = null, namespaceName = null } = {}) => ({
page
:
null
,
perPage
:
null
,
billableMemberToRemove
:
null
,
userDetails
:
{},
});
ee/changelogs/unreleased/vs-billable-members-list-drilldown.yml
0 → 100644
View file @
825e8f8b
---
title
:
Add tests for the details table
merge_request
:
59357
author
:
type
:
added
ee/spec/features/groups/billing/seat_usage_spec.rb
View file @
825e8f8b
...
...
@@ -30,6 +30,28 @@ RSpec.describe 'Groups > Billing > Seat Usage', :js do
expect
(
all
(
'tbody tr'
).
count
).
to
eq
(
3
)
end
end
context
'seat usage details table'
do
it
'expands the details on click'
do
first
(
'[data-testid="toggle-seat-usage-details"]'
).
click
wait_for_requests
expect
(
page
).
to
have_selector
(
'[data-testid="seat-usage-details"]'
)
end
it
'hides the details table on click'
do
# expand the details table first
first
(
'[data-testid="toggle-seat-usage-details"]'
).
click
wait_for_requests
# and collapse it
first
(
'[data-testid="toggle-seat-usage-details"]'
).
click
expect
(
page
).
not_to
have_selector
(
'[data-testid="seat-usage-details"]'
)
end
end
end
context
'when removing user'
do
...
...
ee/spec/frontend/api_spec.js
View file @
825e8f8b
...
...
@@ -819,15 +819,11 @@ describe('Api', () => {
});
describe
(
'
Billable members list
'
,
()
=>
{
let
expectedUrl
;
let
namespaceId
;
beforeEach
(()
=>
{
namespaceId
=
1000
;
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/groups/
${
namespaceId
}
/billable_members`
;
});
const
namespaceId
=
1000
;
describe
(
'
fetchBillableGroupMembersList
'
,
()
=>
{
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/groups/
${
namespaceId
}
/billable_members`
;
it
(
'
GETs the right url
'
,
()
=>
{
mock
.
onGet
(
expectedUrl
).
replyOnce
(
httpStatus
.
OK
,
[]);
...
...
@@ -836,6 +832,21 @@ describe('Api', () => {
});
});
});
describe
(
'
fetchBillableGroupMemberMemberships
'
,
()
=>
{
const
memberId
=
2
;
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/groups/
${
namespaceId
}
/billable_members/
${
memberId
}
/memberships`
;
it
(
'
fetches memberships for the member
'
,
async
()
=>
{
jest
.
spyOn
(
axios
,
'
get
'
);
mock
.
onGet
(
expectedUrl
).
replyOnce
(
httpStatus
.
OK
,
[]);
const
{
data
}
=
await
Api
.
fetchBillableGroupMemberMemberships
(
namespaceId
,
memberId
);
expect
(
data
).
toEqual
([]);
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
expectedUrl
);
});
});
});
describe
(
'
Project analytics: deployment frequency
'
,
()
=>
{
...
...
ee/spec/frontend/billings/mock_data.js
View file @
825e8f8b
...
...
@@ -102,6 +102,17 @@ export const mockDataSeats = {
},
};
export
const
mockMemberDetails
=
[
{
id
:
173
,
source_id
:
155
,
source_full_name
:
'
group_with_ultimate_plan / subgroup
'
,
created_at
:
'
2021-02-25T08:21:32.257Z
'
,
expires_at
:
null
,
access_level
:
{
string_value
:
'
Owner
'
,
integer_value
:
50
},
},
];
export
const
mockTableItems
=
[
{
email
:
'
administrator@email.com
'
,
...
...
ee/spec/frontend/billings/seat_usage/components/subscription_seat_details_spec.js
0 → 100644
View file @
825e8f8b
import
{
GlTable
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
Api
from
'
ee/api
'
;
import
SubscriptionSeatDetails
from
'
ee/billings/seat_usage/components/subscription_seat_details.vue
'
;
import
SubscriptionSeatDetailsLoader
from
'
ee/billings/seat_usage/components/subscription_seat_details_loader.vue
'
;
import
createStore
from
'
ee/billings/seat_usage/store
'
;
import
initState
from
'
ee/billings/seat_usage/store/state
'
;
import
{
mockMemberDetails
}
from
'
ee_jest/billings/mock_data
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
describe
(
'
SubscriptionSeatDetails
'
,
()
=>
{
let
wrapper
;
const
actions
=
{
fetchBillableMemberDetails
:
jest
.
fn
(),
};
const
createComponent
=
()
=>
{
const
store
=
createStore
(
initState
({
namespaceId
:
1
,
isLoading
:
true
}));
wrapper
=
shallowMount
(
SubscriptionSeatDetails
,
{
propsData
:
{
seatMemberId
:
1
,
},
store
:
new
Vuex
.
Store
({
...
store
,
actions
}),
localVue
,
stubs
:
{
GlTable
:
stubComponent
(
GlTable
),
},
});
};
beforeEach
(()
=>
{
Api
.
fetchBillableGroupMemberMemberships
=
jest
.
fn
(()
=>
Promise
.
resolve
({
data
:
mockMemberDetails
}),
);
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
on created
'
,
()
=>
{
it
(
'
calls fetchBillableMemberDetails
'
,
()
=>
{
expect
(
actions
.
fetchBillableMemberDetails
).
toHaveBeenCalledWith
(
expect
.
any
(
Object
),
1
);
});
it
(
'
displays skeleton loader
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
SubscriptionSeatDetailsLoader
).
isVisible
()).
toBe
(
true
);
});
});
});
ee/spec/frontend/billings/seat_usage/store/actions_spec.js
View file @
825e8f8b
...
...
@@ -4,7 +4,7 @@ import * as GroupsApi from 'ee/api/groups_api';
import
*
as
actions
from
'
ee/billings/seat_usage/store/actions
'
;
import
*
as
types
from
'
ee/billings/seat_usage/store/mutation_types
'
;
import
State
from
'
ee/billings/seat_usage/store/state
'
;
import
{
mockDataSeats
}
from
'
ee_jest/billings/mock_data
'
;
import
{
mockDataSeats
,
mockMemberDetails
}
from
'
ee_jest/billings/mock_data
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
createFlash
,
{
FLASH_TYPES
}
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
...
...
@@ -219,4 +219,113 @@ describe('seats actions', () => {
});
});
});
describe
(
'
fetchBillableMemberDetails
'
,
()
=>
{
const
member
=
mockDataSeats
.
data
[
0
];
beforeAll
(()
=>
{
Api
.
fetchBillableGroupMemberMemberships
=
jest
.
fn
()
.
mockResolvedValue
({
data
:
mockMemberDetails
});
});
it
(
'
commits fetchBillableMemberDetails
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetails
,
payload
:
member
.
id
,
state
,
expectedMutations
:
[
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS
,
payload
:
member
.
id
},
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
payload
:
{
memberId
:
member
.
id
,
memberships
:
mockMemberDetails
},
},
],
});
});
it
(
'
calls fetchBillableGroupMemberMemberships api
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetails
,
payload
:
member
.
id
,
state
,
expectedMutations
:
[
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS
,
payload
:
member
.
id
},
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
payload
:
{
memberId
:
member
.
id
,
memberships
:
mockMemberDetails
},
},
],
});
expect
(
Api
.
fetchBillableGroupMemberMemberships
).
toHaveBeenCalledWith
(
null
,
2
);
});
it
(
'
calls fetchBillableGroupMemberMemberships api only once
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetails
,
payload
:
member
.
id
,
state
,
expectedMutations
:
[
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS
,
payload
:
member
.
id
},
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
payload
:
{
memberId
:
member
.
id
,
memberships
:
mockMemberDetails
},
},
],
});
state
.
userDetails
[
member
.
id
]
=
{
items
:
mockMemberDetails
,
isLoading
:
false
};
await
testAction
({
action
:
actions
.
fetchBillableMemberDetails
,
payload
:
member
.
id
,
state
,
expectedMutations
:
[
{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
payload
:
{
memberId
:
member
.
id
,
memberships
:
mockMemberDetails
},
},
],
});
expect
(
Api
.
fetchBillableGroupMemberMemberships
).
toHaveBeenCalledTimes
(
1
);
});
describe
(
'
on API error
'
,
()
=>
{
beforeAll
(()
=>
{
Api
.
fetchBillableGroupMemberMemberships
=
jest
.
fn
().
mockRejectedValue
();
});
it
(
'
dispatches fetchBillableMemberDetailsError
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetailsError
,
state
,
expectedMutations
:
[{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
}],
});
});
});
});
describe
(
'
fetchBillableMemberDetailsError
'
,
()
=>
{
it
(
'
commits fetch billable member details error
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetailsError
,
state
,
expectedMutations
:
[{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
}],
});
});
it
(
'
calls createFlash
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetailsError
,
state
,
expectedMutations
:
[{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
}],
});
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
'
An error occurred while getting a billable member details
'
,
});
});
});
});
ee/spec/frontend/billings/seat_usage/store/getters_spec.js
View file @
825e8f8b
import
*
as
getters
from
'
ee/billings/seat_usage/store/getters
'
;
import
State
from
'
ee/billings/seat_usage/store/state
'
;
import
{
mockDataSeats
,
mockTableItems
}
from
'
ee_jest/billings/mock_data
'
;
import
{
mockDataSeats
,
mockTableItems
,
mockMemberDetails
}
from
'
ee_jest/billings/mock_data
'
;
describe
(
'
Seat usage table getters
'
,
()
=>
{
let
state
;
...
...
@@ -22,4 +22,29 @@ describe('Seat usage table getters', () => {
expect
(
getters
.
tableItems
(
state
)).
toEqual
([]);
});
});
describe
(
'
membershipsById
'
,
()
=>
{
describe
(
'
when data is not availlable
'
,
()
=>
{
it
(
'
returns a base state
'
,
()
=>
{
expect
(
getters
.
membershipsById
(
state
)(
0
)).
toEqual
({
isLoading
:
true
,
items
:
[],
});
});
});
describe
(
'
when data is available
'
,
()
=>
{
it
(
'
returns user details statep
'
,
()
=>
{
state
.
userDetails
[
0
]
=
{
isLoading
:
false
,
items
:
mockMemberDetails
,
};
expect
(
getters
.
membershipsById
(
state
)(
0
)).
toEqual
({
isLoading
:
false
,
items
:
mockMemberDetails
,
});
});
});
});
});
ee/spec/frontend/billings/seat_usage/store/mutations_spec.js
View file @
825e8f8b
import
*
as
types
from
'
ee/billings/seat_usage/store/mutation_types
'
;
import
mutations
from
'
ee/billings/seat_usage/store/mutations
'
;
import
createState
from
'
ee/billings/seat_usage/store/state
'
;
import
{
mockDataSeats
}
from
'
ee_jest/billings/mock_data
'
;
import
{
mockDataSeats
,
mockMemberDetails
}
from
'
ee_jest/billings/mock_data
'
;
describe
(
'
EE billings seats module mutations
'
,
()
=>
{
let
state
;
...
...
@@ -136,4 +136,45 @@ describe('EE billings seats module mutations', () => {
});
});
});
describe
(
'
fetching billable member details
'
,
()
=>
{
const
member
=
mockDataSeats
.
data
[
0
];
describe
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS
,
()
=>
{
it
(
'
sets the state to loading
'
,
()
=>
{
mutations
[
types
.
FETCH_BILLABLE_MEMBER_DETAILS
](
state
,
{
memberId
:
member
.
id
});
expect
(
state
.
userDetails
).
toMatchObject
({
[
member
.
id
.
toString
()]:
{
isLoading
:
true
,
},
});
});
});
describe
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
](
state
,
{
memberId
:
member
.
id
,
memberships
:
mockMemberDetails
,
});
});
it
(
'
sets the state to not loading
'
,
()
=>
{
expect
(
state
.
userDetails
[
member
.
id
.
toString
()].
isLoading
).
toBe
(
false
);
});
it
(
'
sets the memberships to the state
'
,
()
=>
{
expect
(
state
.
userDetails
[
member
.
id
.
toString
()].
items
).
toEqual
(
mockMemberDetails
);
});
});
describe
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
,
()
=>
{
it
(
'
sets the state to not loading
'
,
()
=>
{
mutations
[
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
](
state
,
{
memberId
:
member
.
id
});
expect
(
state
.
userDetails
[
member
.
id
.
toString
()].
isLoading
).
toBe
(
false
);
});
});
});
});
locale/gitlab.pot
View file @
825e8f8b
...
...
@@ -5058,12 +5058,18 @@ msgstr ""
msgid "Billing|An email address is only visible for users with public emails."
msgstr ""
msgid "Billing|An error occurred while getting a billable member details"
msgstr ""
msgid "Billing|An error occurred while loading billable members list"
msgstr ""
msgid "Billing|An error occurred while removing a billable member"
msgstr ""
msgid "Billing|Direct memberships"
msgstr ""
msgid "Billing|Enter at least three characters to search."
msgstr ""
...
...
@@ -5079,6 +5085,9 @@ msgstr ""
msgid "Billing|Remove user %{username} from your subscription"
msgstr ""
msgid "Billing|Toggle seat details"
msgstr ""
msgid "Billing|Type %{username} to confirm"
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