Skip to content

Commit a798862

Browse files
authored
feat(Identity): use account allowances state to allow org creation (#6324)
* feat: add org creation allowance to indentity state * chore: update name of allowances creators * feat: add selectors for orgCreationAllowances * feat: replace use of account type with allowances * chore: update name of allowances reducer * chore: clean up * feat: create separate selector for all allowances * test: update test to use getAllowancesOrgsCreate endpoint * test: move cy interception to test file * chore: send identity state only to honeybadger * fix: prettier * fix: prettier * chore: add createDeleteOrg ff to useEffect hook * chore: clean up
1 parent 461679c commit a798862

File tree

12 files changed

+207
-20
lines changed

12 files changed

+207
-20
lines changed

cypress/e2e/cloud/globalHeaderOrgMenuItems.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ const createOrgsFeatureFlags = {
44
createDeleteOrgs: true,
55
}
66

7+
const getOrgCreationAllowance = (fixtureName: string) => {
8+
cy.fixture(fixtureName).then(orgCreationAllowance => {
9+
cy.intercept(
10+
'GET',
11+
'api/v2/quartz/allowances/orgs/create',
12+
orgCreationAllowance
13+
).as('getAllowancesOrgsCreate')
14+
})
15+
}
16+
717
describe('FREE: global header menu items test', () => {
818
let idpeOrgID: string
919

@@ -29,6 +39,8 @@ describe('FREE: global header menu items test', () => {
2939
Cypress.Cookies.preserveOnce('sid')
3040

3141
makeQuartzUseIDPEOrgID(idpeOrgID)
42+
getOrgCreationAllowance('createOrgAllowance')
43+
3244
cy.visit('/')
3345
})
3446

@@ -66,6 +78,8 @@ describe('PAYG: global header menu items test', () => {
6678

6779
beforeEach(() => {
6880
makeQuartzUseIDPEOrgID(idpeOrgID, 'pay_as_you_go')
81+
getOrgCreationAllowance('createOrgAllowancePAYG')
82+
6983
cy.visit('/')
7084
})
7185

@@ -103,6 +117,8 @@ describe('Contract: global header menu items test', () => {
103117

104118
beforeEach(() => {
105119
makeQuartzUseIDPEOrgID(idpeOrgID, 'contract')
120+
getOrgCreationAllowance('createOrgAllowanceContract')
121+
106122
cy.visit('/')
107123
})
108124

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"allowed": false,
3+
"availableUpgrade": "pay_as_you_go"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"allowed": true,
3+
"availableUpgrade": "none"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"allowed": true,
3+
"availableUpgrade": "contract"
4+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {OrgCreationAllowance} from 'src/identity/apis/org'
2+
import {RemoteDataState} from 'src/types'
3+
4+
export const SET_ORG_CREATION_ALLOWANCE = 'SET_ORG_CREATION_ALLOWANCE'
5+
export const SET_ORG_CREATION_ALLOWANCE_STATUS =
6+
'SET_ORG_CREATION_ALLOWANCE_STATUS'
7+
8+
export type OrgCreationAllowanceActions =
9+
| ReturnType<typeof setOrgCreationAllowance>
10+
| ReturnType<typeof setOrgCreationAllowanceStatus>
11+
12+
export const setOrgCreationAllowance = (allowances: OrgCreationAllowance) =>
13+
({
14+
type: SET_ORG_CREATION_ALLOWANCE,
15+
allowances: allowances,
16+
} as const)
17+
18+
export const setOrgCreationAllowanceStatus = (loadingStatus: RemoteDataState) =>
19+
({
20+
type: SET_ORG_CREATION_ALLOWANCE_STATUS,
21+
loadingStatus: loadingStatus,
22+
} as const)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Libraries
2+
import {Dispatch} from 'react'
3+
4+
// Actions
5+
import {
6+
OrgCreationAllowanceActions,
7+
setOrgCreationAllowance,
8+
setOrgCreationAllowanceStatus,
9+
} from 'src/identity/allowances/actions/creators'
10+
11+
// API
12+
import {fetchOrgCreationAllowance} from 'src/identity/apis/org'
13+
14+
// Types
15+
import {GetState, RemoteDataState} from 'src/types'
16+
17+
// Utils
18+
import {reportErrorThroughHoneyBadger} from 'src/shared/utils/errors'
19+
20+
export const getOrgCreationAllowancesThunk =
21+
() =>
22+
async (
23+
dispatch: Dispatch<OrgCreationAllowanceActions>,
24+
getState: GetState
25+
) => {
26+
try {
27+
dispatch(setOrgCreationAllowanceStatus(RemoteDataState.Loading))
28+
29+
const allowances = await fetchOrgCreationAllowance()
30+
31+
dispatch(setOrgCreationAllowance(allowances))
32+
33+
dispatch(setOrgCreationAllowanceStatus(RemoteDataState.Done))
34+
} catch (error) {
35+
reportErrorThroughHoneyBadger(error, {
36+
name: 'Failed to fetch org creation allowance',
37+
context: {state: getState().identity},
38+
})
39+
}
40+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Libraries
2+
import produce from 'immer'
3+
4+
// Actions
5+
import {
6+
OrgCreationAllowanceActions,
7+
SET_ORG_CREATION_ALLOWANCE,
8+
SET_ORG_CREATION_ALLOWANCE_STATUS,
9+
} from 'src/identity/allowances/actions/creators'
10+
11+
// Types
12+
import {OrgCreationAllowance} from 'src/identity/apis/org'
13+
import {RemoteDataState} from 'src/types'
14+
15+
interface AllowanceState {
16+
orgCreation: OrgCreationAllowanceState
17+
}
18+
export interface OrgCreationAllowanceState extends OrgCreationAllowance {
19+
loadingStatus: RemoteDataState
20+
}
21+
22+
export const initialState: AllowanceState = {
23+
orgCreation: {
24+
allowed: false,
25+
availableUpgrade: 'none',
26+
loadingStatus: RemoteDataState.NotStarted,
27+
},
28+
}
29+
30+
export default (state = initialState, action: OrgCreationAllowanceActions) =>
31+
produce(state, draftState => {
32+
switch (action.type) {
33+
case SET_ORG_CREATION_ALLOWANCE: {
34+
const {allowed, availableUpgrade} = action.allowances
35+
36+
draftState.orgCreation.allowed = allowed
37+
draftState.orgCreation.availableUpgrade = availableUpgrade
38+
return
39+
}
40+
41+
case SET_ORG_CREATION_ALLOWANCE_STATUS: {
42+
draftState.orgCreation.loadingStatus = action.loadingStatus
43+
return
44+
}
45+
}
46+
})

src/identity/apis/auth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import {CLOUD} from 'src/shared/constants'
77

88
// Types
99
import {RemoteDataState} from 'src/types'
10+
import {OrgCreationAllowanceState} from 'src/identity/allowances/reducers'
1011
import {Error as IdpeError, UserResponse as UserResponseIdpe} from 'src/client'
1112
import {ServerError, UnauthorizedError} from 'src/types/error'
1213
import {CurrentAccount} from 'src/identity/apis/account'
1314
import {CurrentOrg, QuartzOrganizations} from 'src/identity/apis/org'
1415

1516
export interface IdentityState {
17+
allowances: {
18+
orgCreation: OrgCreationAllowanceState
19+
}
1620
currentIdentity: CurrentIdentity
1721
quartzOrganizations: QuartzOrganizations
1822
}
19-
2023
export interface IdentityLoadingStatus {
2124
identityStatus: RemoteDataState
2225
billingStatus: RemoteDataState

src/identity/apis/org.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export interface CurrentOrg {
3535
regionName?: string
3636
}
3737

38+
export interface OrgCreationAllowance {
39+
allowed: boolean
40+
availableUpgrade: 'contract' | 'none' | 'pay_as_you_go'
41+
}
42+
3843
export interface QuartzOrganization {
3944
id: string
4045
name: string
@@ -132,17 +137,18 @@ export const fetchDefaultAccountDefaultOrg = async (): Promise<
132137
}
133138

134139
// fetch data regarding whether the user can create new orgs, and associated upgrade options.
135-
export const fetchOrgCreationAllowance = async () => {
136-
const response = await getAllowancesOrgsCreate({})
140+
export const fetchOrgCreationAllowance =
141+
async (): Promise<OrgCreationAllowance> => {
142+
const response = await getAllowancesOrgsCreate({})
137143

138-
if (response.status !== 200) {
139-
throw new GenericError(
140-
'Failed to determine whether this user can create a new organization.'
141-
)
142-
}
144+
if (response.status !== 200) {
145+
throw new GenericError(
146+
'Failed to determine whether this user can create a new organization.'
147+
)
148+
}
143149

144-
return response.data
145-
}
150+
return response.data
151+
}
146152

147153
// fetch the list of organizations associated with a given account ID
148154
export const fetchOrgsByAccountID = async (

src/identity/components/GlobalHeader/OrgDropdown.tsx

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Libraries
2-
import React, {FC} from 'react'
3-
import {IconFont} from '@influxdata/clockface'
2+
import React, {FC, useEffect} from 'react'
3+
import {useDispatch, useSelector} from 'react-redux'
44

55
// Components
6+
import {IconFont, RemoteDataState} from '@influxdata/clockface'
67
import {
78
GlobalHeaderDropdown,
89
MainMenuItem,
@@ -24,12 +25,18 @@ import {
2425
} from 'src/identity/events/multiOrgEvents'
2526
import {event} from 'src/cloud/utils/reporting'
2627

28+
// Thunks
29+
import {getOrgCreationAllowancesThunk} from 'src/identity/allowances/actions/thunks'
30+
2731
// Utils
2832
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
2933

3034
// Selectors
31-
import {useSelector} from 'react-redux'
32-
import {selectCurrentAccountType} from 'src/identity/selectors'
35+
import {
36+
selectOrgCreationAllowance,
37+
selectOrgCreationAllowanceStatus,
38+
selectOrgCreationAvailableUpgrade,
39+
} from 'src/identity/selectors'
3340
import {CreateOrganizationMenuItem} from 'src/identity/components/GlobalHeader/GlobalHeaderDropdown/CreateOrganization/MenuItem'
3441

3542
type OrgSummaryItem = OrganizationSummaries[number]
@@ -43,7 +50,23 @@ const menuStyle = {width: '250px', padding: '16px'}
4350
const orgDropdownStyle = {width: 'auto'}
4451

4552
export const OrgDropdown: FC<Props> = ({activeOrg, orgsList}) => {
46-
const accountType = useSelector(selectCurrentAccountType)
53+
const orgCreationAllowed = useSelector(selectOrgCreationAllowance)
54+
const availableUpgrade = useSelector(selectOrgCreationAvailableUpgrade)
55+
const orgCreationAllowanceStatus = useSelector(
56+
selectOrgCreationAllowanceStatus
57+
)
58+
59+
const dispatch = useDispatch()
60+
61+
useEffect(() => {
62+
if (
63+
isFlagEnabled('createDeleteOrgs') &&
64+
orgCreationAllowanceStatus === RemoteDataState.NotStarted
65+
) {
66+
dispatch(getOrgCreationAllowancesThunk())
67+
}
68+
}, [dispatch, orgCreationAllowanceStatus])
69+
4770
const switchOrg = (org: TypeAheadMenuItem) => {
4871
event(HeaderNavEvent.OrgSwitch, multiOrgTag, {
4972
oldOrgID: activeOrg.id,
@@ -72,7 +95,11 @@ export const OrgDropdown: FC<Props> = ({activeOrg, orgsList}) => {
7295
},
7396
]
7497

75-
if (isFlagEnabled('createDeleteOrgs') && accountType === 'free') {
98+
if (
99+
isFlagEnabled('createDeleteOrgs') &&
100+
!orgCreationAllowed &&
101+
availableUpgrade !== 'none'
102+
) {
76103
orgMainMenu.push({
77104
name: 'Add More Organizations',
78105
iconFont: IconFont.CrownSolid_New,
@@ -87,10 +114,7 @@ export const OrgDropdown: FC<Props> = ({activeOrg, orgsList}) => {
87114
}
88115

89116
const additionalHeaderItems = []
90-
if (
91-
isFlagEnabled('createDeleteOrgs') &&
92-
(accountType === 'contract' || accountType === 'pay_as_you_go')
93-
) {
117+
if (isFlagEnabled('createDeleteOrgs') && orgCreationAllowed) {
94118
additionalHeaderItems.push(
95119
<CreateOrganizationMenuItem key="CreateOrgMenuItem" />
96120
)

0 commit comments

Comments
 (0)