mirror of https://github.com/coder/coder.git
339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
import Table from "@mui/material/Table";
|
|
import TableBody from "@mui/material/TableBody";
|
|
import TableCell from "@mui/material/TableCell";
|
|
import TableContainer from "@mui/material/TableContainer";
|
|
import TableHead from "@mui/material/TableHead";
|
|
import TableRow from "@mui/material/TableRow";
|
|
import { Workspace } from "api/typesGenerated";
|
|
import { FC, ReactNode } from "react";
|
|
import { TableEmpty } from "components/TableEmpty/TableEmpty";
|
|
import {
|
|
TableLoaderSkeleton,
|
|
TableRowSkeleton,
|
|
} from "components/TableLoader/TableLoader";
|
|
import AddOutlined from "@mui/icons-material/AddOutlined";
|
|
import Button from "@mui/material/Button";
|
|
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
|
import { useClickableTableRow } from "hooks/useClickableTableRow";
|
|
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
|
import Box from "@mui/material/Box";
|
|
import { AvatarData } from "components/AvatarData/AvatarData";
|
|
import { Avatar } from "components/Avatar/Avatar";
|
|
import { Stack } from "components/Stack/Stack";
|
|
import { LastUsed } from "pages/WorkspacesPage/LastUsed";
|
|
import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip";
|
|
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge";
|
|
import { getDisplayWorkspaceTemplateName } from "utils/workspace";
|
|
import Checkbox from "@mui/material/Checkbox";
|
|
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
|
|
import Skeleton from "@mui/material/Skeleton";
|
|
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
|
|
import { css } from "@emotion/react";
|
|
import { useTheme } from "@mui/system";
|
|
|
|
export interface WorkspacesTableProps {
|
|
workspaces?: Workspace[];
|
|
checkedWorkspaces: Workspace[];
|
|
error?: unknown;
|
|
isUsingFilter: boolean;
|
|
onUpdateWorkspace: (workspace: Workspace) => void;
|
|
onCheckChange: (checkedWorkspaces: Workspace[]) => void;
|
|
canCheckWorkspaces: boolean;
|
|
}
|
|
|
|
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
|
workspaces,
|
|
checkedWorkspaces,
|
|
isUsingFilter,
|
|
onUpdateWorkspace,
|
|
onCheckChange,
|
|
canCheckWorkspaces,
|
|
}) => {
|
|
const theme = useTheme();
|
|
|
|
return (
|
|
<TableContainer>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell width="40%">
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
{canCheckWorkspaces && (
|
|
<Checkbox
|
|
// Remove the extra padding added for the first cell in the
|
|
// table
|
|
sx={{ marginLeft: "-20px" }}
|
|
disabled={!workspaces || workspaces.length === 0}
|
|
checked={checkedWorkspaces.length === workspaces?.length}
|
|
size="small"
|
|
onChange={(_, checked) => {
|
|
if (!workspaces) {
|
|
return;
|
|
}
|
|
|
|
if (!checked) {
|
|
onCheckChange([]);
|
|
} else {
|
|
onCheckChange(workspaces);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
Name
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell width="25%">Template</TableCell>
|
|
<TableCell width="20%">Last used</TableCell>
|
|
<TableCell width="15%">Status</TableCell>
|
|
<TableCell width="1%" />
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{!workspaces && (
|
|
<TableLoader canCheckWorkspaces={canCheckWorkspaces} />
|
|
)}
|
|
{workspaces && workspaces.length === 0 && (
|
|
<ChooseOne>
|
|
<Cond condition={isUsingFilter}>
|
|
<TableEmpty message="No results matched your search" />
|
|
</Cond>
|
|
|
|
<Cond>
|
|
<TableEmpty
|
|
css={{
|
|
paddingBottom: 0,
|
|
}}
|
|
message="Create a workspace"
|
|
description="A workspace is your personal, customizable development environment in the cloud"
|
|
cta={
|
|
<Button
|
|
component={RouterLink}
|
|
to="/templates"
|
|
startIcon={<AddOutlined />}
|
|
variant="contained"
|
|
>
|
|
Select a Template
|
|
</Button>
|
|
}
|
|
image={
|
|
<div
|
|
css={css`
|
|
max-width: 50%;
|
|
height: ${theme.spacing(34)};
|
|
overflow: hidden;
|
|
margin-top: ${theme.spacing(6)};
|
|
opacity: 0.85;
|
|
|
|
& img {
|
|
max-width: 100%;
|
|
}
|
|
`}
|
|
>
|
|
<img src="/featured/workspaces.webp" alt="" />
|
|
</div>
|
|
}
|
|
/>
|
|
</Cond>
|
|
</ChooseOne>
|
|
)}
|
|
{workspaces &&
|
|
workspaces.map((workspace) => {
|
|
const checked = checkedWorkspaces.some(
|
|
(w) => w.id === workspace.id,
|
|
);
|
|
return (
|
|
<WorkspacesRow
|
|
workspace={workspace}
|
|
key={workspace.id}
|
|
checked={checked}
|
|
>
|
|
<TableCell>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
{canCheckWorkspaces && (
|
|
<Checkbox
|
|
// Remove the extra padding added for the first cell in the
|
|
// table
|
|
sx={{ marginLeft: "-20px" }}
|
|
data-testid={`checkbox-${workspace.id}`}
|
|
size="small"
|
|
disabled={cantBeChecked(workspace)}
|
|
checked={checked}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
}}
|
|
onChange={(e) => {
|
|
if (e.currentTarget.checked) {
|
|
onCheckChange([...checkedWorkspaces, workspace]);
|
|
} else {
|
|
onCheckChange(
|
|
checkedWorkspaces.filter(
|
|
(w) => w.id !== workspace.id,
|
|
),
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
<AvatarData
|
|
title={
|
|
<Stack
|
|
direction="row"
|
|
spacing={0}
|
|
alignItems="center"
|
|
>
|
|
{workspace.name}
|
|
{workspace.outdated && (
|
|
<WorkspaceOutdatedTooltip
|
|
templateName={workspace.template_name}
|
|
latestVersionId={
|
|
workspace.template_active_version_id
|
|
}
|
|
onUpdateVersion={() => {
|
|
onUpdateWorkspace(workspace);
|
|
}}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
}
|
|
subtitle={workspace.owner_name}
|
|
avatar={
|
|
<Avatar
|
|
src={workspace.template_icon}
|
|
variant={
|
|
workspace.template_icon ? "square" : undefined
|
|
}
|
|
fitImage={Boolean(workspace.template_icon)}
|
|
>
|
|
{workspace.name}
|
|
</Avatar>
|
|
}
|
|
/>
|
|
</Box>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
{getDisplayWorkspaceTemplateName(workspace)}
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<LastUsed lastUsedAt={workspace.last_used_at} />
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
<WorkspaceStatusBadge workspace={workspace} />
|
|
{workspace.latest_build.status === "running" &&
|
|
!workspace.health.healthy && (
|
|
<InfoTooltip
|
|
type="warning"
|
|
title="Workspace is unhealthy"
|
|
message="Your workspace is running but some agents are unhealthy."
|
|
/>
|
|
)}
|
|
</Box>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
paddingLeft: (theme) => theme.spacing(2),
|
|
}}
|
|
>
|
|
<KeyboardArrowRight
|
|
sx={{
|
|
color: (theme) => theme.palette.text.secondary,
|
|
width: 20,
|
|
height: 20,
|
|
}}
|
|
/>
|
|
</Box>
|
|
</TableCell>
|
|
</WorkspacesRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
};
|
|
|
|
const WorkspacesRow: FC<{
|
|
workspace: Workspace;
|
|
children: ReactNode;
|
|
checked: boolean;
|
|
}> = ({ workspace, children, checked }) => {
|
|
const navigate = useNavigate();
|
|
|
|
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`;
|
|
const openLinkInNewTab = () => window.open(workspacePageLink, "_blank");
|
|
|
|
const clickableProps = useClickableTableRow({
|
|
onMiddleClick: openLinkInNewTab,
|
|
onClick: (event) => {
|
|
// Order of booleans actually matters here for Windows-Mac compatibility;
|
|
// meta key is Cmd on Macs, but on Windows, it's either the Windows key,
|
|
// or the key does nothing at all (depends on the browser)
|
|
const shouldOpenInNewTab =
|
|
event.shiftKey || event.metaKey || event.ctrlKey;
|
|
|
|
if (shouldOpenInNewTab) {
|
|
openLinkInNewTab();
|
|
} else {
|
|
navigate(workspacePageLink);
|
|
}
|
|
},
|
|
});
|
|
|
|
return (
|
|
<TableRow
|
|
{...clickableProps}
|
|
data-testid={`workspace-${workspace.id}`}
|
|
sx={{
|
|
backgroundColor: (theme) =>
|
|
checked ? theme.palette.action.hover : undefined,
|
|
}}
|
|
>
|
|
{children}
|
|
</TableRow>
|
|
);
|
|
};
|
|
|
|
const TableLoader = ({
|
|
canCheckWorkspaces,
|
|
}: {
|
|
canCheckWorkspaces: boolean;
|
|
}) => {
|
|
return (
|
|
<TableLoaderSkeleton>
|
|
<TableRowSkeleton>
|
|
<TableCell width="40%">
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
{canCheckWorkspaces && (
|
|
<Checkbox size="small" disabled sx={{ marginLeft: "-20px" }} />
|
|
)}
|
|
<AvatarDataSkeleton />
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Skeleton variant="text" width="25%" />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Skeleton variant="text" width="25%" />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Skeleton variant="text" width="25%" />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Skeleton variant="text" width="25%" />
|
|
</TableCell>
|
|
</TableRowSkeleton>
|
|
</TableLoaderSkeleton>
|
|
);
|
|
};
|
|
|
|
const cantBeChecked = (workspace: Workspace) => {
|
|
return ["deleting", "pending"].includes(workspace.latest_build.status);
|
|
};
|