Skip to content

Commit e7bf52f

Browse files
committed
feat: split management settings sidebar into deployment and organization settings
1 parent 962045c commit e7bf52f

22 files changed

+329
-286
lines changed

site/src/components/Sidebar/Sidebar.tsx

+31-5
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { Stack } from "components/Stack/Stack";
44
import { type ClassName, useClassName } from "hooks/useClassName";
55
import type { ElementType, FC, ReactNode } from "react";
66
import { Link, NavLink } from "react-router-dom";
7+
import { cn } from "utils/cn";
78

89
interface SidebarProps {
910
children?: ReactNode;
1011
}
1112

1213
export const Sidebar: FC<SidebarProps> = ({ children }) => {
13-
return <nav css={styles.sidebar}>{children}</nav>;
14+
return <nav className="w-60 flex-shrink-0">{children}</nav>;
1415
};
1516

1617
interface SidebarHeaderProps {
@@ -49,6 +50,35 @@ export const SidebarHeader: FC<SidebarHeaderProps> = ({
4950
);
5051
};
5152

53+
interface SettingsSidebarNavItemProps {
54+
children?: ReactNode;
55+
href: string;
56+
end?: boolean;
57+
}
58+
59+
export const SettingsSidebarNavItem: FC<SettingsSidebarNavItemProps> = ({
60+
children,
61+
href,
62+
end,
63+
}) => {
64+
return (
65+
<NavLink
66+
end={end}
67+
to={href}
68+
className={({ isActive }) =>
69+
cn(
70+
"relative text-sm text-content-secondary no-underline font-medium py-2 px-3 hover:bg-surface-secondary rounded-md transition ease-in-out duration-150 ",
71+
{
72+
"font-semibold text-content-primary": isActive,
73+
},
74+
)
75+
}
76+
>
77+
{children}
78+
</NavLink>
79+
);
80+
};
81+
5282
interface SidebarNavItemProps {
5383
children?: ReactNode;
5484
icon: ElementType;
@@ -78,10 +108,6 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
78108
};
79109

80110
const styles = {
81-
sidebar: {
82-
width: 245,
83-
flexShrink: 0,
84-
},
85111
info: (theme) => ({
86112
...(theme.typography.body2 as CSSObject),
87113
marginBottom: 16,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Loader } from "components/Loader/Loader";
2+
import { useAuthenticated } from "contexts/auth/RequireAuth";
3+
import { RequirePermission } from "contexts/auth/RequirePermission";
4+
import { type FC, Suspense } from "react";
5+
import { Outlet } from "react-router-dom";
6+
import { DeploymentSidebar } from "./DeploymentSidebar";
7+
8+
const DeploymentSettingsLayout: FC = () => {
9+
const { permissions } = useAuthenticated();
10+
11+
// The deployment settings page also contains users, audit logs, and groups
12+
// so this page must be visible if you can see any of these.
13+
const canViewDeploymentSettingsPage =
14+
permissions.viewDeploymentValues ||
15+
permissions.viewAllUsers ||
16+
permissions.viewAnyAuditLog;
17+
18+
return (
19+
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
20+
<div className="px-10 max-w-screen-2xl">
21+
<div className="flex flex-row gap-12 py-10">
22+
<DeploymentSidebar />
23+
<main css={{ flexGrow: 1 }}>
24+
<Suspense fallback={<Loader />}>
25+
<Outlet />
26+
</Suspense>
27+
</main>
28+
</div>
29+
</div>
30+
</RequirePermission>
31+
);
32+
};
33+
34+
export default DeploymentSettingsLayout;

site/src/modules/management/DeploymentSettingsProvider.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@ const DeploymentSettingsProvider: FC = () => {
3131
const { permissions } = useAuthenticated();
3232
const deploymentConfigQuery = useQuery(deploymentConfig());
3333

34-
// The deployment settings page also contains users, audit logs, groups and
35-
// organizations, so this page must be visible if you can see any of these.
34+
// The deployment settings page also contains users, audit logs, and groups
35+
// so this page must be visible if you can see any of these.
3636
const canViewDeploymentSettingsPage =
3737
permissions.viewDeploymentValues ||
3838
permissions.viewAllUsers ||
39-
permissions.editAnyOrganization ||
4039
permissions.viewAnyAuditLog;
4140

4241
// Not a huge problem to unload the content in the event of an error,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useAuthenticated } from "contexts/auth/RequireAuth";
2+
import type { FC } from "react";
3+
import { DeploymentSidebarView } from "./DeploymentSidebarView";
4+
5+
/**
6+
* A sidebar for deployment settings.
7+
*/
8+
export const DeploymentSidebar: FC = () => {
9+
const { permissions } = useAuthenticated();
10+
11+
return <DeploymentSidebarView permissions={permissions} />;
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MockNoPermissions, MockPermissions } from "testHelpers/entities";
3+
import { withDashboardProvider } from "testHelpers/storybook";
4+
import { DeploymentSidebarView } from "./DeploymentSidebarView";
5+
6+
const meta: Meta<typeof DeploymentSidebarView> = {
7+
title: "modules/management/SidebarView",
8+
component: DeploymentSidebarView,
9+
decorators: [withDashboardProvider],
10+
parameters: { showOrganizations: true },
11+
args: {
12+
permissions: MockPermissions,
13+
},
14+
};
15+
16+
export default meta;
17+
type Story = StoryObj<typeof DeploymentSidebarView>;
18+
19+
export const NoCreateOrg: Story = {
20+
args: {
21+
permissions: {
22+
...MockPermissions,
23+
createOrganization: false,
24+
},
25+
},
26+
};
27+
28+
export const NoViewUsers: Story = {
29+
args: {
30+
permissions: {
31+
...MockPermissions,
32+
viewAllUsers: false,
33+
},
34+
},
35+
};
36+
37+
export const NoAuditLog: Story = {
38+
args: {
39+
permissions: {
40+
...MockPermissions,
41+
viewAnyAuditLog: false,
42+
},
43+
},
44+
};
45+
46+
export const NoLicenses: Story = {
47+
args: {
48+
permissions: {
49+
...MockPermissions,
50+
viewAllLicenses: false,
51+
},
52+
},
53+
};
54+
55+
export const NoDeploymentValues: Story = {
56+
args: {
57+
permissions: {
58+
...MockPermissions,
59+
viewDeploymentValues: false,
60+
editDeploymentValues: false,
61+
},
62+
},
63+
};
64+
65+
export const NoPermissions: Story = {
66+
args: {
67+
permissions: MockNoPermissions,
68+
},
69+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
2+
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
3+
import {
4+
Sidebar as BaseSidebar,
5+
SettingsSidebarNavItem as SidebarNavItem,
6+
} from "components/Sidebar/Sidebar";
7+
import type { Permissions } from "contexts/auth/permissions";
8+
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
9+
import type { FC } from "react";
10+
11+
export interface OrganizationWithPermissions extends Organization {
12+
permissions: AuthorizationResponse;
13+
}
14+
15+
interface DeploymentSidebarProps {
16+
/** Site-wide permissions. */
17+
permissions: Permissions;
18+
}
19+
20+
/**
21+
* A combined deployment settings and organization menu.
22+
*/
23+
export const DeploymentSidebarView: FC<DeploymentSidebarProps> = ({
24+
permissions,
25+
}) => {
26+
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();
27+
28+
return (
29+
<BaseSidebar>
30+
<DeploymentSettingsNavigation
31+
permissions={permissions}
32+
isPremium={hasPremiumLicense}
33+
/>
34+
</BaseSidebar>
35+
);
36+
};
37+
38+
interface DeploymentSettingsNavigationProps {
39+
/** Site-wide permissions. */
40+
permissions: Permissions;
41+
isPremium: boolean;
42+
}
43+
44+
/**
45+
* Displays navigation for deployment settings. If active, highlight the main
46+
* menu heading.
47+
*
48+
* Menu items are shown based on the permissions. If organizations can be
49+
* viewed, groups are skipped since they will show under each org instead.
50+
*/
51+
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
52+
permissions,
53+
isPremium,
54+
}) => {
55+
return (
56+
<div>
57+
<div className="flex flex-col gap-1">
58+
{permissions.viewDeploymentValues && (
59+
<SidebarNavItem href="general">General</SidebarNavItem>
60+
)}
61+
{permissions.viewAllLicenses && (
62+
<SidebarNavItem href="licenses">Licenses</SidebarNavItem>
63+
)}
64+
{permissions.editDeploymentValues && (
65+
<SidebarNavItem href="appearance">Appearance</SidebarNavItem>
66+
)}
67+
{permissions.viewDeploymentValues && (
68+
<SidebarNavItem href="userauth">User Authentication</SidebarNavItem>
69+
)}
70+
{permissions.viewDeploymentValues && (
71+
<SidebarNavItem href="external-auth">
72+
External Authentication
73+
</SidebarNavItem>
74+
)}
75+
{/* Not exposing this yet since token exchange is not finished yet.
76+
<SidebarNavItem href="oauth2-provider/ap>
77+
OAuth2 Applications
78+
</SidebarNavItem>*/}
79+
{permissions.viewDeploymentValues && (
80+
<SidebarNavItem href="network">Network</SidebarNavItem>
81+
)}
82+
{permissions.readWorkspaceProxies && (
83+
<SidebarNavItem href="workspace-proxies">
84+
Workspace Proxies
85+
</SidebarNavItem>
86+
)}
87+
{permissions.viewDeploymentValues && (
88+
<SidebarNavItem href="security">Security</SidebarNavItem>
89+
)}
90+
{permissions.viewDeploymentValues && (
91+
<SidebarNavItem href="observability">Observability</SidebarNavItem>
92+
)}
93+
{permissions.viewAllUsers && (
94+
<SidebarNavItem href="users">Users</SidebarNavItem>
95+
)}
96+
{permissions.viewNotificationTemplate && (
97+
<SidebarNavItem href="notifications">
98+
<div className="flex flex-row items-center gap-2">
99+
<span>Notifications</span>
100+
<FeatureStageBadge contentType="beta" size="sm" />
101+
</div>
102+
</SidebarNavItem>
103+
)}
104+
{!isPremium && <SidebarNavItem href="premium">Premium</SidebarNavItem>}
105+
</div>
106+
</div>
107+
);
108+
};
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import type { AuthorizationResponse, Organization } from "api/typesGenerated";
22
import { Loader } from "components/Loader/Loader";
3-
import { Margins } from "components/Margins/Margins";
4-
import { Stack } from "components/Stack/Stack";
53
import { useAuthenticated } from "contexts/auth/RequireAuth";
64
import { RequirePermission } from "contexts/auth/RequirePermission";
75
import { useDashboard } from "modules/dashboard/useDashboard";
86
import { type FC, Suspense, createContext, useContext } from "react";
97
import { Outlet, useParams } from "react-router-dom";
10-
import { Sidebar } from "./Sidebar";
8+
import { OrganizationSidebar } from "./OrganizationSidebar";
119

12-
export const ManagementSettingsContext = createContext<
13-
ManagementSettingsValue | undefined
10+
export const OrganizationSettingsContext = createContext<
11+
OrganizationSettingsValue | undefined
1412
>(undefined);
1513

16-
type ManagementSettingsValue = Readonly<{
14+
type OrganizationSettingsValue = Readonly<{
1715
organizations: readonly Organization[];
1816
organization?: Organization;
1917
}>;
2018

21-
export const useManagementSettings = (): ManagementSettingsValue => {
22-
const context = useContext(ManagementSettingsContext);
19+
export const useOrganizationSettings = (): OrganizationSettingsValue => {
20+
const context = useContext(OrganizationSettingsContext);
2321
if (!context) {
2422
throw new Error(
25-
"useManagementSettings should be used inside of ManagementSettingsLayout",
23+
"useOrganizationSettings should be used inside of OrganizationSettingsLayout",
2624
);
2725
}
2826

@@ -43,47 +41,41 @@ export const canEditOrganization = (
4341
);
4442
};
4543

46-
const ManagementSettingsLayout: FC = () => {
44+
const OrganizationSettingsLayout: FC = () => {
4745
const { permissions } = useAuthenticated();
4846
const { organizations } = useDashboard();
4947
const { organization: orgName } = useParams() as {
5048
organization?: string;
5149
};
5250

53-
// The deployment settings page also contains users, audit logs, groups and
54-
// organizations, so this page must be visible if you can see any of these.
55-
const canViewDeploymentSettingsPage =
56-
permissions.viewDeploymentValues ||
57-
permissions.viewAllUsers ||
58-
permissions.editAnyOrganization ||
59-
permissions.viewAnyAuditLog;
51+
const canViewOrganizationSettingsPage = permissions.editAnyOrganization;
6052

6153
const organization =
6254
organizations && orgName
6355
? organizations.find((org) => org.name === orgName)
6456
: undefined;
6557

6658
return (
67-
<RequirePermission isFeatureVisible={canViewDeploymentSettingsPage}>
68-
<ManagementSettingsContext.Provider
59+
<RequirePermission isFeatureVisible={canViewOrganizationSettingsPage}>
60+
<OrganizationSettingsContext.Provider
6961
value={{
7062
organizations,
7163
organization,
7264
}}
7365
>
74-
<Margins>
75-
<Stack css={{ padding: "48px 0" }} direction="row" spacing={6}>
76-
<Sidebar />
66+
<div className="px-10 max-w-screen-2xl">
67+
<div className="flex flex-row gap-12 py-10">
68+
<OrganizationSidebar />
7769
<main css={{ flexGrow: 1 }}>
7870
<Suspense fallback={<Loader />}>
7971
<Outlet />
8072
</Suspense>
8173
</main>
82-
</Stack>
83-
</Margins>
84-
</ManagementSettingsContext.Provider>
74+
</div>
75+
</div>
76+
</OrganizationSettingsContext.Provider>
8577
</RequirePermission>
8678
);
8779
};
8880

89-
export default ManagementSettingsLayout;
81+
export default OrganizationSettingsLayout;

0 commit comments

Comments
 (0)