Skip to content

fix(site): standardize headers for Admin Settings page #16911

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 31 additions & 39 deletions site/src/components/SettingsHeader/SettingsHeader.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a storybook for it covering the new variants?

Original file line number Diff line number Diff line change
@@ -1,63 +1,54 @@
import { useTheme } from "@emotion/react";
import { Button } from "components/Button/Button";
import { Stack } from "components/Stack/Stack";
import { SquareArrowOutUpRightIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import { twMerge } from "tailwind-merge";

interface HeaderProps {
type HeaderHierarchy = "primary" | "secondary";
type HeaderLevel = `h${1 | 2 | 3 | 4 | 5 | 6}`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it like this so that we can add additional variants over time

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For creating variants, I would recommend you to use class-variance-authority package. You can see how it is used in the Button component.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've used CVA before – I actually used it in the take-home that got me into Coder. I thought it was a bit too much overhead here, but if we're switching to CVA as a go-to, I can swap that in

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of shadcn, our base components are using CVA so I think it would be nice to use it but it is up to you.


type HeaderProps = Readonly<{
title: ReactNode;
description?: ReactNode;
secondary?: boolean;
titleVisualHierarchy?: HeaderHierarchy;
titleHeaderLevel?: HeaderLevel;
docsHref?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it is how we did before but since we are touching this, could we also improve the abstraction? I see something like:

<SettingsHeader actions={[<SeeDocsButton />]}>
  <SettingsHeaderTitle>Mytitle</SettingsHeaderTitle>
  <SettingsHeaderSubtitle>Some text hear</SettingsHeaderSubtitle>
</SettingsHeader>

tooltip?: ReactNode;
}
}>;

export const SettingsHeader: FC<HeaderProps> = ({
title,
description,
docsHref,
secondary,
tooltip,
titleHeaderLevel = "h1",
titleVisualHierarchy = "primary",
}) => {
const theme = useTheme();

const HeaderTitle = titleHeaderLevel;
return (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing this, we have access to the Slot component from Radix. I think it is a better tool for this kinda of "polymorphic" component.

Copy link
Member Author

@Parkreiner Parkreiner Mar 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if that makes sense here. Slot allows any component to be slotted in as a child – it's too permissive with what it lets you compose into it, when there should only ever be six possible values (h1–h6), and all six have the exact same underlying node type

The current setup is a more restricted form of polymorphism

<Stack alignItems="baseline" direction="row" justifyContent="space-between">
<div css={{ maxWidth: 420, marginBottom: 24 }}>
<Stack direction="row" spacing={1} alignItems="center">
<h1
css={[
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a big accessibility/HTML validity issue. We were using this component multiple times on the same page, but only one h1 is allowed to be on a page, total

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I decoupled the secondary boolean into a styling prop and a semantic prop. There may be cases when we need to change the heading level to make the HTML valid, even though the styling should be exactly the same

{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "baseline",
lineHeight: "initial",
margin: 0,
marginBottom: 4,
gap: 8,
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
<hgroup className="flex flex-row justify-between align-baseline">
{/*
* The text-sm class only adjusts the size of the description, but
* we need to apply it here so that it combines with the max-w-prose
* class and makes sure we have a predictable max width for the
* entire section of text.
*/}
<div className="text-sm max-w-prose pb-6">
<div className="flex flex-row gap-2 align-middle">
<HeaderTitle
className={twMerge(
"m-0 pb-1 text-3xl font-bold flex items-center gap-2 leading-tight",
titleVisualHierarchy === "secondary" && "text-2xl font-medium",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The different leading values for the header and description are intentional – leading needs to go down the larger your text is

)}
>
{title}
</h1>
</HeaderTitle>
{tooltip}
</Stack>
</div>

{description && (
<span
css={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
}}
>
<p className="m-0 text-sm text-content-secondary leading-relaxed">
{description}
</span>
</p>
)}
</div>

Expand All @@ -66,9 +57,10 @@ export const SettingsHeader: FC<HeaderProps> = ({
<a href={docsHref} target="_blank" rel="noreferrer">
<SquareArrowOutUpRightIcon />
Read the docs
<span className="sr-only"> (link opens in new tab)</span>
</a>
</Button>
)}
</Stack>
</hgroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export const NetworkSettingsPageView: FC<NetworkSettingsPageViewProps> = ({
<div>
<SettingsHeader
title="Port Forwarding"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Port forwarding lets developers securely access processes on their Coder workspace from a local machine."
docsHref={docs("/admin/networking/port-forwarding")}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const NotificationsPage: FC = () => {
title={
<>
Notifications
<span css={{ position: "relative", top: "-6px" }}>
<span css={{ position: "relative", top: "2px" }}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to adjust this slightly to account for the updated text leading

Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it something we could use CSS variables and calc to make the top value more reliable and easier to understand? By understand I mean, why is it 2px? If not, a comment would be nice.

<FeatureStageBadge
contentType={"beta"}
size="lg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ export const ObservabilitySettingsPageView: FC<
<Stack direction="column" spacing={6}>
<div>
<SettingsHeader title="Observability" />

<SettingsHeader
title="Audit Logging"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Allow auditors to monitor user operations in your deployment."
docsHref={docs("/admin/security/audit-logs")}
/>
Expand Down Expand Up @@ -64,7 +66,8 @@ export const ObservabilitySettingsPageView: FC<
<div>
<SettingsHeader
title="Monitoring"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Monitoring your Coder application with logs and metrics."
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
<div>
<SettingsHeader
title="Browser Only Connections"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Block all workspace access via SSH, port forward, and other non-browser connections."
docsHref={docs("/admin/networking#browser-only-connections")}
/>
Expand All @@ -64,7 +65,8 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
<div>
<SettingsHeader
title="TLS"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Ensure TLS is properly configured for your Coder deployment."
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const UserAuthSettingsPageView = ({

<SettingsHeader
title="Login with OpenID Connect"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Set up authentication to login with OpenID Connect."
docsHref={docs("/admin/users/oidc-auth#openid-connect")}
/>
Expand All @@ -50,7 +51,8 @@ export const UserAuthSettingsPageView = ({
<div>
<SettingsHeader
title="Login with GitHub"
secondary
titleHeaderLevel="h2"
titleVisualHierarchy="secondary"
description="Set up authentication to login with GitHub."
docsHref={docs("/admin/users/github-auth")}
/>
Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/HealthPage/WorkspaceProxyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ export const WorkspaceProxyPage: FC = () => {
<span>OK</span>
) : (
<div css={{ display: "flex", flexDirection: "column" }}>
{[...errors, ...warnings].map((msg, i) => (
{[...errors, ...warnings].map((msg) => (
<span
key={msg}
css={{
":first-letter": { textTransform: "uppercase" },
}}
key={i}
>
{msg}
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { useProxy } from "contexts/ProxyContext";
import type { FC } from "react";
import { Section } from "../Section";
import { WorkspaceProxyView } from "./WorkspaceProxyView";

export const WorkspaceProxyPage: FC = () => {
const description =
"Workspace proxies improve terminal and web app connections to workspaces.";

const {
proxyLatencies,
proxies,
Expand All @@ -17,29 +13,14 @@ export const WorkspaceProxyPage: FC = () => {
} = useProxy();

return (
<Section
Copy link
Member Author

@Parkreiner Parkreiner Mar 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was deleted because the header is now located in the view, which I think is how it should've always been

title="Workspace Proxies"
css={(theme) => ({
"& code": {
background: theme.palette.divider,
fontSize: 12,
padding: "2px 4px",
color: theme.palette.text.primary,
borderRadius: 2,
},
Comment on lines -23 to -29
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code seemed to be part of a previous version of the component. None of the styling ever applied to anything

})}
description={description}
layout="fluid"
>
<WorkspaceProxyView
proxyLatencies={proxyLatencies}
proxies={proxies}
isLoading={proxiesLoading}
hasLoaded={proxiesFetched}
getWorkspaceProxiesError={proxiesError}
preferredProxy={proxy.proxy}
/>
</Section>
<WorkspaceProxyView
proxyLatencies={proxyLatencies}
proxies={proxies}
isLoading={proxiesLoading}
hasLoaded={proxiesFetched}
getWorkspaceProxiesError={proxiesError}
preferredProxy={proxy.proxy}
/>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TableRow from "@mui/material/TableRow";
import type { Region } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { TableEmpty } from "components/TableEmpty/TableEmpty";
import { TableLoader } from "components/TableLoader/TableLoader";
Expand Down Expand Up @@ -34,6 +35,10 @@ export const WorkspaceProxyView: FC<WorkspaceProxyViewProps> = ({
}) => {
return (
<Stack>
<SettingsHeader
title="Workspace Proxies"
description="Workspace proxies improve terminal and web app connections to workspaces."
/>
{Boolean(getWorkspaceProxiesError) && (
<ErrorAlert error={getWorkspaceProxiesError} />
)}
Expand Down
Loading