Skip to content

Commit 890c761

Browse files
committed
Add stories for multi-org sidebar
1 parent 3524cf7 commit 890c761

File tree

3 files changed

+504
-343
lines changed

3 files changed

+504
-343
lines changed
Lines changed: 28 additions & 343 deletions
Original file line numberDiff line numberDiff line change
@@ -1,356 +1,41 @@
1-
import { cx } from "@emotion/css";
2-
import type { Interpolation, Theme } from "@emotion/react";
3-
import AddIcon from "@mui/icons-material/Add";
4-
import SettingsIcon from "@mui/icons-material/Settings";
5-
import type { FC, ReactNode } from "react";
1+
import type { FC } from "react";
62
import { useQuery } from "react-query";
7-
import { Link, NavLink, useLocation, useParams } from "react-router-dom";
3+
import { useParams } from "react-router-dom";
84
import { organizationPermissions } from "api/queries/organizations";
9-
import type { Organization } from "api/typesGenerated";
10-
import { Loader } from "components/Loader/Loader";
11-
import { Sidebar as BaseSidebar } from "components/Sidebar/Sidebar";
12-
import { Stack } from "components/Stack/Stack";
13-
import { UserAvatar } from "components/UserAvatar/UserAvatar";
145
import { useAuthenticated } from "contexts/auth/RequireAuth";
15-
import { type ClassName, useClassName } from "hooks/useClassName";
16-
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
17-
import { AUDIT_LINK, USERS_LINK, withFilter } from "modules/navigation";
186
import { useOrganizationSettings } from "./ManagementSettingsLayout";
19-
7+
import { SidebarView } from "./SidebarView";
8+
9+
/**
10+
* A combined deployment settings and organization menu.
11+
*
12+
* This should only be used with multi-org support. If multi-org support is
13+
* disabled or not licensed, this is the wrong sidebar to use. See
14+
* DeploySettingsPage/Sidebar instead.
15+
*/
2016
export const Sidebar: FC = () => {
2117
const { permissions } = useAuthenticated();
2218
const { organizations } = useOrganizationSettings();
23-
const { organization } = useParams() as { organization?: string };
24-
const { multiple_organizations: organizationsEnabled } =
25-
useFeatureVisibility();
26-
27-
let organizationName = organization;
28-
if (location.pathname === "/organizations" && organizations) {
29-
organizationName = getOrganizationNameByDefault(organizations);
30-
}
31-
32-
// TODO: Do something nice to scroll to the active org.
33-
34-
return (
35-
<BaseSidebar>
36-
{organizationsEnabled && (
37-
<header css={styles.sidebarHeader}>Deployment</header>
38-
)}
39-
<DeploymentSettingsNavigation
40-
organizationsEnabled={organizationsEnabled}
41-
/>
42-
{organizationsEnabled &&
43-
(organizations ? (
44-
<>
45-
<header css={styles.sidebarHeader}>Organizations</header>
46-
{permissions.createOrganization && (
47-
<SidebarNavItem
48-
active="auto"
49-
href="/organizations/new"
50-
icon={<AddIcon />}
51-
>
52-
New organization
53-
</SidebarNavItem>
54-
)}
55-
{organizations.map((org) => (
56-
<OrganizationSettingsNavigation
57-
key={org.id}
58-
organization={org}
59-
active={org.name === organizationName}
60-
/>
61-
))}
62-
</>
63-
) : (
64-
<Loader />
65-
))}
66-
</BaseSidebar>
19+
const { organization: organizationName } = useParams() as {
20+
organization?: string;
21+
};
22+
23+
// If there is no organization name, the settings page will load, and it will
24+
// redirect to the default organization, so eventually there will always be an
25+
// organization name.
26+
const activeOrganization = organizations?.find(
27+
(o) => o.name === organizationName,
6728
);
68-
};
69-
70-
interface DeploymentSettingsNavigationProps {
71-
organizationsEnabled?: boolean;
72-
}
73-
74-
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
75-
organizationsEnabled,
76-
}) => {
77-
const location = useLocation();
78-
const active = location.pathname.startsWith("/deployment");
79-
const { permissions } = useAuthenticated();
80-
81-
return (
82-
<div css={{ paddingBottom: 12 }}>
83-
<SidebarNavItem
84-
active={active}
85-
href={
86-
permissions.viewDeploymentValues
87-
? "/deployment/general"
88-
: "/deployment/workspace-proxies"
89-
}
90-
// 24px matches the width of the organization icons, and the component is smart enough
91-
// to keep the icon itself square. It looks too big if it's 24x24.
92-
icon={<SettingsIcon css={{ width: 24, height: 20 }} />}
93-
>
94-
Deployment
95-
</SidebarNavItem>
96-
{active && (
97-
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
98-
{permissions.viewDeploymentValues && (
99-
<SidebarNavSubItem href="general">General</SidebarNavSubItem>
100-
)}
101-
{permissions.viewDeploymentValues && (
102-
<SidebarNavSubItem href="licenses">Licenses</SidebarNavSubItem>
103-
)}
104-
{permissions.editDeploymentValues && (
105-
<SidebarNavSubItem href="appearance">Appearance</SidebarNavSubItem>
106-
)}
107-
{permissions.viewDeploymentValues && (
108-
<SidebarNavSubItem href="userauth">
109-
User Authentication
110-
</SidebarNavSubItem>
111-
)}
112-
{permissions.viewDeploymentValues && (
113-
<SidebarNavSubItem href="external-auth">
114-
External Authentication
115-
</SidebarNavSubItem>
116-
)}
117-
{/* Not exposing this yet since token exchange is not finished yet.
118-
<SidebarNavSubItem href="oauth2-provider/ap>
119-
OAuth2 Applications
120-
</SidebarNavSubItem>*/}
121-
{permissions.viewDeploymentValues && (
122-
<SidebarNavSubItem href="network">Network</SidebarNavSubItem>
123-
)}
124-
{/* All users can view workspace regions. */}
125-
<SidebarNavSubItem href="workspace-proxies">
126-
Workspace Proxies
127-
</SidebarNavSubItem>
128-
{permissions.viewDeploymentValues && (
129-
<SidebarNavSubItem href="security">Security</SidebarNavSubItem>
130-
)}
131-
{permissions.viewDeploymentValues && (
132-
<SidebarNavSubItem href="observability">
133-
Observability
134-
</SidebarNavSubItem>
135-
)}
136-
{permissions.viewAllUsers && (
137-
<SidebarNavSubItem href={USERS_LINK.slice(1)}>
138-
Users
139-
</SidebarNavSubItem>
140-
)}
141-
{permissions.viewAnyGroup && !organizationsEnabled && (
142-
<SidebarNavSubItem href="groups">Groups</SidebarNavSubItem>
143-
)}
144-
{permissions.viewAnyAuditLog && (
145-
<SidebarNavSubItem href={AUDIT_LINK.slice(1)}>
146-
Auditing
147-
</SidebarNavSubItem>
148-
)}
149-
</Stack>
150-
)}
151-
</div>
152-
);
153-
};
154-
155-
function urlForSubpage(organizationName: string, subpage: string = ""): string {
156-
return `/organizations/${organizationName}/${subpage}`;
157-
}
158-
159-
interface OrganizationSettingsNavigationProps {
160-
organization: Organization;
161-
active: boolean;
162-
}
163-
164-
export const OrganizationSettingsNavigation: FC<
165-
OrganizationSettingsNavigationProps
166-
> = ({ organization, active }) => {
167-
// The menu items only show while the menu is expanded, so we can wait until
168-
// expanded to run the query. Downside is that we have to show a loader
169-
// until the query finishes...maybe we should prefetch permissions for all
170-
// the orgs instead?
171-
const permissionsQuery = useQuery(
172-
organizationPermissions(active ? organization.id : undefined),
29+
const activeOrgPermissionsQuery = useQuery(
30+
organizationPermissions(activeOrganization?.id),
17331
);
17432

17533
return (
176-
<>
177-
<SidebarNavItem
178-
active={active}
179-
href={urlForSubpage(organization.name)}
180-
icon={
181-
<UserAvatar
182-
key={organization.id}
183-
size="sm"
184-
username={organization.display_name}
185-
avatarURL={organization.icon}
186-
/>
187-
}
188-
>
189-
{organization.display_name}
190-
</SidebarNavItem>
191-
{active && !permissionsQuery.data && <Loader />}
192-
{active && permissionsQuery.data && (
193-
<Stack spacing={0.5} css={{ marginBottom: 8, marginTop: 8 }}>
194-
{permissionsQuery.data.editOrganization && (
195-
<SidebarNavSubItem end href={urlForSubpage(organization.name)}>
196-
Organization settings
197-
</SidebarNavSubItem>
198-
)}
199-
{permissionsQuery.data.viewMembers && (
200-
<SidebarNavSubItem
201-
href={urlForSubpage(organization.name, "members")}
202-
>
203-
Members
204-
</SidebarNavSubItem>
205-
)}
206-
{permissionsQuery.data.viewGroups && (
207-
<SidebarNavSubItem
208-
href={urlForSubpage(organization.name, "groups")}
209-
>
210-
Groups
211-
</SidebarNavSubItem>
212-
)}
213-
{/* For now redirect to the site-wide audit page with the organization
214-
pre-filled into the filter. Based on user feedback we might want
215-
to serve a copy of the audit page or even delete this link. */}
216-
{permissionsQuery.data.auditOrganization && (
217-
<SidebarNavSubItem
218-
href={`/deployment${withFilter(
219-
AUDIT_LINK,
220-
`organization:${organization.name}`,
221-
)}`}
222-
>
223-
Auditing
224-
</SidebarNavSubItem>
225-
)}
226-
</Stack>
227-
)}
228-
</>
34+
<SidebarView
35+
activeOrganization={activeOrganization}
36+
activeOrgPermissions={activeOrgPermissionsQuery.data}
37+
organizations={organizations}
38+
permissions={permissions}
39+
/>
22940
);
23041
};
231-
232-
interface SidebarNavItemProps {
233-
active?: boolean | "auto";
234-
children?: ReactNode;
235-
icon?: ReactNode;
236-
href: string;
237-
}
238-
239-
export const SidebarNavItem: FC<SidebarNavItemProps> = ({
240-
active,
241-
children,
242-
href,
243-
icon,
244-
}) => {
245-
const link = useClassName(classNames.link, []);
246-
const activeLink = useClassName(classNames.activeLink, []);
247-
248-
const content = (
249-
<Stack alignItems="center" spacing={1.5} direction="row">
250-
{icon}
251-
{children}
252-
</Stack>
253-
);
254-
255-
if (active === "auto") {
256-
return (
257-
<NavLink
258-
to={href}
259-
className={({ isActive }) => cx([link, isActive && activeLink])}
260-
>
261-
{content}
262-
</NavLink>
263-
);
264-
}
265-
266-
return (
267-
<Link to={href} className={cx([link, active && activeLink])}>
268-
{content}
269-
</Link>
270-
);
271-
};
272-
273-
interface SidebarNavSubItemProps {
274-
children?: ReactNode;
275-
href: string;
276-
end?: boolean;
277-
}
278-
279-
export const SidebarNavSubItem: FC<SidebarNavSubItemProps> = ({
280-
children,
281-
href,
282-
end,
283-
}) => {
284-
const link = useClassName(classNames.subLink, []);
285-
const activeLink = useClassName(classNames.activeSubLink, []);
286-
287-
return (
288-
<NavLink
289-
end={end}
290-
to={href}
291-
className={({ isActive }) => cx([link, isActive && activeLink])}
292-
>
293-
{children}
294-
</NavLink>
295-
);
296-
};
297-
298-
const styles = {
299-
sidebarHeader: {
300-
textTransform: "uppercase",
301-
letterSpacing: "0.15em",
302-
fontSize: 11,
303-
fontWeight: 500,
304-
paddingBottom: 4,
305-
},
306-
} satisfies Record<string, Interpolation<Theme>>;
307-
308-
const classNames = {
309-
link: (css, theme) => css`
310-
color: inherit;
311-
display: block;
312-
font-size: 14px;
313-
text-decoration: none;
314-
padding: 10px 12px 10px 16px;
315-
border-radius: 4px;
316-
transition: background-color 0.15s ease-in-out;
317-
position: relative;
318-
319-
&:hover {
320-
background-color: ${theme.palette.action.hover};
321-
}
322-
323-
border-left: 3px solid transparent;
324-
`,
325-
326-
activeLink: (css, theme) => css`
327-
border-left-color: ${theme.palette.primary.main};
328-
border-top-left-radius: 0;
329-
border-bottom-left-radius: 0;
330-
`,
331-
332-
subLink: (css, theme) => css`
333-
color: inherit;
334-
text-decoration: none;
335-
336-
display: block;
337-
font-size: 13px;
338-
margin-left: 44px;
339-
padding: 4px 12px;
340-
border-radius: 4px;
341-
transition: background-color 0.15s ease-in-out;
342-
margin-bottom: 1px;
343-
position: relative;
344-
345-
&:hover {
346-
background-color: ${theme.palette.action.hover};
347-
}
348-
`,
349-
350-
activeSubLink: (css) => css`
351-
font-weight: 600;
352-
`,
353-
} satisfies Record<string, ClassName>;
354-
355-
const getOrganizationNameByDefault = (organizations: Organization[]) =>
356-
organizations.find((org) => org.is_default)?.name;

0 commit comments

Comments
 (0)