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
671a17dc
Commit
671a17dc
authored
Jul 27, 2021
by
Vitaly Slobodin
Committed by
Savas Vedova
Jul 27, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to sort billable members
parent
fef014d9
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
267 additions
and
266 deletions
+267
-266
ee/app/assets/javascripts/api.js
ee/app/assets/javascripts/api.js
+0
-38
ee/app/assets/javascripts/api/groups_api.js
ee/app/assets/javascripts/api/groups_api.js
+30
-3
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seats.vue
...pts/billings/seat_usage/components/subscription_seats.vue
+37
-51
ee/app/assets/javascripts/billings/seat_usage/constants.js
ee/app/assets/javascripts/billings/seat_usage/constants.js
+19
-0
ee/app/assets/javascripts/billings/seat_usage/store/actions.js
...p/assets/javascripts/billings/seat_usage/store/actions.js
+25
-8
ee/app/assets/javascripts/billings/seat_usage/store/mutation_types.js
...s/javascripts/billings/seat_usage/store/mutation_types.js
+3
-1
ee/app/assets/javascripts/billings/seat_usage/store/mutations.js
...assets/javascripts/billings/seat_usage/store/mutations.js
+13
-5
ee/app/assets/javascripts/billings/seat_usage/store/state.js
ee/app/assets/javascripts/billings/seat_usage/store/state.js
+2
-0
ee/spec/frontend/api/groups_api_spec.js
ee/spec/frontend/api/groups_api_spec.js
+78
-0
ee/spec/frontend/api_spec.js
ee/spec/frontend/api_spec.js
+0
-31
ee/spec/frontend/billings/seat_usage/components/subscription_seats_spec.js
...billings/seat_usage/components/subscription_seats_spec.js
+15
-95
ee/spec/frontend/billings/seat_usage/store/actions_spec.js
ee/spec/frontend/billings/seat_usage/store/actions_spec.js
+10
-22
ee/spec/frontend/billings/seat_usage/store/mutations_spec.js
ee/spec/frontend/billings/seat_usage/store/mutations_spec.js
+29
-9
locale/gitlab.pot
locale/gitlab.pot
+6
-3
No files found.
ee/app/assets/javascripts/api.js
View file @
671a17dc
...
...
@@ -45,9 +45,6 @@ export default {
issueMetricImagesPath
:
'
/api/:version/projects/:id/issues/:issue_iid/metric_images
'
,
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
));
...
...
@@ -370,41 +367,6 @@ export default {
return
axios
.
delete
(
individualMetricImageUrl
);
},
fetchBillableGroupMembersList
(
namespaceId
,
options
=
{},
callback
=
()
=>
{})
{
const
url
=
Api
.
buildUrl
(
this
.
billableGroupMembersPath
).
replace
(
'
:id
'
,
namespaceId
);
const
defaults
=
{
per_page
:
DEFAULT_PER_PAGE
,
page
:
1
,
};
const
passedOptions
=
options
;
// calling search API with empty string will not return results
if
(
!
passedOptions
.
search
)
{
passedOptions
.
search
=
undefined
;
}
return
axios
.
get
(
url
,
{
params
:
{
...
defaults
,
...
passedOptions
,
},
})
.
then
(({
data
,
headers
})
=>
{
callback
(
data
);
return
{
data
,
headers
};
});
},
fetchBillableGroupMemberMemberships
(
namespaceId
,
memberId
)
{
const
url
=
Api
.
buildUrl
(
this
.
billableGroupMemberMembershipsPath
)
.
replace
(
'
:group_id
'
,
namespaceId
)
.
replace
(
'
:member_id
'
,
memberId
);
return
axios
.
get
(
url
);
},
projectGroups
(
id
,
options
)
{
const
url
=
Api
.
buildUrl
(
this
.
projectGroupsPath
).
replace
(
'
:id
'
,
encodeURIComponent
(
id
));
...
...
ee/app/assets/javascripts/api/groups_api.js
View file @
671a17dc
import
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
{
buildApiUrl
}
from
'
~/api/api_utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
const
GROUPS_BILLABLE_MEMBERS_SINGLE_PATH
=
'
/api/:version/groups/:group_id/billable_members/:id
'
;
const
GROUPS_BILLABLE_MEMBERS_PATH
=
'
/api/:version/groups/:id/billable_members
'
;
const
GROUPS_BILLABLE_MEMBERS_SINGLE_MEMBERSHIPS_PATH
=
'
/api/:version/groups/:group_id/billable_members/:member_id/memberships
'
;
export
function
removeBillableMemberFromGroup
(
groupId
,
memberId
,
options
)
{
export
const
fetchBillableGroupMembersList
=
(
namespaceId
,
options
=
{})
=>
{
const
url
=
buildApiUrl
(
GROUPS_BILLABLE_MEMBERS_PATH
).
replace
(
'
:id
'
,
namespaceId
);
const
defaults
=
{
per_page
:
DEFAULT_PER_PAGE
,
page
:
1
,
};
return
axios
.
get
(
url
,
{
params
:
{
...
defaults
,
...
options
,
},
});
};
export
const
fetchBillableGroupMemberMemberships
=
(
namespaceId
,
memberId
)
=>
{
const
url
=
buildApiUrl
(
GROUPS_BILLABLE_MEMBERS_SINGLE_MEMBERSHIPS_PATH
)
.
replace
(
'
:group_id
'
,
namespaceId
)
.
replace
(
'
:member_id
'
,
memberId
);
return
axios
.
get
(
url
);
};
export
const
removeBillableMemberFromGroup
=
(
groupId
,
memberId
)
=>
{
const
url
=
buildApiUrl
(
GROUPS_BILLABLE_MEMBERS_SINGLE_PATH
)
.
replace
(
'
:group_id
'
,
groupId
)
.
replace
(
'
:id
'
,
memberId
);
return
axios
.
delete
(
url
,
{
params
:
{
...
options
}
}
);
}
return
axios
.
delete
(
url
);
}
;
ee/app/assets/javascripts/billings/seat_usage/components/subscription_seats.vue
View file @
671a17dc
...
...
@@ -10,22 +10,21 @@ import {
GlModalDirective
,
GlIcon
,
GlPagination
,
GlSearchBoxByType
,
GlTable
,
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
{
parseInt
,
debounce
}
from
'
lodash
'
;
import
{
mapActions
,
mapState
,
mapGetters
}
from
'
vuex
'
;
import
{
FIELDS
,
AVATAR_SIZE
,
SEARCH_DEBOUNCE_MS
,
REMOVE_BILLABLE_MEMBER_MODAL_ID
,
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_ID
,
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_TITLE
,
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT
,
SORT_OPTIONS
,
}
from
'
ee/billings/seat_usage/constants
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
FilterSortContainerRoot
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
import
RemoveBillableMemberModal
from
'
./remove_billable_member_modal.vue
'
;
import
SubscriptionSeatDetails
from
'
./subscription_seat_details.vue
'
;
...
...
@@ -44,15 +43,10 @@ export default {
GlModal
,
GlIcon
,
GlPagination
,
GlSearchBoxByType
,
GlTable
,
RemoveBillableMemberModal
,
SubscriptionSeatDetails
,
},
data
()
{
return
{
searchQuery
:
''
,
};
FilterSortContainerRoot
,
},
computed
:
{
...
mapState
([
...
...
@@ -63,44 +57,26 @@ export default {
'
namespaceName
'
,
'
namespaceId
'
,
'
billableMemberToRemove
'
,
'
search
'
,
'
sort
'
,
]),
...
mapGetters
([
'
tableItems
'
]),
currentPage
:
{
get
()
{
return
parseInt
(
this
.
page
,
10
)
;
return
this
.
page
;
},
set
(
val
)
{
this
.
fetchBillableMembersList
({
page
:
val
,
search
:
this
.
searchQuery
}
);
this
.
setCurrentPage
(
val
);
},
},
perPageFormatted
()
{
return
parseInt
(
this
.
perPage
,
10
);
},
totalFormatted
()
{
return
parseInt
(
this
.
total
,
10
);
},
emptyText
()
{
if
(
this
.
search
Query
?.
length
<
3
)
{
if
(
this
.
search
?.
length
<
3
)
{
return
s__
(
'
Billing|Enter at least three characters to search.
'
);
}
return
s__
(
'
Billing|No users to display.
'
);
},
},
watch
:
{
searchQuery
()
{
this
.
executeQuery
();
},
},
created
()
{
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue. More info:
// https://stackoverflow.com/a/52988020/1063392
this
.
debouncedSearch
=
debounce
(
function
search
()
{
this
.
fetchBillableMembersList
({
search
:
this
.
searchQuery
});
},
SEARCH_DEBOUNCE_MS
);
this
.
fetchBillableMembersList
();
},
methods
:
{
...
...
@@ -108,20 +84,22 @@ export default {
'
fetchBillableMembersList
'
,
'
resetBillableMembers
'
,
'
setBillableMemberToRemove
'
,
'
setSearchQuery
'
,
'
setCurrentPage
'
,
'
setSortOption
'
,
]),
onSearchEnter
()
{
this
.
debouncedSearch
.
cancel
();
this
.
executeQuery
();
},
executeQuery
()
{
const
queryLength
=
this
.
searchQuery
?.
length
;
const
MIN_SEARCH_LENGTH
=
3
;
applyFilter
(
searchTerms
)
{
const
searchQuery
=
searchTerms
.
reduce
((
terms
,
searchTerm
)
=>
{
if
(
searchTerm
.
type
!==
'
filtered-search-term
'
)
{
return
''
;
}
if
(
queryLength
===
0
||
queryLength
>=
MIN_SEARCH_LENGTH
)
{
this
.
debouncedSearch
();
}
else
if
(
queryLength
<
MIN_SEARCH_LENGTH
)
{
this
.
resetBillableMembers
();
}
return
`
${
terms
}
${
searchTerm
.
value
.
data
}
`
;
},
''
);
this
.
setSearchQuery
(
searchQuery
.
trim
()
||
null
);
},
handleSortOptionChange
(
sortOption
)
{
this
.
setSortOption
(
sortOption
);
},
displayRemoveMemberModal
(
user
)
{
if
(
user
.
removable
)
{
...
...
@@ -135,6 +113,7 @@ export default {
emailNotVisibleTooltipText
:
s__
(
'
Billing|An email address is only visible for users with public emails.
'
,
),
filterUsersPlaceholder
:
__
(
'
Filter users
'
),
},
avatarSize
:
AVATAR_SIZE
,
fields
:
FIELDS
,
...
...
@@ -142,6 +121,7 @@ export default {
cannotRemoveModalId
:
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_ID
,
cannotRemoveModalTitle
:
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_TITLE
,
cannotRemoveModalText
:
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT
,
sortOptions
:
SORT_OPTIONS
,
};
</
script
>
...
...
@@ -160,11 +140,17 @@ export default {
</h4>
<gl-badge>
{{
total
}}
</gl-badge>
</div>
</div>
<gl-search-box-by-type
v-model.trim=
"searchQuery"
:placeholder=
"s__('Billing|Type to search')"
@
keydown.enter.prevent=
"onSearchEnter"
<div
class=
"gl-bg-gray-10 gl-p-3"
>
<filter-sort-container-root
:namespace=
"namespaceId"
:tokens=
"[]"
:search-input-placeholder=
"$options.i18n.filterUsersPlaceholder"
:sort-options=
"$options.sortOptions"
initial-sort-by=
"last_activity_on_desc"
@
onFilter=
"applyFilter"
@
onSort=
"handleSortOptionChange"
/>
</div>
...
...
@@ -249,8 +235,8 @@ export default {
<gl-pagination
v-if=
"currentPage"
v-model=
"currentPage"
:per-page=
"perPage
Formatted
"
:total-items=
"total
Formatted
"
:per-page=
"perPage"
:total-items=
"total"
align=
"center"
class=
"gl-mt-5"
/>
...
...
ee/app/assets/javascripts/billings/seat_usage/constants.js
View file @
671a17dc
...
...
@@ -47,3 +47,22 @@ group and all its subgroups and projects. This action can't be undone.`,
);
export
const
AVATAR_SIZE
=
32
;
export
const
SEARCH_DEBOUNCE_MS
=
DEFAULT_DEBOUNCE_AND_THROTTLE_MS
;
export
const
SORT_OPTIONS
=
[
{
id
:
10
,
title
:
__
(
'
Last Activity
'
),
sortDirection
:
{
descending
:
'
last_activity_on_desc
'
,
ascending
:
'
last_activity_on_asc
'
,
},
},
{
id
:
20
,
title
:
__
(
'
Name
'
),
sortDirection
:
{
descending
:
'
name_desc
'
,
ascending
:
'
name_asc
'
,
},
},
];
ee/app/assets/javascripts/billings/seat_usage/store/actions.js
View file @
671a17dc
import
Api
from
'
ee/api
'
;
import
*
as
GroupsApi
from
'
ee/api/groups_api
'
;
import
createFlash
,
{
FLASH_TYPES
}
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
const
fetchBillableMembersList
=
({
dispatch
,
state
},
{
page
,
search
}
=
{
})
=>
{
dispatch
(
'
requestBillableMembersList
'
);
export
const
fetchBillableMembersList
=
({
commit
,
dispatch
,
state
})
=>
{
commit
(
types
.
REQUEST_BILLABLE_MEMBERS
);
return
Api
.
fetchBillableGroupMembersList
(
state
.
namespaceId
,
{
page
,
search
})
.
then
((
data
)
=>
dispatch
(
'
receiveBillableMembersListSuccess
'
,
data
))
const
{
page
,
search
,
sort
}
=
state
;
return
GroupsApi
.
fetchBillableGroupMembersList
(
state
.
namespaceId
,
{
page
,
search
,
sort
})
.
then
(({
data
,
headers
})
=>
dispatch
(
'
receiveBillableMembersListSuccess
'
,
{
data
,
headers
}))
.
catch
(()
=>
dispatch
(
'
receiveBillableMembersListError
'
));
};
export
const
requestBillableMembersList
=
({
commit
})
=>
commit
(
types
.
REQUEST_BILLABLE_MEMBERS
);
export
const
receiveBillableMembersListSuccess
=
({
commit
},
response
)
=>
commit
(
types
.
RECEIVE_BILLABLE_MEMBERS_SUCCESS
,
response
);
...
...
@@ -68,7 +67,7 @@ export const fetchBillableMemberDetails = ({ dispatch, commit, state }, memberId
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS
,
memberId
);
return
Api
.
fetchBillableGroupMemberMemberships
(
state
.
namespaceId
,
memberId
)
return
Groups
Api
.
fetchBillableGroupMemberMemberships
(
state
.
namespaceId
,
memberId
)
.
then
(({
data
})
=>
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_SUCCESS
,
{
memberId
,
memberships
:
data
}),
)
...
...
@@ -82,3 +81,21 @@ export const fetchBillableMemberDetailsError = ({ commit }, memberId) => {
message
:
s__
(
'
Billing|An error occurred while getting a billable member details
'
),
});
};
export
const
setSearchQuery
=
({
commit
,
dispatch
},
searchQuery
)
=>
{
commit
(
types
.
SET_SEARCH_QUERY
,
searchQuery
);
dispatch
(
'
fetchBillableMembersList
'
);
};
export
const
setCurrentPage
=
({
commit
,
dispatch
},
page
)
=>
{
commit
(
types
.
SET_CURRENT_PAGE
,
page
);
dispatch
(
'
fetchBillableMembersList
'
);
};
export
const
setSortOption
=
({
commit
,
dispatch
},
sortOption
)
=>
{
commit
(
types
.
SET_SORT_OPTION
,
sortOption
);
dispatch
(
'
fetchBillableMembersList
'
);
};
ee/app/assets/javascripts/billings/seat_usage/store/mutation_types.js
View file @
671a17dc
...
...
@@ -2,7 +2,9 @@ export const REQUEST_BILLABLE_MEMBERS = 'REQUEST_BILLABLE_MEMBERS';
export
const
RECEIVE_BILLABLE_MEMBERS_SUCCESS
=
'
RECEIVE_BILLABLE_MEMBERS_SUCCESS
'
;
export
const
RECEIVE_BILLABLE_MEMBERS_ERROR
=
'
RECEIVE_BILLABLE_MEMBERS_ERROR
'
;
export
const
SET_SEARCH
=
'
SET_SEARCH
'
;
export
const
SET_SEARCH_QUERY
=
'
SET_SEARCH_QUERY
'
;
export
const
SET_CURRENT_PAGE
=
'
SET_CURRENT_PAGE
'
;
export
const
SET_SORT_OPTION
=
'
SET_SORT_OPTION
'
;
export
const
RESET_BILLABLE_MEMBERS
=
'
RESET_BILLABLE_MEMBERS
'
;
export
const
REMOVE_BILLABLE_MEMBER
=
'
REMOVE_BILLABLE_MEMBER
'
;
...
...
ee/app/assets/javascripts/billings/seat_usage/store/mutations.js
View file @
671a17dc
...
...
@@ -16,9 +16,9 @@ export default {
const
{
data
,
headers
}
=
payload
;
state
.
members
=
data
;
state
.
total
=
headers
[
HEADER_TOTAL_ENTRIES
]
;
state
.
page
=
headers
[
HEADER_PAGE_NUMBER
]
;
state
.
perPage
=
headers
[
HEADER_ITEMS_PER_PAGE
]
;
state
.
total
=
Number
(
headers
[
HEADER_TOTAL_ENTRIES
])
;
state
.
page
=
Number
(
headers
[
HEADER_PAGE_NUMBER
])
;
state
.
perPage
=
Number
(
headers
[
HEADER_ITEMS_PER_PAGE
])
;
state
.
isLoading
=
false
;
},
...
...
@@ -28,8 +28,16 @@ export default {
state
.
hasError
=
true
;
},
[
types
.
SET_SEARCH
](
state
,
searchString
)
{
state
.
search
=
searchString
??
''
;
[
types
.
SET_SEARCH_QUERY
](
state
,
searchString
)
{
state
.
search
=
searchString
??
null
;
},
[
types
.
SET_CURRENT_PAGE
](
state
,
pageNumber
)
{
state
.
page
=
pageNumber
;
},
[
types
.
SET_SORT_OPTION
](
state
,
sortOption
)
{
state
.
sort
=
sortOption
;
},
[
types
.
RESET_BILLABLE_MEMBERS
](
state
)
{
...
...
ee/app/assets/javascripts/billings/seat_usage/store/state.js
View file @
671a17dc
...
...
@@ -9,4 +9,6 @@ export default ({ namespaceId = null, namespaceName = null } = {}) => ({
perPage
:
null
,
billableMemberToRemove
:
null
,
userDetails
:
{},
search
:
null
,
sort
:
'
last_activity_on_desc
'
,
});
ee/spec/frontend/api/groups_api_spec.js
0 → 100644
View file @
671a17dc
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
*
as
GroupsApi
from
'
ee/api/groups_api
'
;
import
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
describe
(
'
GroupsApi
'
,
()
=>
{
const
dummyApiVersion
=
'
v3000
'
;
const
dummyUrlRoot
=
'
/gitlab
'
;
const
dummyGon
=
{
api_version
:
dummyApiVersion
,
relative_url_root
:
dummyUrlRoot
,
};
let
originalGon
;
let
mock
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
originalGon
=
window
.
gon
;
window
.
gon
=
{
...
dummyGon
};
});
afterEach
(()
=>
{
mock
.
restore
();
window
.
gon
=
originalGon
;
});
describe
(
'
Billable members list
'
,
()
=>
{
const
namespaceId
=
1000
;
describe
(
'
fetchBillableGroupMembersList
'
,
()
=>
{
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/groups/
${
namespaceId
}
/billable_members`
;
it
(
'
GETs the right url
'
,
async
()
=>
{
jest
.
spyOn
(
axios
,
'
get
'
);
mock
.
onGet
(
expectedUrl
).
replyOnce
(
httpStatus
.
OK
,
[]);
const
{
data
}
=
await
GroupsApi
.
fetchBillableGroupMembersList
(
namespaceId
);
expect
(
data
).
toEqual
([]);
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
expectedUrl
,
{
params
:
{
page
:
1
,
per_page
:
DEFAULT_PER_PAGE
},
});
});
});
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
GroupsApi
.
fetchBillableGroupMemberMemberships
(
namespaceId
,
memberId
);
expect
(
data
).
toEqual
([]);
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
expectedUrl
);
});
});
describe
(
'
removeBillableMemberFromGroup
'
,
()
=>
{
const
memberId
=
2
;
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/groups/
${
namespaceId
}
/billable_members/
${
memberId
}
`
;
it
(
'
removes a billable member from a group
'
,
async
()
=>
{
jest
.
spyOn
(
axios
,
'
delete
'
);
mock
.
onDelete
(
expectedUrl
).
replyOnce
(
httpStatus
.
OK
,
[]);
const
{
data
}
=
await
GroupsApi
.
removeBillableMemberFromGroup
(
namespaceId
,
memberId
);
expect
(
data
).
toEqual
([]);
expect
(
axios
.
delete
).
toHaveBeenCalledWith
(
expectedUrl
);
});
});
});
});
ee/spec/frontend/api_spec.js
View file @
671a17dc
...
...
@@ -742,37 +742,6 @@ describe('Api', () => {
});
});
describe
(
'
Billable members list
'
,
()
=>
{
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
,
[]);
return
Api
.
fetchBillableGroupMembersList
(
namespaceId
).
then
(({
data
})
=>
{
expect
(
data
).
toEqual
([]);
});
});
});
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
'
,
()
=>
{
const
projectPath
=
'
test/project
'
;
const
encodedProjectPath
=
encodeURIComponent
(
projectPath
);
...
...
ee/spec/frontend/billings/seat_usage/components/subscription_seats_spec.js
View file @
671a17dc
...
...
@@ -4,7 +4,6 @@ import {
GlTable
,
GlAvatarLink
,
GlAvatarLabeled
,
GlSearchBoxByType
,
GlBadge
,
GlModal
,
}
from
'
@gitlab/ui
'
;
...
...
@@ -14,6 +13,7 @@ import SubscriptionSeats from 'ee/billings/seat_usage/components/subscription_se
import
{
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT
}
from
'
ee/billings/seat_usage/constants
'
;
import
{
mockDataSeats
,
mockTableItems
}
from
'
ee_jest/billings/mock_data
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
FilterSortContainerRoot
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
...
...
@@ -22,6 +22,7 @@ const actionSpies = {
fetchBillableMembersList
:
jest
.
fn
(),
resetBillableMembers
:
jest
.
fn
(),
setBillableMemberToRemove
:
jest
.
fn
(),
setSearchQuery
:
jest
.
fn
(),
};
const
providedFields
=
{
...
...
@@ -39,11 +40,12 @@ const fakeStore = ({ initialState, initialGetters }) =>
state
:
{
isLoading
:
false
,
hasError
:
false
,
namespaceId
:
null
,
namespaceId
:
1
,
members
:
[...
mockDataSeats
.
data
],
total
:
300
,
page
:
1
,
perPage
:
5
,
sort
:
'
last_activity_on_desc
'
,
...
providedFields
,
...
initialState
,
},
...
...
@@ -65,22 +67,21 @@ describe('Subscription Seats', () => {
);
};
const
findTable
=
()
=>
wrapper
.
find
(
GlTable
);
const
findTableEmptyText
=
()
=>
findTable
().
attributes
(
'
empty-text
'
);
const
findTable
=
()
=>
wrapper
.
findComponent
(
GlTable
);
const
findPageHeading
=
()
=>
wrapper
.
find
(
'
[data-testid="heading-info"]
'
);
const
findPageHeadingText
=
()
=>
findPageHeading
().
find
(
'
[data-testid="heading-info-text"]
'
);
const
findPageHeadingBadge
=
()
=>
findPageHeading
().
find
(
GlBadge
);
const
findSearchBox
=
()
=>
wrapper
.
find
(
GlSearchBoxByType
);
const
findPagination
=
()
=>
wrapper
.
find
(
GlPagination
);
const
findSearchBox
=
()
=>
wrapper
.
find
Component
(
FilterSortContainerRoot
);
const
findPagination
=
()
=>
wrapper
.
find
Component
(
GlPagination
);
const
findAllRemoveUserItems
=
()
=>
wrapper
.
findAllByTestId
(
'
remove-user
'
);
const
findErrorModal
=
()
=>
wrapper
.
findComponent
(
GlModal
);
const
serializeUser
=
(
rowWrapper
)
=>
{
const
avatarLink
=
rowWrapper
.
find
(
GlAvatarLink
);
const
avatarLabeled
=
rowWrapper
.
find
(
GlAvatarLabeled
);
const
avatarLink
=
rowWrapper
.
find
Component
(
GlAvatarLink
);
const
avatarLabeled
=
rowWrapper
.
find
Component
(
GlAvatarLabeled
);
return
{
avatarLink
:
{
...
...
@@ -102,7 +103,7 @@ describe('Subscription Seats', () => {
user
:
serializeUser
(
rowWrapper
),
email
:
emailWrapper
.
text
(),
tooltip
:
emailWrapper
.
find
(
'
span
'
).
attributes
(
'
title
'
),
dropdownExists
:
rowWrapper
.
find
(
GlDropdown
).
exists
(),
dropdownExists
:
rowWrapper
.
find
Component
(
GlDropdown
).
exists
(),
};
};
...
...
@@ -177,22 +178,6 @@ describe('Subscription Seats', () => {
});
});
describe
(
'
pagination
'
,
()
=>
{
it
.
each
([
null
,
NaN
,
undefined
,
'
a string
'
,
false
])(
'
will not render given %s for currentPage
'
,
(
value
)
=>
{
wrapper
=
createComponent
({
initialState
:
{
page
:
value
,
},
});
expect
(
findPagination
().
exists
()).
toBe
(
false
);
wrapper
.
destroy
();
},
);
});
describe
(
'
is loading
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
initialState
:
{
isLoading
:
true
}
});
...
...
@@ -212,82 +197,17 @@ describe('Subscription Seats', () => {
wrapper
=
createComponent
();
});
it
(
'
input event triggers the
fetchBillableMembersList
action
'
,
async
()
=>
{
it
(
'
input event triggers the
setSearchQuery
action
'
,
async
()
=>
{
const
SEARCH_STRING
=
'
search string
'
;
// fetchBillableMembersList is called once on created()
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
SEARCH_STRING
);
// fetchBillableMembersList is triggered a second time on input
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
2
);
// fetchBillableMembersList is triggered the second time with the correct argument
expect
(
actionSpies
.
fetchBillableMembersList
.
mock
.
calls
[
1
][
1
]).
toEqual
({
search
:
SEARCH_STRING
,
});
});
});
describe
(
'
typing inside of the search box
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
it
(
'
causes the empty table text to change based on the number of typed characters
'
,
async
()
=>
{
const
EMPTY_TEXT_TOO_SHORT
=
'
Enter at least three characters to search.
'
;
const
EMPTY_TEXT_NO_USERS
=
'
No users to display.
'
;
expect
(
findTableEmptyText
()).
toBe
(
EMPTY_TEXT_TOO_SHORT
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
a
'
);
expect
(
findTableEmptyText
()).
toBe
(
EMPTY_TEXT_TOO_SHORT
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aa
'
);
expect
(
findTableEmptyText
()).
toBe
(
EMPTY_TEXT_TOO_SHORT
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aaa
'
);
expect
(
findTableEmptyText
()).
toBe
(
EMPTY_TEXT_NO_USERS
);
});
it
(
'
dispatches the.resetBillableMembers action when 1 or 2 characters have been typed
'
,
async
()
=>
{
expect
(
actionSpies
.
resetBillableMembers
).
not
.
toHaveBeenCalled
();
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
a
'
);
expect
(
actionSpies
.
resetBillableMembers
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aa
'
);
expect
(
actionSpies
.
resetBillableMembers
).
toHaveBeenCalledTimes
(
2
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aaa
'
);
expect
(
actionSpies
.
resetBillableMembers
).
toHaveBeenCalledTimes
(
2
);
});
it
(
'
dispatches fetchBillableMembersList action when search box is emptied out
'
,
async
()
=>
{
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
a
'
);
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
''
);
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
2
);
});
it
(
'
dispatches fetchBillableMembersList action when more than 2 characters are typed
'
,
async
()
=>
{
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
a
'
);
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aa
'
);
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
1
);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aaa
'
);
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
2
);
await
findSearchBox
().
vm
.
$emit
(
'
onFilter
'
,
[
{
type
:
'
filtered-search-term
'
,
value
:
{
data
:
SEARCH_STRING
}
},
]);
await
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
aaaa
'
);
expect
(
actionSpies
.
fetchBillableMembersList
).
toHaveBeenCalledTimes
(
3
);
expect
(
actionSpies
.
setSearchQuery
).
toHaveBeenCalledWith
(
expect
.
any
(
Object
),
SEARCH_STRING
);
});
});
});
ee/spec/frontend/billings/seat_usage/store/actions_spec.js
View file @
671a17dc
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Api
from
'
ee/api
'
;
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
'
;
...
...
@@ -32,8 +31,9 @@ describe('seats actions', () => {
});
it
(
'
passes correct arguments to Api call
'
,
()
=>
{
const
payload
=
{
page
:
5
,
search
:
'
search string
'
};
const
spy
=
jest
.
spyOn
(
Api
,
'
fetchBillableGroupMembersList
'
);
const
payload
=
{
page
:
5
,
search
:
'
search string
'
,
sort
:
'
last_activity_on_desc
'
};
state
=
Object
.
assign
(
state
,
payload
);
const
spy
=
jest
.
spyOn
(
GroupsApi
,
'
fetchBillableGroupMembersList
'
);
testAction
({
action
:
actions
.
fetchBillableMembersList
,
...
...
@@ -58,12 +58,12 @@ describe('seats actions', () => {
action
:
actions
.
fetchBillableMembersList
,
state
,
expectedActions
:
[
{
type
:
'
requestBillableMembersList
'
},
{
type
:
'
receiveBillableMembersListSuccess
'
,
payload
:
mockDataSeats
,
},
],
expectedMutations
:
[{
type
:
types
.
REQUEST_BILLABLE_MEMBERS
}],
});
});
});
...
...
@@ -77,25 +77,13 @@ describe('seats actions', () => {
testAction
({
action
:
actions
.
fetchBillableMembersList
,
state
,
expectedActions
:
[
{
type
:
'
requestBillableMembersList
'
},
{
type
:
'
receiveBillableMembersListError
'
},
],
expectedActions
:
[{
type
:
'
receiveBillableMembersListError
'
}],
expectedMutations
:
[{
type
:
types
.
REQUEST_BILLABLE_MEMBERS
}],
});
});
});
});
describe
(
'
requestBillableMembersList
'
,
()
=>
{
it
(
'
should commit the request mutation
'
,
()
=>
{
testAction
({
action
:
actions
.
requestBillableMembersList
,
state
,
expectedMutations
:
[{
type
:
types
.
REQUEST_BILLABLE_MEMBERS
}],
});
});
});
describe
(
'
receiveBillableMembersListSuccess
'
,
()
=>
{
it
(
'
should commit the success mutation
'
,
()
=>
{
testAction
({
...
...
@@ -224,7 +212,7 @@ describe('seats actions', () => {
const
member
=
mockDataSeats
.
data
[
0
];
beforeAll
(()
=>
{
Api
.
fetchBillableGroupMemberMemberships
=
jest
Groups
Api
.
fetchBillableGroupMemberMemberships
=
jest
.
fn
()
.
mockResolvedValue
({
data
:
mockMemberDetails
});
});
...
...
@@ -258,7 +246,7 @@ describe('seats actions', () => {
],
});
expect
(
Api
.
fetchBillableGroupMemberMemberships
).
toHaveBeenCalledWith
(
null
,
2
);
expect
(
Groups
Api
.
fetchBillableGroupMemberMemberships
).
toHaveBeenCalledWith
(
null
,
2
);
});
it
(
'
calls fetchBillableGroupMemberMemberships api only once
'
,
async
()
=>
{
...
...
@@ -289,12 +277,12 @@ describe('seats actions', () => {
],
});
expect
(
Api
.
fetchBillableGroupMemberMemberships
).
toHaveBeenCalledTimes
(
1
);
expect
(
Groups
Api
.
fetchBillableGroupMemberMemberships
).
toHaveBeenCalledTimes
(
1
);
});
describe
(
'
on API error
'
,
()
=>
{
beforeAll
(()
=>
{
Api
.
fetchBillableGroupMemberMemberships
=
jest
.
fn
().
mockRejectedValue
();
Groups
Api
.
fetchBillableGroupMemberMemberships
=
jest
.
fn
().
mockRejectedValue
();
});
it
(
'
dispatches fetchBillableMemberDetailsError
'
,
async
()
=>
{
...
...
ee/spec/frontend/billings/seat_usage/store/mutations_spec.js
View file @
671a17dc
...
...
@@ -32,9 +32,9 @@ describe('EE billings seats module mutations', () => {
it
(
'
sets state as expected
'
,
()
=>
{
expect
(
state
.
members
).
toMatchObject
(
mockDataSeats
.
data
);
expect
(
state
.
total
).
toBe
(
'
3
'
);
expect
(
state
.
page
).
toBe
(
'
1
'
);
expect
(
state
.
perPage
).
toBe
(
'
1
'
);
expect
(
state
.
total
).
toBe
(
3
);
expect
(
state
.
page
).
toBe
(
1
);
expect
(
state
.
perPage
).
toBe
(
1
);
});
it
(
'
sets isLoading to false
'
,
()
=>
{
...
...
@@ -56,16 +56,20 @@ describe('EE billings seats module mutations', () => {
});
});
describe
(
types
.
SET_SEARCH
,
()
=>
{
const
SEARCH_STRING
=
'
a search string
'
;
describe
(
types
.
SET_SEARCH_QUERY
,
()
=>
{
it
(
'
sets the search state
'
,
()
=>
{
const
SEARCH_STRING
=
'
a search string
'
;
beforeEach
(()
=>
{
mutations
[
types
.
SET_SEARCH
](
state
,
SEARCH_STRING
);
});
mutations
[
types
.
SET_SEARCH_QUERY
](
state
,
SEARCH_STRING
);
it
(
'
sets the search state
'
,
()
=>
{
expect
(
state
.
search
).
toBe
(
SEARCH_STRING
);
});
it
(
'
sets the search state item to null
'
,
()
=>
{
mutations
[
types
.
SET_SEARCH_QUERY
](
state
);
expect
(
state
.
search
).
toBe
(
null
);
});
});
describe
(
types
.
RESET_BILLABLE_MEMBERS
,
()
=>
{
...
...
@@ -176,5 +180,21 @@ describe('EE billings seats module mutations', () => {
expect
(
state
.
userDetails
[
member
.
id
.
toString
()].
isLoading
).
toBe
(
false
);
});
});
describe
(
types
.
SET_CURRENT_PAGE
,
()
=>
{
it
(
'
sets the page state
'
,
()
=>
{
mutations
[
types
.
SET_CURRENT_PAGE
](
state
,
1
);
expect
(
state
.
page
).
toBe
(
1
);
});
});
describe
(
types
.
SET_SORT_OPTION
,
()
=>
{
it
(
'
sets the sort state
'
,
()
=>
{
mutations
[
types
.
SET_SORT_OPTION
](
state
,
'
last_activity_on_desc
'
);
expect
(
state
.
sort
).
toBe
(
'
last_activity_on_desc
'
);
});
});
});
});
locale/gitlab.pot
View file @
671a17dc
...
...
@@ -5216,9 +5216,6 @@ msgstr ""
msgid "Billing|Type %{username} to confirm"
msgstr ""
msgid "Billing|Type to search"
msgstr ""
msgid "Billing|User was successfully removed"
msgstr ""
...
...
@@ -14050,6 +14047,9 @@ msgstr ""
msgid "Filter results..."
msgstr ""
msgid "Filter users"
msgstr ""
msgid "Filter your repositories by name"
msgstr ""
...
...
@@ -19119,6 +19119,9 @@ msgstr ""
msgid "Last Accessed On"
msgstr ""
msgid "Last Activity"
msgstr ""
msgid "Last Pipeline"
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