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
d7e1c5e0
Commit
d7e1c5e0
authored
Apr 13, 2022
by
Ammar Alakkad
Committed by
Peter Hegman
Apr 13, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use StatisticsCard in usage_quotas/seats
Changelog: other EE: true
parent
8edfaf0b
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
389 additions
and
81 deletions
+389
-81
ee/app/assets/javascripts/usage_quotas/components/statistics_seats_card.stories.js
.../usage_quotas/components/statistics_seats_card.stories.js
+0
-2
ee/app/assets/javascripts/usage_quotas/components/statistics_seats_card.vue
...scripts/usage_quotas/components/statistics_seats_card.vue
+11
-5
ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seats.vue
...ipts/usage_quotas/seats/components/subscription_seats.vue
+51
-17
ee/app/assets/javascripts/usage_quotas/seats/index.js
ee/app/assets/javascripts/usage_quotas/seats/index.js
+5
-0
ee/app/assets/javascripts/usage_quotas/seats/store/actions.js
...pp/assets/javascripts/usage_quotas/seats/store/actions.js
+28
-9
ee/app/assets/javascripts/usage_quotas/seats/store/mutation_types.js
...ts/javascripts/usage_quotas/seats/store/mutation_types.js
+4
-0
ee/app/assets/javascripts/usage_quotas/seats/store/mutations.js
.../assets/javascripts/usage_quotas/seats/store/mutations.js
+21
-0
ee/app/assets/javascripts/usage_quotas/seats/store/state.js
ee/app/assets/javascripts/usage_quotas/seats/store/state.js
+8
-0
ee/app/views/groups/usage_quotas/index.html.haml
ee/app/views/groups/usage_quotas/index.html.haml
+1
-1
ee/spec/frontend/usage_quotas/components/statistics_seats_card_spec.js
...end/usage_quotas/components/statistics_seats_card_spec.js
+1
-9
ee/spec/frontend/usage_quotas/seats/components/subscription_seats_spec.js
.../usage_quotas/seats/components/subscription_seats_spec.js
+51
-11
ee/spec/frontend/usage_quotas/seats/mock_data.js
ee/spec/frontend/usage_quotas/seats/mock_data.js
+21
-0
ee/spec/frontend/usage_quotas/seats/store/actions_spec.js
ee/spec/frontend/usage_quotas/seats/store/actions_spec.js
+99
-10
ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js
ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js
+76
-8
locale/gitlab.pot
locale/gitlab.pot
+12
-9
No files found.
ee/app/assets/javascripts/usage_quotas/components/statistics_seats_card.stories.js
View file @
d7e1c5e0
...
...
@@ -16,10 +16,8 @@ const Template = (_, { argTypes }) => ({
});
export
const
Default
=
Template
.
bind
({});
/* eslint-disable @gitlab/require-i18n-strings */
Default
.
args
=
{
seatsUsed
:
160
,
seatsOwed
:
10
,
purchaseButtonLink
:
'
purchase.com/test
'
,
purchaseButtonText
:
'
Add seats
'
,
};
ee/app/assets/javascripts/usage_quotas/components/statistics_seats_card.vue
View file @
d7e1c5e0
<
script
>
import
{
GlLink
,
GlIcon
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
helpPagePath
}
from
'
~/helpers/help_page_helper
'
;
export
default
{
...
...
@@ -11,6 +11,7 @@ export default {
seatsUsedHelpText
:
__
(
'
Learn more about max seats used
'
),
seatsOwedText
:
__
(
'
Seats owed
'
),
seatsOwedHelpText
:
__
(
'
Learn more about seats owed
'
),
addSeatsText
:
s__
(
'
Billing|Add seats
'
),
},
helpLinks
:
{
seatsOwedLink
:
helpPagePath
(
'
subscriptions/gitlab_com/index
'
,
{
anchor
:
'
seats-owed
'
}),
...
...
@@ -73,7 +74,9 @@ export default {
class=
"gl-font-size-h-display gl-font-weight-bold gl-mb-3"
data-testid=
"seats-used-block"
>
{{
seatsUsed
}}
<span
class=
"gl-relative gl-top-1"
>
{{
seatsUsed
}}
</span>
<span
class=
"gl-font-lg"
>
{{
$options
.
i18n
.
seatsUsedText
}}
</span>
...
...
@@ -90,7 +93,9 @@ export default {
class=
"gl-font-size-h-display gl-font-weight-bold gl-mb-0"
data-testid=
"seats-owed-block"
>
{{
seatsOwed
}}
<span
class=
"gl-relative gl-top-1"
>
{{
seatsOwed
}}
</span>
<span
class=
"gl-font-lg"
>
{{
$options
.
i18n
.
seatsOwedText
}}
</span>
...
...
@@ -104,14 +109,15 @@ export default {
</p>
</div>
<gl-button
v-if=
"purchaseButtonLink
&& purchaseButtonText
"
v-if=
"purchaseButtonLink"
:href=
"purchaseButtonLink"
category=
"primary"
target=
"_blank"
variant=
"confirm"
class=
"gl-ml-3 gl-align-self-start"
data-testid=
"purchase-button"
>
{{
purchaseButton
Text
}}
{{
$options
.
i18n
.
addSeats
Text
}}
</gl-button>
</div>
</
template
>
ee/app/assets/javascripts/usage_quotas/seats/components/subscription_seats.vue
View file @
d7e1c5e0
...
...
@@ -13,6 +13,7 @@ import {
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapState
,
mapGetters
}
from
'
vuex
'
;
import
{
helpPagePath
}
from
'
~/helpers/help_page_helper
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
FIELDS
,
...
...
@@ -25,6 +26,8 @@ import {
}
from
'
ee/usage_quotas/seats/constants
'
;
import
{
s__
,
__
,
sprintf
,
n__
}
from
'
~/locale
'
;
import
FilterSortContainerRoot
from
'
~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
'
;
import
StatisticsCard
from
'
ee/usage_quotas/components/statistics_card.vue
'
;
import
StatisticsSeatsCard
from
'
ee/usage_quotas/components/statistics_seats_card.vue
'
;
import
RemoveBillableMemberModal
from
'
./remove_billable_member_modal.vue
'
;
import
SubscriptionSeatDetails
from
'
./subscription_seat_details.vue
'
;
...
...
@@ -46,6 +49,8 @@ export default {
RemoveBillableMemberModal
,
SubscriptionSeatDetails
,
FilterSortContainerRoot
,
StatisticsCard
,
StatisticsSeatsCard
,
},
computed
:
{
...
mapState
([
...
...
@@ -61,6 +66,12 @@ export default {
'
billableMemberToRemove
'
,
'
search
'
,
'
sort
'
,
'
seatsInSubscription
'
,
'
seatsInUse
'
,
'
maxSeatsUsed
'
,
'
seatsOwed
'
,
'
addSeatsHref
'
,
'
hasNoSubscription
'
,
]),
...
mapGetters
([
'
tableItems
'
]),
currentPage
:
{
...
...
@@ -92,13 +103,24 @@ export default {
shouldShowPendingMembersAlert
()
{
return
this
.
pendingMembersCount
>
0
&&
this
.
pendingMembersPagePath
;
},
seatsInUsePercentage
()
{
return
Math
.
round
((
this
.
seatsInUse
*
100
)
/
this
.
seatsInSubscription
);
},
totalSeatsInSubscription
()
{
return
this
.
hasNoSubscription
?
'
-
'
:
String
(
this
.
seatsInSubscription
);
},
totalSeatsInUse
()
{
return
this
.
hasNoSubscription
?
String
(
this
.
total
)
:
String
(
this
.
seatsInUse
);
},
},
created
()
{
this
.
fetchBillableMembersList
();
this
.
fetchGitlabSubscription
();
},
methods
:
{
...
mapActions
([
'
fetchBillableMembersList
'
,
'
fetchGitlabSubscription
'
,
'
resetBillableMembers
'
,
'
setBillableMemberToRemove
'
,
'
setSearchQuery
'
,
...
...
@@ -141,6 +163,10 @@ export default {
),
filterUsersPlaceholder
:
__
(
'
Filter users
'
),
pendingMembersAlertButtonText
:
s__
(
'
Billing|View pending approvals
'
),
seatsInUseText
:
s__
(
'
Billings|Seats in use / Seats in subscription
'
),
seatsInUseLink
:
helpPagePath
(
'
subscription/gitlab_com/index
'
,
{
anchor
:
'
how-seat-usage-is-determined
'
,
}),
},
avatarSize
:
AVATAR_SIZE
,
fields
:
FIELDS
,
...
...
@@ -164,35 +190,43 @@ export default {
>
{{
pendingMembersAlertMessage
}}
</gl-alert>
<div
class=
"gl-bg-gray-10 gl-p-6 gl-md-display-flex gl-justify-content-space-between gl-align-items-center"
>
<div
data-testid=
"heading-info"
>
<h4
data-testid=
"heading-info-text"
class=
"gl-font-base gl-display-inline-block gl-font-weight-normal"
>
{{
s__
(
'
Billing|Users occupying seats in
'
)
}}
<span
class=
"gl-font-weight-bold"
>
{{
namespaceName
}}
{{
s__
(
'
Billing|Group
'
)
}}
</span>
</h4>
<gl-badge>
{{
total
}}
</gl-badge>
</div>
<div
class=
"gl-bg-gray-10 gl-display-flex gl-sm-flex-direction-column gl-p-5"
>
<statistics-card
:help-link=
"$options.i18n.seatsInUseLink"
:description=
"$options.i18n.seatsInUseText"
:percentage=
"seatsInUsePercentage"
:usage-value=
"totalSeatsInUse"
:total-value=
"totalSeatsInSubscription"
class=
"gl-w-full gl-md-w-half gl-md-mr-5"
/>
<gl-button
v-if=
"seatUsageExportPath"
data-testid=
"export-button"
:href=
"seatUsageExportPath"
>
{{
s__
(
'
Billing|Export list
'
)
}}
</gl-button>
<statistics-seats-card
:seats-used=
"maxSeatsUsed"
:seats-owed=
"seatsOwed"
:purchase-button-link=
"addSeatsHref"
class=
"gl-w-full gl-md-w-half gl-md-mt-0 gl-mt-5"
/>
</div>
<div
class=
"gl-bg-gray-10 gl-p-
3
"
>
<div
class=
"gl-bg-gray-10 gl-p-
5 gl-display-flex
"
>
<filter-sort-container-root
:namespace=
"namespaceId"
:tokens=
"[]"
:search-input-placeholder=
"$options.i18n.filterUsersPlaceholder"
:sort-options=
"$options.sortOptions"
initial-sort-by=
"last_activity_on_desc"
class=
"gl-flex-grow-1"
@
onFilter=
"applyFilter"
@
onSort=
"setSortOption"
/>
<gl-button
v-if=
"seatUsageExportPath"
data-testid=
"export-button"
:href=
"seatUsageExportPath"
class=
"gl-ml-3"
>
{{
s__
(
'
Billing|Export list
'
)
}}
</gl-button>
</div>
<gl-table
...
...
ee/app/assets/javascripts/usage_quotas/seats/index.js
View file @
d7e1c5e0
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
SubscriptionSeats
from
'
./components/subscription_seats.vue
'
;
import
initialStore
from
'
./store
'
;
...
...
@@ -18,6 +19,8 @@ export default (containerId = 'js-seat-usage-app') => {
seatUsageExportPath
,
pendingMembersPagePath
,
pendingMembersCount
,
addSeatsHref
,
hasNoSubscription
,
}
=
el
.
dataset
;
return
new
Vue
({
...
...
@@ -31,6 +34,8 @@ export default (containerId = 'js-seat-usage-app') => {
seatUsageExportPath
,
pendingMembersPagePath
,
pendingMembersCount
,
addSeatsHref
,
hasNoSubscription
:
parseBoolean
(
hasNoSubscription
),
}),
),
render
(
createElement
)
{
...
...
ee/app/assets/javascripts/usage_quotas/seats/store/actions.js
View file @
d7e1c5e0
import
*
as
GroupsApi
from
'
ee/api/groups_api
'
;
import
createFlash
,
{
FLASH_TYPES
}
from
'
~/flash
'
;
import
Api
from
'
ee/api
'
;
import
{
createAlert
,
VARIANT_SUCCESS
}
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
*
as
types
from
'
./mutation_types
'
;
...
...
@@ -13,16 +14,34 @@ export const fetchBillableMembersList = ({ commit, dispatch, state }) => {
.
catch
(()
=>
dispatch
(
'
receiveBillableMembersListError
'
));
};
export
const
fetchGitlabSubscription
=
({
commit
,
dispatch
,
state
})
=>
{
commit
(
types
.
REQUEST_GITLAB_SUBSCRIPTION
);
return
Api
.
userSubscription
(
state
.
namespaceId
)
.
then
(({
data
})
=>
dispatch
(
'
receiveGitlabSubscriptionSuccess
'
,
data
))
.
catch
(()
=>
dispatch
(
'
receiveGitlabSubscriptionError
'
));
};
export
const
receiveBillableMembersListSuccess
=
({
commit
},
response
)
=>
commit
(
types
.
RECEIVE_BILLABLE_MEMBERS_SUCCESS
,
response
);
export
const
receiveBillableMembersListError
=
({
commit
})
=>
{
create
Flash
({
message
:
s__
(
'
Billing|An error occurred while loading billable members list
'
),
create
Alert
({
message
:
s__
(
'
Billing|An error occurred while loading billable members list
.
'
),
});
commit
(
types
.
RECEIVE_BILLABLE_MEMBERS_ERROR
);
};
export
const
receiveGitlabSubscriptionSuccess
=
({
commit
},
response
)
=>
commit
(
types
.
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
,
response
);
export
const
receiveGitlabSubscriptionError
=
({
commit
})
=>
{
createAlert
({
message
:
s__
(
'
Billing|An error occurred while loading GitLab subscription details.
'
),
});
commit
(
types
.
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
);
};
export
const
resetBillableMembers
=
({
commit
})
=>
{
commit
(
types
.
RESET_BILLABLE_MEMBERS
);
};
...
...
@@ -40,17 +59,17 @@ export const removeBillableMember = ({ dispatch, state }) => {
export
const
removeBillableMemberSuccess
=
({
dispatch
,
commit
})
=>
{
dispatch
(
'
fetchBillableMembersList
'
);
create
Flash
({
create
Alert
({
message
:
s__
(
'
Billing|User was successfully removed
'
),
type
:
FLASH_TYPES
.
SUCCESS
,
variant
:
VARIANT_
SUCCESS
,
});
commit
(
types
.
REMOVE_BILLABLE_MEMBER_SUCCESS
);
};
export
const
removeBillableMemberError
=
({
commit
})
=>
{
create
Flash
({
message
:
s__
(
'
Billing|An error occurred while removing a billable member
'
),
create
Alert
({
message
:
s__
(
'
Billing|An error occurred while removing a billable member
.
'
),
});
commit
(
types
.
REMOVE_BILLABLE_MEMBER_ERROR
);
};
...
...
@@ -77,8 +96,8 @@ export const fetchBillableMemberDetails = ({ dispatch, commit, state }, memberId
export
const
fetchBillableMemberDetailsError
=
({
commit
},
memberId
)
=>
{
commit
(
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
,
memberId
);
create
Flash
({
message
:
s__
(
'
Billing|An error occurred while getting a billable member details
'
),
create
Alert
({
message
:
s__
(
'
Billing|An error occurred while getting a billable member details
.
'
),
});
};
...
...
ee/app/assets/javascripts/usage_quotas/seats/store/mutation_types.js
View file @
d7e1c5e0
...
...
@@ -2,6 +2,10 @@ 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
REQUEST_GITLAB_SUBSCRIPTION
=
'
REQUEST_GITLAB_SUBSCRIPTION
'
;
export
const
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
=
'
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
'
;
export
const
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
=
'
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
'
;
export
const
SET_SEARCH_QUERY
=
'
SET_SEARCH_QUERY
'
;
export
const
SET_CURRENT_PAGE
=
'
SET_CURRENT_PAGE
'
;
export
const
SET_SORT_OPTION
=
'
SET_SORT_OPTION
'
;
...
...
ee/app/assets/javascripts/usage_quotas/seats/store/mutations.js
View file @
d7e1c5e0
...
...
@@ -12,6 +12,11 @@ export default {
state
.
hasError
=
false
;
},
[
types
.
REQUEST_GITLAB_SUBSCRIPTION
](
state
)
{
state
.
isLoading
=
true
;
state
.
hasError
=
false
;
},
[
types
.
RECEIVE_BILLABLE_MEMBERS_SUCCESS
](
state
,
payload
)
{
const
{
data
,
headers
}
=
payload
;
state
.
members
=
data
;
...
...
@@ -23,11 +28,27 @@ export default {
state
.
isLoading
=
false
;
},
[
types
.
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
](
state
,
payload
)
{
const
{
usage
}
=
payload
;
state
.
seatsInSubscription
=
usage
?.
seats_in_subscription
??
0
;
state
.
seatsInUse
=
usage
?.
seats_in_use
??
0
;
state
.
maxSeatsUsed
=
usage
?.
max_seats_used
??
0
;
state
.
seatsOwed
=
usage
?.
seats_owed
??
0
;
state
.
isLoading
=
false
;
},
[
types
.
RECEIVE_BILLABLE_MEMBERS_ERROR
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasError
=
true
;
},
[
types
.
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasError
=
true
;
},
[
types
.
SET_SEARCH_QUERY
](
state
,
searchString
)
{
state
.
search
=
searchString
??
null
;
},
...
...
ee/app/assets/javascripts/usage_quotas/seats/store/state.js
View file @
d7e1c5e0
...
...
@@ -4,6 +4,8 @@ export default ({
seatUsageExportPath
=
null
,
pendingMembersPagePath
=
null
,
pendingMembersCount
=
0
,
addSeatsHref
=
''
,
hasNoSubscription
=
null
,
}
=
{})
=>
({
isLoading
:
false
,
hasError
:
false
,
...
...
@@ -20,4 +22,10 @@ export default ({
userDetails
:
{},
search
:
null
,
sort
:
'
last_activity_on_desc
'
,
seatsInSubscription
:
null
,
seatsInUse
:
null
,
maxSeatsUsed
:
null
,
seatsOwed
:
null
,
hasNoSubscription
,
addSeatsHref
,
});
ee/app/views/groups/usage_quotas/index.html.haml
View file @
d7e1c5e0
...
...
@@ -35,7 +35,7 @@
=
s_
(
'UsageQuota|Storage'
)
.tab-content
.tab-pane
#seats-quota-tab
#js-seat-usage-app
{
data:
{
namespace_id:
@group
.
id
,
namespace_name:
@group
.
name
,
seat_usage_export_path:
group_seat_usage_path
(
@group
,
format: :csv
),
pending_members_page_path:
pending_members_page_path
,
pending_members_count:
pending_members_count
}
}
#js-seat-usage-app
{
data:
{
namespace_id:
@group
.
id
,
namespace_name:
@group
.
name
,
seat_usage_export_path:
group_seat_usage_path
(
@group
,
format: :csv
),
pending_members_page_path:
pending_members_page_path
,
pending_members_count:
pending_members_count
,
add_seats_href:
add_seats_url
(
@group
),
has_no_subscription:
@group
.
has_free_or_no_subscription?
.
to_s
}
}
.tab-pane
#pipelines-quota-tab
#js-ci-minutes-usage-group
{
data:
{
namespace_id:
@group
.
id
}
}
=
render
"namespaces/pipelines_quota/list"
,
...
...
ee/spec/frontend/usage_quotas/components/statistics_seats_card_spec.js
View file @
d7e1c5e0
...
...
@@ -5,12 +5,10 @@ import StatisticsSeatsCard from 'ee/usage_quotas/components/statistics_seats_car
describe
(
'
StatisticsSeatsCard
'
,
()
=>
{
let
wrapper
;
const
purchaseButtonLink
=
'
https://gitlab.com/purchase-more-seats
'
;
const
purchaseButtonText
=
'
Add seats
'
;
const
defaultProps
=
{
seatsUsed
:
20
,
seatsOwed
:
5
,
purchaseButtonLink
,
purchaseButtonText
,
};
const
createComponent
=
(
props
=
{})
=>
{
...
...
@@ -71,7 +69,7 @@ describe('StatisticsSeatsCard', () => {
expect
(
purchaseButton
.
exists
()).
toBe
(
true
);
expect
(
purchaseButton
.
attributes
(
'
href
'
)).
toBe
(
purchaseButtonLink
);
expect
(
purchaseButton
.
text
()).
toBe
(
purchaseButtonText
);
expect
(
purchaseButton
.
attributes
(
'
target
'
)).
toBe
(
'
_blank
'
);
});
it
(
'
does not render purchase button if purchase link is not passed
'
,
()
=>
{
...
...
@@ -79,11 +77,5 @@ describe('StatisticsSeatsCard', () => {
expect
(
findPurchaseButton
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render purchase button if purchase text is not passed
'
,
()
=>
{
createComponent
({
purchaseButtonText
:
null
});
expect
(
findPurchaseButton
().
exists
()).
toBe
(
false
);
});
});
});
ee/spec/frontend/usage_quotas/seats/components/subscription_seats_spec.js
View file @
d7e1c5e0
...
...
@@ -11,6 +11,8 @@ import {
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
StatisticsCard
from
'
ee/usage_quotas/components/statistics_card.vue
'
;
import
StatisticsSeatsCard
from
'
ee/usage_quotas/components/statistics_seats_card.vue
'
;
import
SubscriptionSeats
from
'
ee/usage_quotas/seats/components/subscription_seats.vue
'
;
import
{
CANNOT_REMOVE_BILLABLE_MEMBER_MODAL_CONTENT
}
from
'
ee/usage_quotas/seats/constants
'
;
import
{
mockDataSeats
,
mockTableItems
}
from
'
ee_jest/usage_quotas/seats/mock_data
'
;
...
...
@@ -21,6 +23,7 @@ Vue.use(Vuex);
const
actionSpies
=
{
fetchBillableMembersList
:
jest
.
fn
(),
fetchGitlabSubscription
:
jest
.
fn
(),
resetBillableMembers
:
jest
.
fn
(),
setBillableMemberToRemove
:
jest
.
fn
(),
setSearchQuery
:
jest
.
fn
(),
...
...
@@ -70,10 +73,6 @@ describe('Subscription Seats', () => {
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
().
findComponent
(
GlBadge
);
const
findExportButton
=
()
=>
wrapper
.
findByTestId
(
'
export-button
'
);
const
findSearchBox
=
()
=>
wrapper
.
findComponent
(
FilterSortContainerRoot
);
...
...
@@ -81,6 +80,8 @@ describe('Subscription Seats', () => {
const
findAllRemoveUserItems
=
()
=>
wrapper
.
findAllByTestId
(
'
remove-user
'
);
const
findErrorModal
=
()
=>
wrapper
.
findComponent
(
GlModal
);
const
findStatisticsCard
=
()
=>
wrapper
.
findComponent
(
StatisticsCard
);
const
findStatisticsSeatsCard
=
()
=>
wrapper
.
findComponent
(
StatisticsSeatsCard
);
const
serializeUser
=
(
rowWrapper
)
=>
{
const
avatarLink
=
rowWrapper
.
findComponent
(
GlAvatarLink
);
...
...
@@ -142,13 +143,6 @@ describe('Subscription Seats', () => {
wrapper
.
destroy
();
});
describe
(
'
heading text
'
,
()
=>
{
it
(
'
contains the group name and total seats number
'
,
()
=>
{
expect
(
findPageHeadingText
().
text
()).
toMatch
(
providedFields
.
namespaceName
);
expect
(
findPageHeadingBadge
().
text
()).
toMatch
(
'
300
'
);
});
});
describe
(
'
export button
'
,
()
=>
{
it
(
'
has the correct href
'
,
()
=>
{
expect
(
findExportButton
().
attributes
().
href
).
toBe
(
providedFields
.
seatUsageExportPath
);
...
...
@@ -241,6 +235,52 @@ describe('Subscription Seats', () => {
});
});
describe
(
'
statistics cards
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
initialState
:
{
seatsInSubscription
:
3
,
seatsInUse
:
2
,
maxSeatsUsed
:
3
,
seatsOwed
:
1
,
},
});
});
it
(
'
calls the correct action on create
'
,
()
=>
{
expect
(
actionSpies
.
fetchGitlabSubscription
).
toHaveBeenCalled
();
});
it
(
'
renders <statistics-card> with the necessary props
'
,
()
=>
{
const
statisticsCard
=
findStatisticsCard
();
expect
(
statisticsCard
.
exists
()).
toBe
(
true
);
expect
(
statisticsCard
.
props
()).
toEqual
(
expect
.
objectContaining
({
description
:
'
Seats in use / Seats in subscription
'
,
helpLink
:
'
/help/subscription/gitlab_com/index#how-seat-usage-is-determined
'
,
percentage
:
67
,
totalUnit
:
null
,
totalValue
:
'
3
'
,
usageUnit
:
null
,
usageValue
:
'
2
'
,
}),
);
});
it
(
'
renders <statistics-seats-card> with the necessary props
'
,
()
=>
{
const
statisticsSeatsCard
=
findStatisticsSeatsCard
();
expect
(
statisticsSeatsCard
.
exists
()).
toBe
(
true
);
expect
(
statisticsSeatsCard
.
props
()).
toEqual
(
expect
.
objectContaining
({
seatsOwed
:
1
,
seatsUsed
:
3
,
}),
);
});
});
describe
(
'
is loading
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
initialState
:
{
isLoading
:
true
}
});
...
...
ee/spec/frontend/usage_quotas/seats/mock_data.js
View file @
d7e1c5e0
...
...
@@ -123,3 +123,24 @@ export const mockTableItems = [
},
},
];
export
const
mockUserSubscription
=
{
plan
:
{
code
:
null
,
name
:
null
,
trial
:
false
,
auto_renew
:
null
,
upgradable
:
false
,
},
usage
:
{
seats_in_subscription
:
10
,
seats_in_use
:
5
,
max_seats_used
:
2
,
seats_owed
:
3
,
},
billing
:
{
subscription_start_date
:
'
2022-03-08
'
,
subscription_end_date
:
null
,
trial_ends_on
:
null
,
},
};
ee/spec/frontend/usage_quotas/seats/store/actions_spec.js
View file @
d7e1c5e0
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
*
as
GroupsApi
from
'
ee/api/groups_api
'
;
import
Api
from
'
ee/api
'
;
import
*
as
actions
from
'
ee/usage_quotas/seats/store/actions
'
;
import
*
as
types
from
'
ee/usage_quotas/seats/store/mutation_types
'
;
import
State
from
'
ee/usage_quotas/seats/store/state
'
;
import
{
mockDataSeats
,
mockMemberDetails
}
from
'
ee_jest/usage_quotas/seats/mock_data
'
;
import
{
mockDataSeats
,
mockMemberDetails
,
mockUserSubscription
,
}
from
'
ee_jest/usage_quotas/seats/mock_data
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
createFlash
,
{
FLASH_TYPE
S
}
from
'
~/flash
'
;
import
{
createAlert
,
VARIANT_SUCCES
S
}
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatusCodes
from
'
~/lib/utils/http_status
'
;
...
...
@@ -105,7 +110,91 @@ describe('seats actions', () => {
expectedMutations
:
[{
type
:
types
.
RECEIVE_BILLABLE_MEMBERS_ERROR
}],
});
expect
(
createFlash
).
toHaveBeenCalled
();
expect
(
createAlert
).
toHaveBeenCalled
();
});
});
describe
(
'
fetchGitlabSubscription
'
,
()
=>
{
beforeEach
(()
=>
{
gon
.
api_version
=
'
v4
'
;
state
.
namespaceId
=
1
;
});
it
(
'
passes correct arguments to Api call
'
,
()
=>
{
const
spy
=
jest
.
spyOn
(
Api
,
'
userSubscription
'
);
testAction
({
action
:
actions
.
fetchGitlabSubscription
,
state
,
expectedMutations
:
expect
.
anything
(),
expectedActions
:
expect
.
anything
(),
});
expect
(
spy
).
toBeCalledWith
(
state
.
namespaceId
);
});
describe
(
'
on success
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
'
/api/v4/namespaces/1/gitlab_subscription
'
)
.
replyOnce
(
httpStatusCodes
.
OK
,
mockUserSubscription
);
});
it
(
'
should dispatch the request and success actions
'
,
()
=>
{
testAction
({
action
:
actions
.
fetchGitlabSubscription
,
state
,
expectedActions
:
[
{
type
:
'
receiveGitlabSubscriptionSuccess
'
,
payload
:
mockUserSubscription
,
},
],
expectedMutations
:
[{
type
:
types
.
REQUEST_GITLAB_SUBSCRIPTION
}],
});
});
});
describe
(
'
on error
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
'
/api/v4/namespaces/1/gitlab_subscription
'
)
.
replyOnce
(
httpStatusCodes
.
NOT_FOUND
,
{});
});
it
(
'
should dispatch the request and error actions
'
,
()
=>
{
testAction
({
action
:
actions
.
fetchGitlabSubscription
,
state
,
expectedActions
:
[{
type
:
'
receiveGitlabSubscriptionError
'
}],
expectedMutations
:
[{
type
:
types
.
REQUEST_GITLAB_SUBSCRIPTION
}],
});
});
});
});
describe
(
'
receiveGitlabSubscriptionSuccess
'
,
()
=>
{
it
(
'
should commit the success mutation
'
,
()
=>
{
testAction
({
action
:
actions
.
receiveGitlabSubscriptionSuccess
,
payload
:
mockDataSeats
,
state
,
expectedMutations
:
[
{
type
:
types
.
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
,
payload
:
mockDataSeats
},
],
});
});
});
describe
(
'
receiveGitlabSubscriptionError
'
,
()
=>
{
it
(
'
should commit the error mutation
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
receiveGitlabSubscriptionError
,
state
,
expectedMutations
:
[{
type
:
types
.
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
}],
});
expect
(
createAlert
).
toHaveBeenCalled
();
});
});
...
...
@@ -187,9 +276,9 @@ describe('seats actions', () => {
expectedMutations
:
[{
type
:
types
.
REMOVE_BILLABLE_MEMBER_SUCCESS
}],
});
expect
(
create
Flash
).
toHaveBeenCalledWith
({
expect
(
create
Alert
).
toHaveBeenCalledWith
({
message
:
'
User was successfully removed
'
,
type
:
FLASH_TYPES
.
SUCCESS
,
variant
:
VARIANT_
SUCCESS
,
});
});
});
...
...
@@ -202,8 +291,8 @@ describe('seats actions', () => {
expectedMutations
:
[{
type
:
types
.
REMOVE_BILLABLE_MEMBER_ERROR
}],
});
expect
(
create
Flash
).
toHaveBeenCalledWith
({
message
:
'
An error occurred while removing a billable member
'
,
expect
(
create
Alert
).
toHaveBeenCalledWith
({
message
:
'
An error occurred while removing a billable member
.
'
,
});
});
});
...
...
@@ -304,15 +393,15 @@ describe('seats actions', () => {
});
});
it
(
'
calls create
Flash
'
,
async
()
=>
{
it
(
'
calls create
Alert
'
,
async
()
=>
{
await
testAction
({
action
:
actions
.
fetchBillableMemberDetailsError
,
state
,
expectedMutations
:
[{
type
:
types
.
FETCH_BILLABLE_MEMBER_DETAILS_ERROR
}],
});
expect
(
create
Flash
).
toHaveBeenCalledWith
({
message
:
'
An error occurred while getting a billable member details
'
,
expect
(
create
Alert
).
toHaveBeenCalledWith
({
message
:
'
An error occurred while getting a billable member details
.
'
,
});
});
});
...
...
ee/spec/frontend/usage_quotas/seats/store/mutations_spec.js
View file @
d7e1c5e0
import
*
as
types
from
'
ee/usage_quotas/seats/store/mutation_types
'
;
import
mutations
from
'
ee/usage_quotas/seats/store/mutations
'
;
import
createState
from
'
ee/usage_quotas/seats/store/state
'
;
import
{
mockDataSeats
,
mockMemberDetails
}
from
'
ee_jest/usage_quotas/seats/mock_data
'
;
import
{
mockDataSeats
,
mockMemberDetails
,
mockUserSubscription
,
}
from
'
ee_jest/usage_quotas/seats/mock_data
'
;
describe
(
'
EE seats module mutations
'
,
()
=>
{
let
state
;
...
...
@@ -16,11 +20,11 @@ describe('EE seats module mutations', () => {
});
it
(
'
sets isLoading to true
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
Truthy
(
);
expect
(
state
.
isLoading
).
toBe
(
true
);
});
it
(
'
sets hasError to false
'
,
()
=>
{
expect
(
state
.
hasError
).
toBe
Falsy
(
);
expect
(
state
.
hasError
).
toBe
(
false
);
});
});
...
...
@@ -38,7 +42,7 @@ describe('EE seats module mutations', () => {
});
it
(
'
sets isLoading to false
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
Falsy
(
);
expect
(
state
.
isLoading
).
toBe
(
false
);
});
});
...
...
@@ -48,11 +52,75 @@ describe('EE seats module mutations', () => {
});
it
(
'
sets isLoading to false
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
Falsy
(
);
expect
(
state
.
isLoading
).
toBe
(
false
);
});
it
(
'
sets hasError to true
'
,
()
=>
{
expect
(
state
.
hasError
).
toBeTruthy
();
expect
(
state
.
hasError
).
toBe
(
true
);
});
});
describe
(
types
.
REQUEST_GITLAB_SUBSCRIPTION
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
REQUEST_GITLAB_SUBSCRIPTION
](
state
);
});
it
(
'
sets isLoading to true
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
(
true
);
});
it
(
'
sets hasError to false
'
,
()
=>
{
expect
(
state
.
hasError
).
toBe
(
false
);
});
});
describe
(
types
.
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
,
()
=>
{
describe
(
'
when subscription data is passed
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
](
state
,
mockUserSubscription
);
});
it
(
'
sets state as expected
'
,
()
=>
{
expect
(
state
.
seatsInSubscription
).
toBe
(
mockUserSubscription
.
usage
.
seats_in_subscription
);
expect
(
state
.
seatsInUse
).
toBe
(
mockUserSubscription
.
usage
.
seats_in_use
);
expect
(
state
.
maxSeatsUsed
).
toBe
(
mockUserSubscription
.
usage
.
max_seats_used
);
expect
(
state
.
seatsOwed
).
toBe
(
mockUserSubscription
.
usage
.
seats_owed
);
});
it
(
'
sets isLoading to false
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
(
false
);
});
});
describe
(
'
when subscription data is not passed
'
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_GITLAB_SUBSCRIPTION_SUCCESS
](
state
,
{});
});
it
(
'
sets state as expected
'
,
()
=>
{
expect
(
state
.
seatsInSubscription
).
toBe
(
0
);
expect
(
state
.
seatsInUse
).
toBe
(
0
);
expect
(
state
.
maxSeatsUsed
).
toBe
(
0
);
expect
(
state
.
seatsOwed
).
toBe
(
0
);
});
it
(
'
sets isLoading to false
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
(
false
);
});
});
});
describe
(
types
.
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
,
()
=>
{
beforeEach
(()
=>
{
mutations
[
types
.
RECEIVE_GITLAB_SUBSCRIPTION_ERROR
](
state
);
});
it
(
'
sets isLoading to false
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
(
false
);
});
it
(
'
sets hasError to true
'
,
()
=>
{
expect
(
state
.
hasError
).
toBe
(
true
);
});
});
...
...
@@ -85,11 +153,11 @@ describe('EE seats module mutations', () => {
expect
(
state
.
page
).
toBeNull
();
expect
(
state
.
perPage
).
toBeNull
();
expect
(
state
.
isLoading
).
toBe
Falsy
(
);
expect
(
state
.
isLoading
).
toBe
(
false
);
});
it
(
'
sets isLoading to false
'
,
()
=>
{
expect
(
state
.
isLoading
).
toBe
Falsy
(
);
expect
(
state
.
isLoading
).
toBe
(
false
);
});
});
...
...
locale/gitlab.pot
View file @
d7e1c5e0
...
...
@@ -5779,6 +5779,9 @@ msgstr ""
msgid "Billings|Reactivate trial"
msgstr ""
msgid "Billings|Seats in use / Seats in subscription"
msgstr ""
msgid "Billings|Shared runners cannot be enabled until a valid credit card is on file."
msgstr ""
...
...
@@ -5806,22 +5809,28 @@ msgstr ""
msgid "Billing|%{user} was successfully approved"
msgstr ""
msgid "Billing|Add seats"
msgstr ""
msgid "Billing|An email address is only visible for users with public emails."
msgstr ""
msgid "Billing|An error occurred while approving %{user}"
msgstr ""
msgid "Billing|An error occurred while getting a billable member details"
msgid "Billing|An error occurred while getting a billable member details."
msgstr ""
msgid "Billing|An error occurred while loading GitLab subscription details."
msgstr ""
msgid "Billing|An error occurred while loading billable members list"
msgid "Billing|An error occurred while loading billable members list
.
"
msgstr ""
msgid "Billing|An error occurred while loading pending members list"
msgstr ""
msgid "Billing|An error occurred while removing a billable member"
msgid "Billing|An error occurred while removing a billable member
.
"
msgstr ""
msgid "Billing|Awaiting member signup"
...
...
@@ -5839,9 +5848,6 @@ msgstr ""
msgid "Billing|Export list"
msgstr ""
msgid "Billing|Group"
msgstr ""
msgid "Billing|Group invite"
msgstr ""
...
...
@@ -5869,9 +5875,6 @@ msgstr ""
msgid "Billing|User was successfully removed"
msgstr ""
msgid "Billing|Users occupying seats in"
msgstr ""
msgid "Billing|View pending approvals"
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