refactor: avoid @emotion/css when possible (#10807)

This commit is contained in:
Kayla Washburn
2023-11-21 11:29:43 -07:00
committed by GitHub
parent 26b5390f4b
commit 8cd8901db5
20 changed files with 644 additions and 603 deletions
+29 -28
View File
@@ -1,15 +1,17 @@
import IconButton from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import Check from "@mui/icons-material/Check";
import { css, type Interpolation, type Theme } from "@emotion/react";
import { type FC, type ReactNode } from "react";
import { useClipboard } from "hooks/useClipboard";
import { css } from "@emotion/react";
import { FileCopyIcon } from "../Icons/FileCopyIcon";
interface CopyButtonProps {
children?: ReactNode;
text: string;
ctaCopy?: string;
wrapperClassName?: string;
buttonClassName?: string;
wrapperStyles?: Interpolation<Theme>;
buttonStyles?: Interpolation<Theme>;
tooltipTitle?: string;
}
@@ -21,48 +23,40 @@ export const Language = {
/**
* Copy button used inside the CodeBlock component internally
*/
export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
export const CopyButton: FC<CopyButtonProps> = ({
text,
ctaCopy,
wrapperClassName = "",
buttonClassName = "",
wrapperStyles,
buttonStyles,
tooltipTitle = Language.tooltipTitle,
}) => {
const { isCopied, copy: copyToClipboard } = useClipboard(text);
const fileCopyIconStyles = css`
width: 20px;
height: 20px;
`;
return (
<Tooltip title={tooltipTitle} placement="top">
<div
className={wrapperClassName}
css={{
display: "flex",
}}
>
<div css={[{ display: "flex" }, wrapperStyles]}>
<IconButton
className={buttonClassName}
css={(theme) => css`
border-radius: 8px;
padding: 8px;
min-width: 32px;
css={[
(theme) => css`
border-radius: 8px;
padding: 8px;
min-width: 32px;
&:hover {
background: ${theme.palette.background.paper};
}
`}
&:hover {
background: ${theme.palette.background.paper};
}
`,
buttonStyles,
]}
onClick={copyToClipboard}
size="small"
aria-label={Language.ariaLabel}
variant="text"
>
{isCopied ? (
<Check css={fileCopyIconStyles} />
<Check css={styles.copyIcon} />
) : (
<FileCopyIcon css={fileCopyIconStyles} />
<FileCopyIcon css={styles.copyIcon} />
)}
{ctaCopy && <div css={{ marginLeft: 8 }}>{ctaCopy}</div>}
</IconButton>
@@ -70,3 +64,10 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
</Tooltip>
);
};
const styles = {
copyIcon: css`
width: 20px;
height: 20px;
`,
} satisfies Record<string, Interpolation<Theme>>;
@@ -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<string, Interpolation<Theme>>;
export interface DeploymentBannerViewProps {
health?: HealthcheckReport;
stats?: DeploymentStats;
fetchStats?: () => void;
}
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (props) => {
const { health, stats, fetchStats } = props;
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
health,
stats,
fetchStats,
}) => {
const theme = useTheme();
const summaryTooltip = useClassName(classNames.summaryTooltip, []);
const aggregatedMinutes = useMemo(() => {
if (!stats) {
@@ -114,6 +92,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (props) => {
clearTimeout(timeout);
};
}, [fetchStats, stats]);
const lastAggregated = useMemo(() => {
if (!stats) {
return;
@@ -127,34 +106,6 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (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<DeploymentBannerViewProps> = (props) => {
}}
>
<Tooltip
classes={{ tooltip: statusSummaryStyle }}
classes={{ tooltip: summaryTooltip }}
title={
unhealthy ? (
<>
@@ -214,11 +165,15 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (props) => {
css={{ marginRight: -16 }}
>
{unhealthy ? (
<Link component={RouterLink} to="/health" css={statusBadgeStyle}>
<Link
component={RouterLink}
to="/health"
css={[styles.statusBadge, styles.unhealthy]}
>
<ErrorIcon />
</Link>
) : (
<div css={statusBadgeStyle}>
<div css={styles.statusBadge}>
<RocketIcon />
</div>
)}
@@ -380,19 +335,15 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (props) => {
);
};
const ValueSeparator: FC = () => {
const theme = useTheme();
const separatorStyles = css`
color: ${theme.palette.text.disabled};
`;
return <div css={separatorStyles}>/</div>;
};
const WorkspaceBuildValue: FC<{
interface WorkspaceBuildValueProps {
status: WorkspaceStatus;
count?: number;
}> = ({ status, count }) => {
}
const WorkspaceBuildValue: FC<WorkspaceBuildValueProps> = ({
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 <div css={styles.separator}>/</div>;
};
const HealthIssue: FC<PropsWithChildren> = ({ children }) => {
return (
<Stack direction="row" spacing={1} alignItems="center">
@@ -424,3 +379,62 @@ const HealthIssue: FC<PropsWithChildren> = ({ children }) => {
</Stack>
);
};
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<string, ClassName>;
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<string, Interpolation<Theme>>;
@@ -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<RuntimeErrorStateProps> = ({ error }) => {
<div css={styles.stackHeader}>
Stacktrace
<CopyButton
buttonClassName={css`
background-color: transparent;
border: 0;
border-radius: 999px;
min-height: 32px;
min-width: 32px;
height: 32px;
width: 32px;
& svg {
width: 16px;
height: 16px;
}
`}
buttonStyles={styles.copyButton}
text={error.stack}
tooltipTitle="Copy 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<string, Interpolation<Theme>>;
@@ -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<EnterpriseSnackbarProps>
> = ({ 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<EnterpriseSnackbarProps> = ({
children,
onClose,
variant = "info",
ContentProps = {},
action,
...snackbarProps
}) => {
const content = useClassName(classNames.content(variant), [variant]);
return (
<Snackbar
@@ -49,7 +43,6 @@ export const EnterpriseSnackbar: FC<
vertical: "bottom",
horizontal: "right",
}}
{...rest}
action={
<div css={styles.actionWrapper}>
{action}
@@ -60,10 +53,13 @@ export const EnterpriseSnackbar: FC<
}
ContentProps={{
...ContentProps,
className: snackbarContentStyles,
className: content,
}}
onClose={onClose}
/>
{...snackbarProps}
>
{children}
</Snackbar>
);
};
@@ -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",
+109 -70
View File
@@ -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<HelpPopoverProps> = ({
onOpen,
onClose,
children,
...props
}) => {
const popover = useClassName(classNames.popover, []);
const paper = useClassName(classNames.paper, []);
return (
<Popover
className={className`
pointer-events: none;
`}
classes={{
paper: className`
${theme.typography.body2 as CSSObject}
margin-top: 4px;
width: 304px;
padding: 20px;
color: ${theme.palette.text.secondary};
pointer-events: auto;
`,
}}
className={popover}
classes={{ paper }}
onClose={onClose}
anchorOrigin={{
vertical: "bottom",
@@ -86,13 +77,23 @@ export const HelpPopover: FC<
);
};
export const HelpTooltip: FC<PropsWithChildren<HelpTooltipProps>> = ({
export interface HelpTooltipProps {
// Useful to test on storybook
open?: boolean;
size?: Size;
icon?: Icon;
buttonStyles?: Interpolation<Theme>;
iconStyles?: Interpolation<Theme>;
children?: ReactNode;
}
export const HelpTooltip: FC<HelpTooltipProps> = ({
children,
open = false,
size = "medium",
icon: Icon = HelpIcon,
iconClassName,
buttonClassName,
buttonStyles,
iconStyles,
}) => {
const theme = useTheme();
const anchorRef = useRef<HTMLButtonElement>(null);
@@ -108,24 +109,26 @@ export const HelpTooltip: FC<PropsWithChildren<HelpTooltipProps>> = ({
<button
ref={anchorRef}
aria-describedby={id}
css={css`
display: flex;
align-items: center;
justify-content: center;
width: ${theme.spacing(getButtonSpacingFromSize(size))};
height: ${theme.spacing(getButtonSpacingFromSize(size))};
padding: 0;
border: 0;
background: transparent;
color: ${theme.palette.text.primary};
opacity: 0.5;
cursor: pointer;
css={[
css`
display: flex;
align-items: center;
justify-content: center;
width: ${theme.spacing(getButtonSpacingFromSize(size))};
height: ${theme.spacing(getButtonSpacingFromSize(size))};
padding: 0;
border: 0;
background: transparent;
color: ${theme.palette.text.primary};
opacity: 0.5;
cursor: pointer;
&:hover {
opacity: 0.75;
}
`}
className={buttonClassName}
&:hover {
opacity: 0.75;
}
`,
buttonStyles,
]}
onClick={(event) => {
event.stopPropagation();
setIsOpen(true);
@@ -139,11 +142,13 @@ export const HelpTooltip: FC<PropsWithChildren<HelpTooltipProps>> = ({
aria-label="More info"
>
<Icon
css={{
width: theme.spacing(getIconSpacingFromSize(size)),
height: theme.spacing(getIconSpacingFromSize(size)),
}}
className={iconClassName}
css={[
{
width: theme.spacing(getIconSpacingFromSize(size)),
height: theme.spacing(getIconSpacingFromSize(size)),
},
iconStyles,
]}
/>
</button>
<HelpPopover
@@ -161,12 +166,26 @@ export const HelpTooltip: FC<PropsWithChildren<HelpTooltipProps>> = ({
);
};
export const HelpTooltipTitle: FC<PropsWithChildren> = ({ children }) => {
return <h4 css={styles.title}>{children}</h4>;
export const HelpTooltipTitle: FC<HTMLAttributes<HTMLHeadingElement>> = ({
children,
...attrs
}) => {
return (
<h4 css={styles.title} {...attrs}>
{children}
</h4>
);
};
export const HelpTooltipText = (props: BoxProps) => {
return <Box component="p" css={styles.text} {...props} />;
export const HelpTooltipText: FC<HTMLAttributes<HTMLParagraphElement>> = ({
children,
...attrs
}) => {
return (
<p css={styles.text} {...attrs}>
{children}
</p>
);
};
export const HelpTooltipLink: FC<PropsWithChildren<{ href: string }>> = ({
@@ -181,13 +200,19 @@ export const HelpTooltipLink: FC<PropsWithChildren<{ href: string }>> = ({
);
};
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<HelpTooltipActionProps> = ({
children,
icon: Icon,
onClick,
ariaLabel,
}) => {
const tooltip = useHelpTooltip();
return (
@@ -206,9 +231,7 @@ export const HelpTooltipAction: FC<
);
};
export const HelpTooltipLinksGroup: FC<PropsWithChildren<unknown>> = ({
children,
}) => {
export const HelpTooltipLinksGroup: FC<PropsWithChildren> = ({ children }) => {
return (
<Stack spacing={1} css={styles.linksGroup}>
{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<string, ClassName>;
const styles = {
title: (theme) => ({
marginTop: 0,
+18 -15
View File
@@ -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<InfoTooltipProps> = (props) => {
const { title, message, type = "info" } = props;
export const InfoTooltip: FC<InfoTooltipProps> = ({
title,
message,
type = "info",
}) => {
const theme = useTheme();
const iconColor = theme.experimental.roles[type].outline;
return (
<HelpTooltip
size="small"
icon={InfoIcon}
iconClassName={css`
color: ${iconColor};
`}
buttonClassName={css`
opacity: 1;
&:hover {
opacity: 1;
}
`}
iconStyles={{ color: theme.experimental.roles[type].outline }}
buttonStyles={styles.button}
>
<HelpTooltipTitle>{title}</HelpTooltipTitle>
<HelpTooltipText>{message}</HelpTooltipText>
</HelpTooltip>
);
};
const styles = {
button: css`
opacity: 1;
&:hover {
opacity: 1;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
@@ -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<PortForwardButtonProps> = (props) => {
const theme = useTheme();
export const PortForwardButton: FC<PortForwardButtonProps> = (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<PortForwardButtonProps> = (props) => {
<SecondaryAgentButton disabled={!portsQuery.data}>
Ports
{portsQuery.data ? (
<Box
sx={{
fontSize: 12,
fontWeight: 500,
height: 20,
minWidth: 20,
padding: "0 4px",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.gray[11],
ml: 1,
}}
>
{portsQuery.data.ports.length}
</Box>
<div css={styles.portCount}>{portsQuery.data.ports.length}</div>
) : (
<CircularProgress size={10} sx={{ ml: 1 }} />
<CircularProgress size={10} css={{ marginLeft: 8 }} />
)}
</SecondaryAgentButton>
</PopoverTrigger>
<PopoverContent
horizontal="right"
classes={{
paper: css`
padding: 0;
width: 304px;
color: ${theme.palette.text.secondary};
margin-top: 4px;
`,
}}
>
<PopoverContent horizontal="right" classes={{ paper }}>
<PortForwardPopoverView {...props} ports={portsQuery.data?.ports} />
</PopoverContent>
</Popover>
);
};
export const PortForwardPopoverView: React.FC<
PortForwardButtonProps & { ports?: WorkspaceAgentListeningPort[] }
> = (props) => {
interface PortForwardPopoverViewProps extends PortForwardButtonProps {
ports?: WorkspaceAgentListeningPort[];
}
export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
host,
workspaceName,
agent,
username,
ports,
}) => {
const theme = useTheme();
const { host, workspaceName, agent, username, ports } = props;
return (
<>
<Box
<div
css={{
padding: 20,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<HelpTooltipTitle>Forwarded ports</HelpTooltipTitle>
<HelpTooltipText sx={{ color: theme.palette.text.secondary }}>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
{ports?.length === 0
? "No open ports were detected."
: "The forwarded ports are exclusively accessible to you."}
</HelpTooltipText>
<Box css={{ marginTop: 12 }}>
{ports?.map((p) => {
<div css={{ marginTop: 12 }}>
{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 (
<Link
underline="none"
sx={{
color: theme.palette.text.primary,
fontSize: 14,
display: "flex",
alignItems: "center",
gap: 1,
py: 0.5,
fontWeight: 500,
}}
key={p.port}
css={styles.portLink}
key={port.port}
href={url}
target="_blank"
rel="noreferrer"
>
<OpenInNewOutlined sx={{ width: 14, height: 14 }} />
<OpenInNewOutlined css={{ width: 14, height: 14 }} />
{label}
<Box
component="span"
sx={{
ml: "auto",
color: theme.palette.text.secondary,
fontSize: 13,
fontWeight: 400,
}}
>
{p.port}
</Box>
<span css={styles.portNumber}>{port.port}</span>
</Link>
);
})}
</Box>
</Box>
</div>
</div>
<Box css={{ padding: 20 }}>
<div css={{ padding: 20 }}>
<HelpTooltipTitle>Forward port</HelpTooltipTitle>
<HelpTooltipText sx={{ color: theme.palette.text.secondary }}>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
Access ports running on the agent:
</HelpTooltipText>
<Box
component="form"
sx={{
border: `1px solid ${theme.palette.divider}`,
borderRadius: "4px",
mt: 2,
display: "flex",
alignItems: "center",
"&:focus-within": {
borderColor: theme.palette.primary.main,
},
}}
<form
css={styles.newPortForm}
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
@@ -185,45 +143,110 @@ export const PortForwardPopoverView: React.FC<
window.open(url, "_blank");
}}
>
<Box
<input
aria-label="Port number"
name="portNumber"
component="input"
type="number"
placeholder="Type a port number..."
min={0}
max={65535}
required
css={{
fontSize: 14,
height: 34,
padding: "0 12px",
background: "none",
border: 0,
outline: "none",
color: theme.palette.text.primary,
appearance: "textfield",
display: "block",
width: "100%",
}}
css={styles.newPortInput}
/>
<OpenInNewOutlined
<Button
type="submit"
variant="text"
css={{
flexShrink: 0,
width: 14,
height: 14,
marginRight: 12,
color: theme.palette.text.primary,
paddingLeft: 12,
paddingRight: 12,
minWidth: 0,
}}
/>
</Box>
size="small"
>
<OpenInNewOutlined
css={{
flexShrink: 0,
width: 14,
height: 14,
color: theme.palette.text.primary,
}}
/>
</Button>
</form>
<HelpTooltipLinksGroup>
<HelpTooltipLink href={docs("/networking/port-forwarding#dashboard")}>
Learn more
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</Box>
</div>
</>
);
};
const classNames = {
paper: (css, theme) => css`
padding: 0;
width: 304px;
color: ${theme.palette.text.secondary};
margin-top: 4px;
`,
} satisfies Record<string, ClassName>;
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<string, Interpolation<Theme>>;
@@ -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<PropsWithChildren<SSHButtonProps>> = ({
isDefaultOpen = false,
sshPrefix,
}) => {
const theme = useTheme();
const paper = useClassName(classNames.paper, []);
return (
<Popover isDefaultOpen={isDefaultOpen}>
@@ -37,17 +37,7 @@ export const SSHButton: FC<PropsWithChildren<SSHButtonProps>> = ({
<SecondaryAgentButton>SSH</SecondaryAgentButton>
</PopoverTrigger>
<PopoverContent
horizontal="right"
classes={{
paper: css`
padding: 16px 24px 24px;
width: 304px;
color: ${theme.palette.text.secondary};
margin-top: 2px;
`,
}}
>
<PopoverContent horizontal="right" classes={{ paper }}>
<HelpTooltipText>
Run the following commands to connect with SSH:
</HelpTooltipText>
@@ -93,6 +83,15 @@ export const SSHButton: FC<PropsWithChildren<SSHButtonProps>> = ({
);
};
const classNames = {
paper: (css, theme) => css`
padding: 16px 24px 24px;
width: 304px;
color: ${theme.palette.text.secondary};
margin-top: 2px;
`,
} satisfies Record<string, ClassName>;
const styles = {
codeExamples: {
marginTop: 12,
+31 -26
View File
@@ -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<PropsWithChildren> = ({ children }) => {
return (
<div
css={(theme) => ({
@@ -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<TabLinkProps> = ({
className,
children,
...linkProps
}) => {
const tabLink = useClassName(classNames.tabLink, []);
const activeTabLink = useClassName(classNames.activeTabLink, []);
return (
<NavLink
className={({ isActive }) =>
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 (
<NavLink
className={({ isActive }) =>
combineClasses([
baseTabLink,
isActive ? activeTabLink : undefined,
props.className as string,
])
}
{...props}
/>
);
};
`,
} satisfies Record<string, ClassName>;
@@ -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<UserAutocompleteProps> = ({
}
getOptionLabel={(option) => option.email}
renderOption={(props, option) => (
<Box component="li" {...props}>
<li {...props}>
<AvatarData
title={option.username}
subtitle={option.email}
src={option.avatar_url}
/>
</Box>
</li>
)}
renderInput={(params) => (
<TextField
@@ -126,12 +125,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
{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<UserAutocompleteProps> = ({
/>
);
};
const root = css`
padding-left: 14px !important; // Same padding left as input
gap: 4px;
`;
@@ -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<TooltipProps> = ({
<HelpTooltip
size="small"
icon={InfoIcon}
iconClassName={css`
color: ${theme.experimental.roles.notice.outline};
`}
buttonClassName={css`
opacity: 1;
&:hover {
opacity: 1;
}
`}
iconStyles={styles.icon}
buttonStyles={styles.button}
>
<HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle>
<HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
py: 1,
fontSize: 13,
}}
>
<Box>
<Box
sx={{
color: (theme) => theme.palette.text.primary,
fontWeight: 600,
}}
>
New version
</Box>
<Box>
<div css={styles.container}>
<div>
<div css={styles.bold}>New version</div>
<div>
{activeVersion ? (
<Link
href={`/templates/${templateName}/versions/${activeVersion.name}`}
target="_blank"
sx={{ color: (theme) => theme.palette.primary.light }}
css={{ color: theme.palette.primary.light }}
>
{activeVersion.name}
</Link>
) : (
<Skeleton variant="text" height={20} width={100} />
)}
</Box>
</Box>
</div>
</div>
<Box>
<Box
sx={{
color: (theme) => theme.palette.text.primary,
fontWeight: 600,
}}
>
Message
</Box>
<Box>
<div>
<div css={styles.bold}>Message</div>
<div>
{activeVersion ? (
activeVersion.message === "" ? (
"No message"
@@ -109,9 +77,9 @@ export const WorkspaceOutdatedTooltip: FC<TooltipProps> = ({
) : (
<Skeleton variant="text" height={20} width={150} />
)}
</Box>
</Box>
</Box>
</div>
</div>
</div>
<HelpTooltipLinksGroup>
<HelpTooltipAction
@@ -125,3 +93,31 @@ export const WorkspaceOutdatedTooltip: FC<TooltipProps> = ({
</HelpTooltip>
);
};
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<string, Interpolation<Theme>>;
@@ -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<CreateTokenData>;
@@ -30,10 +30,6 @@ interface CreateTokenFormProps {
creationFailed: boolean;
}
const sectionInfoStyles = css`
min-width: 300px;
`;
export const CreateTokenForm: FC<CreateTokenFormProps> = ({
form,
maxTokenLifetime,
@@ -65,7 +61,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
<FormSection
title="Name"
description="What is this token for?"
classes={{ sectionInfo: sectionInfoStyles }}
classes={{ sectionInfo: classNames.sectionInfo }}
>
<FormFields>
<TextField
@@ -88,7 +84,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
.format("MMMM DD, YYYY")}`
: "Please set a token expiration."
}
classes={{ sectionInfo: sectionInfoStyles }}
classes={{ sectionInfo: classNames.sectionInfo }}
>
<FormFields>
<Stack direction="row">
@@ -152,3 +148,9 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
</HorizontalForm>
);
};
const classNames = {
sectionInfo: css`
min-width: 300px;
`,
};
@@ -50,16 +50,7 @@ export const MissingTemplateVariablesDialog: FC<
>
<DialogTitle
id="update-build-parameters-title"
classes={{
root: css`
padding: 24px 40px;
& h2 {
font-size: 20px;
font-weight: 400;
}
`,
}}
classes={{ root: classNames.root }}
>
Template variables
</DialogTitle>
@@ -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",
@@ -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<string, string> = {
owner:
@@ -28,13 +28,21 @@ const roleDescriptions: Record<string, string> = {
"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<OptionProps> = ({
value,
name,
description,
isChecked,
onChange,
}) => {
return (
<label htmlFor={name} css={styles.option}>
<Stack direction="row" alignItems="flex-start">
@@ -76,7 +84,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
userLoginType,
oidcRoleSync,
}) => {
const theme = useTheme();
const paper = useClassName(classNames.paper, []);
const handleChange = (roleName: string) => {
if (selectedRoleNames.has(roleName)) {
@@ -114,15 +122,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
</IconButton>
</PopoverTrigger>
<PopoverContent
classes={{
paper: css`
width: 360px;
margin-top: 8px;
background: ${theme.palette.background.paperLight};
`,
}}
>
<PopoverContent classes={{ paper }}>
<fieldset
css={styles.fieldset}
disabled={isLoading}
@@ -157,6 +157,14 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
);
};
const classNames = {
paper: (css, theme) => css`
width: 360px;
margin-top: 8px;
background: ${theme.palette.background.paperLight};
`,
} satisfies Record<string, ClassName>;
const styles = {
editButton: (theme) => ({
color: theme.palette.text.secondary,
@@ -2,7 +2,6 @@ import { type FC, useRef, useState } from "react";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
import AlertTitle from "@mui/material/AlertTitle";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import { css } from "@emotion/css";
@@ -80,7 +79,7 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
) => option.id === value.id}
getOptionLabel={(option) => option.name}
renderOption={(props, option: TemplateVersion) => (
<Box component="li" {...props}>
<li {...props}>
<AvatarData
avatar={
<Avatar src={option.created_by.avatar_url}>
@@ -100,7 +99,7 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
>
{option.name}
{option.message && (
<InfoIcon sx={{ width: 12, height: 12 }} />
<InfoIcon css={{ width: 12, height: 12 }} />
)}
</Stack>
{template?.active_version_id === option.id && (
@@ -110,7 +109,7 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
}
subtitle={createDayString(option.created_at)}
/>
</Box>
</li>
)}
renderInput={(params) => (
<>
@@ -128,12 +127,7 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
{params.InputProps.endAdornment}
</>
),
classes: {
// Same `padding-left` as input
root: css`
padding-left: 14px !important;
`,
},
classes: { root: classNames.root },
}}
/>
</>
@@ -159,3 +153,10 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
/>
);
};
const classNames = {
// Same `padding-left` as input
root: css`
padding-left: 14px !important;
`,
};
@@ -56,16 +56,7 @@ export const UpdateBuildParametersDialog: FC<
>
<DialogTitle
id="update-build-parameters-title"
classes={{
root: css`
padding: 24px 40px;
& h2 {
font-size: 20px;
font-weight: 400;
}
`,
}}
classes={{ root: classNames.root }}
>
Workspace parameters
</DialogTitle>
@@ -123,6 +114,17 @@ export const UpdateBuildParametersDialog: FC<
);
};
const classNames = {
root: css`
padding: 24px 40px;
& h2 {
font-size: 20px;
font-weight: 400;
}
`,
};
const styles = {
content: {
padding: "0 40px",
@@ -123,11 +123,7 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
// If a transition is set, there is a moment on new load where the
// bar accelerates to progressValue and then rapidly decelerates, which
// is not indicative of true progress.
classes={{
bar: css`
transition: none;
`,
}}
classes={{ bar: classNames.bar }}
/>
<div css={styles.barHelpers}>
<div css={styles.label}>
@@ -141,6 +137,12 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
);
};
const classNames = {
bar: css`
transition: none;
`,
};
const styles = {
stack: {
paddingLeft: 2,
+43 -32
View File
@@ -61,16 +61,6 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
const quotaQuery = useQuery(workspaceQuota(workspace.owner_name));
const quotaBudget = quotaQuery.data?.budget;
const paperStyles = css`
padding: 24px;
max-width: 288px;
margin-top: ${8};
border-radius: 4px;
display: flex;
flex-direction: column;
gap: ${8};
`;
return (
<>
<Stats aria-label={Language.workspaceDetails} css={styles.stats}>
@@ -146,7 +136,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
</PopoverTrigger>
<PopoverContent
id="schedule-sub"
classes={{ paper: paperStyles }}
classes={{ paper: classNames.paper }}
horizontal="right"
>
<DecreaseTimeContent
@@ -168,7 +158,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
</PopoverTrigger>
<PopoverContent
id="schedule-add"
classes={{ paper: paperStyles }}
classes={{ paper: classNames.paper }}
horizontal="right"
>
<AddTimeContent
@@ -197,9 +187,14 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
);
};
const AddTimeContent = (props: {
interface AddTimeContentProps {
maxDeadlineIncrease: number;
onDeadlinePlus: (value: number) => void;
}
const AddTimeContent: FC<AddTimeContentProps> = ({
maxDeadlineIncrease,
onDeadlinePlus,
}) => {
const popover = usePopover();
@@ -216,7 +211,7 @@ const AddTimeContent = (props: {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const hours = Number(formData.get("hours"));
props.onDeadlinePlus(hours);
onDeadlinePlus(hours);
popover.setIsOpen(false);
}}
>
@@ -227,12 +222,10 @@ const AddTimeContent = (props: {
size="small"
fullWidth
css={styles.timePopoverField}
InputProps={{
className: timePopoverFieldInputStyles,
}}
InputProps={{ className: classNames.deadlineFormInput }}
inputProps={{
min: 0,
max: props.maxDeadlineIncrease,
max: maxDeadlineIncrease,
step: 1,
defaultValue: 1,
}}
@@ -246,9 +239,14 @@ const AddTimeContent = (props: {
);
};
export const DecreaseTimeContent = (props: {
onDeadlineMinus: (hours: number) => void;
interface DecreaseTimeContentProps {
maxDeadlineDecrease: number;
onDeadlineMinus: (hours: number) => void;
}
export const DecreaseTimeContent: FC<DecreaseTimeContentProps> = ({
maxDeadlineDecrease,
onDeadlineMinus,
}) => {
const popover = usePopover();
@@ -265,7 +263,7 @@ export const DecreaseTimeContent = (props: {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const hours = Number(formData.get("hours"));
props.onDeadlineMinus(hours);
onDeadlineMinus(hours);
popover.setIsOpen(false);
}}
>
@@ -276,12 +274,10 @@ export const DecreaseTimeContent = (props: {
size="small"
fullWidth
css={styles.timePopoverField}
InputProps={{
className: timePopoverFieldInputStyles,
}}
InputProps={{ className: classNames.deadlineFormInput }}
inputProps={{
min: 0,
max: props.maxDeadlineDecrease,
max: maxDeadlineDecrease,
step: 1,
defaultValue: 1,
}}
@@ -295,8 +291,11 @@ export const DecreaseTimeContent = (props: {
);
};
const AutoStopDisplay = (props: { workspace: Workspace }) => {
const { workspace } = props;
interface AutoStopDisplayProps {
workspace: Workspace;
}
const AutoStopDisplay: FC<AutoStopDisplayProps> = ({ workspace }) => {
const display = autostopDisplay(workspace);
if (display.tooltip) {
@@ -366,11 +365,23 @@ const isShutdownSoon = (workspace: Workspace): boolean => {
return diff < oneHour;
};
const timePopoverFieldInputStyles = css`
font-size: 14px;
padding: 0px;
border-radius: 4px;
`;
const classNames = {
paper: css`
padding: 24px;
max-width: 288px;
margin-top: 8px;
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 8px;
`,
deadlineFormInput: css`
font-size: 14px;
padding: 0px;
border-radius: 4px;
`,
};
const styles = {
stats: (theme) => ({
-31
View File
@@ -1,31 +0,0 @@
import { combineClasses } from "./combineClasses";
const staticStyles = {
text: "MuiText",
success: "MuiText-Green",
warning: "MuiText-Red",
};
describe("combineClasses", () => {
it.each([
// Falsy
[undefined, undefined],
[{ [staticStyles.text]: false }, undefined],
[{ [staticStyles.text]: undefined }, undefined],
[[], undefined],
// Truthy
[{ [staticStyles.text]: true }, "MuiText"],
[
{ [staticStyles.text]: true, [staticStyles.warning]: true },
"MuiText MuiText-Red",
],
[[staticStyles.text], "MuiText"],
// Mixed
[{ [staticStyles.text]: true, [staticStyles.success]: false }, "MuiText"],
[[staticStyles.text, staticStyles.success], "MuiText MuiText-Green"],
])(`classNames(%p) returns %p`, (staticClasses, result) => {
expect(combineClasses(staticClasses)).toBe(result);
});
});
-47
View File
@@ -1,47 +0,0 @@
export const appendCSSString = (
cssString: string,
cssClass: string,
): string => {
if (cssString === "") {
return cssClass;
}
return `${cssString} ${cssClass}`;
};
/**
* @param classes May be an object or an array. When using an object, each key
* is a css class name and its associated value indicates whether it should be
* applied. When using an array, only truthy values are applied.
* @returns A string with each class that should be included. Classes are
* separated by two spaces. If no classes are included, returns `undefined`.
* @example
* combineClasses() // -> undefined
* combineClasses({ text: true }) // -> "text"
* combineClasses({ text: true, success: true }) // -> "text success"
* combineClasses({ text: true, success: false }) // -> "text"
* combineClasses([ "text", false, "" ]) // -> "text"
*/
export const combineClasses = (
classes:
| Array<string | false | undefined>
| Record<string, boolean | undefined> = {},
): string | undefined => {
let result = "";
if (Array.isArray(classes)) {
for (const cssClass of classes) {
if (cssClass) {
result = appendCSSString(result, cssClass);
}
}
} else {
for (const cssClass in classes) {
const useClass = classes[cssClass];
if (useClass) {
result = appendCSSString(result, cssClass);
}
}
}
return result.length > 0 ? result : undefined;
};