From 44c64360a6f53faed27d5caac291acf3f975e615 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 20 Nov 2023 21:49:17 +0000 Subject: [PATCH 1/4] refactor: avoid @emotion/css when possible --- .../DeploymentBanner/DeploymentBannerView.tsx | 158 ++++++----- .../ErrorBoundary/RuntimeErrorState.tsx | 33 +-- .../GlobalSnackbar/EnterpriseSnackbar.tsx | 53 ++-- .../components/HelpTooltip/HelpTooltip.tsx | 181 ++++++++----- .../components/InfoTooltip/InfoTooltip.tsx | 33 +-- .../Resources/PortForwardButton.tsx | 249 ++++++++++-------- .../Resources/SSHButton/SSHButton.tsx | 33 ++- site/src/components/Tabs/Tabs.tsx | 57 ++-- .../UserAutocomplete/UserAutocomplete.tsx | 17 +- .../WorkspaceOutdatedTooltip.tsx | 92 ++++--- site/src/hooks/useClassName.ts | 21 ++ .../pages/CreateTokenPage/CreateTokenForm.tsx | 16 +- .../MissingTemplateVariablesDialog.tsx | 22 +- .../UsersPage/UsersTable/EditRolesButton.tsx | 36 ++- .../WorkspacePage/ChangeVersionDialog.tsx | 21 +- .../UpdateBuildParametersDialog.tsx | 22 +- .../WorkspacePage/WorkspaceBuildProgress.tsx | 12 +- .../pages/WorkspacePage/WorkspaceStats.tsx | 75 +++--- site/src/utils/combineClasses.test.ts | 31 --- site/src/utils/combineClasses.ts | 47 ---- 20 files changed, 631 insertions(+), 578 deletions(-) create mode 100644 site/src/hooks/useClassName.ts delete mode 100644 site/src/utils/combineClasses.test.ts delete mode 100644 site/src/utils/combineClasses.ts diff --git a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx index 3fea96bc4cc4f..16a01a15c3ac3 100644 --- a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx @@ -5,10 +5,10 @@ import type { } from "api/typesGenerated"; import { type FC, + type PropsWithChildren, useMemo, useEffect, useState, - PropsWithChildren, } from "react"; import prettyBytes from "pretty-bytes"; import BuildingIcon from "@mui/icons-material/Build"; @@ -23,7 +23,6 @@ import WebTerminalIcon from "@mui/icons-material/WebAsset"; import CollectedIcon from "@mui/icons-material/Compare"; import RefreshIcon from "@mui/icons-material/Refresh"; import Button from "@mui/material/Button"; -import { css as className } from "@emotion/css"; import { css, type CSSObject, @@ -40,44 +39,23 @@ import { getDisplayWorkspaceStatus } from "utils/workspace"; import { colors } from "theme/colors"; import { HelpTooltipTitle } from "components/HelpTooltip/HelpTooltip"; import { Stack } from "components/Stack/Stack"; +import { type ClassName, useClassName } from "hooks/useClassName"; export const bannerHeight = 36; -const styles = { - group: css` - display: flex; - align-items: center; - `, - category: (theme) => ({ - marginRight: 16, - color: theme.palette.text.primary, - }), - values: (theme) => ({ - display: "flex", - gap: 8, - color: theme.palette.text.secondary, - }), - value: css` - display: flex; - align-items: center; - gap: 4px; - - & svg { - width: 12px; - height: 12px; - } - `, -} satisfies Record>; - export interface DeploymentBannerViewProps { health?: HealthcheckReport; stats?: DeploymentStats; fetchStats?: () => void; } -export const DeploymentBannerView: FC = (props) => { - const { health, stats, fetchStats } = props; +export const DeploymentBannerView: FC = ({ + health, + stats, + fetchStats, +}) => { const theme = useTheme(); + const summaryTooltip = useClassName(classNames.summaryTooltip, []); const aggregatedMinutes = useMemo(() => { if (!stats) { @@ -114,6 +92,7 @@ export const DeploymentBannerView: FC = (props) => { clearTimeout(timeout); }; }, [fetchStats, stats]); + const lastAggregated = useMemo(() => { if (!stats) { return; @@ -127,34 +106,6 @@ export const DeploymentBannerView: FC = (props) => { }, [timeUntilRefresh, stats]); const unhealthy = health && !health.healthy; - - const statusBadgeStyle = css` - display: flex; - align-items: center; - justify-content: center; - background-color: ${unhealthy ? colors.red[10] : undefined}; - padding: 0 12px; - height: 100%; - color: #fff; - - & svg { - width: 16px; - height: 16px; - } - `; - - const statusSummaryStyle = className` - ${theme.typography.body2 as CSSObject} - - margin: 0 0 4px 12px; - width: 400px; - padding: 16px; - color: ${theme.palette.text.primary}; - background-color: ${theme.palette.background.paper}; - border: 1px solid ${theme.palette.divider}; - pointer-events: none; - `; - const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1; return ( @@ -178,7 +129,7 @@ export const DeploymentBannerView: FC = (props) => { }} > @@ -214,11 +165,15 @@ export const DeploymentBannerView: FC = (props) => { css={{ marginRight: -16 }} > {unhealthy ? ( - + ) : ( -
+
)} @@ -380,19 +335,15 @@ export const DeploymentBannerView: FC = (props) => { ); }; -const ValueSeparator: FC = () => { - const theme = useTheme(); - const separatorStyles = css` - color: ${theme.palette.text.disabled}; - `; - - return
/
; -}; - -const WorkspaceBuildValue: FC<{ +interface WorkspaceBuildValueProps { status: WorkspaceStatus; count?: number; -}> = ({ status, count }) => { +} + +const WorkspaceBuildValue: FC = ({ + status, + count, +}) => { const displayStatus = getDisplayWorkspaceStatus(status); let statusText = displayStatus.text; let icon = displayStatus.icon; @@ -416,6 +367,10 @@ const WorkspaceBuildValue: FC<{ ); }; +const ValueSeparator: FC = () => { + return
/
; +}; + const HealthIssue: FC = ({ children }) => { return ( @@ -424,3 +379,62 @@ const HealthIssue: FC = ({ children }) => { ); }; + +const classNames = { + summaryTooltip: (css, theme) => css` + ${theme.typography.body2 as CSSObject} + + margin: 0 0 4px 12px; + width: 400px; + padding: 16px; + color: ${theme.palette.text.primary}; + background-color: ${theme.palette.background.paper}; + border: 1px solid ${theme.palette.divider}; + pointer-events: none; + `, +} satisfies Record; + +const styles = { + statusBadge: css` + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + height: 100%; + color: #fff; + + & svg { + width: 16px; + height: 16px; + } + `, + unhealthy: css` + background-color: ${colors.red[10]}; + `, + group: css` + display: flex; + align-items: center; + `, + category: (theme) => ({ + marginRight: 16, + color: theme.palette.text.primary, + }), + values: (theme) => ({ + display: "flex", + gap: 8, + color: theme.palette.text.secondary, + }), + value: css` + display: flex; + align-items: center; + gap: 4px; + + & svg { + width: 12px; + height: 12px; + } + `, + separator: (theme) => ({ + color: theme.palette.text.disabled, + }), +} satisfies Record>; diff --git a/site/src/components/ErrorBoundary/RuntimeErrorState.tsx b/site/src/components/ErrorBoundary/RuntimeErrorState.tsx index 5563cacb2d9e2..a9f4fe085ee0f 100644 --- a/site/src/components/ErrorBoundary/RuntimeErrorState.tsx +++ b/site/src/components/ErrorBoundary/RuntimeErrorState.tsx @@ -3,8 +3,7 @@ import Link from "@mui/material/Link"; import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; -import { css } from "@emotion/css"; -import { type Interpolation, type Theme } from "@emotion/react"; +import { css, type Interpolation, type Theme } from "@emotion/react"; import type { BuildInfoResponse } from "api/typesGenerated"; import { CopyButton } from "components/CopyButton/CopyButton"; import { CoderIcon } from "components/Icons/CoderIcon"; @@ -97,20 +96,7 @@ export const RuntimeErrorState: FC = ({ error }) => {
Stacktrace @@ -209,4 +195,19 @@ const styles = { fontSize: 12, color: theme.palette.text.secondary, }), + + copyButton: css` + background-color: transparent; + border: 0; + border-radius: 999px; + min-height: 32px; + min-width: 32px; + height: 32px; + width: 32px; + + & svg { + width: 16px; + height: 16px; + } + `, } satisfies Record>; diff --git a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx index 634c963423c20..7bec0aed353fd 100644 --- a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx @@ -1,11 +1,11 @@ import IconButton from "@mui/material/IconButton"; import Snackbar, { - SnackbarProps as MuiSnackbarProps, + type SnackbarProps as MuiSnackbarProps, } from "@mui/material/Snackbar"; import CloseIcon from "@mui/icons-material/Close"; import { type FC } from "react"; -import { css } from "@emotion/css"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import { type Interpolation, type Theme } from "@emotion/react"; +import { type ClassName, useClassName } from "hooks/useClassName"; type EnterpriseSnackbarVariant = "error" | "info" | "success"; @@ -27,21 +27,15 @@ export interface EnterpriseSnackbarProps extends MuiSnackbarProps { * * See original component's Material UI documentation here: https://material-ui.com/components/snackbars/ */ -export const EnterpriseSnackbar: FC< - React.PropsWithChildren -> = ({ onClose, variant = "info", ContentProps = {}, action, ...rest }) => { - const theme = useTheme(); - - const snackbarContentStyles = 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}; - `; +export const EnterpriseSnackbar: FC = ({ + children, + onClose, + variant = "info", + ContentProps = {}, + action, + ...snackbarProps +}) => { + const content = useClassName(classNames.content(variant), [variant]); return ( {action} @@ -60,10 +53,13 @@ export const EnterpriseSnackbar: FC< } ContentProps={{ ...ContentProps, - className: snackbarContentStyles, + className: content, }} onClose={onClose} - /> + {...snackbarProps} + > + {children} + ); }; @@ -78,6 +74,21 @@ const variantColor = (variant: EnterpriseSnackbarVariant, theme: Theme) => { } }; +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", diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index ca9647e8be8cf..e5d06d67b6490 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -1,7 +1,7 @@ import Link from "@mui/material/Link"; // This is used as base for the main HelpTooltip component // eslint-disable-next-line no-restricted-imports -- Read above -import Popover, { PopoverProps } from "@mui/material/Popover"; +import Popover, { type PopoverProps } from "@mui/material/Popover"; import HelpIcon from "@mui/icons-material/HelpOutline"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { @@ -9,25 +9,19 @@ import { useContext, useRef, useState, - FC, - PropsWithChildren, + type FC, + type PropsWithChildren, + type HTMLAttributes, + type ReactNode, } from "react"; import { Stack } from "components/Stack/Stack"; -import Box, { BoxProps } from "@mui/material/Box"; -import { type CSSObject, css as className } from "@emotion/css"; +import type { CSSObject } from "@emotion/css"; import { css, type Interpolation, type Theme, useTheme } from "@emotion/react"; +import { type ClassName, useClassName } from "hooks/useClassName"; type Icon = typeof HelpIcon; type Size = "small" | "medium"; -export interface HelpTooltipProps { - // Useful to test on storybook - open?: boolean; - size?: Size; - icon?: Icon; - iconClassName?: string; - buttonClassName?: string; -} export const HelpTooltipContext = createContext< { open: boolean; onClose: () => void } | undefined @@ -45,27 +39,24 @@ const useHelpTooltip = () => { return helpTooltipContext; }; -export const HelpPopover: FC< - PopoverProps & { onOpen: () => void; onClose: () => void } -> = ({ onOpen, onClose, children, ...props }) => { - const theme = useTheme(); +interface HelpPopoverProps extends PopoverProps { + onOpen: () => void; + onClose: () => void; +} + +export const HelpPopover: FC = ({ + onOpen, + onClose, + children, + ...props +}) => { + const popover = useClassName(classNames.popover, []); + const paper = useClassName(classNames.paper, []); return ( > = ({ +export interface HelpTooltipProps { + // Useful to test on storybook + open?: boolean; + size?: Size; + icon?: Icon; + buttonStyles?: Interpolation; + iconStyles?: Interpolation; + children?: ReactNode; +} + +export const HelpTooltip: FC = ({ children, open = false, size = "medium", icon: Icon = HelpIcon, - iconClassName, - buttonClassName, + buttonStyles, + iconStyles, }) => { const theme = useTheme(); const anchorRef = useRef(null); @@ -108,24 +109,26 @@ export const HelpTooltip: FC> = ({ > = ({ ); }; -export const HelpTooltipTitle: FC = ({ children }) => { - return

{children}

; +export const HelpTooltipTitle: FC> = ({ + children, + ...attrs +}) => { + return ( +

+ {children} +

+ ); }; -export const HelpTooltipText = (props: BoxProps) => { - return ; +export const HelpTooltipText: FC> = ({ + children, + ...attrs +}) => { + return ( +

+ {children} +

+ ); }; export const HelpTooltipLink: FC> = ({ @@ -181,13 +200,19 @@ export const HelpTooltipLink: FC> = ({ ); }; -export const HelpTooltipAction: FC< - PropsWithChildren<{ - icon: Icon; - onClick: () => void; - ariaLabel?: string; - }> -> = ({ children, icon: Icon, onClick, ariaLabel }) => { +interface HelpTooltipActionProps { + children?: ReactNode; + icon: Icon; + onClick: () => void; + ariaLabel?: string; +} + +export const HelpTooltipAction: FC = ({ + children, + icon: Icon, + onClick, + ariaLabel, +}) => { const tooltip = useHelpTooltip(); return ( @@ -206,9 +231,7 @@ export const HelpTooltipAction: FC< ); }; -export const HelpTooltipLinksGroup: FC> = ({ - children, -}) => { +export const HelpTooltipLinksGroup: FC = ({ children }) => { return ( {children} @@ -236,6 +259,22 @@ const getIconSpacingFromSize = (size?: Size): number => { } }; +const classNames = { + popover: (css) => css` + pointer-events: none; + `, + + paper: (css, theme) => css` + ${theme.typography.body2 as CSSObject} + + margin-top: 4px; + width: 304px; + padding: 20px; + color: ${theme.palette.text.secondary}; + pointer-events: auto; + `, +} satisfies Record; + const styles = { title: (theme) => ({ marginTop: 0, diff --git a/site/src/components/InfoTooltip/InfoTooltip.tsx b/site/src/components/InfoTooltip/InfoTooltip.tsx index 436ed45b04b59..1939c12382c66 100644 --- a/site/src/components/InfoTooltip/InfoTooltip.tsx +++ b/site/src/components/InfoTooltip/InfoTooltip.tsx @@ -5,8 +5,7 @@ import { HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; import InfoIcon from "@mui/icons-material/InfoOutlined"; -import { css } from "@emotion/css"; -import { useTheme } from "@emotion/react"; +import { Interpolation, Theme, css, useTheme } from "@emotion/react"; interface InfoTooltipProps { // TODO: use a `ThemeRole` type or something @@ -15,28 +14,32 @@ interface InfoTooltipProps { message: ReactNode; } -export const InfoTooltip: FC = (props) => { - const { title, message, type = "info" } = props; - +export const InfoTooltip: FC = ({ + title, + message, + type = "info", +}) => { const theme = useTheme(); - const iconColor = theme.experimental.roles[type].outline; return ( {title} {message} ); }; + +const styles = { + button: css` + opacity: 1; + + &:hover { + opacity: 1; + } + `, +} satisfies Record>; diff --git a/site/src/components/Resources/PortForwardButton.tsx b/site/src/components/Resources/PortForwardButton.tsx index df4f6b773f6b3..f643cb4d1d7fe 100644 --- a/site/src/components/Resources/PortForwardButton.tsx +++ b/site/src/components/Resources/PortForwardButton.tsx @@ -1,18 +1,11 @@ -import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; import CircularProgress from "@mui/material/CircularProgress"; import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; -import { css } from "@emotion/css"; -import { useTheme } from "@emotion/react"; +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import type { FC } from "react"; import { useQuery } from "react-query"; import { colors } from "theme/colors"; -import { - HelpTooltipLink, - HelpTooltipLinksGroup, - HelpTooltipText, - HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip"; -import { SecondaryAgentButton } from "components/Resources/AgentButton"; import { docs } from "utils/docs"; import { getAgentListeningPorts } from "api/api"; import type { @@ -20,6 +13,14 @@ import type { WorkspaceAgentListeningPort, } from "api/typesGenerated"; import { portForwardURL } from "utils/portForward"; +import { type ClassName, useClassName } from "hooks/useClassName"; +import { + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, +} from "components/HelpTooltip/HelpTooltip"; +import { SecondaryAgentButton } from "components/Resources/AgentButton"; import { Popover, PopoverContent, @@ -33,12 +34,15 @@ export interface PortForwardButtonProps { agent: WorkspaceAgent; } -export const PortForwardButton: React.FC = (props) => { - const theme = useTheme(); +export const PortForwardButton: FC = (props) => { + const { agent } = props; + + const paper = useClassName(classNames.paper, []); + const portsQuery = useQuery({ - queryKey: ["portForward", props.agent.id], - queryFn: () => getAgentListeningPorts(props.agent.id), - enabled: props.agent.status === "connected", + queryKey: ["portForward", agent.id], + queryFn: () => getAgentListeningPorts(agent.id), + enabled: agent.status === "connected", refetchInterval: 5_000, }); @@ -48,129 +52,83 @@ export const PortForwardButton: React.FC = (props) => { Ports {portsQuery.data ? ( - - {portsQuery.data.ports.length} - +
{portsQuery.data.ports.length}
) : ( - + )}
- +
); }; -export const PortForwardPopoverView: React.FC< - PortForwardButtonProps & { ports?: WorkspaceAgentListeningPort[] } -> = (props) => { +interface PortForwardPopoverViewProps extends PortForwardButtonProps { + ports?: WorkspaceAgentListeningPort[]; +} + +export const PortForwardPopoverView: FC = ({ + host, + workspaceName, + agent, + username, + ports, +}) => { const theme = useTheme(); - const { host, workspaceName, agent, username, ports } = props; return ( <> - Forwarded ports - + {ports?.length === 0 ? "No open ports were detected." : "The forwarded ports are exclusively accessible to you."} - - {ports?.map((p) => { +
+ {ports?.map((port) => { const url = portForwardURL( host, - p.port, + port.port, agent.name, workspaceName, username, ); - const label = p.process_name !== "" ? p.process_name : p.port; + const label = + port.process_name !== "" ? port.process_name : port.port; return ( - + {label} - - {p.port} - + {port.port} ); })} - - +
+
- +
Forward port - + Access ports running on the agent: - { e.preventDefault(); const formData = new FormData(e.currentTarget); @@ -185,45 +143,102 @@ export const PortForwardPopoverView: React.FC< window.open(url, "_blank"); }} > - - - + + Learn more - +
); }; + +const classNames = { + paper: (css, theme) => css` + padding: 0; + width: 304px; + color: ${theme.palette.text.secondary}; + margin-top: 4px; + `, +} satisfies Record; + +const styles = { + portCount: { + fontSize: 12, + fontWeight: 500, + height: 20, + minWidth: 20, + padding: "0 4px", + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + backgroundColor: colors.gray[11], + marginLeft: 8, + }, + + portLink: (theme) => ({ + color: theme.palette.text.primary, + fontSize: 14, + display: "flex", + alignItems: "center", + gap: 8, + paddingTop: 4, + paddingBottom: 4, + fontWeight: 500, + }), + + portNumber: (theme) => ({ + marginLeft: "auto", + color: theme.palette.text.secondary, + fontSize: 13, + fontWeight: 400, + }), + + newPortForm: (theme) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: "4px", + marginTop: 16, + display: "flex", + alignItems: "center", + "&:focus-within": { + borderColor: theme.palette.primary.main, + }, + }), + + newPortInput: (theme) => ({ + fontSize: 14, + height: 34, + padding: "0 12px", + background: "none", + border: 0, + outline: "none", + color: theme.palette.text.primary, + appearance: "textfield", + display: "block", + width: "100%", + }), +} satisfies Record>; diff --git a/site/src/components/Resources/SSHButton/SSHButton.tsx b/site/src/components/Resources/SSHButton/SSHButton.tsx index c1c6afac210e8..f15dbc24b6e28 100644 --- a/site/src/components/Resources/SSHButton/SSHButton.tsx +++ b/site/src/components/Resources/SSHButton/SSHButton.tsx @@ -1,5 +1,4 @@ -import { css } from "@emotion/css"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import { type Interpolation, type Theme } from "@emotion/react"; import { type FC, type PropsWithChildren } from "react"; import { HelpTooltipLink, @@ -7,14 +6,15 @@ import { HelpTooltipText, } from "components/HelpTooltip/HelpTooltip"; import { docs } from "utils/docs"; -import { CodeExample } from "../../CodeExample/CodeExample"; -import { Stack } from "../../Stack/Stack"; -import { SecondaryAgentButton } from "../AgentButton"; +import { type ClassName, useClassName } from "hooks/useClassName"; +import { CodeExample } from "components/CodeExample/CodeExample"; import { Popover, PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; +import { Stack } from "components/Stack/Stack"; +import { SecondaryAgentButton } from "../AgentButton"; export interface SSHButtonProps { workspaceName: string; @@ -29,7 +29,7 @@ export const SSHButton: FC> = ({ isDefaultOpen = false, sshPrefix, }) => { - const theme = useTheme(); + const paper = useClassName(classNames.paper, []); return ( @@ -37,17 +37,7 @@ export const SSHButton: FC> = ({ SSH - + Run the following commands to connect with SSH: @@ -93,6 +83,15 @@ export const SSHButton: FC> = ({ ); }; +const classNames = { + paper: (css, theme) => css` + padding: 16px 24px 24px; + width: 304px; + color: ${theme.palette.text.secondary}; + margin-top: 2px; + `, +} satisfies Record; + const styles = { codeExamples: { marginTop: 12, diff --git a/site/src/components/Tabs/Tabs.tsx b/site/src/components/Tabs/Tabs.tsx index 99e2e6419af9d..f2888afd3672b 100644 --- a/site/src/components/Tabs/Tabs.tsx +++ b/site/src/components/Tabs/Tabs.tsx @@ -1,11 +1,10 @@ -import { ReactNode } from "react"; +import { cx } from "@emotion/css"; +import { type FC, type PropsWithChildren } from "react"; import { NavLink, NavLinkProps } from "react-router-dom"; -import { combineClasses } from "utils/combineClasses"; import { Margins } from "components/Margins/Margins"; -import { css } from "@emotion/css"; -import { useTheme } from "@emotion/react"; +import { type ClassName, useClassName } from "hooks/useClassName"; -export const Tabs = ({ children }: { children: ReactNode }) => { +export const Tabs: FC = ({ children }) => { return (
({ @@ -26,10 +25,30 @@ export const Tabs = ({ children }: { children: ReactNode }) => { ); }; -export const TabLink = (props: NavLinkProps) => { - const theme = useTheme(); +interface TabLinkProps extends NavLinkProps { + className?: string; +} - const baseTabLink = css` +export const TabLink: FC = ({ + className, + children, + ...linkProps +}) => { + const tabLink = useClassName(classNames.tabLink, []); + const activeTabLink = useClassName(classNames.activeTabLink, []); + + return ( + + cx([tabLink, isActive && activeTabLink, className]) + } + {...linkProps} + /> + ); +}; + +const classNames = { + tabLink: (css, theme) => css` text-decoration: none; color: ${theme.palette.text.secondary}; font-size: 14px; @@ -39,9 +58,8 @@ export const TabLink = (props: NavLinkProps) => { &:hover { color: ${theme.palette.text.primary}; } - `; - - const activeTabLink = css` + `, + activeTabLink: (css, theme) => css` color: ${theme.palette.text.primary}; position: relative; @@ -54,18 +72,5 @@ export const TabLink = (props: NavLinkProps) => { background: ${theme.palette.primary.main}; position: absolute; } - `; - - return ( - - combineClasses([ - baseTabLink, - isActive ? activeTabLink : undefined, - props.className as string, - ]) - } - {...props} - /> - ); -}; + `, +} satisfies Record; diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index 68595bf419c9a..d5edac6650fce 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -11,7 +11,6 @@ import { type FC, useState, } from "react"; -import Box from "@mui/material/Box"; import { useDebouncedFunction } from "hooks/debounce"; import { useQuery } from "react-query"; import { users } from "api/queries/users"; @@ -90,13 +89,13 @@ export const UserAutocomplete: FC = ({ } getOptionLabel={(option) => option.email} renderOption={(props, option) => ( - +
  • - +
  • )} renderInput={(params) => ( = ({ {params.InputProps.endAdornment} ), - classes: { - root: css` - padding-left: 14px !important; // Same padding left as input - gap: 4px; - `, - }, + classes: { root }, }} InputLabelProps={{ shrink: true, @@ -141,3 +135,8 @@ export const UserAutocomplete: FC = ({ /> ); }; + +const root = css` + padding-left: 14px !important; // Same padding left as input + gap: 4px; +`; diff --git a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index 6bb63618fe332..c93d91366ecfc 100644 --- a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -1,12 +1,10 @@ import RefreshIcon from "@mui/icons-material/Refresh"; import InfoIcon from "@mui/icons-material/InfoOutlined"; -import Box from "@mui/material/Box"; import Skeleton from "@mui/material/Skeleton"; import Link from "@mui/material/Link"; import { type FC } from "react"; import { useQuery } from "react-query"; -import { css } from "@emotion/css"; -import { useTheme } from "@emotion/react"; +import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; import { templateVersion } from "api/queries/templates"; import { HelpTooltip, @@ -43,63 +41,33 @@ export const WorkspaceOutdatedTooltip: FC = ({ {Language.outdatedLabel} {Language.versionTooltipText} - - - theme.palette.text.primary, - fontWeight: 600, - }} - > - New version - - +
    +
    +
    New version
    +
    {activeVersion ? ( theme.palette.primary.light }} + css={{ color: theme.palette.primary.light }} > {activeVersion.name} ) : ( )} - - +
    +
    - - theme.palette.text.primary, - fontWeight: 600, - }} - > - Message - - +
    +
    Message
    +
    {activeVersion ? ( activeVersion.message === "" ? ( "No message" @@ -109,9 +77,9 @@ export const WorkspaceOutdatedTooltip: FC = ({ ) : ( )} - - - +
    +
    +
    = ({
    ); }; + +const styles = { + icon: (theme) => ({ + color: theme.experimental.roles.notice.outline, + }), + + button: css` + opacity: 1; + + &:hover { + opacity: 1; + } + `, + + container: { + display: "flex", + flexDirection: "column", + gap: 8, + paddingTop: 8, + paddingBottom: 8, + fontSize: 13, + }, + + bold: (theme) => ({ + color: theme.palette.text.primary, + fontWeight: 600, + }), +} satisfies Record>; diff --git a/site/src/hooks/useClassName.ts b/site/src/hooks/useClassName.ts new file mode 100644 index 0000000000000..595308dead9ca --- /dev/null +++ b/site/src/hooks/useClassName.ts @@ -0,0 +1,21 @@ +/* eslint-disable react-hooks/exhaustive-deps -- false positives */ + +import { css } from "@emotion/css"; +import { type DependencyList, useMemo } from "react"; +import { type Theme, useTheme } from "@emotion/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. + */ +export function useClassName(styles: ClassName, deps: DependencyList): string { + const theme = useTheme(); + const className = useMemo(() => { + return styles(css, theme); + }, [...deps, theme]); + + return className; +} diff --git a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx index 318fbdab89a3c..befb3a30327c5 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx @@ -1,3 +1,4 @@ +import { css } from "@emotion/css"; import { type FC, useState, useEffect } from "react"; import TextField from "@mui/material/TextField"; import MenuItem from "@mui/material/MenuItem"; @@ -19,7 +20,6 @@ import { filterByMaxTokenLifetime, customLifetimeDay, } from "./utils"; -import { css } from "@emotion/css"; interface CreateTokenFormProps { form: FormikContextType; @@ -30,10 +30,6 @@ interface CreateTokenFormProps { creationFailed: boolean; } -const sectionInfoStyles = css` - min-width: 300px; -`; - export const CreateTokenForm: FC = ({ form, maxTokenLifetime, @@ -65,7 +61,7 @@ export const CreateTokenForm: FC = ({ = ({ .format("MMMM DD, YYYY")}` : "Please set a token expiration." } - classes={{ sectionInfo: sectionInfoStyles }} + classes={{ sectionInfo: classNames.sectionInfo }} > @@ -152,3 +148,9 @@ export const CreateTokenForm: FC = ({ ); }; + +const classNames = { + sectionInfo: css` + min-width: 300px; + `, +}; diff --git a/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx b/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx index 3545cf9fca2bf..2d50c5bb86773 100644 --- a/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx @@ -50,16 +50,7 @@ export const MissingTemplateVariablesDialog: FC< > Template variables @@ -113,6 +104,17 @@ export const MissingTemplateVariablesDialog: FC< ); }; +const classNames = { + root: css` + padding: 24px 40px; + + & h2 { + font-size: 20px; + font-weight: 400; + } + `, +}; + const styles = { content: { padding: "0 40px", diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx index aee37a8f1f8cd..216654ee39f4d 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx @@ -1,5 +1,4 @@ -import { css } from "@emotion/css"; -import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import { type Interpolation, type Theme } from "@emotion/react"; import IconButton from "@mui/material/IconButton"; import { EditSquare } from "components/Icons/EditSquare"; import { type FC } from "react"; @@ -17,6 +16,7 @@ import { PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; +import { type ClassName, useClassName } from "hooks/useClassName"; const roleDescriptions: Record = { owner: @@ -28,13 +28,21 @@ const roleDescriptions: Record = { "Everybody is a member. This is a shared and default role for all users.", }; -const Option: React.FC<{ +interface OptionProps { value: string; name: string; description: string; isChecked: boolean; onChange: (roleName: string) => void; -}> = ({ value, name, description, isChecked, onChange }) => { +} + +const Option: FC = ({ + value, + name, + description, + isChecked, + onChange, +}) => { return (