mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: avoid @emotion/css when possible (#10807)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user