mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
refactor(site): improve templates empty state (#10518)
This commit is contained in:
@@ -1,86 +1,123 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import type { TemplateExample } from "api/typesGenerated";
|
||||
import { type FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { HTMLProps } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
export interface TemplateExampleCardProps {
|
||||
type TemplateExampleCardProps = {
|
||||
example: TemplateExample;
|
||||
className?: string;
|
||||
}
|
||||
activeTag?: string;
|
||||
} & HTMLProps<HTMLDivElement>;
|
||||
|
||||
export const TemplateExampleCard: FC<TemplateExampleCardProps> = ({
|
||||
example,
|
||||
className,
|
||||
}) => {
|
||||
export const TemplateExampleCard = (props: TemplateExampleCardProps) => {
|
||||
const { example, activeTag, ...divProps } = props;
|
||||
return (
|
||||
<Link
|
||||
to={`/starter-templates/${example.id}`}
|
||||
css={styles.template}
|
||||
className={className}
|
||||
key={example.id}
|
||||
<div
|
||||
css={(theme) => ({
|
||||
width: "320px",
|
||||
padding: 24,
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
textAlign: "left",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
})}
|
||||
{...divProps}
|
||||
>
|
||||
<div css={styles.templateIcon}>
|
||||
<img src={example.icon} alt="" />
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
flexShrink: 0,
|
||||
paddingTop: 4,
|
||||
width: 32,
|
||||
height: 32,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={example.icon}
|
||||
alt=""
|
||||
css={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
||||
{example.tags.map((tag) => {
|
||||
const isActive = activeTag === tag;
|
||||
|
||||
return (
|
||||
<RouterLink key={tag} to={`/starter-templates?tag=${tag}`}>
|
||||
<Pill
|
||||
text={tag}
|
||||
css={(theme) => ({
|
||||
borderColor: isActive
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.divider,
|
||||
cursor: "pointer",
|
||||
backgroundColor: isActive
|
||||
? theme.palette.primary.dark
|
||||
: undefined,
|
||||
"&: hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</RouterLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div css={styles.templateInfo}>
|
||||
<span css={styles.templateName}>{example.name}</span>
|
||||
<span css={styles.templateDescription}>{example.description}</span>
|
||||
|
||||
<div>
|
||||
<h4 css={{ fontSize: 14, fontWeight: 600, margin: 0, marginBottom: 4 }}>
|
||||
{example.name}
|
||||
</h4>
|
||||
<span
|
||||
css={(theme) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "1.6",
|
||||
display: "block",
|
||||
})}
|
||||
>
|
||||
{example.description}{" "}
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/starter-templates/${example.id}`}
|
||||
css={{ display: "inline-block", fontSize: 13, marginTop: 4 }}
|
||||
>
|
||||
Read more
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
flexDirection: "column",
|
||||
paddingTop: 24,
|
||||
marginTop: "auto",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
fullWidth
|
||||
to={`/templates/new?exampleId=${example.id}`}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
template: (theme) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8,
|
||||
background: theme.palette.background.paper,
|
||||
textDecoration: "none",
|
||||
textAlign: "left",
|
||||
color: "inherit",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "fit-content",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
},
|
||||
}),
|
||||
|
||||
templateIcon: {
|
||||
width: 96,
|
||||
height: 96,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
|
||||
"& img": {
|
||||
height: 32,
|
||||
},
|
||||
},
|
||||
|
||||
templateInfo: {
|
||||
padding: "16px 16px 16px 0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
templateName: {
|
||||
fontSize: 16,
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
|
||||
templateDescription: (theme) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -87,8 +87,8 @@ describe("Setup Page", () => {
|
||||
element: <SetupPage />,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <h1>Workspaces</h1>,
|
||||
path: "/templates",
|
||||
element: <h1>Templates</h1>,
|
||||
},
|
||||
],
|
||||
{ initialEntries: ["/setup"] },
|
||||
@@ -96,6 +96,6 @@ describe("Setup Page", () => {
|
||||
);
|
||||
await waitForLoaderToBeRemoved();
|
||||
await fillForm();
|
||||
await waitFor(() => screen.findByText("Workspaces"));
|
||||
await waitFor(() => screen.findByText("Templates"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export const SetupPage: FC = () => {
|
||||
onSubmit={async (firstUser) => {
|
||||
await createFirstUserMutation.mutateAsync(firstUser);
|
||||
await signIn(firstUser.email, firstUser.password);
|
||||
navigate("/");
|
||||
navigate("/templates");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -60,9 +60,11 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
|
||||
{Boolean(!starterTemplatesByTag) && <Loader />}
|
||||
|
||||
<Stack direction="row" spacing={4}>
|
||||
<Stack direction="row" spacing={4} alignItems="flex-start">
|
||||
{starterTemplatesByTag && tags && (
|
||||
<Stack css={styles.filter}>
|
||||
<Stack
|
||||
css={{ width: 208, flexShrink: 0, position: "sticky", top: 48 }}
|
||||
>
|
||||
<span css={styles.filterCaption}>Filter</span>
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
@@ -79,10 +81,24 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<div css={styles.templates}>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 32,
|
||||
height: "max-content",
|
||||
}}
|
||||
>
|
||||
{visibleTemplates &&
|
||||
visibleTemplates.map((example) => (
|
||||
<TemplateExampleCard example={example} key={example.id} />
|
||||
<TemplateExampleCard
|
||||
css={(theme) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
})}
|
||||
example={example}
|
||||
key={example.id}
|
||||
activeTag={activeTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
@@ -91,11 +107,6 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
};
|
||||
|
||||
const styles = {
|
||||
filter: {
|
||||
width: 208,
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
filterCaption: (theme) => ({
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 600,
|
||||
@@ -119,12 +130,4 @@ const styles = {
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
}),
|
||||
|
||||
templates: {
|
||||
flex: "1",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
||||
gap: 16,
|
||||
gridAutoRows: "min-content",
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -44,12 +44,11 @@ export const EmptyTemplates: FC<{
|
||||
if (canCreateTemplates) {
|
||||
return (
|
||||
<TableEmpty
|
||||
message="Create a Template"
|
||||
message="Create your first template"
|
||||
description={
|
||||
<>
|
||||
Templates are written in Terraform and describe the infrastructure
|
||||
for workspaces (e.g., docker_container, aws_instance,
|
||||
kubernetes_pod). Select a starter template below or
|
||||
for workspaces. You can start using a starter template below or{" "}
|
||||
<Link
|
||||
href={docs("/templates/tutorial")}
|
||||
target="_blank"
|
||||
@@ -64,11 +63,7 @@ export const EmptyTemplates: FC<{
|
||||
<Stack alignItems="center" spacing={4}>
|
||||
<div css={styles.featuredExamples}>
|
||||
{featuredExamples.map((example) => (
|
||||
<TemplateExampleCard
|
||||
example={example}
|
||||
key={example.id}
|
||||
css={styles.template}
|
||||
/>
|
||||
<TemplateExampleCard example={example} key={example.id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -76,7 +71,7 @@ export const EmptyTemplates: FC<{
|
||||
size="small"
|
||||
component={RouterLink}
|
||||
to="/starter-templates"
|
||||
css={styles.viewAllButton}
|
||||
css={{ borderRadius: 9999 }}
|
||||
>
|
||||
View all starter templates
|
||||
</Button>
|
||||
@@ -117,23 +112,10 @@ const styles = {
|
||||
},
|
||||
},
|
||||
|
||||
featuredExamples: {
|
||||
maxWidth: 800,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
||||
gap: 16,
|
||||
gridAutoRows: "min-content",
|
||||
},
|
||||
|
||||
template: (theme) => ({
|
||||
backgroundColor: theme.palette.background.paperLight,
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.divider,
|
||||
},
|
||||
featuredExamples: (theme) => ({
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
|
||||
viewAllButton: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -122,7 +122,7 @@ export const WorkspacesEmpty = (props: {
|
||||
</div>
|
||||
<div>
|
||||
<h4 css={{ fontSize: 14, fontWeight: 600, margin: 0 }}>
|
||||
{t.display_name}
|
||||
{t.display_name.length > 0 ? t.display_name : t.name}
|
||||
</h4>
|
||||
<span
|
||||
css={(theme) => ({
|
||||
|
||||
Reference in New Issue
Block a user