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" {
interface Theme extends MuiTheme {}

View File

@ -2,7 +2,7 @@
// eslint-disable-next-line no-restricted-imports -- Read above
import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
import { FC } from "react";
import { css, type Theme } from "@emotion/react";
import { css, type Interpolation, type Theme } from "@emotion/react";
export type AvatarProps = MuiAvatarProps & {
size?: "sm" | "md" | "xl";
@ -11,26 +11,26 @@ export type AvatarProps = MuiAvatarProps & {
};
const sizeStyles = {
sm: (theme: Theme) => ({
sm: (theme) => ({
width: theme.spacing(3),
height: theme.spacing(3),
fontSize: theme.spacing(1.5),
}),
md: {},
xl: (theme: Theme) => ({
xl: (theme) => ({
width: theme.spacing(6),
height: theme.spacing(6),
fontSize: theme.spacing(3),
}),
};
} satisfies Record<string, Interpolation<Theme>>;
const colorStyles = {
light: {},
darken: (theme: Theme) => ({
darken: (theme) => ({
background: theme.palette.divider,
color: theme.palette.text.primary,
}),
};
} satisfies Record<string, Interpolation<Theme>>;
const fitImageStyles = css`
& .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 { FC } from "react";
import { Stack } from "components/Stack/Stack";
import { makeStyles } from "@mui/styles";
export interface AvatarDataProps {
title: string | JSX.Element;
@ -16,7 +16,7 @@ export const AvatarData: FC<AvatarDataProps> = ({
src,
avatar,
}) => {
const styles = useStyles();
const theme = useTheme();
if (!avatar) {
avatar = <Avatar src={src}>{title}</Avatar>;
@ -27,38 +27,41 @@ export const AvatarData: FC<AvatarDataProps> = ({
spacing={1.5}
direction="row"
alignItems="center"
className={styles.root}
>
{avatar}
<Stack spacing={0} className={styles.info}>
<span className={styles.title}>{title}</span>
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
</Stack>
</Stack>
);
};
const useStyles = makeStyles((theme) => ({
root: {
css={{
minHeight: theme.spacing(5), // Make it predictable for the skeleton
width: "100%",
lineHeight: "150%",
},
}}
>
{avatar}
info: {
<Stack
spacing={0}
css={{
width: "100%",
},
title: {
}}
>
<span
css={{
color: theme.palette.text.primary,
fontWeight: 600,
},
subtitle: {
}}
>
{title}
</span>
{subtitle && (
<span
css={{
fontSize: 12,
color: theme.palette.text.secondary,
lineHeight: "150%",
maxWidth: 540,
},
}));
}}
>
{subtitle}
</span>
)}
</Stack>
</Stack>
);
};

View File

@ -1,9 +1,8 @@
import IconButton from "@mui/material/Button";
import { makeStyles } from "@mui/styles";
import Tooltip from "@mui/material/Tooltip";
import Check from "@mui/icons-material/Check";
import { useClipboard } from "hooks/useClipboard";
import { combineClasses } from "utils/combineClasses";
import { css } from "@emotion/react";
import { FileCopyIcon } from "../Icons/FileCopyIcon";
interface CopyButtonProps {
@ -29,51 +28,53 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
buttonClassName = "",
tooltipTitle = Language.tooltipTitle,
}) => {
const styles = useStyles();
const { isCopied, copy: copyToClipboard } = useClipboard(text);
const fileCopyIconStyles = css`
width: 20px;
height: 20px;
`;
return (
<Tooltip title={tooltipTitle} placement="top">
<div
className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}
className={wrapperClassName}
css={{
display: "flex",
}}
>
<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}
size="small"
aria-label={Language.ariaLabel}
variant="text"
>
{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>
</div>
</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 prettyBytes from "pretty-bytes";
import BuildingIcon from "@mui/icons-material/Build";
import { makeStyles } from "@mui/styles";
import { RocketIcon } from "components/Icons/RocketIcon";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
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 Button from "@mui/material/Button";
import { getDisplayWorkspaceStatus } from "utils/workspace";
import { css, type Theme, type Interpolation, useTheme } from "@emotion/react";
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 {
fetchStats?: () => void;
stats?: DeploymentStats;
@ -31,7 +57,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
stats,
fetchStats,
}) => {
const styles = useStyles();
const theme = useTheme();
const aggregatedMinutes = useMemo(() => {
if (!stats) {
return;
@ -80,15 +106,46 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
}, [timeUntilRefresh, stats]);
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!">
<div className={styles.rocket}>
<div
css={css`
display: flex;
align-items: center;
& svg {
width: 16px;
height: 16px;
}
${theme.breakpoints.down("lg")} {
display: none;
}
`}
>
<RocketIcon />
</div>
</Tooltip>
<div className={styles.group}>
<div className={styles.category}>Workspaces</div>
<div className={styles.values}>
<div css={styles.group}>
<div css={styles.category}>Workspaces</div>
<div css={styles.values}>
<WorkspaceBuildValue
status="pending"
count={stats?.workspaces.pending}
@ -115,21 +172,21 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
/>
</div>
</div>
<div className={styles.group}>
<div css={styles.group}>
<Tooltip title={`Activity in the last ~${aggregatedMinutes} minutes`}>
<div className={styles.category}>Transmission</div>
<div css={styles.category}>Transmission</div>
</Tooltip>
<div className={styles.values}>
<div css={styles.values}>
<Tooltip title="Data sent to workspaces">
<div className={styles.value}>
<div css={styles.value}>
<DownloadIcon />
{stats ? prettyBytes(stats.workspaces.rx_bytes) : "-"}
</div>
</Tooltip>
<ValueSeparator />
<Tooltip title="Data sent from workspaces">
<div className={styles.value}>
<div css={styles.value}>
<UploadIcon />
{stats ? prettyBytes(stats.workspaces.tx_bytes) : "-"}
</div>
@ -142,20 +199,26 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
: "The average latency of user connections to workspaces"
}
>
<div className={styles.value}>
<div css={styles.value}>
<LatencyIcon />
{displayLatency > 0 ? displayLatency?.toFixed(2) + " ms" : "-"}
</div>
</Tooltip>
</div>
</div>
<div className={styles.group}>
<div className={styles.category}>Active Connections</div>
<div css={styles.group}>
<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">
<div className={styles.value}>
<VSCodeIcon className={styles.iconStripColor} />
<div css={styles.value}>
<VSCodeIcon
css={css`
& * {
fill: currentColor;
}
`}
/>
{typeof stats?.session_count.vscode === "undefined"
? "-"
: stats?.session_count.vscode}
@ -163,7 +226,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
</Tooltip>
<ValueSeparator />
<Tooltip title="SSH Sessions">
<div className={styles.value}>
<div css={styles.value}>
<TerminalIcon />
{typeof stats?.session_count.ssh === "undefined"
? "-"
@ -172,7 +235,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
</Tooltip>
<ValueSeparator />
<Tooltip title="Web Terminal Sessions">
<div className={styles.value}>
<div css={styles.value}>
<WebTerminalIcon />
{typeof stats?.session_count.reconnecting_pty === "undefined"
? "-"
@ -181,9 +244,17 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
</Tooltip>
</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!">
<div className={styles.value}>
<div css={styles.value}>
<CollectedIcon />
{lastAggregated}
</div>
@ -191,7 +262,23 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
<Tooltip title="A countdown until stats are fetched again. Click to refresh!">
<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={() => {
if (fetchStats) {
fetchStats();
@ -209,15 +296,18 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
};
const ValueSeparator: FC = () => {
const styles = useStyles();
return <div className={styles.valueSeparator}>/</div>;
const theme = useTheme();
const separatorStyles = css`
color: ${theme.palette.text.disabled};
`;
return <div css={separatorStyles}>/</div>;
};
const WorkspaceBuildValue: FC<{
status: WorkspaceStatus;
count?: number;
}> = ({ status, count }) => {
const styles = useStyles();
const displayStatus = getDisplayWorkspaceStatus(status);
let statusText = displayStatus.text;
let icon = displayStatus.icon;
@ -232,7 +322,7 @@ const WorkspaceBuildValue: FC<{
component={RouterLink}
to={`/workspaces?filter=${encodeURIComponent("status:" + status)}`}
>
<div className={styles.value}>
<div css={styles.value}>
{icon}
{typeof count === "undefined" ? "-" : count}
</div>
@ -240,88 +330,3 @@ const WorkspaceBuildValue: FC<{
</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 { makeStyles } from "@mui/styles";
import { css } from "@emotion/react";
import { useState } from "react";
import { Expander } from "components/Expander/Expander";
import { Pill } from "components/Pill/Pill";
import { useState } from "react";
import { colors } from "theme/colors";
export const Language = {
@ -14,6 +20,13 @@ export const Language = {
moreDetails: "More",
};
const styles = {
leftContent: (theme) => ({
marginRight: theme.spacing(1),
marginLeft: theme.spacing(1),
}),
} satisfies Record<string, Interpolation<Theme>>;
export interface LicenseBannerViewProps {
errors: string[];
warnings: string[];
@ -23,17 +36,28 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
errors,
warnings,
}) => {
const styles = useStyles();
const theme = useTheme();
const [showDetails, setShowDetails] = useState(false);
const isError = errors.length > 0;
const messages = [...errors, ...warnings];
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) {
return (
<div className={`${styles.container} ${type}`}>
<div css={containerStyles}>
<Pill text={Language.licenseIssue} type={type} lightBorder />
<div className={styles.leftContent}>
<div css={styles.leftContent}>
<span>{messages[0]}</span>
&nbsp;
<Link color="white" fontWeight="medium" href="mailto:sales@coder.com">
@ -42,30 +66,27 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
</div>
</div>
);
} else {
}
return (
<div className={`${styles.container} ${type}`}>
<div css={containerStyles}>
<Pill
text={Language.licenseIssues(messages.length)}
type={type}
lightBorder
/>
<div className={styles.leftContent}>
<div css={styles.leftContent}>
<div>
{Language.exceeded}
&nbsp;
<Link
color="white"
fontWeight="medium"
href="mailto:sales@coder.com"
>
<Link color="white" fontWeight="medium" href="mailto:sales@coder.com">
{Language.upgrade}
</Link>
</div>
<Expander expanded={showDetails} setExpanded={setShowDetails}>
<ul className={styles.list}>
<ul css={{ padding: theme.spacing(1), margin: 0 }}>
{messages.map((message) => (
<li className={styles.listItem} key={message}>
<li css={{ margin: theme.spacing(0.5) }} key={message}>
{message}
</li>
))}
@ -74,33 +95,4 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
</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 List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import { makeStyles } from "@mui/styles";
import MenuIcon from "@mui/icons-material/Menu";
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 { colors } from "theme/colors";
import * as TypesGen from "api/typesGenerated";
import { navHeight } from "theme/constants";
import { combineClasses } from "utils/combineClasses";
import { UserDropdown } from "./UserDropdown/UserDropdown";
import Box from "@mui/material/Box";
import Menu from "@mui/material/Menu";
@ -25,6 +23,7 @@ import { BUTTON_SM_HEIGHT } from "theme/theme";
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
import { usePermissions } from "hooks/usePermissions";
import Typography from "@mui/material/Typography";
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
export const USERS_LINK = `/users?filter=${encodeURIComponent(
"status:active",
@ -50,52 +49,128 @@ export const Language = {
deployment: "Deployment",
};
const NavItems: React.FC<
React.PropsWithChildren<{
const styles = {
desktopNavItems: (theme) => css`
display: none;
${theme.breakpoints.up("md")} {
display: flex;
}
`,
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;
}>
> = ({ className, canViewAuditLog, canViewDeployment, canViewAllUsers }) => {
const styles = useStyles();
}
const NavItems: React.FC<NavItemsProps> = (props) => {
const { className, canViewAuditLog, canViewDeployment, canViewAllUsers } =
props;
const location = useLocation();
const theme = useTheme();
return (
<List className={combineClasses([styles.navItems, className])}>
<ListItem button className={styles.item}>
<List css={{ padding: 0 }} className={className}>
<ListItem button css={styles.item}>
<NavLink
className={combineClasses([
css={[
styles.link,
location.pathname.startsWith("/@") && "active",
])}
location.pathname.startsWith("/@") && {
color: theme.palette.text.primary,
fontWeight: 500,
},
]}
to="/workspaces"
>
{Language.workspaces}
</NavLink>
</ListItem>
<ListItem button className={styles.item}>
<NavLink className={styles.link} to="/templates">
<ListItem button css={styles.item}>
<NavLink css={styles.link} to="/templates">
{Language.templates}
</NavLink>
</ListItem>
{canViewAllUsers && (
<ListItem button className={styles.item}>
<NavLink className={styles.link} to={USERS_LINK}>
<ListItem button css={styles.item}>
<NavLink css={styles.link} to={USERS_LINK}>
{Language.users}
</NavLink>
</ListItem>
)}
{canViewAuditLog && (
<ListItem button className={styles.item}>
<NavLink className={styles.link} to="/audit">
<ListItem button css={styles.item}>
<NavLink css={styles.link} to="/audit">
{Language.audit}
</NavLink>
</ListItem>
)}
{canViewDeployment && (
<ListItem button className={styles.item}>
<NavLink className={styles.link} to="/deployment/general">
<ListItem button css={styles.item}>
<NavLink css={styles.link} to="/deployment/general">
{Language.deployment}
</NavLink>
</ListItem>
@ -114,15 +189,20 @@ export const NavbarView: FC<NavbarViewProps> = ({
canViewAllUsers,
proxyContextValue,
}) => {
const styles = useStyles();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
return (
<nav className={styles.root}>
<div className={styles.wrapper}>
<nav
css={(theme) => ({
height: navHeight,
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
})}
>
<div css={styles.wrapper}>
<IconButton
aria-label="Open menu"
className={styles.mobileMenuButton}
css={styles.mobileMenuButton}
onClick={() => {
setIsDrawerOpen(true);
}}
@ -136,9 +216,9 @@ export const NavbarView: FC<NavbarViewProps> = ({
open={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
>
<div className={styles.drawer}>
<div className={styles.drawerHeader}>
<div className={combineClasses([styles.logo, styles.drawerLogo])}>
<div css={{ width: 250 }}>
<div css={styles.drawerHeader}>
<div css={[styles.logo, styles.drawerLogo]}>
{logo_url ? (
<img src={logo_url} alt="Custom Logo" />
) : (
@ -154,7 +234,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
</div>
</Drawer>
<NavLink className={styles.logo} to="/workspaces">
<NavLink css={styles.logo} to="/workspaces">
{logo_url ? (
<img src={logo_url} alt="Custom Logo" />
) : (
@ -163,7 +243,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
</NavLink>
<NavItems
className={styles.desktopNavItems}
css={styles.desktopNavItems}
canViewAuditLog={canViewAuditLog}
canViewDeployment={canViewDeployment}
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 { makeStyles } from "@mui/styles";
import { FC, PropsWithChildren } from "react";
import type { FC, PropsWithChildren } from "react";
type BorderedMenuVariant = "user-dropdown";
@ -13,23 +14,17 @@ export const BorderedMenu: FC<PropsWithChildren<BorderedMenuProps>> = ({
variant,
...rest
}) => {
const styles = useStyles();
const theme = useTheme();
const paper = css`
width: 260px;
border-radius: ${theme.shape.borderRadius};
box-shadow: ${theme.shadows[6]};
`;
return (
<Popover
classes={{ paper: styles.paperRoot }}
data-variant={variant}
{...rest}
>
<Popover classes={{ paper }} data-variant={variant} {...rest}>
{children}
</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 MenuItem from "@mui/material/MenuItem";
import { makeStyles } from "@mui/styles";
import AccountIcon from "@mui/icons-material/AccountCircleOutlined";
import BugIcon from "@mui/icons-material/BugReportOutlined";
import ChatIcon from "@mui/icons-material/ChatOutlined";
@ -11,7 +10,12 @@ import { Link } from "react-router-dom";
import * as TypesGen from "api/typesGenerated";
import DocsIcon from "@mui/icons-material/MenuBook";
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 = {
accountLabel: "Account",
@ -19,6 +23,61 @@ export const Language = {
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 {
user: TypesGen.User;
buildInfo?: TypesGen.BuildInfoResponse;
@ -34,63 +93,55 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
onPopoverClose,
onSignOut,
}) => {
const styles = useStyles();
return (
<div>
<Stack className={styles.info} spacing={0}>
<span className={styles.userName}>{user.username}</span>
<span className={styles.userEmail}>{user.email}</span>
<Stack css={styles.info} spacing={0}>
<span css={styles.userName}>{user.username}</span>
<span css={styles.userEmail}>{user.email}</span>
</Stack>
<Divider className={styles.divider} />
<Divider css={{ marginBottom: 8 }} />
<Link to="/settings/account" className={styles.link}>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
<AccountIcon className={styles.menuItemIcon} />
<span className={styles.menuItemText}>{Language.accountLabel}</span>
<Link to="/settings/account" css={styles.link}>
<MenuItem css={styles.menuItem} onClick={onPopoverClose}>
<AccountIcon css={styles.menuItemIcon} />
<span css={styles.menuItemText}>{Language.accountLabel}</span>
</MenuItem>
</Link>
<MenuItem className={styles.menuItem} onClick={onSignOut}>
<LogoutIcon className={styles.menuItemIcon} />
<span className={styles.menuItemText}>{Language.signOutLabel}</span>
<MenuItem css={styles.menuItem} onClick={onSignOut}>
<LogoutIcon css={styles.menuItemIcon} />
<span css={styles.menuItemText}>{Language.signOutLabel}</span>
</MenuItem>
<Divider className={styles.divider} />
{supportLinks && (
<>
{supportLinks &&
supportLinks.map((link) => (
<Divider />
{supportLinks.map((link) => (
<a
href={includeBuildInfo(link.target, buildInfo)}
key={link.name}
target="_blank"
rel="noreferrer"
className={styles.link}
css={styles.link}
>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
{link.icon === "bug" && (
<BugIcon className={styles.menuItemIcon} />
)}
{link.icon === "chat" && (
<ChatIcon className={styles.menuItemIcon} />
)}
{link.icon === "docs" && (
<DocsIcon className={styles.menuItemIcon} />
)}
<span className={styles.menuItemText}>{link.name}</span>
<MenuItem css={styles.menuItem} onClick={onPopoverClose}>
{link.icon === "bug" && <BugIcon css={styles.menuItemIcon} />}
{link.icon === "chat" && <ChatIcon css={styles.menuItemIcon} />}
{link.icon === "docs" && <DocsIcon css={styles.menuItemIcon} />}
<span css={styles.menuItemText}>{link.name}</span>
</MenuItem>
</a>
))}
</>
)}
{supportLinks && <Divider className={styles.divider} />}
<Divider css={{ marginBottom: "0 !important" }} />
<Stack className={styles.info} spacing={0}>
<Stack css={styles.info} spacing={0}>
<a
title="Browse Source Code"
className={combineClasses([styles.footerText, styles.buildInfo])}
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
@ -98,76 +149,12 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
{buildInfo?.version} <LaunchIcon />
</a>
<div className={styles.footerText}>{Language.copyrightText}</div>
<div css={styles.footerText}>{Language.copyrightText}</div>
</Stack>
</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 = (
href: string,
buildInfo?: TypesGen.BuildInfoResponse,

View File

@ -1,119 +1,10 @@
import { makeStyles } from "@mui/styles";
import { Stack } from "components/Stack/Stack";
import { PropsWithChildren, FC } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { combineClasses } from "utils/combineClasses";
import type { PropsWithChildren, FC } from "react";
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 = useStyles();
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: {
const styles = {
badge: (theme) => ({
fontSize: 10,
height: 24,
fontWeight: 600,
@ -125,45 +16,121 @@ const useStyles = makeStyles((theme) => ({
alignItems: "center",
width: "fit-content",
whiteSpace: "nowrap",
},
}),
enterpriseBadge: {
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: {
enabledBadge: (theme) => ({
border: `1px solid ${theme.palette.success.light}`,
backgroundColor: theme.palette.success.dark,
textTransform: "none",
color: "white",
fontFamily: MONOSPACE_FONT_FAMILY,
textDecoration: "none",
fontSize: 12,
},
enabledBadge: {
border: `1px solid ${theme.palette.success.light}`,
backgroundColor: theme.palette.success.dark,
},
errorBadge: {
}),
errorBadge: (theme) => ({
border: `1px solid ${theme.palette.error.light}`,
backgroundColor: theme.palette.error.dark,
},
warnBadge: {
}),
warnBadge: (theme) => ({
border: `1px solid ${theme.palette.warning.light}`,
backgroundColor: theme.palette.warning.dark,
},
}),
} satisfies Record<string, Interpolation<Theme>>;
disabledBadge: {
export const EnabledBadge: FC = () => {
return <span css={[styles.badge, styles.enabledBadge]}>Enabled</span>;
};
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 { Stack } from "components/Stack/Stack";
import { Sidebar } from "./Sidebar";
@ -31,15 +30,18 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
export const DeploySettingsLayout: FC = () => {
const deploymentConfigQuery = useQuery(deploymentConfig());
const styles = useStyles();
const permissions = usePermissions();
return (
<RequirePermission isFeatureVisible={permissions.viewDeploymentValues}>
<Margins>
<Stack className={styles.wrapper} direction="row" spacing={6}>
<Stack
css={(theme) => ({ padding: theme.spacing(6, 0) })}
direction="row"
spacing={6}
>
<Sidebar />
<main className={styles.content}>
<main css={{ maxWidth: 800, width: "100%" }}>
{deploymentConfigQuery.data ? (
<DeploySettingsContext.Provider
value={{
@ -59,14 +61,3 @@ export const DeploySettingsLayout: FC = () => {
</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 { FC, ReactNode, FormEventHandler } from "react";
import type { FC, ReactNode, FormEventHandler } from "react";
import Button from "@mui/material/Button";
import { type CSSObject, useTheme } from "@emotion/react";
export const Fieldset: FC<{
interface FieldsetProps {
children: ReactNode;
title: string | JSX.Element;
subtitle?: string | JSX.Element;
@ -10,7 +10,10 @@ export const Fieldset: FC<{
button?: JSX.Element | false;
onSubmit: FormEventHandler<HTMLFormElement>;
isSubmitting?: boolean;
}> = ({
}
export const Fieldset: FC<FieldsetProps> = (props) => {
const {
title,
subtitle,
children,
@ -18,18 +21,62 @@ export const Fieldset: FC<{
button,
onSubmit,
isSubmitting,
}) => {
const styles = useStyles();
} = props;
const theme = useTheme();
return (
<form className={styles.fieldset} onSubmit={onSubmit}>
<header className={styles.header}>
<div className={styles.title}>{title}</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
<div className={styles.body}>{children}</div>
<form
css={{
borderRadius: theme.spacing(1),
border: `1px solid ${theme.palette.divider}`,
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>
<footer className={styles.footer}>
<div className={styles.validation}>{validation}</div>
<footer
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 type="submit" disabled={isSubmitting}>
Submit
@ -39,40 +86,3 @@ export const Fieldset: FC<{
</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 { makeStyles } from "@mui/styles";
import LaunchOutlined from "@mui/icons-material/LaunchOutlined";
import type { FC } from "react";
import { useTheme } from "@emotion/react";
import { Stack } from "components/Stack/Stack";
import { FC } from "react";
export const Header: FC<{
title: string | JSX.Element;
@ -10,16 +10,41 @@ export const Header: FC<{
secondary?: boolean;
docsHref?: string;
}> = ({ title, description, docsHref, secondary }) => {
const styles = useStyles();
const theme = useTheme();
return (
<Stack alignItems="baseline" direction="row" justifyContent="space-between">
<div className={styles.headingGroup}>
<h1 className={`${styles.title} ${secondary ? "secondary" : ""}`}>
<div css={{ maxWidth: 420, marginBottom: theme.spacing(3) }}>
<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}
</h1>
{description && (
<span className={styles.description}>{description}</span>
<span
css={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: "160%",
}}
>
{description}
</span>
)}
</div>
@ -36,32 +61,3 @@ export const Header: FC<{
</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%",
},
}));