chore: use emotion for styling (pt. 2) (#9951)

This commit is contained in:
Kayla Washburn
2023-10-02 10:48:11 -06:00
committed by GitHub
parent fabcc41a6b
commit 148fa819ae
13 changed files with 678 additions and 741 deletions

View File

@ -1,4 +1,4 @@
import type { Theme as MuiTheme } from "@mui/system"; import type { DefaultTheme as MuiTheme } from "@mui/system";
declare module "@emotion/react" { declare module "@emotion/react" {
interface Theme extends MuiTheme {} interface Theme extends MuiTheme {}

View File

@ -2,7 +2,7 @@
// eslint-disable-next-line no-restricted-imports -- Read above // eslint-disable-next-line no-restricted-imports -- Read above
import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar"; import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
import { FC } from "react"; import { FC } from "react";
import { css, type Theme } from "@emotion/react"; import { css, type Interpolation, type Theme } from "@emotion/react";
export type AvatarProps = MuiAvatarProps & { export type AvatarProps = MuiAvatarProps & {
size?: "sm" | "md" | "xl"; size?: "sm" | "md" | "xl";
@ -11,26 +11,26 @@ export type AvatarProps = MuiAvatarProps & {
}; };
const sizeStyles = { const sizeStyles = {
sm: (theme: Theme) => ({ sm: (theme) => ({
width: theme.spacing(3), width: theme.spacing(3),
height: theme.spacing(3), height: theme.spacing(3),
fontSize: theme.spacing(1.5), fontSize: theme.spacing(1.5),
}), }),
md: {}, md: {},
xl: (theme: Theme) => ({ xl: (theme) => ({
width: theme.spacing(6), width: theme.spacing(6),
height: theme.spacing(6), height: theme.spacing(6),
fontSize: theme.spacing(3), fontSize: theme.spacing(3),
}), }),
}; } satisfies Record<string, Interpolation<Theme>>;
const colorStyles = { const colorStyles = {
light: {}, light: {},
darken: (theme: Theme) => ({ darken: (theme) => ({
background: theme.palette.divider, background: theme.palette.divider,
color: theme.palette.text.primary, color: theme.palette.text.primary,
}), }),
}; } satisfies Record<string, Interpolation<Theme>>;
const fitImageStyles = css` const fitImageStyles = css`
& .MuiAvatar-img { & .MuiAvatar-img {

View File

@ -1,7 +1,7 @@
import type { FC } from "react";
import { useTheme } from "@emotion/react";
import { Avatar } from "components/Avatar/Avatar"; import { Avatar } from "components/Avatar/Avatar";
import { FC } from "react";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { makeStyles } from "@mui/styles";
export interface AvatarDataProps { export interface AvatarDataProps {
title: string | JSX.Element; title: string | JSX.Element;
@ -16,7 +16,7 @@ export const AvatarData: FC<AvatarDataProps> = ({
src, src,
avatar, avatar,
}) => { }) => {
const styles = useStyles(); const theme = useTheme();
if (!avatar) { if (!avatar) {
avatar = <Avatar src={src}>{title}</Avatar>; avatar = <Avatar src={src}>{title}</Avatar>;
@ -27,38 +27,41 @@ export const AvatarData: FC<AvatarDataProps> = ({
spacing={1.5} spacing={1.5}
direction="row" direction="row"
alignItems="center" alignItems="center"
className={styles.root} css={{
minHeight: theme.spacing(5), // Make it predictable for the skeleton
width: "100%",
lineHeight: "150%",
}}
> >
{avatar} {avatar}
<Stack spacing={0} className={styles.info}> <Stack
<span className={styles.title}>{title}</span> spacing={0}
{subtitle && <span className={styles.subtitle}>{subtitle}</span>} css={{
width: "100%",
}}
>
<span
css={{
color: theme.palette.text.primary,
fontWeight: 600,
}}
>
{title}
</span>
{subtitle && (
<span
css={{
fontSize: 12,
color: theme.palette.text.secondary,
lineHeight: "150%",
maxWidth: 540,
}}
>
{subtitle}
</span>
)}
</Stack> </Stack>
</Stack> </Stack>
); );
}; };
const useStyles = makeStyles((theme) => ({
root: {
minHeight: theme.spacing(5), // Make it predictable for the skeleton
width: "100%",
lineHeight: "150%",
},
info: {
width: "100%",
},
title: {
color: theme.palette.text.primary,
fontWeight: 600,
},
subtitle: {
fontSize: 12,
color: theme.palette.text.secondary,
lineHeight: "150%",
maxWidth: 540,
},
}));

View File

@ -1,9 +1,8 @@
import IconButton from "@mui/material/Button"; import IconButton from "@mui/material/Button";
import { makeStyles } from "@mui/styles";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Check from "@mui/icons-material/Check"; import Check from "@mui/icons-material/Check";
import { useClipboard } from "hooks/useClipboard"; import { useClipboard } from "hooks/useClipboard";
import { combineClasses } from "utils/combineClasses"; import { css } from "@emotion/react";
import { FileCopyIcon } from "../Icons/FileCopyIcon"; import { FileCopyIcon } from "../Icons/FileCopyIcon";
interface CopyButtonProps { interface CopyButtonProps {
@ -29,51 +28,53 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
buttonClassName = "", buttonClassName = "",
tooltipTitle = Language.tooltipTitle, tooltipTitle = Language.tooltipTitle,
}) => { }) => {
const styles = useStyles();
const { isCopied, copy: copyToClipboard } = useClipboard(text); const { isCopied, copy: copyToClipboard } = useClipboard(text);
const fileCopyIconStyles = css`
width: 20px;
height: 20px;
`;
return ( return (
<Tooltip title={tooltipTitle} placement="top"> <Tooltip title={tooltipTitle} placement="top">
<div <div
className={combineClasses([styles.copyButtonWrapper, wrapperClassName])} className={wrapperClassName}
css={{
display: "flex",
}}
> >
<IconButton <IconButton
className={combineClasses([styles.copyButton, buttonClassName])} className={buttonClassName}
css={(theme) => css`
border-radius: ${theme.shape.borderRadius}px;
padding: ${theme.spacing(0.85)};
min-width: 32px;
&:hover {
background: ${theme.palette.background.paper};
}
`}
onClick={copyToClipboard} onClick={copyToClipboard}
size="small" size="small"
aria-label={Language.ariaLabel} aria-label={Language.ariaLabel}
variant="text" variant="text"
> >
{isCopied ? ( {isCopied ? (
<Check className={styles.fileCopyIcon} /> <Check css={fileCopyIconStyles} />
) : ( ) : (
<FileCopyIcon className={styles.fileCopyIcon} /> <FileCopyIcon css={fileCopyIconStyles} />
)}
{ctaCopy && (
<div
css={(theme) => ({
marginLeft: theme.spacing(1),
})}
>
{ctaCopy}
</div>
)} )}
{ctaCopy && <div className={styles.buttonCopy}>{ctaCopy}</div>}
</IconButton> </IconButton>
</div> </div>
</Tooltip> </Tooltip>
); );
}; };
const useStyles = makeStyles((theme) => ({
copyButtonWrapper: {
display: "flex",
},
copyButton: {
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.85),
minWidth: 32,
"&:hover": {
background: theme.palette.background.paper,
},
},
fileCopyIcon: {
width: 20,
height: 20,
},
buttonCopy: {
marginLeft: theme.spacing(1),
},
}));

View File

@ -2,7 +2,6 @@ import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated";
import { FC, useMemo, useEffect, useState } from "react"; import { FC, useMemo, useEffect, useState } from "react";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import BuildingIcon from "@mui/icons-material/Build"; import BuildingIcon from "@mui/icons-material/Build";
import { makeStyles } from "@mui/styles";
import { RocketIcon } from "components/Icons/RocketIcon"; import { RocketIcon } from "components/Icons/RocketIcon";
import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
@ -19,9 +18,36 @@ import CollectedIcon from "@mui/icons-material/Compare";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { getDisplayWorkspaceStatus } from "utils/workspace"; import { getDisplayWorkspaceStatus } from "utils/workspace";
import { css, type Theme, type Interpolation, useTheme } from "@emotion/react";
export const bannerHeight = 36; export const bannerHeight = 36;
const styles = {
group: css`
display: flex;
align-items: center;
`,
category: (theme) => ({
marginRight: theme.spacing(2),
color: theme.palette.text.primary,
}),
values: (theme) => ({
display: "flex",
gap: theme.spacing(1),
color: theme.palette.text.secondary,
}),
value: (theme) => css`
display: flex;
align-items: center;
gap: ${theme.spacing(0.5)};
& svg {
width: 12px;
height: 12px;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
export interface DeploymentBannerViewProps { export interface DeploymentBannerViewProps {
fetchStats?: () => void; fetchStats?: () => void;
stats?: DeploymentStats; stats?: DeploymentStats;
@ -31,7 +57,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
stats, stats,
fetchStats, fetchStats,
}) => { }) => {
const styles = useStyles(); const theme = useTheme();
const aggregatedMinutes = useMemo(() => { const aggregatedMinutes = useMemo(() => {
if (!stats) { if (!stats) {
return; return;
@ -80,15 +106,46 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
}, [timeUntilRefresh, stats]); }, [timeUntilRefresh, stats]);
return ( return (
<div className={styles.container}> <div
css={{
position: "sticky",
height: bannerHeight,
bottom: 0,
zIndex: 1,
padding: theme.spacing(0, 2),
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 12,
gap: theme.spacing(4),
borderTop: `1px solid ${theme.palette.divider}`,
overflowX: "auto",
whiteSpace: "nowrap",
}}
>
<Tooltip title="Status of your Coder deployment. Only visible for admins!"> <Tooltip title="Status of your Coder deployment. Only visible for admins!">
<div className={styles.rocket}> <div
css={css`
display: flex;
align-items: center;
& svg {
width: 16px;
height: 16px;
}
${theme.breakpoints.down("lg")} {
display: none;
}
`}
>
<RocketIcon /> <RocketIcon />
</div> </div>
</Tooltip> </Tooltip>
<div className={styles.group}> <div css={styles.group}>
<div className={styles.category}>Workspaces</div> <div css={styles.category}>Workspaces</div>
<div className={styles.values}> <div css={styles.values}>
<WorkspaceBuildValue <WorkspaceBuildValue
status="pending" status="pending"
count={stats?.workspaces.pending} count={stats?.workspaces.pending}
@ -115,21 +172,21 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
/> />
</div> </div>
</div> </div>
<div className={styles.group}> <div css={styles.group}>
<Tooltip title={`Activity in the last ~${aggregatedMinutes} minutes`}> <Tooltip title={`Activity in the last ~${aggregatedMinutes} minutes`}>
<div className={styles.category}>Transmission</div> <div css={styles.category}>Transmission</div>
</Tooltip> </Tooltip>
<div className={styles.values}> <div css={styles.values}>
<Tooltip title="Data sent to workspaces"> <Tooltip title="Data sent to workspaces">
<div className={styles.value}> <div css={styles.value}>
<DownloadIcon /> <DownloadIcon />
{stats ? prettyBytes(stats.workspaces.rx_bytes) : "-"} {stats ? prettyBytes(stats.workspaces.rx_bytes) : "-"}
</div> </div>
</Tooltip> </Tooltip>
<ValueSeparator /> <ValueSeparator />
<Tooltip title="Data sent from workspaces"> <Tooltip title="Data sent from workspaces">
<div className={styles.value}> <div css={styles.value}>
<UploadIcon /> <UploadIcon />
{stats ? prettyBytes(stats.workspaces.tx_bytes) : "-"} {stats ? prettyBytes(stats.workspaces.tx_bytes) : "-"}
</div> </div>
@ -142,20 +199,26 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
: "The average latency of user connections to workspaces" : "The average latency of user connections to workspaces"
} }
> >
<div className={styles.value}> <div css={styles.value}>
<LatencyIcon /> <LatencyIcon />
{displayLatency > 0 ? displayLatency?.toFixed(2) + " ms" : "-"} {displayLatency > 0 ? displayLatency?.toFixed(2) + " ms" : "-"}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div className={styles.group}> <div css={styles.group}>
<div className={styles.category}>Active Connections</div> <div css={styles.category}>Active Connections</div>
<div className={styles.values}> <div css={styles.values}>
<Tooltip title="VS Code Editors with the Coder Remote Extension"> <Tooltip title="VS Code Editors with the Coder Remote Extension">
<div className={styles.value}> <div css={styles.value}>
<VSCodeIcon className={styles.iconStripColor} /> <VSCodeIcon
css={css`
& * {
fill: currentColor;
}
`}
/>
{typeof stats?.session_count.vscode === "undefined" {typeof stats?.session_count.vscode === "undefined"
? "-" ? "-"
: stats?.session_count.vscode} : stats?.session_count.vscode}
@ -163,7 +226,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
</Tooltip> </Tooltip>
<ValueSeparator /> <ValueSeparator />
<Tooltip title="SSH Sessions"> <Tooltip title="SSH Sessions">
<div className={styles.value}> <div css={styles.value}>
<TerminalIcon /> <TerminalIcon />
{typeof stats?.session_count.ssh === "undefined" {typeof stats?.session_count.ssh === "undefined"
? "-" ? "-"
@ -172,7 +235,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
</Tooltip> </Tooltip>
<ValueSeparator /> <ValueSeparator />
<Tooltip title="Web Terminal Sessions"> <Tooltip title="Web Terminal Sessions">
<div className={styles.value}> <div css={styles.value}>
<WebTerminalIcon /> <WebTerminalIcon />
{typeof stats?.session_count.reconnecting_pty === "undefined" {typeof stats?.session_count.reconnecting_pty === "undefined"
? "-" ? "-"
@ -181,9 +244,17 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div className={styles.refresh}> <div
css={{
color: theme.palette.text.primary,
marginLeft: "auto",
display: "flex",
alignItems: "center",
gap: theme.spacing(2),
}}
>
<Tooltip title="The last time stats were aggregated. Workspaces report statistics periodically, so it may take a bit for these to update!"> <Tooltip title="The last time stats were aggregated. Workspaces report statistics periodically, so it may take a bit for these to update!">
<div className={styles.value}> <div css={styles.value}>
<CollectedIcon /> <CollectedIcon />
{lastAggregated} {lastAggregated}
</div> </div>
@ -191,7 +262,23 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
<Tooltip title="A countdown until stats are fetched again. Click to refresh!"> <Tooltip title="A countdown until stats are fetched again. Click to refresh!">
<Button <Button
className={`${styles.value} ${styles.refreshButton}`} css={css`
${styles.value(theme)}
margin: 0;
padding: 0 8px;
height: unset;
min-height: unset;
font-size: unset;
color: unset;
border: 0;
min-width: unset;
font-family: inherit;
& svg {
margin-right: ${theme.spacing(0.5)};
}
`}
onClick={() => { onClick={() => {
if (fetchStats) { if (fetchStats) {
fetchStats(); fetchStats();
@ -209,15 +296,18 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
}; };
const ValueSeparator: FC = () => { const ValueSeparator: FC = () => {
const styles = useStyles(); const theme = useTheme();
return <div className={styles.valueSeparator}>/</div>; const separatorStyles = css`
color: ${theme.palette.text.disabled};
`;
return <div css={separatorStyles}>/</div>;
}; };
const WorkspaceBuildValue: FC<{ const WorkspaceBuildValue: FC<{
status: WorkspaceStatus; status: WorkspaceStatus;
count?: number; count?: number;
}> = ({ status, count }) => { }> = ({ status, count }) => {
const styles = useStyles();
const displayStatus = getDisplayWorkspaceStatus(status); const displayStatus = getDisplayWorkspaceStatus(status);
let statusText = displayStatus.text; let statusText = displayStatus.text;
let icon = displayStatus.icon; let icon = displayStatus.icon;
@ -232,7 +322,7 @@ const WorkspaceBuildValue: FC<{
component={RouterLink} component={RouterLink}
to={`/workspaces?filter=${encodeURIComponent("status:" + status)}`} to={`/workspaces?filter=${encodeURIComponent("status:" + status)}`}
> >
<div className={styles.value}> <div css={styles.value}>
{icon} {icon}
{typeof count === "undefined" ? "-" : count} {typeof count === "undefined" ? "-" : count}
</div> </div>
@ -240,88 +330,3 @@ const WorkspaceBuildValue: FC<{
</Tooltip> </Tooltip>
); );
}; };
const useStyles = makeStyles((theme) => ({
rocket: {
display: "flex",
alignItems: "center",
"& svg": {
width: 16,
height: 16,
},
[theme.breakpoints.down("lg")]: {
display: "none",
},
},
container: {
position: "sticky",
height: bannerHeight,
bottom: 0,
zIndex: 1,
padding: theme.spacing(0, 2),
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 12,
gap: theme.spacing(4),
borderTop: `1px solid ${theme.palette.divider}`,
overflowX: "auto",
whiteSpace: "nowrap",
},
group: {
display: "flex",
alignItems: "center",
},
category: {
marginRight: theme.spacing(2),
color: theme.palette.text.primary,
},
values: {
display: "flex",
gap: theme.spacing(1),
color: theme.palette.text.secondary,
},
valueSeparator: {
color: theme.palette.text.disabled,
},
value: {
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
"& svg": {
width: 12,
height: 12,
},
},
iconStripColor: {
"& *": {
fill: "currentColor",
},
},
refresh: {
color: theme.palette.text.primary,
marginLeft: "auto",
display: "flex",
alignItems: "center",
gap: theme.spacing(2),
},
refreshButton: {
margin: 0,
padding: "0px 8px",
height: "unset",
minHeight: "unset",
fontSize: "unset",
color: "unset",
border: 0,
minWidth: "unset",
fontFamily: "inherit",
"& svg": {
marginRight: theme.spacing(0.5),
},
},
}));

View File

@ -1,8 +1,14 @@
import {
type CSSObject,
type Interpolation,
type Theme,
useTheme,
} from "@emotion/react";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { makeStyles } from "@mui/styles"; import { css } from "@emotion/react";
import { useState } from "react";
import { Expander } from "components/Expander/Expander"; import { Expander } from "components/Expander/Expander";
import { Pill } from "components/Pill/Pill"; import { Pill } from "components/Pill/Pill";
import { useState } from "react";
import { colors } from "theme/colors"; import { colors } from "theme/colors";
export const Language = { export const Language = {
@ -14,6 +20,13 @@ export const Language = {
moreDetails: "More", moreDetails: "More",
}; };
const styles = {
leftContent: (theme) => ({
marginRight: theme.spacing(1),
marginLeft: theme.spacing(1),
}),
} satisfies Record<string, Interpolation<Theme>>;
export interface LicenseBannerViewProps { export interface LicenseBannerViewProps {
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
@ -23,17 +36,28 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
errors, errors,
warnings, warnings,
}) => { }) => {
const styles = useStyles(); const theme = useTheme();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
const isError = errors.length > 0; const isError = errors.length > 0;
const messages = [...errors, ...warnings]; const messages = [...errors, ...warnings];
const type = isError ? "error" : "warning"; const type = isError ? "error" : "warning";
const containerStyles = css`
${theme.typography.body2 as CSSObject}
display: flex;
align-items: center;
padding: ${theme.spacing(1.5)};
background-color: ${type === "error"
? colors.red[12]
: theme.palette.warning.main};
`;
if (messages.length === 1) { if (messages.length === 1) {
return ( return (
<div className={`${styles.container} ${type}`}> <div css={containerStyles}>
<Pill text={Language.licenseIssue} type={type} lightBorder /> <Pill text={Language.licenseIssue} type={type} lightBorder />
<div className={styles.leftContent}> <div css={styles.leftContent}>
<span>{messages[0]}</span> <span>{messages[0]}</span>
&nbsp; &nbsp;
<Link color="white" fontWeight="medium" href="mailto:sales@coder.com"> <Link color="white" fontWeight="medium" href="mailto:sales@coder.com">
@ -42,65 +66,33 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
</div> </div>
</div> </div>
); );
} else {
return (
<div className={`${styles.container} ${type}`}>
<Pill
text={Language.licenseIssues(messages.length)}
type={type}
lightBorder
/>
<div className={styles.leftContent}>
<div>
{Language.exceeded}
&nbsp;
<Link
color="white"
fontWeight="medium"
href="mailto:sales@coder.com"
>
{Language.upgrade}
</Link>
</div>
<Expander expanded={showDetails} setExpanded={setShowDetails}>
<ul className={styles.list}>
{messages.map((message) => (
<li className={styles.listItem} key={message}>
{message}
</li>
))}
</ul>
</Expander>
</div>
</div>
);
} }
return (
<div css={containerStyles}>
<Pill
text={Language.licenseIssues(messages.length)}
type={type}
lightBorder
/>
<div css={styles.leftContent}>
<div>
{Language.exceeded}
&nbsp;
<Link color="white" fontWeight="medium" href="mailto:sales@coder.com">
{Language.upgrade}
</Link>
</div>
<Expander expanded={showDetails} setExpanded={setShowDetails}>
<ul css={{ padding: theme.spacing(1), margin: 0 }}>
{messages.map((message) => (
<li css={{ margin: theme.spacing(0.5) }} key={message}>
{message}
</li>
))}
</ul>
</Expander>
</div>
</div>
);
}; };
const useStyles = makeStyles((theme) => ({
container: {
...theme.typography.body2,
padding: theme.spacing(1.5),
backgroundColor: theme.palette.warning.main,
display: "flex",
alignItems: "center",
"&.error": {
backgroundColor: colors.red[12],
},
},
flex: {
display: "column",
},
leftContent: {
marginRight: theme.spacing(1),
marginLeft: theme.spacing(1),
},
list: {
padding: theme.spacing(1),
margin: 0,
},
listItem: {
margin: theme.spacing(0.5),
},
}));

View File

@ -2,15 +2,13 @@ import Drawer from "@mui/material/Drawer";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import List from "@mui/material/List"; import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import { makeStyles } from "@mui/styles";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import { CoderIcon } from "components/Icons/CoderIcon"; import { CoderIcon } from "components/Icons/CoderIcon";
import { FC, useRef, useState } from "react"; import { type FC, type ReactNode, useRef, useState } from "react";
import { NavLink, useLocation, useNavigate } from "react-router-dom"; import { NavLink, useLocation, useNavigate } from "react-router-dom";
import { colors } from "theme/colors"; import { colors } from "theme/colors";
import * as TypesGen from "api/typesGenerated"; import * as TypesGen from "api/typesGenerated";
import { navHeight } from "theme/constants"; import { navHeight } from "theme/constants";
import { combineClasses } from "utils/combineClasses";
import { UserDropdown } from "./UserDropdown/UserDropdown"; import { UserDropdown } from "./UserDropdown/UserDropdown";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
@ -25,6 +23,7 @@ import { BUTTON_SM_HEIGHT } from "theme/theme";
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"; import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
import { usePermissions } from "hooks/usePermissions"; import { usePermissions } from "hooks/usePermissions";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
export const USERS_LINK = `/users?filter=${encodeURIComponent( export const USERS_LINK = `/users?filter=${encodeURIComponent(
"status:active", "status:active",
@ -50,52 +49,128 @@ export const Language = {
deployment: "Deployment", deployment: "Deployment",
}; };
const NavItems: React.FC< const styles = {
React.PropsWithChildren<{ desktopNavItems: (theme) => css`
className?: string; display: none;
canViewAuditLog: boolean;
canViewDeployment: boolean; ${theme.breakpoints.up("md")} {
canViewAllUsers: boolean; display: flex;
}> }
> = ({ className, canViewAuditLog, canViewDeployment, canViewAllUsers }) => { `,
const styles = useStyles(); mobileMenuButton: (theme) => css`
${theme.breakpoints.up("md")} {
display: none;
}
`,
wrapper: (theme) => css`
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
${theme.breakpoints.up("md")} {
justify-content: flex-start;
}
`,
drawerHeader: (theme) => ({
padding: theme.spacing(2),
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
}),
logo: (theme) => css`
align-items: center;
display: flex;
height: ${navHeight}px;
color: ${theme.palette.text.primary};
padding: ${theme.spacing(2)};
// svg is for the Coder logo, img is for custom images
& svg,
& img {
height: 100%;
object-fit: contain;
}
`,
drawerLogo: (theme) => ({
padding: 0,
maxHeight: theme.spacing(5),
}),
item: {
padding: 0,
},
link: (theme) => css`
align-items: center;
color: ${colors.gray[6]};
display: flex;
flex: 1;
font-size: 16px;
padding: ${theme.spacing(1.5)} ${theme.spacing(2)};
text-decoration: none;
transition: background-color 0.15s ease-in-out;
&:hover {
background-color: theme.palette.action.hover;
}
${theme.breakpoints.up("md")} {
height: ${navHeight};
padding: 0 ${theme.spacing(3)};
}
`,
} satisfies Record<string, Interpolation<Theme>>;
interface NavItemsProps {
children?: ReactNode;
className?: string;
canViewAuditLog: boolean;
canViewDeployment: boolean;
canViewAllUsers: boolean;
}
const NavItems: React.FC<NavItemsProps> = (props) => {
const { className, canViewAuditLog, canViewDeployment, canViewAllUsers } =
props;
const location = useLocation(); const location = useLocation();
const theme = useTheme();
return ( return (
<List className={combineClasses([styles.navItems, className])}> <List css={{ padding: 0 }} className={className}>
<ListItem button className={styles.item}> <ListItem button css={styles.item}>
<NavLink <NavLink
className={combineClasses([ css={[
styles.link, styles.link,
location.pathname.startsWith("/@") && "active", location.pathname.startsWith("/@") && {
])} color: theme.palette.text.primary,
fontWeight: 500,
},
]}
to="/workspaces" to="/workspaces"
> >
{Language.workspaces} {Language.workspaces}
</NavLink> </NavLink>
</ListItem> </ListItem>
<ListItem button className={styles.item}> <ListItem button css={styles.item}>
<NavLink className={styles.link} to="/templates"> <NavLink css={styles.link} to="/templates">
{Language.templates} {Language.templates}
</NavLink> </NavLink>
</ListItem> </ListItem>
{canViewAllUsers && ( {canViewAllUsers && (
<ListItem button className={styles.item}> <ListItem button css={styles.item}>
<NavLink className={styles.link} to={USERS_LINK}> <NavLink css={styles.link} to={USERS_LINK}>
{Language.users} {Language.users}
</NavLink> </NavLink>
</ListItem> </ListItem>
)} )}
{canViewAuditLog && ( {canViewAuditLog && (
<ListItem button className={styles.item}> <ListItem button css={styles.item}>
<NavLink className={styles.link} to="/audit"> <NavLink css={styles.link} to="/audit">
{Language.audit} {Language.audit}
</NavLink> </NavLink>
</ListItem> </ListItem>
)} )}
{canViewDeployment && ( {canViewDeployment && (
<ListItem button className={styles.item}> <ListItem button css={styles.item}>
<NavLink className={styles.link} to="/deployment/general"> <NavLink css={styles.link} to="/deployment/general">
{Language.deployment} {Language.deployment}
</NavLink> </NavLink>
</ListItem> </ListItem>
@ -114,15 +189,20 @@ export const NavbarView: FC<NavbarViewProps> = ({
canViewAllUsers, canViewAllUsers,
proxyContextValue, proxyContextValue,
}) => { }) => {
const styles = useStyles();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
return ( return (
<nav className={styles.root}> <nav
<div className={styles.wrapper}> css={(theme) => ({
height: navHeight,
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
})}
>
<div css={styles.wrapper}>
<IconButton <IconButton
aria-label="Open menu" aria-label="Open menu"
className={styles.mobileMenuButton} css={styles.mobileMenuButton}
onClick={() => { onClick={() => {
setIsDrawerOpen(true); setIsDrawerOpen(true);
}} }}
@ -136,9 +216,9 @@ export const NavbarView: FC<NavbarViewProps> = ({
open={isDrawerOpen} open={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)} onClose={() => setIsDrawerOpen(false)}
> >
<div className={styles.drawer}> <div css={{ width: 250 }}>
<div className={styles.drawerHeader}> <div css={styles.drawerHeader}>
<div className={combineClasses([styles.logo, styles.drawerLogo])}> <div css={[styles.logo, styles.drawerLogo]}>
{logo_url ? ( {logo_url ? (
<img src={logo_url} alt="Custom Logo" /> <img src={logo_url} alt="Custom Logo" />
) : ( ) : (
@ -154,7 +234,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
</div> </div>
</Drawer> </Drawer>
<NavLink className={styles.logo} to="/workspaces"> <NavLink css={styles.logo} to="/workspaces">
{logo_url ? ( {logo_url ? (
<img src={logo_url} alt="Custom Logo" /> <img src={logo_url} alt="Custom Logo" />
) : ( ) : (
@ -163,7 +243,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
</NavLink> </NavLink>
<NavItems <NavItems
className={styles.desktopNavItems} css={styles.desktopNavItems}
canViewAuditLog={canViewAuditLog} canViewAuditLog={canViewAuditLog}
canViewDeployment={canViewDeployment} canViewDeployment={canViewDeployment}
canViewAllUsers={canViewAllUsers} canViewAllUsers={canViewAllUsers}
@ -385,93 +465,3 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
</> </>
); );
}; };
const useStyles = makeStyles((theme) => ({
displayInitial: {
display: "initial",
},
root: {
height: navHeight,
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
},
wrapper: {
position: "relative",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
[theme.breakpoints.up("md")]: {
justifyContent: "flex-start",
},
},
drawer: {
width: 250,
},
drawerHeader: {
padding: theme.spacing(2),
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
},
navItems: {
padding: 0,
},
desktopNavItems: {
display: "none",
[theme.breakpoints.up("md")]: {
display: "flex",
},
},
mobileMenuButton: {
[theme.breakpoints.up("md")]: {
display: "none",
},
},
logo: {
alignItems: "center",
display: "flex",
height: navHeight,
color: theme.palette.text.primary,
padding: theme.spacing(2),
// svg is for the Coder logo, img is for custom images
"& svg, & img": {
height: "100%",
objectFit: "contain",
},
},
drawerLogo: {
padding: 0,
maxHeight: theme.spacing(5),
},
title: {
flex: 1,
textAlign: "center",
},
item: {
padding: 0,
},
link: {
alignItems: "center",
color: colors.gray[6],
display: "flex",
flex: 1,
fontSize: 16,
padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
textDecoration: "none",
transition: "background-color 0.15s ease-in-out",
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
// NavLink adds this class when the current route matches.
"&.active": {
color: theme.palette.text.primary,
fontWeight: 500,
},
[theme.breakpoints.up("md")]: {
height: navHeight,
padding: `0 ${theme.spacing(3)}`,
},
},
}));

View File

@ -1,6 +1,7 @@
import { css } from "@emotion/css";
import { useTheme } from "@emotion/react";
import Popover, { PopoverProps } from "@mui/material/Popover"; import Popover, { PopoverProps } from "@mui/material/Popover";
import { makeStyles } from "@mui/styles"; import type { FC, PropsWithChildren } from "react";
import { FC, PropsWithChildren } from "react";
type BorderedMenuVariant = "user-dropdown"; type BorderedMenuVariant = "user-dropdown";
@ -13,23 +14,17 @@ export const BorderedMenu: FC<PropsWithChildren<BorderedMenuProps>> = ({
variant, variant,
...rest ...rest
}) => { }) => {
const styles = useStyles(); const theme = useTheme();
const paper = css`
width: 260px;
border-radius: ${theme.shape.borderRadius};
box-shadow: ${theme.shadows[6]};
`;
return ( return (
<Popover <Popover classes={{ paper }} data-variant={variant} {...rest}>
classes={{ paper: styles.paperRoot }}
data-variant={variant}
{...rest}
>
{children} {children}
</Popover> </Popover>
); );
}; };
const useStyles = makeStyles((theme) => ({
paperRoot: {
width: 260,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[6],
},
}));

View File

@ -1,6 +1,5 @@
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import { makeStyles } from "@mui/styles";
import AccountIcon from "@mui/icons-material/AccountCircleOutlined"; import AccountIcon from "@mui/icons-material/AccountCircleOutlined";
import BugIcon from "@mui/icons-material/BugReportOutlined"; import BugIcon from "@mui/icons-material/BugReportOutlined";
import ChatIcon from "@mui/icons-material/ChatOutlined"; import ChatIcon from "@mui/icons-material/ChatOutlined";
@ -11,7 +10,12 @@ import { Link } from "react-router-dom";
import * as TypesGen from "api/typesGenerated"; import * as TypesGen from "api/typesGenerated";
import DocsIcon from "@mui/icons-material/MenuBook"; import DocsIcon from "@mui/icons-material/MenuBook";
import LogoutIcon from "@mui/icons-material/ExitToAppOutlined"; import LogoutIcon from "@mui/icons-material/ExitToAppOutlined";
import { combineClasses } from "utils/combineClasses"; import {
css,
type CSSObject,
type Interpolation,
type Theme,
} from "@emotion/react";
export const Language = { export const Language = {
accountLabel: "Account", accountLabel: "Account",
@ -19,6 +23,61 @@ export const Language = {
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`, copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
}; };
const styles = {
info: (theme) => [
theme.typography.body2 as CSSObject,
{
padding: theme.spacing(2.5),
},
],
userName: {
fontWeight: 600,
},
userEmail: (theme) => ({
color: theme.palette.text.secondary,
width: "100%",
textOverflow: "ellipsis",
overflow: "hidden",
}),
link: {
textDecoration: "none",
color: "inherit",
},
menuItem: (theme) => css`
gap: ${theme.spacing(2.5)};
padding: ${theme.spacing(1, 2.5)};
&:hover {
background-color: ${theme.palette.action.hover};
transition: background-color 0.3s ease;
}
`,
menuItemIcon: (theme) => ({
color: theme.palette.text.secondary,
width: theme.spacing(2.5),
height: theme.spacing(2.5),
}),
menuItemText: {
fontSize: 14,
},
footerText: (theme) => css`
font-size: 12px;
text-decoration: none;
color: ${theme.palette.text.secondary};
display: flex;
align-items: center;
gap: 4px;
& svg {
width: 12px;
height: 12px;
}
`,
buildInfo: (theme) => ({
color: theme.palette.text.primary,
}),
} satisfies Record<string, Interpolation<Theme>>;
export interface UserDropdownContentProps { export interface UserDropdownContentProps {
user: TypesGen.User; user: TypesGen.User;
buildInfo?: TypesGen.BuildInfoResponse; buildInfo?: TypesGen.BuildInfoResponse;
@ -34,63 +93,55 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
onPopoverClose, onPopoverClose,
onSignOut, onSignOut,
}) => { }) => {
const styles = useStyles();
return ( return (
<div> <div>
<Stack className={styles.info} spacing={0}> <Stack css={styles.info} spacing={0}>
<span className={styles.userName}>{user.username}</span> <span css={styles.userName}>{user.username}</span>
<span className={styles.userEmail}>{user.email}</span> <span css={styles.userEmail}>{user.email}</span>
</Stack> </Stack>
<Divider className={styles.divider} /> <Divider css={{ marginBottom: 8 }} />
<Link to="/settings/account" className={styles.link}> <Link to="/settings/account" css={styles.link}>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}> <MenuItem css={styles.menuItem} onClick={onPopoverClose}>
<AccountIcon className={styles.menuItemIcon} /> <AccountIcon css={styles.menuItemIcon} />
<span className={styles.menuItemText}>{Language.accountLabel}</span> <span css={styles.menuItemText}>{Language.accountLabel}</span>
</MenuItem> </MenuItem>
</Link> </Link>
<MenuItem className={styles.menuItem} onClick={onSignOut}> <MenuItem css={styles.menuItem} onClick={onSignOut}>
<LogoutIcon className={styles.menuItemIcon} /> <LogoutIcon css={styles.menuItemIcon} />
<span className={styles.menuItemText}>{Language.signOutLabel}</span> <span css={styles.menuItemText}>{Language.signOutLabel}</span>
</MenuItem> </MenuItem>
<Divider className={styles.divider} /> {supportLinks && (
<>
<> <Divider />
{supportLinks && {supportLinks.map((link) => (
supportLinks.map((link) => (
<a <a
href={includeBuildInfo(link.target, buildInfo)} href={includeBuildInfo(link.target, buildInfo)}
key={link.name} key={link.name}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={styles.link} css={styles.link}
> >
<MenuItem className={styles.menuItem} onClick={onPopoverClose}> <MenuItem css={styles.menuItem} onClick={onPopoverClose}>
{link.icon === "bug" && ( {link.icon === "bug" && <BugIcon css={styles.menuItemIcon} />}
<BugIcon className={styles.menuItemIcon} /> {link.icon === "chat" && <ChatIcon css={styles.menuItemIcon} />}
)} {link.icon === "docs" && <DocsIcon css={styles.menuItemIcon} />}
{link.icon === "chat" && ( <span css={styles.menuItemText}>{link.name}</span>
<ChatIcon className={styles.menuItemIcon} />
)}
{link.icon === "docs" && (
<DocsIcon className={styles.menuItemIcon} />
)}
<span className={styles.menuItemText}>{link.name}</span>
</MenuItem> </MenuItem>
</a> </a>
))} ))}
</> </>
)}
{supportLinks && <Divider className={styles.divider} />} <Divider css={{ marginBottom: "0 !important" }} />
<Stack className={styles.info} spacing={0}> <Stack css={styles.info} spacing={0}>
<a <a
title="Browse Source Code" title="Browse Source Code"
className={combineClasses([styles.footerText, styles.buildInfo])} css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url} href={buildInfo?.external_url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -98,76 +149,12 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
{buildInfo?.version} <LaunchIcon /> {buildInfo?.version} <LaunchIcon />
</a> </a>
<div className={styles.footerText}>{Language.copyrightText}</div> <div css={styles.footerText}>{Language.copyrightText}</div>
</Stack> </Stack>
</div> </div>
); );
}; };
const useStyles = makeStyles((theme) => ({
info: {
padding: theme.spacing(2.5),
...theme.typography.body2,
},
userName: {
fontWeight: 600,
},
userEmail: {
color: theme.palette.text.secondary,
width: "100%",
textOverflow: "ellipsis",
overflow: "hidden",
},
link: {
textDecoration: "none",
color: "inherit",
},
menuItem: {
gap: theme.spacing(2.5),
padding: theme.spacing(1, 2.5),
"&:hover": {
backgroundColor: theme.palette.action.hover,
transition: "background-color 0.3s ease",
},
},
menuItemIcon: {
color: theme.palette.text.secondary,
width: theme.spacing(2.5),
height: theme.spacing(2.5),
},
menuItemText: {
fontSize: 14,
},
divider: {
margin: theme.spacing(1, 0),
"&:first-of-type": {
marginTop: 0,
},
"&:last-of-type": {
marginBottom: 0,
},
},
footerText: {
fontSize: 12,
textDecoration: "none",
color: theme.palette.text.secondary,
display: "flex",
alignItems: "center",
gap: 4,
"& svg": {
width: 12,
height: 12,
},
},
buildInfo: {
color: theme.palette.text.primary,
},
}));
const includeBuildInfo = ( const includeBuildInfo = (
href: string, href: string,
buildInfo?: TypesGen.BuildInfoResponse, buildInfo?: TypesGen.BuildInfoResponse,

View File

@ -1,119 +1,10 @@
import { makeStyles } from "@mui/styles"; import type { PropsWithChildren, FC } from "react";
import { Stack } from "components/Stack/Stack";
import { PropsWithChildren, FC } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { combineClasses } from "utils/combineClasses";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import { type Interpolation, type Theme } from "@emotion/react";
import { Stack } from "components/Stack/Stack";
export const EnabledBadge: FC = () => { const styles = {
const styles = useStyles(); badge: (theme) => ({
return (
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
Enabled
</span>
);
};
export const EntitledBadge: FC = () => {
const styles = useStyles();
return (
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
Entitled
</span>
);
};
export const HealthyBadge: FC<{ derpOnly: boolean }> = ({ derpOnly }) => {
const styles = useStyles();
let text = "Healthy";
if (derpOnly) {
text = "Healthy (DERP Only)";
}
return (
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
{text}
</span>
);
};
export const NotHealthyBadge: FC = () => {
const styles = useStyles();
return (
<span className={combineClasses([styles.badge, styles.errorBadge])}>
Unhealthy
</span>
);
};
export const NotRegisteredBadge: FC = () => {
const styles = useStyles();
return (
<Tooltip title="Workspace Proxy has never come online and needs to be started.">
<span className={combineClasses([styles.badge, styles.warnBadge])}>
Never Seen
</span>
</Tooltip>
);
};
export const NotReachableBadge: FC = () => {
const styles = useStyles();
return (
<Tooltip title="Workspace Proxy not responding to http(s) requests.">
<span className={combineClasses([styles.badge, styles.warnBadge])}>
Not Dialable
</span>
</Tooltip>
);
};
export const DisabledBadge: FC = () => {
const styles = useStyles();
return (
<span className={combineClasses([styles.badge, styles.disabledBadge])}>
Disabled
</span>
);
};
export const EnterpriseBadge: FC = () => {
const styles = useStyles();
return (
<span className={combineClasses([styles.badge, styles.enterpriseBadge])}>
Enterprise
</span>
);
};
export const AlphaBadge: FC = () => {
const styles = useStyles();
return (
<span className={combineClasses([styles.badge, styles.alphaBadge])}>
Alpha
</span>
);
};
export const Badges: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles();
return (
<Stack
className={styles.badges}
direction="row"
alignItems="center"
spacing={1}
>
{children}
</Stack>
);
};
const useStyles = makeStyles((theme) => ({
badges: {
margin: theme.spacing(0, 0, 2),
},
badge: {
fontSize: 10, fontSize: 10,
height: 24, height: 24,
fontWeight: 600, fontWeight: 600,
@ -125,45 +16,121 @@ const useStyles = makeStyles((theme) => ({
alignItems: "center", alignItems: "center",
width: "fit-content", width: "fit-content",
whiteSpace: "nowrap", whiteSpace: "nowrap",
}, }),
enterpriseBadge: { enabledBadge: (theme) => ({
backgroundColor: theme.palette.info.dark,
border: `1px solid ${theme.palette.info.light}`,
},
alphaBadge: {
border: `1px solid ${theme.palette.error.light}`,
backgroundColor: theme.palette.error.dark,
},
versionBadge: {
border: `1px solid ${theme.palette.success.light}`, border: `1px solid ${theme.palette.success.light}`,
backgroundColor: theme.palette.success.dark, backgroundColor: theme.palette.success.dark,
textTransform: "none", }),
color: "white", errorBadge: (theme) => ({
fontFamily: MONOSPACE_FONT_FAMILY,
textDecoration: "none",
fontSize: 12,
},
enabledBadge: {
border: `1px solid ${theme.palette.success.light}`,
backgroundColor: theme.palette.success.dark,
},
errorBadge: {
border: `1px solid ${theme.palette.error.light}`, border: `1px solid ${theme.palette.error.light}`,
backgroundColor: theme.palette.error.dark, backgroundColor: theme.palette.error.dark,
}, }),
warnBadge: (theme) => ({
warnBadge: {
border: `1px solid ${theme.palette.warning.light}`, border: `1px solid ${theme.palette.warning.light}`,
backgroundColor: theme.palette.warning.dark, backgroundColor: theme.palette.warning.dark,
}, }),
} satisfies Record<string, Interpolation<Theme>>;
disabledBadge: { export const EnabledBadge: FC = () => {
border: `1px solid ${theme.palette.divider}`, return <span css={[styles.badge, styles.enabledBadge]}>Enabled</span>;
backgroundColor: theme.palette.background.paper, };
},
})); export const EntitledBadge: FC = () => {
return <span css={[styles.badge, styles.enabledBadge]}>Entitled</span>;
};
interface HealthyBadge {
derpOnly: boolean;
}
export const HealthyBadge: FC<HealthyBadge> = (props) => {
const { derpOnly } = props;
return (
<span css={[styles.badge, styles.enabledBadge]}>
{derpOnly ? "Healthy (DERP only)" : "Healthy"}
</span>
);
};
export const NotHealthyBadge: FC = () => {
return <span css={[styles.badge, styles.errorBadge]}>Unhealthy</span>;
};
export const NotRegisteredBadge: FC = () => {
return (
<Tooltip title="Workspace Proxy has never come online and needs to be started.">
<span css={[styles.badge, styles.warnBadge]}>Never seen</span>
</Tooltip>
);
};
export const NotReachableBadge: FC = () => {
return (
<Tooltip title="Workspace Proxy not responding to http(s) requests.">
<span css={[styles.badge, styles.warnBadge]}>Not reachable</span>
</Tooltip>
);
};
export const DisabledBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
border: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
}),
]}
>
Disabled
</span>
);
};
export const EnterpriseBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
backgroundColor: theme.palette.info.dark,
border: `1px solid ${theme.palette.info.light}`,
}),
]}
>
Enterprise
</span>
);
};
export const AlphaBadge: FC = () => {
return (
<span
css={[
styles.badge,
(theme) => ({
border: `1px solid ${theme.palette.error.light}`,
backgroundColor: theme.palette.error.dark,
}),
]}
>
Alpha
</span>
);
};
export const Badges: FC<PropsWithChildren> = ({ children }) => {
return (
<Stack
css={(theme) => ({
margin: theme.spacing(0, 0, 2),
})}
direction="row"
alignItems="center"
spacing={1}
>
{children}
</Stack>
);
};

