Skip to content
This repository was archived by the owner on Jul 19, 2023. It is now read-only.

Commit 02ee7dc

Browse files
authored
feat: support multitenancy in the /ui (#676)
1 parent bcfc1ea commit 02ee7dc

File tree

13 files changed

+599
-22
lines changed

13 files changed

+599
-22
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@
5050
},
5151
"dependencies": {
5252
"@fortawesome/react-fontawesome": "~0.1.11",
53-
"@mui/base": "^5.0.0-alpha.98",
54-
"@mui/material": "^5.10.11",
53+
"@mui/base": "5.0.0-alpha.98",
54+
"@mui/material": "5.10.11",
55+
"@mui/types": "7.2.0",
5556
"@react-hook/resize-observer": "^1.2.4",
5657
"@react-hook/window-size": "^3.0.7",
5758
"@reduxjs/toolkit": "^1.6.2",
@@ -63,6 +64,7 @@
6364
"react-datepicker": "^4.7.0",
6465
"react-debounce-input": "^3.2.5",
6566
"react-dom": "^18.2.0",
67+
"react-flatten-children": "^1.1.2",
6668
"react-flot": "^1.3.0",
6769
"react-helmet": "^6.1.0",
6870
"react-notifications-component": "~3.1.0",

public/app/app.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ComparisonView } from './pages/ComparisonView';
1515
import { DiffView } from './pages/DiffView';
1616
import { LoadAppNames } from './components/LoadAppNames';
1717
import { Sidebar } from './components/Sidebar';
18+
import { TenantWall } from './components/TenantWall';
1819
import { baseurl } from './overrides/util/baseurl';
1920

