coder/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx

328 lines
9.3 KiB
TypeScript

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";
import { Link as RouterLink } from "react-router-dom";
import Link from "@mui/material/Link";
import { VSCodeIcon } from "components/Icons/VSCodeIcon";
import DownloadIcon from "@mui/icons-material/CloudDownload";
import UploadIcon from "@mui/icons-material/CloudUpload";
import LatencyIcon from "@mui/icons-material/SettingsEthernet";
import WebTerminalIcon from "@mui/icons-material/WebAsset";
import { TerminalIcon } from "components/Icons/TerminalIcon";
import dayjs from "dayjs";
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";
export const bannerHeight = 36;
export interface DeploymentBannerViewProps {
fetchStats?: () => void;
stats?: DeploymentStats;
}
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
stats,
fetchStats,
}) => {
const styles = useStyles();
const aggregatedMinutes = useMemo(() => {
if (!stats) {
return;
}
return dayjs(stats.collected_at).diff(stats.aggregated_from, "minutes");
}, [stats]);
const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1;
const [timeUntilRefresh, setTimeUntilRefresh] = useState(0);
useEffect(() => {
if (!stats || !fetchStats) {
return;
}
let timeUntilRefresh = dayjs(stats.next_update_at).diff(
stats.collected_at,
"seconds",
);
setTimeUntilRefresh(timeUntilRefresh);
let canceled = false;
const loop = () => {
if (canceled) {
return undefined;
}
setTimeUntilRefresh(timeUntilRefresh--);
if (timeUntilRefresh > 0) {
return window.setTimeout(loop, 1000);
}
fetchStats();
};
const timeout = setTimeout(loop, 1000);
return () => {
canceled = true;
clearTimeout(timeout);
};
}, [fetchStats, stats]);
const lastAggregated = useMemo(() => {
if (!stats) {
return;
}
if (!fetchStats) {
// Storybook!
return "just now";
}
return dayjs().to(dayjs(stats.collected_at));
// eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update!
}, [timeUntilRefresh, stats]);
return (
<div className={styles.container}>
<Tooltip title="Status of your Coder deployment. Only visible for admins!">
<div className={styles.rocket}>
<RocketIcon />
</div>
</Tooltip>
<div className={styles.group}>
<div className={styles.category}>Workspaces</div>
<div className={styles.values}>
<WorkspaceBuildValue
status="pending"
count={stats?.workspaces.pending}
/>
<ValueSeparator />
<WorkspaceBuildValue
status="starting"
count={stats?.workspaces.building}
/>
<ValueSeparator />
<WorkspaceBuildValue
status="running"
count={stats?.workspaces.running}
/>
<ValueSeparator />
<WorkspaceBuildValue
status="stopped"
count={stats?.workspaces.stopped}
/>
<ValueSeparator />
<WorkspaceBuildValue
status="failed"
count={stats?.workspaces.failed}
/>
</div>
</div>
<div className={styles.group}>
<Tooltip title={`Activity in the last ~${aggregatedMinutes} minutes`}>
<div className={styles.category}>Transmission</div>
</Tooltip>
<div className={styles.values}>
<Tooltip title="Data sent to workspaces">
<div className={styles.value}>
<DownloadIcon />
{stats ? prettyBytes(stats.workspaces.rx_bytes) : "-"}
</div>
</Tooltip>
<ValueSeparator />
<Tooltip title="Data sent from workspaces">
<div className={styles.value}>
<UploadIcon />
{stats ? prettyBytes(stats.workspaces.tx_bytes) : "-"}
</div>
</Tooltip>
<ValueSeparator />
<Tooltip
title={
displayLatency < 0
? "No recent workspace connections have been made"
: "The average latency of user connections to workspaces"
}
>
<div className={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 className={styles.values}>
<Tooltip title="VS Code Editors with the Coder Remote Extension">
<div className={styles.value}>
<VSCodeIcon className={styles.iconStripColor} />
{typeof stats?.session_count.vscode === "undefined"
? "-"
: stats?.session_count.vscode}
</div>
</Tooltip>
<ValueSeparator />
<Tooltip title="SSH Sessions">
<div className={styles.value}>
<TerminalIcon />
{typeof stats?.session_count.ssh === "undefined"
? "-"
: stats?.session_count.ssh}
</div>
</Tooltip>
<ValueSeparator />
<Tooltip title="Web Terminal Sessions">
<div className={styles.value}>
<WebTerminalIcon />
{typeof stats?.session_count.reconnecting_pty === "undefined"
? "-"
: stats?.session_count.reconnecting_pty}
</div>
</Tooltip>
</div>
</div>
<div className={styles.refresh}>
<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}>
<CollectedIcon />
{lastAggregated}
</div>
</Tooltip>
<Tooltip title="A countdown until stats are fetched again. Click to refresh!">
<Button
className={`${styles.value} ${styles.refreshButton}`}
onClick={() => {
if (fetchStats) {
fetchStats();
}
}}
variant="text"
>
<RefreshIcon />
{timeUntilRefresh}s
</Button>
</Tooltip>
</div>
</div>
);
};
const ValueSeparator: FC = () => {
const styles = useStyles();
return <div className={styles.valueSeparator}>/</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;
if (status === "starting") {
icon = <BuildingIcon />;
statusText = "Building";
}
return (
<Tooltip title={`${statusText} Workspaces`}>
<Link
component={RouterLink}
to={`/workspaces?filter=${encodeURIComponent("status:" + status)}`}
>
<div className={styles.value}>
{icon}
{typeof count === "undefined" ? "-" : count}
</div>
</Link>
</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),
},
},
}));