coder/site/src/pages/TemplatesPage/TemplatesPageView.tsx

282 lines
8.1 KiB
TypeScript

import { type Interpolation, type Theme } from "@emotion/react";
import Button from "@mui/material/Button";
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 { type FC } from "react";
import { useNavigate } from "react-router-dom";
import { createDayString } from "utils/createDayString";
import {
formatTemplateBuildTime,
formatTemplateActiveDevelopers,
} from "utils/templates";
import { AvatarData } from "components/AvatarData/AvatarData";
import { Margins } from "components/Margins/Margins";
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import {
HelpTooltip,
HelpTooltipContent,
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipText,
HelpTooltipTitle,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import { EmptyTemplates } from "./EmptyTemplates";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import type { Template, TemplateExample } from "api/typesGenerated";
import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined";
import { ExternalAvatar } from "components/Avatar/Avatar";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { docs } from "utils/docs";
import Skeleton from "@mui/material/Skeleton";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
import { DeprecatedBadge } from "components/Badges/Badges";
import { CreateTemplateButton } from "./CreateTemplateButton";
export const Language = {
developerCount: (activeCount: number): string => {
return `${formatTemplateActiveDevelopers(activeCount)} developer${
activeCount !== 1 ? "s" : ""
}`;
},
nameLabel: "Name",
buildTimeLabel: "Build time",
usedByLabel: "Used by",
lastUpdatedLabel: "Last updated",
templateTooltipTitle: "What is template?",
templateTooltipText:
"With templates you can create a common configuration for your workspaces using Terraform.",
templateTooltipLink: "Manage templates",
};
const TemplateHelpTooltip: FC = () => {
return (
<HelpTooltip>
<HelpTooltipTrigger />
<HelpTooltipContent>
<HelpTooltipTitle>{Language.templateTooltipTitle}</HelpTooltipTitle>
<HelpTooltipText>{Language.templateTooltipText}</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href={docs("/templates")}>
{Language.templateTooltipLink}
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</HelpTooltipContent>
</HelpTooltip>
);
};
interface TemplateRowProps {
template: Template;
}
const TemplateRow: FC<TemplateRowProps> = ({ template }) => {
const templatePageLink = `/templates/${template.name}`;
const hasIcon = template.icon && template.icon !== "";
const navigate = useNavigate();
const { css: clickableCss, ...clickableRow } = useClickableTableRow({
onClick: () => navigate(templatePageLink),
});
return (
<TableRow
key={template.id}
data-testid={`template-${template.id}`}
{...clickableRow}
css={[clickableCss, styles.tableRow]}
>
<TableCell>
<AvatarData
title={
template.display_name.length > 0
? template.display_name
: template.name
}
subtitle={template.description}
avatar={
hasIcon && (
<ExternalAvatar variant="square" fitImage src={template.icon} />
)
}
/>
</TableCell>
<TableCell css={styles.secondary}>
{Language.developerCount(template.active_user_count)}
</TableCell>
<TableCell css={styles.secondary}>
{formatTemplateBuildTime(template.build_time_stats.start.P50)}
</TableCell>
<TableCell data-chromatic="ignore" css={styles.secondary}>
{createDayString(template.updated_at)}
</TableCell>
<TableCell css={styles.actionCell}>
{template.deprecated ? (
<DeprecatedBadge />
) : (
<Button
size="small"
css={styles.actionButton}
className="actionButton"
startIcon={<ArrowForwardOutlined />}
title={`Create a workspace using the ${template.display_name} template`}
onClick={(e) => {
e.stopPropagation();
navigate(`/templates/${template.name}/workspace`);
}}
>
Create Workspace
</Button>
)}
</TableCell>
</TableRow>
);
};
export interface TemplatesPageViewProps {
error?: unknown;
examples: TemplateExample[] | undefined;
templates: Template[] | undefined;
canCreateTemplates: boolean;
}
export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
templates,
error,
examples,
canCreateTemplates,
}) => {
const isLoading = !templates;
const isEmpty = templates && templates.length === 0;
const navigate = useNavigate();
return (
<Margins>
<PageHeader
actions={
canCreateTemplates && <CreateTemplateButton onNavigate={navigate} />
}
>
<PageHeaderTitle>
<Stack spacing={1} direction="row" alignItems="center">
Templates
<TemplateHelpTooltip />
</Stack>
</PageHeaderTitle>
{templates && templates.length > 0 && (
<PageHeaderSubtitle>
Select a template to create a workspace.
</PageHeaderSubtitle>
)}
</PageHeader>
{error ? (
<ErrorAlert error={error} />
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="35%">{Language.nameLabel}</TableCell>
<TableCell width="15%">{Language.usedByLabel}</TableCell>
<TableCell width="10%">{Language.buildTimeLabel}</TableCell>
<TableCell width="15%">{Language.lastUpdatedLabel}</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && <TableLoader />}
{isEmpty ? (
<EmptyTemplates
canCreateTemplates={canCreateTemplates}
examples={examples ?? []}
/>
) : (
templates?.map((template) => (
<TemplateRow key={template.id} template={template} />
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</Margins>
);
};
const TableLoader: FC = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
<AvatarDataSkeleton />
</div>
</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 styles = {
templateIconWrapper: {
// Same size then the avatar component
width: 36,
height: 36,
padding: 2,
"& img": {
width: "100%",
},
},
actionCell: {
whiteSpace: "nowrap",
},
secondary: (theme) => ({
color: theme.palette.text.secondary,
}),
tableRow: (theme) => ({
"&:hover .actionButton": {
color: theme.experimental.l2.hover.text,
borderColor: theme.experimental.l2.hover.outline,
},
}),
actionButton: (theme) => ({
transition: "none",
color: theme.palette.text.secondary,
"&:hover": {
borderColor: theme.palette.text.primary,
},
}),
} satisfies Record<string, Interpolation<Theme>>;