2021
const container = document.getElementById('reactRoot') as HTMLElement;
@@ -28,19 +29,21 @@ function App() {
2829
<div className="app">
2930
<Sidebar />
3031
<div className="pyroscope-app">
31-
<LoadAppNames>
32-
<Switch>
33-
<Route exact path={ROUTES.SINGLE_VIEW}>
34-
<SingleView />
35-
</Route>
36-
<Route path={ROUTES.COMPARISON_VIEW}>
37-
<ComparisonView />
38-
</Route>
39-
<Route path={ROUTES.COMPARISON_DIFF_VIEW}>
40-
<DiffView />
41-
</Route>
42-
</Switch>
43-
</LoadAppNames>
32+
<TenantWall>
33+
<LoadAppNames>
34+
<Switch>
35+
<Route exact path={ROUTES.SINGLE_VIEW}>
36+
<SingleView />
37+
</Route>
38+
<Route path={ROUTES.COMPARISON_VIEW}>
39+
<ComparisonView />
40+
</Route>
41+
<Route path={ROUTES.COMPARISON_DIFF_VIEW}>
42+
<DiffView />
43+
</Route>
44+
</Switch>
45+
</LoadAppNames>
46+
</TenantWall>
4447
</div>
4548
</div>
4649
</Router>

public/app/components/Sidebar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons';
33
import { faChartBar } from '@fortawesome/free-solid-svg-icons/faChartBar';
44
import { faColumns } from '@fortawesome/free-solid-svg-icons/faColumns';
55
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
6+
import { faUser } from '@fortawesome/free-solid-svg-icons/faUser';
67

78
import {
89
MenuItem,
@@ -26,6 +27,7 @@ import { useWindowWidth } from '@react-hook/window-size';
2627
import { isRouteActive, ROUTES } from '../pages/routes';
2728
import Logo from '../static/logo.svg';
2829
import styles from './Sidebar.module.css';
30+
import { SidebarTenant } from './SidebarTenant';
2931

3032
export function Sidebar() {
3133
const collapsed = useAppSelector(selectSidebarCollapsed);
@@ -87,6 +89,7 @@ export function Sidebar() {
8789
exact
8890
/>
8991
</MenuItem>
92+
<SidebarTenant />
9093
</Menu>
9194
</SidebarContent>
9295
<SidebarFooter>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.menuDivider {
2+
border-top: 1px solid rgba(255, 255, 255, 0.2);
3+
}
4+
5+
.accountDropdown {
6+
border: none;
7+
padding: 0;
8+
width: 100%;
9+
text-align: left;
10+
}
11+
12+
.dropdown {
13+
border: 1px solid rgba(255, 255, 255, 0.2) !important;
14+
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.4) !important;
15+
}
16+
17+
.orgID {
18+
margin-right: 15px;
19+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { faCog } from '@fortawesome/free-solid-svg-icons/faCog';
2+
import { faUser } from '@fortawesome/free-solid-svg-icons/faUser';
3+
import { MenuButton, MenuProps, MenuHeader } from '@szhsin/react-menu';
4+
import Dropdown, { MenuItem as DropdownMenuItem } from '@webapp/ui/Dropdown';
5+
import flattenChildren from 'react-flatten-children';
6+
import Icon from '@webapp/ui/Icon';
7+
import { MenuItem } from '@webapp/ui/Sidebar';
8+
import {
9+
selectIsMultiTenant,
10+
selectTenantID,
11+
actions,
12+
} from '@phlare/redux/reducers/tenant';
13+
import { useAppSelector, useAppDispatch } from '../redux/hooks';
14+
import styles from './SidebarTenant.module.css';
15+
import cx from 'classnames';
16+
17+
export interface DropdownProps {
18+
children: JSX.Element[] | JSX.Element;
19+
offsetX: MenuProps['offsetX'];
20+
offsetY: MenuProps['offsetY'];
21+
direction: MenuProps['direction'];
22+
label: string;
23+
className: string;
24+
menuButton: JSX.Element;
25+
}
26+
27+
function FlatDropdown({
28+
children,
29+
offsetX,
30+
offsetY,
31+
direction,
32+
label,
33+
className,
34+
menuButton,
35+
}: DropdownProps) {
36+
return (
37+
<Dropdown
38+
offsetX={offsetX}
39+
offsetY={offsetY}
40+
direction={direction}
41+
label={label}
42+
className={className}
43+
menuButton={menuButton}
44+
>
45+
{flattenChildren(children) as unknown as JSX.Element}
46+
</Dropdown>
47+
);
48+
}
49+
50+
export function SidebarTenant() {
51+
const isMultiTenant = useAppSelector(selectIsMultiTenant);
52+
const orgID = useAppSelector(selectTenantID);
53+
const dispatch = useAppDispatch();
54+
55+
if (!isMultiTenant) {
56+
return <></>;
57+
}
58+
59+
return (
60+
<>
61+
<FlatDropdown
62+
offsetX={10}
63+
offsetY={5}
64+
direction="top"
65+
label=""
66+
className={styles.dropdown}
67+
menuButton={
68+
<MenuButton className={styles.accountDropdown}>
69+
<MenuItem icon={<Icon icon={faUser} />}>Tenant</MenuItem>
70+
</MenuButton>
71+
}
72+
>
73+
<MenuHeader>Current Tenant</MenuHeader>
74+
<DropdownMenuItem
75+
className={styles.menuItemDisabled}
76+
onClick={() => {
77+
void dispatch(actions.setWantsToChange());
78+
}}
79+
>
80+
<div className={styles.menuItemWithButton}>
81+
<span className={cx(styles.menuItemWithButtonTitle, styles.orgID)}>
82+
Tenant ID: {orgID}
83+
</span>
84+
<Icon icon={faCog} />
85+
</div>
86+
</DropdownMenuItem>
87+
</FlatDropdown>
88+
</>
89+
);
90+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { useAppDispatch, useAppSelector } from '../redux/hooks';
3+
import TextField from '@webapp/ui/Form/TextField';
4+
import {
5+
Dialog,
6+
DialogBody,
7+
DialogFooter,
8+
DialogHeader,
9+
} from '@webapp/ui/Dialog';
10+
import Button from '@webapp/ui/Button';
11+
import {
12+
checkTenancyIsRequired,
13+
selectTenancy,
14+
actions,
15+
selectTenantID,
16+
} from '@phlare/redux/reducers/tenant';
17+
18+
export function TenantWall({ children }: { children: React.ReactNode }) {
19+
const dispatch = useAppDispatch();
20+
const tenancy = useAppSelector(selectTenancy);
21+
const currentTenant = useAppSelector(selectTenantID);
22+
23+
useEffect(() => {
24+
void dispatch(checkTenancyIsRequired());
25+
}, [dispatch]);
26+
27+
// Don't rerender all the children when this component changes
28+
// For example, when user wants to change the tenant ID
29+
const memoedChildren = React.useMemo(() => children, []);
30+
31+
switch (tenancy) {
32+
case 'unknown':
33+
case 'loading': {
34+
return <></>;
35+
}
36+
case 'wants_to_change': {
37+
return (
38+
<>
39+
<SelectTenantIDDialog
40+
currentTenantID={currentTenant}
41+
onSaved={(tenantID) => {
42+
void dispatch(actions.setTenantID(tenantID));
43+
}}
44+
/>
45+
{memoedChildren}
46+
</>
47+
);
48+
}
49+
case 'needs_tenant_id': {
50+
return (
51+
<SelectTenantIDDialog
52+
currentTenantID={currentTenant}
53+
onSaved={(tenantID) => {
54+
void dispatch(actions.setTenantID(tenantID));
55+
}}
56+
/>
57+
);
58+
}
59+
case 'multi_tenant':
60+
case 'single_tenant': {
61+
return <>{memoedChildren}</>;
62+
}
63+
}
64+
}
65+
66+
function SelectTenantIDDialog({
67+
currentTenantID,
68+
onSaved,
69+
}: {
70+
currentTenantID?: string;
71+
onSaved: (tenantID: string) => void;
72+
}) {
73+
const [isDialogOpen] = useState(true);
74+
const handleFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
75+
e.preventDefault();
76+
77+
const target = e.target as typeof e.target & {
78+
tenantID: { value: string };
79+
};
80+
81+
onSaved(target.tenantID.value);
82+
};
83+
84+
return (
85+
<>
86+
<Dialog open={isDialogOpen} aria-labelledby="dialog-header">
87+
<>
88+
<DialogHeader>
89+
<h3 id="dialog-header">Enter a Tenant ID</h3>
90+
</DialogHeader>
91+
<form
92+
onSubmit={(e) => {
93+
void handleFormSubmit(e);
94+
}}
95+
>
96+
<DialogBody>
97+
<>
98+
<p>
99+
Your instance has been detected as a multitenant one. Please
100+
enter a Tenant ID (You can change it at any time via the
101+
sidebar).
102+
</p>
103+
<p>
104+
Notice that if you migrated from a non-multitenant version,
105+
data can be found under Tenant ID "anonymous".
106+
</p>
107+
108+
<TextField
109+
defaultValue={currentTenantID}
110+
label="Tenant ID"
111+
required
112+
id="tenantID"
113+
type="text"
114+
autoFocus
115+
/>
116+
</>
117+
</DialogBody>
118+
<DialogFooter>
119+
<Button type="submit" kind="secondary">
120+
Submit
121+
</Button>
122+
</DialogFooter>
123+
</form>
124+
</>
125+
</Dialog>
126+
</>
127+
);
128+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Result } from '@webapp/util/fp';
2+
import {
3+
type RequestError,
4+
request as ogRequest,
5+
} from '../../../../node_modules/pyroscope-oss/webapp/javascript/services/base';
6+
import { tenantIDFromStorage } from '@phlare/services/tenant';
7+
8+
export * from '../../../../node_modules/pyroscope-oss/webapp/javascript/services/base';
9+
10+
/**
11+
* request wraps around the original request
12+
* while sending the OrgID if available
13+
*/
14+
export async function request(
15+
request: RequestInfo,
16+
config?: RequestInit
17+
): Promise<Result<unknown, RequestError>> {
18+
let headers = config?.headers;
19+
20+
// Reuse headers if they were passed
21+
if (!config?.headers?.hasOwnProperty('X-Scope-OrgID')) {
22+
headers = {
23+
...config?.headers,
24+
'X-Scope-OrgID': tenantIDFromStorage(),
25+
};
26+
}
27+
28+
return ogRequest(request, {
29+
...config,
30+
headers,
31+
});
32+
}

0 commit comments

Comments
 (0)