Skip to content

Commit 47a491e

Browse files
feat: add multiorg, multiaccount GlobalHeader to UI
1 parent ec2eef7 commit 47a491e

File tree

21 files changed

+511
-17
lines changed

21 files changed

+511
-17
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
"dependencies": {
165165
"@codingame/monaco-jsonrpc": "^0.3.1",
166166
"@docsearch/react": "^3.0.0-alpha.37",
167-
"@influxdata/clockface": "^4.7.3",
167+
"@influxdata/clockface": "^4.7.4",
168168
"@influxdata/flux-lsp-browser": "0.8.20",
169169
"@influxdata/giraffe": "^2.30.0",
170170
"@influxdata/influxdb-templates": "0.9.0",

src/App.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
} from 'src/overlays/components/OverlayController'
2727
import PageSpinner from 'src/perf/components/PageSpinner'
2828
import EngagementLink from 'src/cloud/components/onboarding/EngagementLink'
29+
import {GlobalHeaderContainer} from 'src/identity/components/GlobalHeaderContainer'
30+
2931
const SetOrg = lazy(() => import('src/shared/containers/SetOrg'))
3032
const CreateOrgOverlay = lazy(() =>
3133
import('src/organizations/components/CreateOrgOverlay')
@@ -37,6 +39,10 @@ import {AppState} from 'src/types'
3739
// Utils
3840
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
3941
import {CLOUD} from 'src/shared/constants'
42+
import {shouldUseQuartzIdentity} from './identity/utils/shouldUseQuartzIdentity'
43+
44+
// Styles
45+
const fullScreen = {height: '100%', width: '100%'}
4046

4147
const App: FC = () => {
4248
const {theme, presentationMode} = useContext(AppSettingContext)
@@ -87,10 +93,15 @@ const App: FC = () => {
8793
<EngagementLink />
8894
<TreeNav />
8995
<Suspense fallback={<PageSpinner />}>
90-
<Switch>
91-
<Route path="/orgs/new" component={CreateOrgOverlay} />
92-
<Route path="/orgs/:orgID" component={SetOrg} />
93-
</Switch>
96+
<div style={fullScreen}>
97+
{CLOUD && isFlagEnabled('multiOrg') && shouldUseQuartzIdentity() && (
98+
<GlobalHeaderContainer />
99+
)}
100+
<Switch>
101+
<Route path="/orgs/new" component={CreateOrgOverlay} />
102+
<Route path="/orgs/:orgID" component={SetOrg} />
103+
</Switch>
104+
</div>
94105
</Suspense>
95106
</AppWrapper>
96107
)

src/identity/actions/thunks/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ export const getBillingProviderThunk = () => async (
6565
dispatch(setQuartzIdentityStatus(RemoteDataState.Loading))
6666

6767
const currentState = getState()
68-
const accountId = currentState.identity.account.id
68+
const accountId = currentState.identity.currentIdentity.account.id
6969

7070
const accountDetails = await fetchAccountDetails(accountId)
7171

7272
dispatch(setCurrentBillingProvider(accountDetails.billingProvider))
7373
dispatch(setQuartzIdentityStatus(RemoteDataState.Done))
7474
const updatedState = getState()
75-
const legacyMe = convertIdentityToMe(updatedState.identity)
75+
const legacyMe = convertIdentityToMe(updatedState.identity.currentIdentity)
7676
dispatch(setQuartzMe(legacyMe, RemoteDataState.Done))
7777
dispatch(setQuartzMeStatus(RemoteDataState.Done))
7878
} catch (error) {
@@ -90,15 +90,15 @@ export const getCurrentOrgDetailsThunk = () => async (
9090
dispatch(setQuartzIdentityStatus(RemoteDataState.Loading))
9191

9292
const state = getState()
93-
const orgId = state.identity.org.id
93+
const orgId = state.identity.currentIdentity.org.id
9494

9595
const orgDetails = await fetchOrgDetails(orgId)
9696

9797
dispatch(setCurrentOrgDetails(orgDetails))
9898
dispatch(setQuartzIdentityStatus(RemoteDataState.Done))
9999

100100
const updatedState = getState()
101-
const legacyMe = convertIdentityToMe(updatedState.identity)
101+
const legacyMe = convertIdentityToMe(updatedState.identity.currentIdentity)
102102
dispatch(setQuartzMe(legacyMe, RemoteDataState.Done))
103103
dispatch(setQuartzMeStatus(RemoteDataState.Done))
104104
} catch (error) {

src/identity/apis/auth.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import {
44
getIdentity,
55
getMe as getMeQuartz,
66
getOrg,
7+
getOrgs,
8+
putOrgsDefault,
79
Account,
810
Identity,
911
IdentityAccount,
1012
IdentityUser,
1113
Me as MeQuartz,
1214
Organization,
15+
OrganizationSummaries,
1316
} from 'src/client/unityRoutes'
1417

1518
import {
@@ -45,6 +48,16 @@ export interface CurrentOrg {
4548
regionName?: string
4649
}
4750

51+
export interface IdentityState {
52+
currentIdentity: CurrentIdentity
53+
quartzOrganizations: QuartzOrganizations
54+
}
55+
56+
export type QuartzOrganizations = {
57+
orgs: OrganizationSummaries
58+
status?: RemoteDataState
59+
}
60+
4861
export interface CurrentIdentity {
4962
user: IdentityUser
5063
account: CurrentAccount
@@ -186,3 +199,34 @@ export const fetchOrgDetails = async (orgId: string): Promise<Organization> => {
186199
const orgDetails = response.data
187200
return orgDetails
188201
}
202+
203+
// fetch list of user's current organizations
204+
export const fetchQuartzOrgs = async (): Promise<OrganizationSummaries> => {
205+
const response = await getOrgs({})
206+
207+
if (response.status === 401) {
208+
throw new UnauthorizedError(response.data.message)
209+
}
210+
211+
if (response.status === 500) {
212+
throw new ServerError(response.data.message)
213+
}
214+
215+
return response.data
216+
}
217+
218+
// change default organization for a given account
219+
export const putDefaultQuartzOrg = async (orgId: string) => {
220+
const response = await putOrgsDefault({
221+
data: {
222+
id: orgId,
223+
},
224+
})
225+
226+
// Only status codes thrown at moment are 204 and 5xx.
227+
if (response.status !== 204) {
228+
throw new ServerError(response.data.message)
229+
}
230+
231+
return response.data
232+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, {FC} from 'react'
2+
import {IconFont} from '@influxdata/clockface'
3+
import {MenuDropdown, SubMenuItem} from '@influxdata/clockface'
4+
import {OrganizationSummaries, UserAccount} from 'src/client/unityRoutes'
5+
6+
type OrgSummaryItem = OrganizationSummaries[number]
7+
8+
interface Props {
9+
activeOrg: OrgSummaryItem
10+
activeAccount: UserAccount
11+
accountsList: UserAccount[]
12+
}
13+
14+
const style = {width: 'auto'}
15+
const menuStyle = {width: '250px'}
16+
17+
export const AccountDropdown: FC<Props> = ({
18+
activeOrg,
19+
activeAccount,
20+
accountsList,
21+
}) => {
22+
const selectedAccount = {
23+
id: activeAccount.id.toString(),
24+
name: activeAccount.name,
25+
}
26+
27+
const accountMainMenu = [
28+
{
29+
name: 'Settings',
30+
iconFont: IconFont.CogOutline,
31+
href: `/orgs/${activeOrg.id}/accounts/settings`,
32+
},
33+
{
34+
name: 'Billing',
35+
iconFont: IconFont.Bill,
36+
href: `/orgs/${activeOrg.id}/billing`,
37+
},
38+
]
39+
40+
// Quartz handles switching accounts by having the user hit this URL.
41+
const switchAccount = (account: SubMenuItem) => {
42+
window.location.href = `orgs/${activeOrg.id}/accounts/${account.id}`
43+
}
44+
45+
return (
46+
<MenuDropdown
47+
selectedOption={selectedAccount}
48+
options={accountMainMenu}
49+
subMenuOptions={accountsList}
50+
menuHeaderIcon={IconFont.Switch_New}
51+
menuHeaderText="Switch Account"
52+
style={style}
53+
menuStyle={menuStyle}
54+
onSelectOption={switchAccount}
55+
/>
56+
)
57+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Types
2+
import {UserAccount} from 'src/client/unityRoutes'
3+
import {OrganizationSummaries} from 'src/client/unityRoutes'
4+
5+
export const emptyAccount: UserAccount = {
6+
id: 0,
7+
name: '',
8+
isActive: false,
9+
isDefault: false,
10+
}
11+
12+
export const emptyOrg: OrganizationSummaries[number] = {
13+
id: '',
14+
name: '',
15+
isActive: false,
16+
isDefault: false,
17+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Library imports
2+
import React, {useContext, useEffect, useState, FC} from 'react'
3+
import {useSelector, useDispatch} from 'react-redux'
4+
import {
5+
ComponentSize,
6+
FlexBox,
7+
IconFont,
8+
Icon,
9+
JustifyContent,
10+
} from '@influxdata/clockface'
11+
12+
// Selectors and Context
13+
import {selectQuartzIdentity} from 'src/identity/selectors'
14+
import {UserAccountContext} from 'src/accounts/context/userAccount'
15+
16+
// Components
17+
import {OrgDropdown} from 'src/identity/components/GlobalHeader/OrgDropdown'
18+
import {AccountDropdown} from 'src/identity/components/GlobalHeader/AccountDropdown'
19+
20+
// Thunks
21+
import {getQuartzOrganizationsThunk} from 'src/identity/quartzOrganizations/actions/thunks'
22+
23+
// Styles
24+
import 'src/identity/components/GlobalHeader/GlobalHeaderStyle.scss'
25+
26+
import {
27+
emptyAccount,
28+
emptyOrg,
29+
} from 'src/identity/components/GlobalHeader/DefaultEntities'
30+
import {alphaSortSelectedFirst} from 'src/identity/utils/alphaSortSelectedFirst'
31+
32+
export const GlobalHeader: FC = () => {
33+
const dispatch = useDispatch()
34+
const identity = useSelector(selectQuartzIdentity)
35+
const orgsList = identity.quartzOrganizations.orgs
36+
const {userAccounts} = useContext(UserAccountContext)
37+
38+
const accountsList = userAccounts ? userAccounts : [emptyAccount] // eslint-disable-line react-hooks/exhaustive-deps
39+
40+
const [sortedOrgs, setSortedOrgs] = useState([emptyOrg])
41+
const [sortedAccounts, setSortedAccts] = useState([emptyAccount])
42+
43+
const [activeOrg, setActiveOrg] = useState(emptyOrg)
44+
const [activeAccount, setActiveAccount] = useState(emptyAccount)
45+
46+
useEffect(() => {
47+
dispatch(getQuartzOrganizationsThunk())
48+
}, [dispatch])
49+
50+
useEffect(() => {
51+
if (accountsList[0].id !== 0) {
52+
const currentActiveAccount = accountsList?.find(
53+
account => account?.isActive === true
54+
)
55+
56+
setActiveAccount(currentActiveAccount)
57+
58+
setSortedAccts(alphaSortSelectedFirst(accountsList, currentActiveAccount))
59+
}
60+
}, [accountsList])
61+
62+
useEffect(() => {
63+
if (orgsList[0].id !== '') {
64+
const currentActiveOrg = orgsList?.find(org => org.isActive === true)
65+
66+
setActiveOrg(currentActiveOrg)
67+
68+
setSortedOrgs(alphaSortSelectedFirst(orgsList, currentActiveOrg))
69+
}
70+
}, [orgsList])
71+
72+
return (
73+
<FlexBox
74+
margin={ComponentSize.Large}
75+
justifyContent={JustifyContent.SpaceBetween}
76+
className="multiaccountorg--header"
77+
>
78+
<FlexBox margin={ComponentSize.Medium}>
79+
{activeOrg && activeAccount && (
80+
<>
81+
<AccountDropdown
82+
activeOrg={activeOrg}
83+
activeAccount={activeAccount}
84+
accountsList={sortedAccounts}
85+
/>
86+
<Icon glyph={IconFont.CaretRight} />
87+
<OrgDropdown activeOrg={activeOrg} orgsList={sortedOrgs} />
88+
</>
89+
)}
90+
</FlexBox>
91+
</FlexBox>
92+
)
93+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.multiaccountorg--header {
2+
height: 60px;
3+
padding-top: 12px;
4+
padding-right: 32px;
5+
padding-bottom: 12px;
6+
padding-left: 22px;
7+
margin-top: 0px;
8+
margin-right: 0px;
9+
margin-bottom: 0px;
10+
margin-left: 0px;
11+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, {FC} from 'react'
2+
import {MenuDropdown, SubMenuItem} from '@influxdata/clockface'
3+
import {IconFont} from '@influxdata/clockface'
4+
import {OrganizationSummaries} from 'src/client/unityRoutes'
5+
6+
const switchOrg = (org: SubMenuItem) => {
7+
window.location.href = `/orgs/${org.id}`
8+
}
9+
10+
type OrgSummaryItem = OrganizationSummaries[number]
11+
12+
interface Props {
13+
activeOrg: OrgSummaryItem
14+
orgsList: OrganizationSummaries
15+
}
16+
17+
const style = {width: 'auto'}
18+
const menuStyle = {width: '250px'}
19+
20+
export const OrgDropdown: FC<Props> = ({activeOrg, orgsList}) => {
21+
const orgMainMenu = [
22+
{
23+
name: 'Settings',
24+
iconFont: IconFont.CogOutline,
25+
href: `/orgs/${activeOrg.id}/about`,
26+
},
27+
{
28+
name: 'Members',
29+
iconFont: IconFont.CogOutline,
30+
href: `/orgs/${activeOrg.id}/users`,
31+
},
32+
{
33+
name: 'Usage',
34+
iconFont: IconFont.CogOutline,
35+
href: `/orgs/${activeOrg.id}/usage`,
36+
},
37+
]
38+
39+
return (
40+
<MenuDropdown
41+
selectedOption={activeOrg}
42+
options={orgMainMenu}
43+
subMenuOptions={orgsList}
44+
menuHeaderIcon={IconFont.Switch_New}
45+
menuHeaderText="Switch Organization"
46+
searchText="Search Organizations"
47+
style={style}
48+
menuStyle={menuStyle}
49+
onSelectOption={switchOrg}
50+
/>
51+
)
52+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React, {FC} from 'react'
2+
import {UserAccountProvider} from 'src/accounts/context/userAccount'
3+
import {GlobalHeader} from 'src/identity/components/GlobalHeader/GlobalHeader'
4+
5+
export const GlobalHeaderContainer: FC = () => {
6+
return (
7+
<UserAccountProvider>
8+
<GlobalHeader />
9+
</UserAccountProvider>
10+
)
11+
}

0 commit comments

Comments
 (0)