View File

@ -1,4 +1,3 @@
import { makeStyles } from "@mui/styles";
import { Margins } from "components/Margins/Margins"; import { Margins } from "components/Margins/Margins";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { Sidebar } from "./Sidebar"; import { Sidebar } from "./Sidebar";
@ -31,15 +30,18 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
export const DeploySettingsLayout: FC = () => { export const DeploySettingsLayout: FC = () => {
const deploymentConfigQuery = useQuery(deploymentConfig()); const deploymentConfigQuery = useQuery(deploymentConfig());
const styles = useStyles();
const permissions = usePermissions(); const permissions = usePermissions();
return ( return (
<RequirePermission isFeatureVisible={permissions.viewDeploymentValues}> <RequirePermission isFeatureVisible={permissions.viewDeploymentValues}>
<Margins> <Margins>
<Stack className={styles.wrapper} direction="row" spacing={6}> <Stack
css={(theme) => ({ padding: theme.spacing(6, 0) })}
direction="row"
spacing={6}
>
<Sidebar /> <Sidebar />
<main className={styles.content}> <main css={{ maxWidth: 800, width: "100%" }}>
{deploymentConfigQuery.data ? ( {deploymentConfigQuery.data ? (
<DeploySettingsContext.Provider <DeploySettingsContext.Provider
value={{ value={{
@ -59,14 +61,3 @@ export const DeploySettingsLayout: FC = () => {
</RequirePermission> </RequirePermission>
); );
}; };
const useStyles = makeStyles((theme) => ({
wrapper: {
padding: theme.spacing(6, 0),
},
content: {
maxWidth: 800,
width: "100%",
},
}));

View File

@ -1,8 +1,8 @@
import { makeStyles } from "@mui/styles"; import type { FC, ReactNode, FormEventHandler } from "react";
import { FC, ReactNode, FormEventHandler } from "react";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { type CSSObject, useTheme } from "@emotion/react";
export const Fieldset: FC<{ interface FieldsetProps {
children: ReactNode; children: ReactNode;
title: string | JSX.Element; title: string | JSX.Element;
subtitle?: string | JSX.Element; subtitle?: string | JSX.Element;
@ -10,26 +10,73 @@ export const Fieldset: FC<{
button?: JSX.Element | false; button?: JSX.Element | false;
onSubmit: FormEventHandler<HTMLFormElement>; onSubmit: FormEventHandler<HTMLFormElement>;
isSubmitting?: boolean; isSubmitting?: boolean;
}> = ({ }
title,
subtitle, export const Fieldset: FC<FieldsetProps> = (props) => {
children, const {
validation, title,
button, subtitle,
onSubmit, children,
isSubmitting, validation,
}) => { button,
const styles = useStyles(); onSubmit,
isSubmitting,
} = props;
const theme = useTheme();
return ( return (
<form className={styles.fieldset} onSubmit={onSubmit}> <form
<header className={styles.header}> css={{
<div className={styles.title}>{title}</div> borderRadius: theme.spacing(1),
{subtitle && <div className={styles.subtitle}>{subtitle}</div>} border: `1px solid ${theme.palette.divider}`,
<div className={styles.body}>{children}</div> background: theme.palette.background.paper,
marginTop: theme.spacing(4),
}}
onSubmit={onSubmit}
>
<header css={{ padding: theme.spacing(3) }}>
<div
css={{
fontSize: theme.spacing(2.5),
margin: 0,
fontWeight: 600,
}}
>
{title}
</div>
{subtitle && (
<div
css={{
color: theme.palette.text.secondary,
fontSize: 14,
marginTop: theme.spacing(1),
}}
>
{subtitle}
</div>
)}
<div
css={[
theme.typography.body2 as CSSObject,
{ paddingTop: theme.spacing(2) },
]}
>
{children}
</div>
</header> </header>
<footer className={styles.footer}> <footer
<div className={styles.validation}>{validation}</div> css={[
theme.typography.body2 as CSSObject,
{
background: theme.palette.background.paperLight,
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
]}
>
<div css={{ color: theme.palette.text.secondary }}>{validation}</div>
{button || ( {button || (
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
Submit Submit
@ -39,40 +86,3 @@ export const Fieldset: FC<{
</form> </form>
); );
}; };
const useStyles = makeStyles((theme) => ({
fieldset: {
borderRadius: theme.spacing(1),
border: `1px solid ${theme.palette.divider}`,
background: theme.palette.background.paper,
marginTop: theme.spacing(4),
},
title: {
fontSize: theme.spacing(2.5),
margin: 0,
fontWeight: 600,
},
subtitle: {
color: theme.palette.text.secondary,
fontSize: 14,
marginTop: theme.spacing(1),
},
body: {
...theme.typography.body2,
paddingTop: theme.spacing(2),
},
validation: {
color: theme.palette.text.secondary,
},
header: {
padding: theme.spacing(3),
},
footer: {
...theme.typography.body2,
background: theme.palette.background.paperLight,
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
}));

View File

@ -1,8 +1,8 @@
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { makeStyles } from "@mui/styles";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import type { FC } from "react";
import { useTheme } from "@emotion/react";
import { Stack } from "components/Stack/Stack"; import { Stack } from "components/Stack/Stack";
import { FC } from "react";
export const Header: FC<{ export const Header: FC<{
title: string | JSX.Element; title: string | JSX.Element;
@ -10,16 +10,41 @@ export const Header: FC<{
secondary?: boolean; secondary?: boolean;
docsHref?: string; docsHref?: string;
}> = ({ title, description, docsHref, secondary }) => { }> = ({ title, description, docsHref, secondary }) => {
const styles = useStyles(); const theme = useTheme();
return ( return (
<Stack alignItems="baseline" direction="row" justifyContent="space-between"> <Stack alignItems="baseline" direction="row" justifyContent="space-between">
<div className={styles.headingGroup}> <div css={{ maxWidth: 420, marginBottom: theme.spacing(3) }}>
<h1 className={`${styles.title} ${secondary ? "secondary" : ""}`}> <h1
css={[
{
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "center",
lineHeight: "initial",
margin: 0,
marginBottom: theme.spacing(0.5),
gap: theme.spacing(1),
},
secondary && {
fontSize: 24,
fontWeight: 500,
},
]}
>
{title} {title}
</h1> </h1>
{description && ( {description && (
<span className={styles.description}>{description}</span> <span
css={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
}}
>
{description}
</span>
)} )}
</div> </div>
@ -36,32 +61,3 @@ export const Header: FC<{
</Stack> </Stack>
); );
}; };
const useStyles = makeStyles((theme) => ({
headingGroup: {
maxWidth: 420,
marginBottom: theme.spacing(3),
},
title: {
fontSize: 32,
fontWeight: 700,
display: "flex",
alignItems: "center",
lineHeight: "initial",
margin: 0,
marginBottom: theme.spacing(0.5),
gap: theme.spacing(1),
"&.secondary": {
fontSize: 24,
fontWeight: 500,
},
},
description: {
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
},
}));