diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx index d0ec4fe084252..640f943130623 100644 --- a/site/src/components/CodeExample/CodeExample.tsx +++ b/site/src/components/CodeExample/CodeExample.tsx @@ -1,22 +1,25 @@ import { makeStyles } from "@material-ui/core/styles" import { FC } from "react" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" +import { combineClasses } from "../../util/combineClasses" import { CopyButton } from "../CopyButton/CopyButton" export interface CodeExampleProps { code: string + className?: string + buttonClassName?: string } /** * Component to show single-line code examples, with a copy button */ -export const CodeExample: FC = ({ code }) => { +export const CodeExample: FC = ({ code, className, buttonClassName }) => { const styles = useStyles() return ( -
- {code} - +
+ {code} +
) } @@ -30,8 +33,17 @@ const useStyles = makeStyles((theme) => ({ background: theme.palette.background.default, color: theme.palette.primary.contrastText, fontFamily: MONOSPACE_FONT_FAMILY, - fontSize: 13, - padding: theme.spacing(2), + fontSize: 14, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(0.5), + }, + code: { + padding: `${theme.spacing(0.5)}px ${theme.spacing(0.75)}px ${theme.spacing(0.5)}px ${theme.spacing(2)}px`, + }, + button: { + border: 0, + minWidth: 42, + minHeight: 42, borderRadius: theme.shape.borderRadius, }, })) diff --git a/site/src/components/EmptyState/EmptyState.tsx b/site/src/components/EmptyState/EmptyState.tsx index c344a45033da6..0192574a93d20 100644 --- a/site/src/components/EmptyState/EmptyState.tsx +++ b/site/src/components/EmptyState/EmptyState.tsx @@ -2,13 +2,17 @@ import Box from "@material-ui/core/Box" import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import { FC, ReactNode } from "react" +import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" +import { combineClasses } from "../../util/combineClasses" export interface EmptyStateProps { /** Text Message to display, placed inside Typography component */ message: string /** Longer optional description to display below the message */ - description?: string + description?: string | React.ReactNode + descriptionClassName?: string cta?: ReactNode + className?: string } /** @@ -20,17 +24,21 @@ export interface EmptyStateProps { * that you can directly pass props through to to customize the shape and layout of it. */ export const EmptyState: FC = (props) => { - const { message, description, cta, ...boxProps } = props + const { message, description, cta, descriptionClassName, className, ...boxProps } = props const styles = useStyles() return ( - +
{message} {description && ( - + {description} )} @@ -48,17 +56,20 @@ const useStyles = makeStyles( justifyContent: "center", alignItems: "center", textAlign: "center", - minHeight: 120, + minHeight: 300, padding: theme.spacing(3), + fontFamily: MONOSPACE_FONT_FAMILY, }, header: { marginBottom: theme.spacing(3), }, title: { - fontWeight: 400, + fontWeight: 600, + fontFamily: "inherit", }, description: { marginTop: theme.spacing(1), + fontFamily: "inherit", }, }), { name: "EmptyState" }, diff --git a/site/src/components/Footer/Footer.tsx b/site/src/components/Footer/Footer.tsx index d4764cf496fc9..d248903ecffda 100644 --- a/site/src/components/Footer/Footer.tsx +++ b/site/src/components/Footer/Footer.tsx @@ -27,7 +27,12 @@ export const Footer: React.FC = ({ children }) => {
{buildInfoState.context.buildInfo && (
- + {Language.buildInfoText(buildInfoState.context.buildInfo)}
@@ -38,6 +43,7 @@ export const Footer: React.FC = ({ children }) => { const useFooterStyles = makeStyles((theme) => ({ root: { + opacity: 0.6, textAlign: "center", flex: "0", paddingTop: theme.spacing(2), @@ -50,4 +56,8 @@ const useFooterStyles = makeStyles((theme) => ({ buildInfo: { margin: theme.spacing(0.25), }, + link: { + color: theme.palette.text.secondary, + fontWeight: 600, + }, })) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 8a97781aff211..e134d0e940593 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -33,6 +33,11 @@ export default { const Template: Story = (args) => +export const NoTemplates = Template.bind({}) +NoTemplates.args = { + templates: [], +} + export const NoParameters = Template.bind({}) NoParameters.args = { templates: [MockTemplate], diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 93e6728d57e04..ebb398f86b490 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -8,6 +8,8 @@ import { FC, useState } from "react" import { Link as RouterLink } from "react-router-dom" import * as Yup from "yup" import * as TypesGen from "../../api/typesGenerated" +import { CodeExample } from "../../components/CodeExample/CodeExample" +import { EmptyState } from "../../components/EmptyState/EmptyState" import { FormFooter } from "../../components/FormFooter/FormFooter" import { FullPageForm } from "../../components/FullPageForm/FullPageForm" import { Loader } from "../../components/Loader/Loader" @@ -18,6 +20,17 @@ import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formU export const Language = { templateLabel: "Template", nameLabel: "Name", + emptyMessage: "Let's create your first template", + emptyDescription: ( + <> + To create a workspace you need to have a template. You can{" "} + + create one from scratch + {" "} + or use a built-in template by typing the following Coder CLI command: + + ), + templateLink: "Read more about this template", } export interface CreateWorkspacePageViewProps { @@ -98,7 +111,18 @@ export const CreateWorkspacePageView: FC = (props) {props.loadingTemplates && } - {props.templates && ( + {props.templates && props.templates.length === 0 && ( + + } + /> + )} + {props.templates && props.templates.length > 0 && ( = (props) to={`/templates/${selectedTemplate.name}`} target="_blank" > - Read more about this template + {Language.templateLink} ) } @@ -179,4 +203,21 @@ const useStyles = makeStyles((theme) => ({ marginLeft: theme.spacing(0.5), }, }, + emptyState: { + padding: 0, + fontFamily: "inherit", + textAlign: "left", + minHeight: "auto", + alignItems: "flex-start", + }, + emptyStateDescription: { + lineHeight: "160%", + }, + code: { + background: theme.palette.background.paper, + width: "100%", + }, + codeButton: { + background: theme.palette.background.paper, + }, })) diff --git a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx index 0aa525f8591bc..c2400ba1a70b3 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx @@ -31,7 +31,7 @@ describe("TemplatesPage", () => { render() // Then - await screen.findByText(Language.emptyViewCreate) + await screen.findByText(Language.emptyMessage) }) it("renders a filled templates page", async () => { diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 8f038a236aaab..e7629627d1216 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -8,9 +8,10 @@ import TableRow from "@material-ui/core/TableRow" import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime" import { FC } from "react" -import { Link as RouterLink } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { AvatarData } from "../../components/AvatarData/AvatarData" +import { CodeExample } from "../../components/CodeExample/CodeExample" +import { EmptyState } from "../../components/EmptyState/EmptyState" import { Margins } from "../../components/Margins/Margins" import { Stack } from "../../components/Stack/Stack" import { TableLoader } from "../../components/TableLoader/TableLoader" @@ -24,9 +25,17 @@ export const Language = { nameLabel: "Name", usedByLabel: "Used by", lastUpdatedLabel: "Last updated", - emptyViewCreateCTA: "Create a template", - emptyViewCreate: "to standardize development workspaces for your team.", - emptyViewNoPerms: "No templates have been created! Contact your Coder administrator.", + emptyViewNoPerms: "Contact your Coder administrator to create a template. You can share the code below.", + emptyMessage: "Create your first template", + emptyDescription: ( + <> + To create a workspace you need to have a template. You can{" "} + + create one from scratch + {" "} + or use a built-in template using the following Coder CLI command: + + ), } export interface TemplatesPageViewProps { @@ -53,18 +62,12 @@ export const TemplatesPageView: FC = (props) => { {!props.loading && !props.templates?.length && ( -
- {props.canCreateTemplate ? ( - - - {Language.emptyViewCreateCTA} - -  {Language.emptyViewCreate} - - ) : ( - {Language.emptyViewNoPerms} - )} -
+ } + />
)} @@ -92,20 +95,9 @@ export const TemplatesPageView: FC = (props) => { const useStyles = makeStyles((theme) => ({ root: { - marginTop: theme.spacing(3), + marginTop: theme.spacing(10), }, - welcome: { - paddingTop: theme.spacing(12), - paddingBottom: theme.spacing(12), - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - "& span": { - maxWidth: 600, - textAlign: "center", - fontSize: theme.spacing(2), - lineHeight: `${theme.spacing(3)}px`, - }, + emptyDescription: { + maxWidth: theme.spacing(62), }, })) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 1180c10536360..462ce080add5d 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -23,7 +23,7 @@ describe("WorkspacesPage", () => { render() // Then - await screen.findByText(Language.emptyView) + await screen.findByText(Language.emptyMessage) }) it("renders a filled workspaces page", async () => { diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 740bbae0b69fc..880698a8a0c8a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -1,5 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" -import { ProvisionerJobStatus, Workspace } from "../../api/typesGenerated" +import { ProvisionerJobStatus, Workspace, WorkspaceTransition } from "../../api/typesGenerated" import { MockWorkspace } from "../../testHelpers/entities" import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView" @@ -10,7 +10,10 @@ export default { const Template: Story = (args) => -const createWorkspaceWithStatus = (status: ProvisionerJobStatus, transition = "start"): Workspace => { +const createWorkspaceWithStatus = ( + status: ProvisionerJobStatus, + transition: WorkspaceTransition = "start", +): Workspace => { return { ...MockWorkspace, latest_build: { @@ -46,4 +49,6 @@ AllStates.args = { } export const Empty = Template.bind({}) -Empty.args = {} +Empty.args = { + workspaces: [], +} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 32d39658783d6..2a412116be7bb 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -14,15 +14,18 @@ import { FC } from "react" import { Link as RouterLink } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { AvatarData } from "../../components/AvatarData/AvatarData" +import { EmptyState } from "../../components/EmptyState/EmptyState" import { Margins } from "../../components/Margins/Margins" import { Stack } from "../../components/Stack/Stack" +import { TableLoader } from "../../components/TableLoader/TableLoader" import { getDisplayStatus } from "../../util/workspace" dayjs.extend(relativeTime) export const Language = { createButton: "Create workspace", - emptyView: "so you can check out your repositories, edit your source code, and build and test your software.", + emptyMessage: "Create your first workspace", + emptyDescription: "Start editing your source code and building your software", } export interface WorkspacesPageViewProps { @@ -53,50 +56,53 @@ export const WorkspacesPageView: FC = (props) => { - {!props.loading && !props.workspaces?.length && ( + {props.loading && } + {props.workspaces && props.workspaces.length === 0 && ( -
- - - Create a workspace + + -  {Language.emptyView} - -
+ } + />
)} - {props.workspaces?.map((workspace) => { - const status = getDisplayStatus(theme, workspace.latest_build) - return ( - - - - - {workspace.template_name} - - {workspace.outdated ? ( - outdated - ) : ( - up to date - )} - - - - {dayjs().to(dayjs(workspace.latest_build.created_at))} - - - - {status.status} - - - ) - })} + {props.workspaces && + props.workspaces.map((workspace) => { + const status = getDisplayStatus(theme, workspace.latest_build) + return ( + + + + + {workspace.template_name} + + {workspace.outdated ? ( + outdated + ) : ( + up to date + )} + + + + {dayjs().to(dayjs(workspace.latest_build.created_at))} + + + + {status.status} + + + ) + })}
diff --git a/site/src/xServices/createWorkspace/createWorkspaceXService.ts b/site/src/xServices/createWorkspace/createWorkspaceXService.ts index 13ac3f791143a..297819d1b2cb6 100644 --- a/site/src/xServices/createWorkspace/createWorkspaceXService.ts +++ b/site/src/xServices/createWorkspace/createWorkspaceXService.ts @@ -56,6 +56,11 @@ export const createWorkspaceMachine = createMachine( invoke: { src: "getTemplates", onDone: [ + { + actions: ["assignTemplates"], + target: "waitingForTemplateGetCreated", + cond: "areTemplatesEmpty", + }, { actions: ["assignTemplates", "assignPreSelectedTemplate"], target: "gettingTemplateSchema", @@ -71,6 +76,25 @@ export const createWorkspaceMachine = createMachine( }, }, }, + waitingForTemplateGetCreated: { + initial: "refreshingTemplates", + states: { + refreshingTemplates: { + invoke: { + src: "getTemplates", + onDone: [ + { target: "waiting", cond: "areTemplatesEmpty" }, + { target: "#createWorkspaceState.selectingTemplate", actions: ["assignTemplates"] }, + ], + }, + }, + waiting: { + after: { + 2_000: "refreshingTemplates", + }, + }, + }, + }, selectingTemplate: { on: { SELECT_TEMPLATE: { @@ -147,6 +171,7 @@ export const createWorkspaceMachine = createMachine( const template = event.data.find((template) => template.name === ctx.preSelectedTemplateName) return !!template }, + areTemplatesEmpty: (_, event) => event.data.length === 0, }, actions: { assignTemplates: assign({