Skip to content

Commit 8bcda22

Browse files
ParkreinerEdwardAngert
authored andcommitted
fix(site): standardize headers for Admin Settings page (#16911)
## Changes made - Switched almost all headers to use the `SettingHeader` component - Redesigned component to be more composition-based, to stay in line with the patterns we're starting to use more throughout the codebase - Refactored `SettingHeader` to be based on Radix and Tailwind, rather than Emotion/MUI - Added additional props to `SettingHeader` to help resolve issues with the component creating invalid HTML - Beefed up `SettingHeader` to have better out-of-the-box accessibility - Addressed some typographic problems in `SettingHeader` - Addressed some responsive layout problems for `SettingsHeader` - Added first-ever stories for `SettingsHeader` ## Notes - There are still a few headers that aren't using `SettingHeader` yet. There were some UI edge cases that meant I couldn't reliably bring it in without consulting the Design team first. I'm a little less worried about them, because they at least *look* like the other headers, but it'd be nice if we could centralize everything in a followup PR
1 parent 556d972 commit 8bcda22

27 files changed

+556
-246
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { docs } from "utils/docs";
3+
import {
4+
SettingsHeader,
5+
SettingsHeaderDescription,
6+
SettingsHeaderDocsLink,
7+
SettingsHeaderTitle,
8+
} from "./SettingsHeader";
9+
10+
const meta: Meta<typeof SettingsHeader> = {
11+
title: "components/SettingsHeader",
12+
component: SettingsHeader,
13+
};
14+
15+
export default meta;
16+
type Story = StoryObj<typeof SettingsHeader>;
17+
18+
export const PrimaryHeaderOnly: Story = {
19+
args: {
20+
children: <SettingsHeaderTitle>This is a header</SettingsHeaderTitle>,
21+
},
22+
};
23+
24+
export const PrimaryHeaderWithDescription: Story = {
25+
args: {
26+
children: (
27+
<>
28+
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle>
29+
<SettingsHeaderDescription>
30+
This description can be any ReactNode. This provides more options for
31+
composition.
32+
</SettingsHeaderDescription>
33+
</>
34+
),
35+
},
36+
};
37+
38+
export const PrimaryHeaderWithDescriptionAndDocsLink: Story = {
39+
args: {
40+
children: (
41+
<>
42+
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle>
43+
<SettingsHeaderDescription>
44+
This description can be any ReactNode. This provides more options for
45+
composition.
46+
</SettingsHeaderDescription>
47+
</>
48+
),
49+
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
50+
},
51+
};
52+
53+
export const SecondaryHeaderWithDescription: Story = {
54+
args: {
55+
children: (
56+
<>
57+
<SettingsHeaderTitle level="h6" hierarchy="secondary">
58+
This is a secondary header.
59+
</SettingsHeaderTitle>
60+
<SettingsHeaderDescription>
61+
The header's styling is completely independent of its semantics. Both
62+
can be adjusted independently to help avoid invalid HTML.
63+
</SettingsHeaderDescription>
64+
</>
65+
),
66+
},
67+
};
68+
69+
export const SecondaryHeaderWithDescriptionAndDocsLink: Story = {
70+
args: {
71+
children: (
72+
<>
73+
<SettingsHeaderTitle level="h3" hierarchy="secondary">
74+
Another secondary header
75+
</SettingsHeaderTitle>
76+
<SettingsHeaderDescription>
77+
Nothing to add, really.
78+
</SettingsHeaderDescription>
79+
</>
80+
),
81+
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
82+
},
83+
};
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,107 @@
1-
import { useTheme } from "@emotion/react";
1+
import { type VariantProps, cva } from "class-variance-authority";
22
import { Button } from "components/Button/Button";
3-
import { Stack } from "components/Stack/Stack";
43
import { SquareArrowOutUpRightIcon } from "lucide-react";
5-
import type { FC, ReactNode } from "react";
4+
import type { FC, PropsWithChildren, ReactNode } from "react";
5+
import { cn } from "utils/cn";
66

7-
interface HeaderProps {
8-
title: ReactNode;
9-
description?: ReactNode;
10-
secondary?: boolean;
11-
docsHref?: string;
12-
tooltip?: ReactNode;
13-
}
14-
15-
export const SettingsHeader: FC<HeaderProps> = ({
16-
title,
17-
description,
18-
docsHref,
19-
secondary,
20-
tooltip,
7+
type SettingsHeaderProps = Readonly<
8+
PropsWithChildren<{
9+
actions?: ReactNode;
10+
className?: string;
11+
}>
12+
>;
13+
export const SettingsHeader: FC<SettingsHeaderProps> = ({
14+
children,
15+
actions,
16+
className,
2117
}) => {
22-
const theme = useTheme();
18+
return (
19+
<hgroup className="flex flex-col justify-between items-start gap-2 pb-6 sm:flex-row">
20+
{/*
21+
* The text-sm class is only meant to adjust the font size of
22+
* SettingsDescription, but we need to apply it here. That way,
23+
* text-sm combines with the max-w-prose class and makes sure
24+
* we have a predictable max width for the header + description by
25+
* default.
26+
*/}
27+
<div className={cn("text-sm max-w-prose", className)}>{children}</div>
28+
{actions}
29+
</hgroup>
30+
);
31+
};
2332

33+
type SettingsHeaderDocsLinkProps = Readonly<
34+
PropsWithChildren<{ href: string }>
35+
>;
36+
export const SettingsHeaderDocsLink: FC<SettingsHeaderDocsLinkProps> = ({
37+
href,
38+
children = "Read the docs",
39+
}) => {
2440
return (
25-
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
26-
<div css={{ maxWidth: 420, marginBottom: 24 }}>
27-
<Stack direction="row" spacing={1} alignItems="center">
28-
<h1
29-
css={[
30-
{
31-
fontSize: 32,
32-
fontWeight: 700,
33-
display: "flex",
34-
alignItems: "baseline",
35-
lineHeight: "initial",
36-
margin: 0,
37-
marginBottom: 4,
38-
gap: 8,
39-
},
40-
secondary && {
41-
fontSize: 24,
42-
fontWeight: 500,
43-
},
44-
]}
45-
>
46-
{title}
47-
</h1>
48-
{tooltip}
49-
</Stack>
41+
<Button asChild variant="outline">
42+
<a href={href} target="_blank" rel="noreferrer">
43+
<SquareArrowOutUpRightIcon />
44+
{children}
45+
<span className="sr-only"> (link opens in new tab)</span>
46+
</a>
47+
</Button>
48+
);
49+
};
5050

51-
{description && (
52-
<span
53-
css={{
54-
fontSize: 14,
55-
color: theme.palette.text.secondary,
56-
lineHeight: "160%",
57-
}}
58-
>
59-
{description}
60-
</span>
61-
)}
62-
</div>
51+
const titleVariants = cva("m-0 pb-1 flex items-center gap-2 leading-tight", {
52+
variants: {
53+
hierarchy: {
54+
primary: "text-3xl font-bold",
55+
secondary: "text-2xl font-medium",
56+
},
57+
},
58+
defaultVariants: {
59+
hierarchy: "primary",
60+
},
61+
});
62+
type SettingsHeaderTitleProps = Readonly<
63+
PropsWithChildren<
64+
VariantProps<typeof titleVariants> & {
65+
level?: `h${1 | 2 | 3 | 4 | 5 | 6}`;
66+
tooltip?: ReactNode;
67+
className?: string;
68+
}
69+
>
70+
>;
71+
export const SettingsHeaderTitle: FC<SettingsHeaderTitleProps> = ({
72+
children,
73+
tooltip,
74+
className,
75+
level = "h1",
76+
hierarchy = "primary",
77+
}) => {
78+
// Explicitly not using Radix's Slot component, because we don't want to
79+
// allow any arbitrary element to be composed into this. We specifically
80+
// only want to allow the six HTML headers. Anything else will likely result
81+
// in invalid markup
82+
const Title = level;
83+
return (
84+
<div className="flex flex-row gap-2 items-center">
85+
<Title className={cn(titleVariants({ hierarchy }), className)}>
86+
{children}
87+
</Title>
88+
{tooltip}
89+
</div>
90+
);
91+
};
6392

64-
{docsHref && (
65-
<Button asChild variant="outline">
66-
<a href={docsHref} target="_blank" rel="noreferrer">
67-
<SquareArrowOutUpRightIcon />
68-
Read the docs
69-
</a>
70-
</Button>
71-
)}
72-
</Stack>
93+
type SettingsHeaderDescriptionProps = Readonly<
94+
PropsWithChildren<{
95+
className?: string;
96+
}>
97+
>;
98+
export const SettingsHeaderDescription: FC<SettingsHeaderDescriptionProps> = ({
99+
children,
100+
className,
101+
}) => {
102+
return (
103+
<p className={cn("m-0 text-content-secondary leading-relaxed", className)}>
104+
{children}
105+
</p>
73106
);
74107
};

site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
} from "components/Badges/Badges";
99
import { Button } from "components/Button/Button";
1010
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
11-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
11+
import {
12+
SettingsHeader,
13+
SettingsHeaderDescription,
14+
SettingsHeaderTitle,
15+
} from "components/SettingsHeader/SettingsHeader";
1216
import {
1317
Popover,
1418
PopoverContent,
@@ -54,10 +58,12 @@ export const AppearanceSettingsPageView: FC<
5458

5559
return (
5660
<>
57-
<SettingsHeader
58-
title="Appearance"
59-
description="Customize the look and feel of your Coder deployment."
60-
/>
61+
<SettingsHeader>
62+
<SettingsHeaderTitle>Appearance</SettingsHeaderTitle>
63+
<SettingsHeaderDescription>
64+
Customize the look and feel of your Coder deployment.
65+
</SettingsHeaderDescription>
66+
</SettingsHeader>
6167

6268
<Badges>
6369
<Popover mode="hover">

site/src/pages/DeploymentSettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx

+14-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import TableRow from "@mui/material/TableRow";
88
import type { DeploymentValues, ExternalAuthConfig } from "api/typesGenerated";
99
import { Alert } from "components/Alert/Alert";
1010
import { PremiumBadge } from "components/Badges/Badges";
11-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
11+
import {
12+
SettingsHeader,
13+
SettingsHeaderDescription,
14+
SettingsHeaderDocsLink,
15+
SettingsHeaderTitle,
16+
} from "components/SettingsHeader/SettingsHeader";
1217
import type { FC } from "react";
1318
import { docs } from "utils/docs";
1419

@@ -22,10 +27,14 @@ export const ExternalAuthSettingsPageView: FC<
2227
return (
2328
<>
2429
<SettingsHeader
25-
title="External Authentication"
26-
description="Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and OpenID Connect to authenticate developers with external services."
27-
docsHref={docs("/admin/external-auth")}
28-
/>
30+
actions={<SettingsHeaderDocsLink href={docs("/admin/external-auth")} />}
31+
>
32+
<SettingsHeaderTitle>External Authentication</SettingsHeaderTitle>
33+
<SettingsHeaderDescription>
34+
Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and
35+
OpenID Connect to authenticate developers with external services.
36+
</SettingsHeaderDescription>
37+
</SettingsHeader>
2938

3039
<video
3140
autoPlay

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
33
import { Button } from "components/Button/Button";
44
import { FileUpload } from "components/FileUpload/FileUpload";
55
import { displayError } from "components/GlobalSnackbar/utils";
6-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
6+
import {
7+
SettingsHeader,
8+
SettingsHeaderDescription,
9+
SettingsHeaderTitle,
10+
} from "components/SettingsHeader/SettingsHeader";
711
import { Stack } from "components/Stack/Stack";
812
import { ChevronLeftIcon } from "lucide-react";
913
import type { FC } from "react";
@@ -50,10 +54,13 @@ export const AddNewLicensePageView: FC<AddNewLicenseProps> = ({
5054
direction="row"
5155
justifyContent="space-between"
5256
>
53-
<SettingsHeader
54-
title="Add a license"
55-
description="Get access to high availability, RBAC, quotas, and more."
56-
/>
57+
<SettingsHeader>
58+
<SettingsHeaderTitle>Add a license</SettingsHeaderTitle>
59+
<SettingsHeaderDescription>
60+
Get access to high availability, RBAC, quotas, and more.
61+
</SettingsHeaderDescription>
62+
</SettingsHeader>
63+
5764
<Button asChild variant="outline">
5865
<RouterLink to="/deployment/licenses">
5966
<ChevronLeftIcon />

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import Skeleton from "@mui/material/Skeleton";
88
import Tooltip from "@mui/material/Tooltip";
99
import type { GetLicensesResponse } from "api/api";
1010
import type { UserStatusChangeCount } from "api/typesGenerated";
11-
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
11+
import {
12+
SettingsHeader,
13+
SettingsHeaderDescription,
14+
SettingsHeaderTitle,
15+
} from "components/SettingsHeader/SettingsHeader";
1216
import { Stack } from "components/Stack/Stack";
1317
import { useWindowSize } from "hooks/useWindowSize";
1418
import type { FC } from "react";
@@ -60,10 +64,12 @@ const LicensesSettingsPageView: FC<Props> = ({
6064
direction="row"
6165
justifyContent="space-between"
6266
>
63-
<SettingsHeader
64-
title="Licenses"
65-
description="Manage licenses to unlock Premium features."
66-
/>
67+
<SettingsHeader>
68+
<SettingsHeaderTitle>Licenses</SettingsHeaderTitle>
69+
<SettingsHeaderDescription>
70+
Manage licenses to unlock Premium features.
71+
</SettingsHeaderDescription>
72+
</SettingsHeader>
6773

6874
<Stack direction="row" spacing={2}>
6975
<Button

0 commit comments

Comments
 (0)