mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
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:
97
site/src/pages/WorkspacesPage/WorkspacesEmpty.tsx
Normal file
97
site/src/pages/WorkspacesPage/WorkspacesEmpty.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
Reference in New Issue
Block a user