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
This commit is contained in:
Michael Smith
2025-04-01 10:13:56 -04:00
committed by GitHub
parent 1e11e823c9
commit fcac4abcca
27 changed files with 557 additions and 247 deletions
@@ -0,0 +1,83 @@
import type { Meta, StoryObj } from "@storybook/react";
import { docs } from "utils/docs";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "./SettingsHeader";
const meta: Meta<typeof SettingsHeader> = {
title: "components/SettingsHeader",
component: SettingsHeader,
};
export default meta;
type Story = StoryObj<typeof SettingsHeader>;
export const PrimaryHeaderOnly: Story = {
args: {
children: <SettingsHeaderTitle>This is a header</SettingsHeaderTitle>,
},
};
export const PrimaryHeaderWithDescription: Story = {
args: {
children: (
<>
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle>
<SettingsHeaderDescription>
This description can be any ReactNode. This provides more options for
composition.
</SettingsHeaderDescription>
</>
),
},
};
export const PrimaryHeaderWithDescriptionAndDocsLink: Story = {
args: {
children: (
<>
<SettingsHeaderTitle>Another primary header</SettingsHeaderTitle>
<SettingsHeaderDescription>
This description can be any ReactNode. This provides more options for
composition.
</SettingsHeaderDescription>
</>
),
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
},
};
export const SecondaryHeaderWithDescription: Story = {
args: {
children: (
<>
<SettingsHeaderTitle level="h6" hierarchy="secondary">
This is a secondary header.
</SettingsHeaderTitle>
<SettingsHeaderDescription>
The header's styling is completely independent of its semantics. Both
can be adjusted independently to help avoid invalid HTML.
</SettingsHeaderDescription>
</>
),
},
};
export const SecondaryHeaderWithDescriptionAndDocsLink: Story = {
args: {
children: (
<>
<SettingsHeaderTitle level="h3" hierarchy="secondary">
Another secondary header
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Nothing to add, really.
</SettingsHeaderDescription>
</>
),
actions: <SettingsHeaderDocsLink href={docs("/admin/external-auth")} />,
},
};
@@ -1,74 +1,107 @@
import { useTheme } from "@emotion/react";
import { type VariantProps, cva } from "class-variance-authority";
import { Button } from "components/Button/Button";
import { Stack } from "components/Stack/Stack";
import { SquareArrowOutUpRightIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import type { FC, PropsWithChildren, ReactNode } from "react";
import { cn } from "utils/cn";
interface HeaderProps {
title: ReactNode;
description?: ReactNode;
secondary?: boolean;
docsHref?: string;
tooltip?: ReactNode;
}
export const SettingsHeader: FC<HeaderProps> = ({
title,
description,
docsHref,
secondary,
tooltip,
type SettingsHeaderProps = Readonly<
PropsWithChildren<{
actions?: ReactNode;
className?: string;
}>
>;
export const SettingsHeader: FC<SettingsHeaderProps> = ({
children,
actions,
className,
}) => {
const theme = useTheme();
return (
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
<div css={{ maxWidth: 420, marginBottom: 24 }}>
<Stack direction="row" spacing={1} alignItems="center">
<h1
css={[
{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "baseline",
lineHeight: "initial",
margin: 0,
marginBottom: 4,
gap: 8,
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
>
{title}
</h1>
{tooltip}
</Stack>
<hgroup className="flex flex-col justify-between items-start gap-2 pb-6 sm:flex-row">
{/*
* 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", className)}>{children}</div>
{actions}
</hgroup>
);
};
{description && (
<span
css={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
}}
>
{description}
</span>
)}
</div>
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>
);
};
{docsHref && (
<Button asChild variant="outline">
<a href={docsHref} target="_blank" rel="noreferrer">
<SquareArrowOutUpRightIcon />
Read the docs
</a>
</Button>
)}
</Stack>
const titleVariants = cva("m-0 pb-1 flex items-center gap-2 leading-tight", {
variants: {
hierarchy: {
primary: "text-3xl font-bold",
secondary: "text-2xl font-medium",
},
},
defaultVariants: {
hierarchy: "primary",
},
});
type SettingsHeaderTitleProps = Readonly<
PropsWithChildren<
VariantProps<typeof titleVariants> & {
level?: `h${1 | 2 | 3 | 4 | 5 | 6}`;
tooltip?: ReactNode;
className?: string;
}
>
>;
export const SettingsHeaderTitle: FC<SettingsHeaderTitleProps> = ({
children,
tooltip,
className,
level = "h1",
hierarchy = "primary",
}) => {
// 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
const Title = level;
return (
<div className="flex flex-row gap-2 items-center">
<Title className={cn(titleVariants({ hierarchy }), className)}>
{children}
</Title>
{tooltip}
</div>
);
};
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>
);
};
@@ -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,
@@ -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">
@@ -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";
@@ -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
@@ -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";
@@ -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 />
@@ -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";
@@ -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
@@ -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 {
@@ -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"),
@@ -33,11 +42,20 @@ export const NetworkSettingsPageView: FC<NetworkSettingsPageViewProps> = ({
<div>
<SettingsHeader
title="Port Forwarding"
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 !==
@@ -6,7 +6,12 @@ import {
} from "api/queries/notifications";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
import { useDeploymentConfig } from "modules/management/DeploymentConfigProvider";
@@ -43,22 +48,24 @@ export const NotificationsPage: FC = () => {
<Helmet>
<title>{pageTitle("Notifications Settings")}</title>
</Helmet>
<SettingsHeader
title={
<>
Notifications
<span css={{ position: "relative", top: "-6px" }}>
<FeatureStageBadge
contentType={"beta"}
size="lg"
css={{ marginBottom: "5px", fontSize: "0.75rem" }}
/>
</span>
</>
actions={
<SettingsHeaderDocsLink
href={docs("/admin/monitoring/notifications")}
/>
}
description="Control delivery methods for notifications on this deployment."
docsHref={docs("/admin/monitoring/notifications")}
/>
>
<SettingsHeaderTitle
tooltip={<FeatureStageBadge contentType="beta" size="lg" />}
>
Notifications
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Control delivery methods for notifications on this deployment.
</SettingsHeaderDescription>
</SettingsHeader>
<Tabs active={tabState.value}>
<TabsList>
<TabLink to="?tab=events" value="events">
@@ -1,7 +1,11 @@
import type * as TypesGen from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Button } from "components/Button/Button";
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";
@@ -26,10 +30,13 @@ export const CreateOAuth2AppPageView: FC<CreateOAuth2AppProps> = ({
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="Add an OAuth2 application"
description="Configure an application to use Coder as an OAuth2 provider."
/>
<SettingsHeader>
<SettingsHeaderTitle>Add an OAuth2 application</SettingsHeaderTitle>
<SettingsHeaderDescription>
Configure an application to use Coder as an OAuth2 provider.
</SettingsHeaderDescription>
</SettingsHeader>
<Button variant="outline" asChild>
<RouterLink to="/deployment/oauth2-provider/apps">
<ChevronLeftIcon />
@@ -17,7 +17,11 @@ import { CopyableValue } from "components/CopyableValue/CopyableValue";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { TableLoader } from "components/TableLoader/TableLoader";
import { ChevronLeftIcon } from "lucide-react";
@@ -75,10 +79,13 @@ export const EditOAuth2AppPageView: FC<EditOAuth2AppProps> = ({
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="Edit OAuth2 application"
description="Configure an application to use Coder as an OAuth2 provider."
/>
<SettingsHeader>
<SettingsHeaderTitle>Edit OAuth2 application</SettingsHeaderTitle>
<SettingsHeaderDescription>
Configure an application to use Coder as an OAuth2 provider.
</SettingsHeaderDescription>
</SettingsHeader>
<Button variant="outline" asChild>
<RouterLink to="/deployment/oauth2-provider/apps">
<ChevronLeftIcon />
@@ -11,7 +11,11 @@ import TableRow from "@mui/material/TableRow";
import type * as TypesGen from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Avatar } from "components/Avatar/Avatar";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { TableLoader } from "components/TableLoader/TableLoader";
import { useClickableTableRow } from "hooks/useClickableTableRow";
@@ -37,10 +41,12 @@ const OAuth2AppsSettingsPageView: FC<OAuth2AppsSettingsProps> = ({
justifyContent="space-between"
>
<div>
<SettingsHeader
title="OAuth2 Applications"
description="Configure applications to use Coder as an OAuth2 provider."
/>
<SettingsHeader>
<SettingsHeaderTitle>OAuth2 Applications</SettingsHeaderTitle>
<SettingsHeaderDescription>
Configure applications to use Coder as an OAuth2 provider.
</SettingsHeaderDescription>
</SettingsHeader>
</div>
<Button
@@ -5,7 +5,12 @@ import {
PremiumBadge,
} from "components/Badges/Badges";
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderDocsLink,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import {
Popover,
@@ -30,13 +35,24 @@ export const ObservabilitySettingsPageView: FC<
<>
<Stack direction="column" spacing={6}>
<div>
<SettingsHeader title="Observability" />
<SettingsHeader>
<SettingsHeaderTitle>Observability</SettingsHeaderTitle>
</SettingsHeader>
<SettingsHeader
title="Audit Logging"
secondary
description="Allow auditors to monitor user operations in your deployment."
docsHref={docs("/admin/security/audit-logs")}
/>
actions={
<SettingsHeaderDocsLink
href={docs("/admin/security/audit-logs")}
/>
}
>
<SettingsHeaderTitle hierarchy="secondary" level="h2">
Audit Logging
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Allow auditors to monitor user operations in your deployment.
</SettingsHeaderDescription>
</SettingsHeader>
<Badges>
<Popover mode="hover">
@@ -62,11 +78,14 @@ export const ObservabilitySettingsPageView: FC<
</div>
<div>
<SettingsHeader
title="Monitoring"
secondary
description="Monitoring your Coder application with logs and metrics."
/>
<SettingsHeader>
<SettingsHeaderTitle hierarchy="secondary" level="h2">
Monitoring
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Monitoring your Coder application with logs and metrics.
</SettingsHeaderDescription>
</SettingsHeader>
<OptionsTable
options={options.filter((o) =>
@@ -5,7 +5,12 @@ import type {
SerpentOption,
} from "api/typesGenerated";
import { Link } from "components/Link/Link";
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 { useDeploymentOptions } from "utils/deployOptions";
@@ -30,10 +35,14 @@ export const OverviewPageView: FC<OverviewPageViewProps> = ({
return (
<>
<SettingsHeader
title="General"
description="Information about your Coder deployment."
docsHref={docs("/admin/setup")}
/>
actions={<SettingsHeaderDocsLink href={docs("/admin/setup")} />}
>
<SettingsHeaderTitle>General</SettingsHeaderTitle>
<SettingsHeaderDescription>
Information about your Coder deployment.
</SettingsHeaderDescription>
</SettingsHeader>
<Stack spacing={4}>
<UserEngagementChart
data={dailyActiveUsers?.entries.map((i) => ({
@@ -5,7 +5,12 @@ import {
EnabledBadge,
PremiumBadge,
} 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 {
@@ -31,10 +36,12 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
return (
<Stack direction="column" spacing={6}>
<div>
<SettingsHeader
title="Security"
description="Ensure your Coder deployment is secure."
/>
<SettingsHeader>
<SettingsHeaderTitle>Security</SettingsHeaderTitle>
<SettingsHeaderDescription>
Ensure your Coder deployment is secure.
</SettingsHeaderDescription>
</SettingsHeader>
<OptionsTable
options={useDeploymentOptions(
@@ -48,11 +55,20 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
<div>
<SettingsHeader
title="Browser Only Connections"
secondary
description="Block all workspace access via SSH, port forward, and other non-browser connections."
docsHref={docs("/admin/networking#browser-only-connections")}
/>
actions={
<SettingsHeaderDocsLink
href={docs("/admin/networking#browser-only-connections")}
/>
}
>
<SettingsHeaderTitle level="h2" hierarchy="secondary">
Browser-Only Connections
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Block all workspace access via SSH, port forward, and other
non-browser connections.
</SettingsHeaderDescription>
</SettingsHeader>
<Badges>
{featureBrowserOnlyEnabled ? <EnabledBadge /> : <DisabledBadge />}
@@ -62,11 +78,14 @@ export const SecuritySettingsPageView: FC<SecuritySettingsPageViewProps> = ({
{tlsOptions.length > 0 && (
<div>
<SettingsHeader
title="TLS"
secondary
description="Ensure TLS is properly configured for your Coder deployment."
/>
<SettingsHeader>
<SettingsHeaderTitle level="h2" hierarchy="secondary">
TLS
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Ensure TLS is properly configured for your Coder deployment.
</SettingsHeaderDescription>
</SettingsHeader>
<OptionsTable options={tlsOptions} />
</div>
@@ -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 {
deploymentGroupHasParent,
@@ -27,14 +32,24 @@ export const UserAuthSettingsPageView = ({
<>
<Stack direction="column" spacing={6}>
<div>
<SettingsHeader title="User Authentication" />
<SettingsHeader>
<SettingsHeaderTitle>User Authentication</SettingsHeaderTitle>
</SettingsHeader>
<SettingsHeader
title="Login with OpenID Connect"
secondary
description="Set up authentication to login with OpenID Connect."
docsHref={docs("/admin/users/oidc-auth#openid-connect")}
/>
actions={
<SettingsHeaderDocsLink
href={docs("/admin/users/oidc-auth#openid-connect")}
/>
}
>
<SettingsHeaderTitle level="h2" hierarchy="secondary">
Login with OpenID Connect
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Set up authentication to login with OpenID Connect.
</SettingsHeaderDescription>
</SettingsHeader>
<Badges>{oidcEnabled ? <EnabledBadge /> : <DisabledBadge />}</Badges>
@@ -49,11 +64,17 @@ export const UserAuthSettingsPageView = ({
<div>
<SettingsHeader
title="Login with GitHub"
secondary
description="Set up authentication to login with GitHub."
docsHref={docs("/admin/users/github-auth")}
/>
actions={
<SettingsHeaderDocsLink href={docs("/admin/users/github-auth")} />
}
>
<SettingsHeaderTitle level="h2" hierarchy="secondary">
Login with GitHub
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Set up authentication to login with GitHub.
</SettingsHeaderDescription>
</SettingsHeader>
<Badges>
{githubEnabled ? <EnabledBadge /> : <DisabledBadge />}
@@ -10,7 +10,11 @@ import {
HorizontalForm,
} from "components/Form/Form";
import { IconField } from "components/IconField/IconField";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Spinner } from "components/Spinner/Spinner";
import { useFormik } from "formik";
import type { FC } from "react";
@@ -53,10 +57,12 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
return (
<>
<SettingsHeader
title="New Group"
description="Create a group in this organization."
/>
<SettingsHeader>
<SettingsHeaderTitle>New Group</SettingsHeaderTitle>
<SettingsHeaderDescription>
Create a group in this organization.
</SettingsHeaderDescription>
</SettingsHeader>
<HorizontalForm onSubmit={form.handleSubmit}>
<FormSection
+14 -5
View File
@@ -32,7 +32,11 @@ import {
MoreMenuTrigger,
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import {
Table,
@@ -106,10 +110,15 @@ export const GroupPage: FC = () => {
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title={groupData?.display_name || groupData?.name}
description="Manage members for this group."
/>
<SettingsHeader>
<SettingsHeaderTitle>
{groupData?.display_name || groupData?.name || "Unknown Group"}
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Manage members for this group.
</SettingsHeaderDescription>
</SettingsHeader>
{canUpdateGroup && (
<Stack direction="row" spacing={2}>
<Button
+13 -5
View File
@@ -6,7 +6,11 @@ import { Button } from "components/Button/Button";
import { EmptyState } from "components/EmptyState/EmptyState";
import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "modules/permissions/RequirePermission";
@@ -80,10 +84,14 @@ export const GroupsPage: FC = () => {
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="Groups"
description={`Manage groups for this ${showOrganizations ? "organization" : "deployment"}.`}
/>
<SettingsHeader>
<SettingsHeaderTitle>Groups</SettingsHeaderTitle>
<SettingsHeaderDescription>
Manage groups for this{" "}
{showOrganizations ? "organization" : "deployment"}.
</SettingsHeaderDescription>
</SettingsHeader>
{groupsEnabled && permissions.createGroup && (
<Button asChild>
<RouterLink to="create">
@@ -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>
@@ -24,7 +24,11 @@ import type {
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Button } from "components/Button/Button";
import { FormFields, FormFooter, VerticalForm } from "components/Form/Form";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import { useFormik } from "formik";
@@ -78,10 +82,15 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title={`${role ? "Edit" : "Create"} Custom Role`}
description="Set a name and permissions for this role."
/>
<SettingsHeader>
<SettingsHeaderTitle>
{role ? "Edit" : "Create"} Custom Role
</SettingsHeaderTitle>
<SettingsHeaderDescription>
Set a name and permissions for this role.
</SettingsHeaderDescription>
</SettingsHeader>
<div className="flex space-x-2 items-center">
<Button
variant="outline"
@@ -4,7 +4,11 @@ import type { Role } from "api/typesGenerated";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { EmptyState } from "components/EmptyState/EmptyState";
import { displayError, displaySuccess } 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 { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
@@ -71,10 +75,12 @@ export const CustomRolesPage: FC = () => {
direction="row"
justifyContent="space-between"
>
<SettingsHeader
title="Roles"
description="Manage roles for this organization."
/>
<SettingsHeader>
<SettingsHeaderTitle>Roles</SettingsHeaderTitle>
<SettingsHeaderDescription>
Manage roles for this organization.
</SettingsHeaderDescription>
</SettingsHeader>
</Stack>
<CustomRolesPageView
@@ -19,7 +19,10 @@ import {
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import { PaginationContainer } from "components/PaginationWidget/PaginationContainer";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import {
Table,
@@ -79,7 +82,10 @@ export const OrganizationMembersPageView: FC<
}) => {
return (
<div>
<SettingsHeader title="Members" />
<SettingsHeader>
<SettingsHeaderTitle>Members</SettingsHeaderTitle>
</SettingsHeader>
<div className="flex flex-col gap-4">
{Boolean(error) && <ErrorAlert error={error} />}
@@ -9,7 +9,10 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Loader } from "components/Loader/Loader";
import { Paywall } from "components/Paywall/Paywall";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
import type { FC } from "react";
@@ -39,7 +42,10 @@ export const OrganizationProvisionersPageView: FC<
direction="row"
justifyContent="space-between"
>
<SettingsHeader title="Provisioners" />
<SettingsHeader>
<SettingsHeaderTitle>Provisioners</SettingsHeaderTitle>
</SettingsHeader>
{!showPaywall && (
<Button
endIcon={<OpenInNewIcon />}
@@ -14,7 +14,10 @@ import {
HorizontalForm,
} from "components/Form/Form";
import { IconField } from "components/IconField/IconField";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Spinner } from "components/Spinner/Spinner";
import { useFormik } from "formik";
import { type FC, useState } from "react";
@@ -66,7 +69,10 @@ export const OrganizationSettingsPageView: FC<
return (
<div>
<SettingsHeader title="Settings" />
<SettingsHeader>
<SettingsHeaderTitle>Settings</SettingsHeaderTitle>
</SettingsHeader>
{Boolean(error) && !isApiValidationError(error) && (
<div css={{ marginBottom: 32 }}>
<ErrorAlert error={error} />
@@ -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,
@@ -17,29 +13,14 @@ export const WorkspaceProxyPage: FC = () => {
} = useProxy();
return (
<Section
title="Workspace Proxies"
css={(theme) => ({
"& code": {
background: theme.palette.divider,
fontSize: 12,
padding: "2px 4px",
color: theme.palette.text.primary,
borderRadius: 2,
},
})}
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}
/>
);
};
@@ -7,6 +7,11 @@ 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,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { TableEmpty } from "components/TableEmpty/TableEmpty";
import { TableLoader } from "components/TableLoader/TableLoader";
@@ -34,6 +39,14 @@ export const WorkspaceProxyView: FC<WorkspaceProxyViewProps> = ({
}) => {
return (
<Stack>
<SettingsHeader>
<SettingsHeaderTitle>Workspace Proxies</SettingsHeaderTitle>
<SettingsHeaderDescription>
Workspace proxies improve terminal and web app connections to
workspaces.
</SettingsHeaderDescription>
</SettingsHeader>
{Boolean(getWorkspaceProxiesError) && (
<ErrorAlert error={getWorkspaceProxiesError} />
)}
+21 -18
View File
@@ -5,7 +5,11 @@ import {
PaginationContainer,
type PaginationResult,
} from "components/PaginationWidget/PaginationContainer";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import { UserPlusIcon } from "lucide-react";
import type { ComponentProps, FC } from "react";
@@ -68,24 +72,23 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
}) => {
return (
<>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
<SettingsHeader
actions={
canCreateUser && (
<Button asChild>
<RouterLink to="create">
<UserPlusIcon />
Create user
</RouterLink>
</Button>
)
}
>
<SettingsHeader
title="Users"
description="Manage user accounts and permissions."
/>
{canCreateUser && (
<Button asChild>
<RouterLink to="create">
<UserPlusIcon />
Create user
</RouterLink>
</Button>
)}
</Stack>
<SettingsHeaderTitle>Users</SettingsHeaderTitle>
<SettingsHeaderDescription>
Manage user accounts and permissions.
</SettingsHeaderDescription>
</SettingsHeader>
<UsersFilter {...filterProps} />