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 1 commit
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
Prev Previous commit
Next Next commit
refactor: update API design of SettingsHeader
  • Loading branch information
Parkreiner committed Mar 28, 2025
commit 50eddd01ef0d45ce21cfdfd0212849dcccbe0727
150 changes: 90 additions & 60 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,79 +1,109 @@
import { cva, type VariantProps } from "class-variance-authority";
import { Button } from "components/Button/Button";
import { SquareArrowOutUpRightIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import type { FC, PropsWithChildren, ReactNode } from "react";
import { cn } from "utils/cn";

const headerVariants = cva("m-0 pb-1 flex items-center gap-2 leading-tight", {
type SettingsHeaderProps = Readonly<
PropsWithChildren<{
actions?: ReactNode;
className?: string;
}>
>;
export const SettingsHeader: FC<SettingsHeaderProps> = ({
children,
actions,
className,
}) => {
return (
<hgroup className="flex flex-row justify-between align-baseline">
{/*
* The text-sm class is only meant to adjust the font size of
* SettingsDescription, but we need to apply it here. That way,
* text-sm combines with the max-w-prose class and makes sure
* we have a predictable max width for the header + description by
* default.
*/}
<div className={cn("text-sm max-w-prose pb-6", className)}>
{children}
</div>
{actions}
</hgroup>
);
};

type SettingsHeaderDocsLinkProps = Readonly<
PropsWithChildren<{ href: string }>
>;
export const SettingsHeaderDocsLink: FC<SettingsHeaderDocsLinkProps> = ({
href,
children = "Read the docs",
}) => {
return (
<Button asChild variant="outline">
<a href={href} target="_blank" rel="noreferrer">
<SquareArrowOutUpRightIcon />
{children}
<span className="sr-only"> (link opens in new tab)</span>
</a>
</Button>
);
};

const titleVariants = cva("m-0 pb-1 flex items-center gap-2 leading-tight", {
variants: {
titleVisualHierarchy: {
hierarchy: {
primary: "text-3xl font-bold",
secondary: "text-2xl font-medium",
},
},
defaultVariants: {
titleVisualHierarchy: "primary",
hierarchy: "primary",
},
});

type HeaderLevel = `h${1 | 2 | 3 | 4 | 5 | 6}`;

type HeaderProps = Readonly<
VariantProps<typeof headerVariants> & {
title: ReactNode;
description?: ReactNode;
titleHeaderLevel?: HeaderLevel;
docsHref?: string;
tooltip?: ReactNode;
}
type SettingsHeaderTitleProps = Readonly<
PropsWithChildren<
VariantProps<typeof titleVariants> & {
level?: `h${1 | 2 | 3 | 4 | 5 | 6}`;
tooltip?: ReactNode;
className?: string;
}
>
>;

export const SettingsHeader: FC<HeaderProps> = ({
title,
description,
docsHref,
export const SettingsHeaderTitle: FC<SettingsHeaderTitleProps> = ({
children,
tooltip,
titleHeaderLevel = "h1",
titleVisualHierarchy = "primary",
className,
level = "h1",
hierarchy = "primary",
}) => {
const HeaderTitle = titleHeaderLevel;
// Explicitly not using Radix's Slot component, because we don't want to
// allow any arbitrary element to be composed into this. We specifically
// only want to allow the six HTML headers. Anything else will likely result
// in invalid markup
Comment on lines +78 to +81
Copy link
Member Author

Choose a reason for hiding this comment

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

Added a comment to call out why the code isn't using Slot

const Title = level;
return (
<hgroup className="flex flex-row justify-between align-baseline">
{/*
* The text-sm class only adjusts the font size of the description,
* but we need to apply it here and not on the <p> tag itself. That
* way, text-sm combines with the max-w-prose class and makes sure
* we have a predictable max width for the header + description.
*/}
<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",
)}
>
{title}
</HeaderTitle>
{tooltip}
</div>

{description && (
<p className="m-0 text-content-secondary leading-relaxed">
{description}
</p>
)}
</div>
<div className="flex flex-row gap-2 align-middle">
<Title className={cn(titleVariants({ hierarchy }), className)}>
{children}
</Title>
{tooltip}
</div>
);
};

{docsHref && (
<Button asChild variant="outline">
<a href={docsHref} target="_blank" rel="noreferrer">
<SquareArrowOutUpRightIcon />
Read the docs
<span className="sr-only"> (link opens in new tab)</span>
</a>
</Button>
)}
</hgroup>
type SettingsHeaderDescriptionProps = Readonly<
PropsWithChildren<{
className?: string;
}>
>;
export const SettingsHeaderDescription: FC<SettingsHeaderDescriptionProps> = ({
children,
className,
}) => {
return (
<p className={cn("m-0 text-content-secondary leading-relaxed", className)}>
{children}
</p>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from "components/Badges/Badges";
import { Button } from "components/Button/Button";
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import {
Popover,
PopoverContent,
Expand Down Expand Up @@ -54,10 +58,12 @@ export const AppearanceSettingsPageView: FC<

return (
<>
<SettingsHeader
title="Appearance"
description="Customize the look and feel of your Coder deployment."
/>
<SettingsHeader>
<SettingsHeaderTitle>Appearance</SettingsHeaderTitle>
<SettingsHeaderDescription>
Customize the look and feel of your Coder deployment.
</SettingsHeaderDescription>
</SettingsHeader>

<Badges>
<Popover mode="hover">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import TableRow from "@mui/material/TableRow";
import type { DeploymentValues, ExternalAuthConfig } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { PremiumBadge } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import type { FC } from "react";
import { docs } from "utils/docs";

Expand All @@ -22,10 +27,14 @@ export const ExternalAuthSettingsPageView: FC<
return (
<>
<SettingsHeader
title="External Authentication"
description="Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and OpenID Connect to authenticate developers with external services."
docsHref={docs("/admin/external-auth")}
/>
actions={<SettingsHeaderDocsLink href={docs("/admin/external-auth")} />}
>
<SettingsHeaderTitle>External Authentication</SettingsHeaderTitle>
<SettingsHeaderDescription>
Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and
OpenID Connect to authenticate developers with external services.
</SettingsHeaderDescription>
</SettingsHeader>

<video
autoPlay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Button } from "components/Button/Button";
import { FileUpload } from "components/FileUpload/FileUpload";
import { displayError } from "components/GlobalSnackbar/utils";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { ChevronLeftIcon } from "lucide-react";
import type { FC } from "react";
Expand Down Expand Up @@ -50,10 +54,13 @@ export const AddNewLicensePageView: FC<AddNewLicenseProps> = ({
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="Add a license"
description="Get access to high availability, RBAC, quotas, and more."
/>
<SettingsHeader>
<SettingsHeaderTitle>Add a license</SettingsHeaderTitle>
<SettingsHeaderDescription>
Get access to high availability, RBAC, quotas, and more.
</SettingsHeaderDescription>
</SettingsHeader>

<Button asChild variant="outline">
<RouterLink to="/deployment/licenses">
<ChevronLeftIcon />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import type { GetLicensesResponse } from "api/api";
import type { UserStatusChangeCount } from "api/typesGenerated";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useWindowSize } from "hooks/useWindowSize";
import type { FC } from "react";
Expand Down Expand Up @@ -60,10 +64,12 @@ const LicensesSettingsPageView: FC<Props> = ({
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="Licenses"
description="Manage licenses to unlock Premium features."
/>
<SettingsHeader>
<SettingsHeaderTitle>Licenses</SettingsHeaderTitle>
<SettingsHeaderDescription>
Manage licenses to unlock Premium features.
</SettingsHeaderDescription>
</SettingsHeader>

<Stack direction="row" spacing={2}>
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { SerpentOption } from "api/typesGenerated";
import { Badges, DisabledBadge, EnabledBadge } from "components/Badges/Badges";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
import {
Expand All @@ -20,10 +25,14 @@ export const NetworkSettingsPageView: FC<NetworkSettingsPageViewProps> = ({
<Stack direction="column" spacing={6}>
<div>
<SettingsHeader
title="Network"
description="Configure your deployment connectivity."
docsHref={docs("/admin/networking")}
/>
actions={<SettingsHeaderDocsLink href={docs("/admin/networking")} />}
>
<SettingsHeaderTitle>Network</SettingsHeaderTitle>
<SettingsHeaderDescription>
Configure your deployment connectivity.
</SettingsHeaderDescription>
</SettingsHeader>

<OptionsTable
options={options.filter((o) =>
deploymentGroupHasParent(o.group, "Networking"),
Expand All @@ -33,12 +42,20 @@ export const NetworkSettingsPageView: FC<NetworkSettingsPageViewProps> = ({

<div>
<SettingsHeader
title="Port Forwarding"
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")}
/>
actions={
<SettingsHeaderDocsLink
href={docs("/admin/networking/port-forwarding")}
/>
}
>
<SettingsHeaderTitle level="h2" hierarchy="secondary">
Port Forwarding
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Port forwarding lets developers securely access processes on their
Coder workspace from a local machine.
</SettingsHeaderDescription>
</SettingsHeader>

<Badges>
{useDeploymentOptions(options, "Wildcard Access URL")[0].value !==
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy