mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: remove some usage of useClassName (#19346)
This commit is contained in:
Vendored
+2
@@ -8,6 +8,7 @@ import type {
|
||||
} from "api/typesGenerated";
|
||||
import type { Permissions } from "modules/permissions";
|
||||
import type { QueryKey } from "react-query";
|
||||
import type { ReactRouterAddonStoryParameters } from "storybook-addon-remix-react-router";
|
||||
|
||||
declare module "@storybook/react-vite" {
|
||||
type WebSocketEvent =
|
||||
@@ -24,5 +25,6 @@ declare module "@storybook/react-vite" {
|
||||
permissions?: Partial<Permissions>;
|
||||
deploymentValues?: DeploymentValues;
|
||||
deploymentOptions?: SerpentOption[];
|
||||
reactRouter?: ReactRouterAddonStoryParameters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Snackbar, {
|
||||
type SnackbarProps as MuiSnackbarProps,
|
||||
} from "@mui/material/Snackbar";
|
||||
import { type ClassName, useClassName } from "hooks/useClassName";
|
||||
import { X as XIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
type EnterpriseSnackbarVariant = "error" | "info" | "success";
|
||||
|
||||
@@ -35,8 +34,6 @@ export const EnterpriseSnackbar: FC<EnterpriseSnackbarProps> = ({
|
||||
action,
|
||||
...snackbarProps
|
||||
}) => {
|
||||
const content = useClassName(classNames.content(variant), [variant]);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
@@ -44,20 +41,23 @@ export const EnterpriseSnackbar: FC<EnterpriseSnackbarProps> = ({
|
||||
horizontal: "right",
|
||||
}}
|
||||
action={
|
||||
<div css={styles.actionWrapper}>
|
||||
<div className="flex items-center">
|
||||
{action}
|
||||
<IconButton onClick={onClose} css={{ padding: 0 }}>
|
||||
<IconButton onClick={onClose} className="p-0">
|
||||
<XIcon
|
||||
css={styles.closeIcon}
|
||||
aria-label="close"
|
||||
className="size-icon-sm"
|
||||
className="size-icon-sm text-content-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
ContentProps={{
|
||||
...ContentProps,
|
||||
className: content,
|
||||
className: cn(
|
||||
"rounded-lg bg-surface-secondary text-content-primary shadow",
|
||||
"py-2 pl-6 pr-4 items-[inherit] border-0 border-l-[4px]",
|
||||
variantColor(variant),
|
||||
),
|
||||
}}
|
||||
onClose={onClose}
|
||||
{...snackbarProps}
|
||||
@@ -67,39 +67,13 @@ export const EnterpriseSnackbar: FC<EnterpriseSnackbarProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const variantColor = (variant: EnterpriseSnackbarVariant, theme: Theme) => {
|
||||
const variantColor = (variant: EnterpriseSnackbarVariant) => {
|
||||
switch (variant) {
|
||||
case "error":
|
||||
return theme.palette.error.main;
|
||||
return "border-border-destructive";
|
||||
case "info":
|
||||
return theme.palette.info.main;
|
||||
return "border-highlight-sky";
|
||||
case "success":
|
||||
return theme.palette.success.main;
|
||||
return "border-border-success";
|
||||
}
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
content:
|
||||
(variant: EnterpriseSnackbarVariant): ClassName =>
|
||||
(css, theme) =>
|
||||
css`
|
||||
border: 1px solid ${theme.palette.divider};
|
||||
border-left: 4px solid ${variantColor(variant, theme)};
|
||||
border-radius: 8px;
|
||||
padding: 8px 24px 8px 16px;
|
||||
box-shadow: ${theme.shadows[6]};
|
||||
align-items: inherit;
|
||||
background-color: ${theme.palette.background.paper};
|
||||
color: ${theme.palette.text.secondary};
|
||||
`,
|
||||
};
|
||||
|
||||
const styles = {
|
||||
actionWrapper: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
closeIcon: (theme) => ({
|
||||
color: theme.palette.primary.contrastText,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LockIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { Outlet } from "react-router";
|
||||
import { Sidebar, SidebarHeader, SidebarNavItem } from "./Sidebar";
|
||||
|
||||
const meta: Meta<typeof Sidebar> = {
|
||||
@@ -18,30 +19,73 @@ export default meta;
|
||||
type Story = StoryObj<typeof Sidebar>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<Sidebar>
|
||||
<SidebarHeader
|
||||
avatar={<Avatar fallback="Jon" />}
|
||||
title="Jon"
|
||||
subtitle="jon@coder.com"
|
||||
/>
|
||||
<SidebarNavItem href="account" icon={UserIcon}>
|
||||
Account
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="schedule" icon={CalendarCogIcon}>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="security" icon={LockIcon}>
|
||||
Security
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="ssh-keys" icon={FingerprintIcon}>
|
||||
SSH Keys
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="tokens" icon={KeyIcon}>
|
||||
Tokens
|
||||
</SidebarNavItem>
|
||||
</Sidebar>
|
||||
),
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Story />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
render: () => (
|
||||
<Sidebar>
|
||||
<SidebarHeader
|
||||
avatar={<Avatar fallback="Jon" />}
|
||||
title="Jon"
|
||||
subtitle="jon@coder.com"
|
||||
/>
|
||||
<SidebarNavItem href="account" icon={UserIcon}>
|
||||
Account
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="schedule" icon={CalendarCogIcon}>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="security" icon={LockIcon}>
|
||||
Security
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="ssh-keys" icon={FingerprintIcon}>
|
||||
SSH Keys
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="tokens" icon={KeyIcon}>
|
||||
Tokens
|
||||
</SidebarNavItem>
|
||||
</Sidebar>
|
||||
),
|
||||
parameters: {
|
||||
reactRouter: {
|
||||
location: {
|
||||
path: "/account",
|
||||
},
|
||||
routing: [
|
||||
{
|
||||
path: "/",
|
||||
useStoryElement: true,
|
||||
children: [
|
||||
{
|
||||
path: "account",
|
||||
element: <>Account page</>,
|
||||
},
|
||||
{
|
||||
path: "schedule",
|
||||
element: <>Schedule page</>,
|
||||
},
|
||||
{
|
||||
path: "security",
|
||||
element: <>Security page</>,
|
||||
},
|
||||
{
|
||||
path: "ssh-keys",
|
||||
element: <>SSH Keys</>,
|
||||
},
|
||||
{
|
||||
path: "tokens",
|
||||
element: <>Tokens page</>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { cx } from "@emotion/css";
|
||||
import type { CSSObject, Interpolation, Theme } from "@emotion/react";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { type ClassName, useClassName } from "hooks/useClassName";
|
||||
import type { ElementType, FC, ReactNode } from "react";
|
||||
import { Link, NavLink } from "react-router";
|
||||
import { cn } from "utils/cn";
|
||||
@@ -21,6 +18,11 @@ interface SidebarHeaderProps {
|
||||
linkTo?: string;
|
||||
}
|
||||
|
||||
const titleStyles = {
|
||||
normal:
|
||||
"text-semibold overflow-hidden whitespace-nowrap text-content-primary",
|
||||
};
|
||||
|
||||
export const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||
avatar,
|
||||
title,
|
||||
@@ -28,7 +30,7 @@ export const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||
linkTo,
|
||||
}) => {
|
||||
return (
|
||||
<Stack direction="row" spacing={1} css={styles.info}>
|
||||
<Stack direction="row" spacing={1} className="mb-4">
|
||||
{avatar}
|
||||
<div
|
||||
css={{
|
||||
@@ -38,13 +40,15 @@ export const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||
}}
|
||||
>
|
||||
{linkTo ? (
|
||||
<Link css={styles.title} to={linkTo}>
|
||||
<Link className={cn(titleStyles.normal, "no-underline")} to={linkTo}>
|
||||
{title}
|
||||
</Link>
|
||||
) : (
|
||||
<span css={styles.title}>{title}</span>
|
||||
<span className={titleStyles.normal}>{title}</span>
|
||||
)}
|
||||
<span css={styles.subtitle}>{subtitle}</span>
|
||||
<span className="text-content-secondary text-sm overflow-hidden overflow-ellipsis">
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
@@ -88,14 +92,18 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
|
||||
href,
|
||||
icon: Icon,
|
||||
}) => {
|
||||
const link = useClassName(classNames.link, []);
|
||||
const activeLink = useClassName(classNames.activeLink, []);
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
end
|
||||
to={href}
|
||||
className={({ isActive }) => cx([link, isActive && activeLink])}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"block relative text-sm text-inherit mb-px p-3 pl-4 rounded-sm",
|
||||
"transition-colors no-underline hover:bg-surface-secondary",
|
||||
isActive &&
|
||||
"bg-surface-secondary border-0 border-solid border-l-[3px] border-highlight-sky",
|
||||
)
|
||||
}
|
||||
>
|
||||
<Stack alignItems="center" spacing={1.5} direction="row">
|
||||
<Icon css={{ width: 16, height: 16 }} />
|
||||
@@ -104,60 +112,3 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
info: (theme) => ({
|
||||
...(theme.typography.body2 as CSSObject),
|
||||
marginBottom: 16,
|
||||
}),
|
||||
|
||||
title: (theme) => ({
|
||||
fontWeight: 600,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: "none",
|
||||
}),
|
||||
subtitle: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
const classNames = {
|
||||
link: (css, theme) => css`
|
||||
color: inherit;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
padding: 12px 12px 12px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
margin-bottom: 1px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.palette.action.hover};
|
||||
}
|
||||
`,
|
||||
|
||||
activeLink: (css, theme) => css`
|
||||
background-color: ${theme.palette.action.hover};
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: ${theme.palette.primary.main};
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
`,
|
||||
} satisfies Record<string, ClassName>;
|
||||
|
||||
@@ -5,9 +5,9 @@ import { type DependencyList, useMemo } from "react";
|
||||
export type ClassName = (cssFn: typeof css, theme: Theme) => string;
|
||||
|
||||
/**
|
||||
* An escape hatch for when you really need to manually pass around a
|
||||
* `className`. Prefer using the `css` prop whenever possible. If you
|
||||
* can't use that, then this might be helpful for you.
|
||||
* @deprecated This hook was used as an escape hatch to generate class names
|
||||
* using emotion when no other styling method would work. There is no valid new
|
||||
* usage of this hook. Use Tailwind classes instead.
|
||||
*/
|
||||
export function useClassName(styles: ClassName, deps: DependencyList): string {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cx } from "@emotion/css";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import NotificationsOffOutlined from "@mui/icons-material/NotificationsOffOutlined";
|
||||
import ReplayIcon from "@mui/icons-material/Replay";
|
||||
@@ -9,17 +8,26 @@ import { health, refreshHealth } from "api/queries/debug";
|
||||
import type { HealthSeverity } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { type ClassName, useClassName } from "hooks/useClassName";
|
||||
import kebabCase from "lodash/fp/kebabCase";
|
||||
import { DashboardFullPage } from "modules/dashboard/DashboardLayout";
|
||||
import { type FC, Suspense } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { NavLink, Outlet } from "react-router";
|
||||
import { cn } from "utils/cn";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { HealthIcon } from "./Content";
|
||||
|
||||
const linkStyles = {
|
||||
normal: `
|
||||
text-content-secondary border-none text-sm w-full flex items-center gap-3
|
||||
text-left h-9 px-6 cursor-pointer no-underline transition-colors
|
||||
hover:bg-surface-secondary hover:text-content-primary
|
||||
`,
|
||||
active: "bg-surface-secondary text-content-primary",
|
||||
};
|
||||
|
||||
export const HealthLayout: FC = () => {
|
||||
const theme = useTheme();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -44,9 +52,6 @@ export const HealthLayout: FC = () => {
|
||||
} as const;
|
||||
const visibleSections = filterVisibleSections(sections);
|
||||
|
||||
const link = useClassName(classNames.link, []);
|
||||
const activeLink = useClassName(classNames.activeLink, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -70,38 +75,11 @@ export const HealthLayout: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<DashboardFullPage>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexBasis: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
width: 256,
|
||||
flexShrink: 0,
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div className="flex basis-0 flex-1 overflow-hidden">
|
||||
<div className="w-64 shrink-0 text-sm border-0 border-solid border-r border-r-border">
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
<div>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<HealthIcon size={32} severity={healthStatus.severity} />
|
||||
|
||||
<Tooltip title="Refresh health checks">
|
||||
@@ -116,20 +94,15 @@ export const HealthLayout: FC = () => {
|
||||
{isRefreshing ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<ReplayIcon css={{ width: 20, height: 20 }} />
|
||||
<ReplayIcon className="size-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div css={{ fontWeight: 500, marginTop: 16 }}>
|
||||
<div className="font-medium mt-4">
|
||||
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
<div className="text-content-secondary line-height-[150%]">
|
||||
{healthStatus.healthy
|
||||
? Object.keys(visibleSections).some((key) => {
|
||||
const section =
|
||||
@@ -142,34 +115,28 @@ export const HealthLayout: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<span css={{ fontWeight: 500 }}>Last check</span>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Last check</span>
|
||||
<span
|
||||
data-chromatic="ignore"
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
className="text-content-secondary line-height-[150%]"
|
||||
>
|
||||
{createDayString(healthStatus.time)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<span css={{ fontWeight: 500 }}>Version</span>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Version</span>
|
||||
<span
|
||||
data-chromatic="ignore"
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
className="text-content-secondary line-height-[150%]"
|
||||
>
|
||||
{healthStatus.coder_version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav css={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<nav className="flex flex-col gap-px">
|
||||
{Object.entries(visibleSections)
|
||||
.sort()
|
||||
.map(([key, label]) => {
|
||||
@@ -182,7 +149,7 @@ export const HealthLayout: FC = () => {
|
||||
key={key}
|
||||
to={`/health/${kebabCase(key)}`}
|
||||
className={({ isActive }) =>
|
||||
cx([link, isActive && activeLink])
|
||||
cn(linkStyles.normal, isActive && linkStyles.active)
|
||||
}
|
||||
>
|
||||
<HealthIcon
|
||||
@@ -205,7 +172,7 @@ export const HealthLayout: FC = () => {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div css={{ overflowY: "auto", width: "100%" }}>
|
||||
<div className="overflow-y-auto w-full">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet context={healthStatus} />
|
||||
</Suspense>
|
||||
@@ -229,35 +196,3 @@ const filterVisibleSections = <T extends object>(sections: T) => {
|
||||
|
||||
return visible;
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
link: (css, theme) =>
|
||||
css({
|
||||
background: "none",
|
||||
pointerEvents: "auto",
|
||||
color: theme.palette.text.secondary,
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
textAlign: "left",
|
||||
height: 36,
|
||||
padding: "0 24px",
|
||||
cursor: "pointer",
|
||||
textDecoration: "none",
|
||||
|
||||
"&:hover": {
|
||||
background: theme.palette.action.hover,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}),
|
||||
|
||||
activeLink: (css, theme) =>
|
||||
css({
|
||||
background: theme.palette.action.hover,
|
||||
pointerEvents: "none",
|
||||
color: theme.palette.text.primary,
|
||||
}),
|
||||
} satisfies Record<string, ClassName>;
|
||||
|
||||
Reference in New Issue
Block a user