mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
chore: use emotion for styling (pt. 2) (#9951)
This commit is contained in:
2
site/src/@types/emotion.d.ts
vendored
2
site/src/@types/emotion.d.ts
vendored
@ -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 {}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
<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}
|
|
||||||
|
|
||||||
<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}
|
||||||
|
|
||||||
|
<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),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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)}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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],
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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%",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
@ -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%",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
Reference in New Issue
Block a user