refactor(site): improve first workspace creation time (#10510)

One tiny improvement to make the onboarding faster. When a user has no workspace, show the existent templates with direct links to the workspace creation instead of asking them to see all templates, select one, and after, click on "Create workspace". 

Before:
<img width="1351" alt="Screenshot 2023-11-03 at 10 11 32" src="https://github.com/coder/coder/assets/3165839/46050f16-0196-477a-90e2-a0f475c8b707">

After:
<img width="1360" alt="Screenshot 2023-11-03 at 10 11 43" src="https://github.com/coder/coder/assets/3165839/5bef3d50-b192-49b5-8bdf-dec9654f529f">
This commit is contained in:
Bruno Quaresma
2023-11-03 11:03:21 -03:00
committed by GitHub
parent c9aeea6f64
commit ca353cb81c
3 changed files with 107 additions and 51 deletions

View File

@ -0,0 +1,97 @@
import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined";
import Button from "@mui/material/Button";
import { Template } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { TableEmpty } from "components/TableEmpty/TableEmpty";
import { Link } from "react-router-dom";
export const WorkspacesEmpty = (props: {
isUsingFilter: boolean;
templates?: Template[];
}) => {
const { isUsingFilter, templates } = props;
const totalFeaturedTemplates = 6;
const featuredTemplates = templates?.slice(0, totalFeaturedTemplates);
if (isUsingFilter) {
return <TableEmpty message="No results matched your search" />;
}
return (
<TableEmpty
message="Create a workspace"
description="A workspace is your personal, customizable development environment in the cloud. Select one template below to start."
cta={
<div>
<div
css={(theme) => ({
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(2),
marginBottom: theme.spacing(3),
justifyContent: "center",
maxWidth: "800px",
})}
>
{featuredTemplates?.map((t) => (
<Link
to={`/templates/${t.name}/workspace`}
key={t.id}
css={(theme) => ({
width: "320px",
padding: theme.spacing(2),
borderRadius: 6,
border: `1px solid ${theme.palette.divider}`,
textAlign: "left",
display: "flex",
gap: theme.spacing(2),
textDecoration: "none",
color: "inherit",
"&:hover": {
backgroundColor: theme.palette.background.paperLight,
},
})}
>
<div css={{ flexShrink: 0, paddingTop: 4 }}>
<Avatar
variant={t.icon ? "square" : undefined}
fitImage={Boolean(t.icon)}
src={t.icon}
size="sm"
>
{t.name}
</Avatar>
</div>
<div>
<h4 css={{ fontSize: 14, fontWeight: 600, margin: 0 }}>
{t.display_name}
</h4>
<span
css={(theme) => ({
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: "0.5",
})}
>
{t.description}
</span>
</div>
</Link>
))}
</div>
{templates && templates.length > totalFeaturedTemplates && (
<Button
component={Link}
to="/templates"
variant="contained"
startIcon={<ArrowForwardOutlined />}
>
See all templates
</Button>
)}
</div>
}
/>
);
};

View File

@ -47,7 +47,6 @@ export interface WorkspacesPageViewProps {
onCheckChange: (checkedWorkspaces: Workspace[]) => void;
onDeleteAll: () => void;
canCheckWorkspaces: boolean;
templatesFetchStatus: TemplateQuery["status"];
templates: TemplateQuery["data"];
}
@ -156,6 +155,7 @@ export const WorkspacesPageView: FC<
checkedWorkspaces={checkedWorkspaces}
onCheckChange={onCheckChange}
canCheckWorkspaces={canCheckWorkspaces}
templates={templates}
/>
{count !== undefined && (
<PaginationWidgetBase

View File

@ -4,16 +4,13 @@ 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 { Template, 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 { Link as RouterLink, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import Box from "@mui/material/Box";
@ -28,8 +25,7 @@ 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";
import { WorkspacesEmpty } from "./WorkspacesEmpty";
export interface WorkspacesTableProps {
workspaces?: Workspace[];
@ -39,6 +35,7 @@ export interface WorkspacesTableProps {
onUpdateWorkspace: (workspace: Workspace) => void;
onCheckChange: (checkedWorkspaces: Workspace[]) => void;
canCheckWorkspaces: boolean;
templates?: Template[];
}
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
@ -48,9 +45,8 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
onUpdateWorkspace,
onCheckChange,
canCheckWorkspaces,
templates,
}) => {
const theme = useTheme();
return (
<TableContainer>
<Table>
@ -93,47 +89,10 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
<TableLoader canCheckWorkspaces={canCheckWorkspaces} />
)}
{workspaces && workspaces.length === 0 && (
<>
{isUsingFilter ? (
<TableEmpty message="No results matched your search" />
) : (
<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"
data-testid="button-select-template"
>
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>
}
/>
)}
</>
<WorkspacesEmpty
templates={templates}
isUsingFilter={isUsingFilter}
/>
)}
{workspaces &&
workspaces.map((workspace) => {