Skip to content

fix: push create workspace UX to templates page #2142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,6 @@ export const AppRouter: FC = () => (
</AuthAndFrame>
}
/>

<Route
path="new"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>

<Route path="templates">
Expand All @@ -77,14 +68,24 @@ export const AppRouter: FC = () => (
}
/>

<Route
path=":template"
element={
<AuthAndFrame>
<TemplatePage />
</AuthAndFrame>
}
/>
<Route path=":template">
<Route
index
element={
<AuthAndFrame>
<TemplatePage />
</AuthAndFrame>
}
/>
<Route
path="workspace"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>
</Route>

<Route path="users">
Expand Down
15 changes: 15 additions & 0 deletions site/src/components/PageHeader/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export const PageHeaderSubtitle: React.FC = ({ children }) => {
return <h2 className={styles.subtitle}>{children}</h2>
}

export const PageHeaderText: React.FC = ({ children }) => {
const styles = useStyles()

return <h3 className={styles.text}>{children}</h3>
}

const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
Expand All @@ -58,6 +64,15 @@ const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(1),
},

text: {
fontSize: theme.spacing(2),
color: theme.palette.text.secondary,
fontWeight: 400,
display: "block",
margin: 0,
marginTop: theme.spacing(1),
},

actions: {
marginLeft: "auto",
},
Expand Down
12 changes: 2 additions & 10 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import * as API from "../../api/api"
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
import { renderWithAuth } from "../../testHelpers/renderHelpers"
import { Language as FormLanguage } from "../../util/formUtils"
import CreateWorkspacePage from "./CreateWorkspacePage"
import { Language } from "./CreateWorkspacePageView"

const renderCreateWorkspacePage = () => {
return renderWithAuth(<CreateWorkspacePage />, {
route: "/workspaces/new?template=" + MockTemplate.name,
path: "/workspaces/new",
route: "/templates/" + MockTemplate.name + "/workspace",
path: "/templates/:template/workspace",
})
}

Expand All @@ -29,13 +28,6 @@ describe("CreateWorkspacePage", () => {
expect(element).toBeDefined()
})

it("shows validation error message", async () => {
renderCreateWorkspacePage()
await fillForm({ name: "$$$" })
const errorMessage = await screen.findByText(FormLanguage.nameInvalidChars(Language.nameLabel))
expect(errorMessage).toBeDefined()
})

it("succeeds", async () => {
renderCreateWorkspacePage()
// You have to spy the method before it is used.
Expand Down
18 changes: 6 additions & 12 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useMachine } from "@xstate/react"
import { FC } from "react"
import { Helmet } from "react-helmet"
import { useNavigate, useSearchParams } from "react-router-dom"
import { Template } from "../../api/typesGenerated"
import { useNavigate, useParams } from "react-router-dom"
import { useOrganizationId } from "../../hooks/useOrganizationId"
import { pageTitle } from "../../util/page"
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"

const CreateWorkspacePage: FC = () => {
const organizationId = useOrganizationId()
const [searchParams] = useSearchParams()
const preSelectedTemplateName = searchParams.get("template")
const { template } = useParams()
const templateName = template ? template : ""
const navigate = useNavigate()
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
context: { organizationId, preSelectedTemplateName },
context: { organizationId, templateName },
actions: {
onCreateWorkspace: (_, event) => {
navigate(`/@${event.data.owner_name}/${event.data.name}`)
Expand All @@ -31,24 +30,19 @@ const CreateWorkspacePage: FC = () => {
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
templateName={createWorkspaceState.context.templateName}
templates={createWorkspaceState.context.templates}
selectedTemplate={createWorkspaceState.context.selectedTemplate}
templateSchema={createWorkspaceState.context.templateSchema}
onCancel={() => {
navigate(preSelectedTemplateName ? "/templates" : "/workspaces")
navigate("/templates")
}}
onSubmit={(request) => {
send({
type: "CREATE_WORKSPACE",
request,
})
}}
onSelectTemplate={(template: Template) => {
send({
type: "SELECT_TEMPLATE",
template,
})
}}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ export default {

const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />

export const NoTemplates = Template.bind({})
NoTemplates.args = {
templates: [],
}

export const NoParameters = Template.bind({})
NoParameters.args = {
templates: [MockTemplate],
Expand Down
97 changes: 13 additions & 84 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import Link from "@material-ui/core/Link"
import MenuItem from "@material-ui/core/MenuItem"
import { makeStyles } from "@material-ui/core/styles"
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
import OpenInNewIcon from "@material-ui/icons/OpenInNew"
import TextField from "@material-ui/core/TextField"
import { FormikContextType, useFormik } from "formik"
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"
Expand All @@ -20,29 +14,18 @@ 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{" "}
<Link target="_blank" href="https://github.com/coder/coder/blob/main/docs/templates.md">
create one from scratch
</Link>{" "}
or use a built-in template by typing the following Coder CLI command:
</>
),
templateLink: "Read more about this template",
}

export interface CreateWorkspacePageViewProps {
loadingTemplates: boolean
loadingTemplateSchema: boolean
creatingWorkspace: boolean
templateName: string
templates?: TypesGen.Template[]
selectedTemplate?: TypesGen.Template
templateSchema?: TypesGen.ParameterSchema[]
onCancel: () => void
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
onSelectTemplate: (template: TypesGen.Template) => void
}

export const validationSchema = Yup.object({
Expand All @@ -51,7 +34,8 @@ export const validationSchema = Yup.object({

export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props) => {
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
const styles = useStyles()
useStyles()

const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
initialValues: {
name: "",
Expand Down Expand Up @@ -84,75 +68,20 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
},
})
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
const selectedTemplate =
props.templates &&
form.values.template_id &&
props.templates.find((template) => template.id === form.values.template_id)

const handleTemplateChange: TextFieldProps["onChange"] = (event) => {
if (!props.templates) {
throw new Error("Templates are not loaded")
}

const templateId = event.target.value
const selectedTemplate = props.templates.find((template) => template.id === templateId)

if (!selectedTemplate) {
throw new Error(`Template ${templateId} not found`)
}

form.setFieldValue("template_id", selectedTemplate.id)
props.onSelectTemplate(selectedTemplate)
}

return (
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
<form onSubmit={form.handleSubmit}>
{props.loadingTemplates && <Loader />}

<Stack>
{props.templates && props.templates.length === 0 && (
<EmptyState
className={styles.emptyState}
message={Language.emptyMessage}
description={Language.emptyDescription}
descriptionClassName={styles.emptyStateDescription}
cta={
<CodeExample className={styles.code} buttonClassName={styles.codeButton} code="coder template init" />
}
/>
)}
{props.templates && props.templates.length > 0 && (
<TextField
{...getFieldHelpers("template_id")}
disabled={form.isSubmitting}
onChange={handleTemplateChange}
autoFocus
fullWidth
label={Language.templateLabel}
variant="outlined"
select
helperText={
selectedTemplate && (
<Link
className={styles.readMoreLink}
component={RouterLink}
to={`/templates/${selectedTemplate.name}`}
target="_blank"
>
{Language.templateLink} <OpenInNewIcon />
</Link>
)
}
>
{props.templates.map((template) => (
<MenuItem key={template.id} value={template.id}>
{template.name}
</MenuItem>
))}
</TextField>
)}

<TextField
disabled
fullWidth
label={Language.templateLabel}
value={props.selectedTemplate?.name || props.templateName}
variant="outlined"
/>

{props.loadingTemplateSchema && <Loader />}
{props.selectedTemplate && props.templateSchema && (
<>
<TextField
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/TemplatePage/TemplatePageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
<Margins>
<PageHeader
actions={
<Link underline="none" component={RouterLink} to={`/workspaces/new?template=${template.name}`}>
<Link underline="none" component={RouterLink} to={`/templates/${template.name}/workspace`}>
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
</Link>
}
Expand Down
5 changes: 4 additions & 1 deletion site/src/pages/TemplatesPage/TemplatesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
HelpTooltipTitle,
} from "../../components/HelpTooltip/HelpTooltip"
import { Margins } from "../../components/Margins/Margins"
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import { PageHeader, PageHeaderText, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import { Stack } from "../../components/Stack/Stack"
import { TableLoader } from "../../components/TableLoader/TableLoader"

Expand Down Expand Up @@ -84,6 +84,9 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
<TemplateHelpTooltip />
</Stack>
</PageHeaderTitle>
{props.templates && props.templates.length > 0 && (
<PageHeaderText>Choose a template to create a new workspace.</PageHeaderText>
)}
</PageHeader>

<Table>
Expand Down
18 changes: 10 additions & 8 deletions site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
HelpTooltipTitle,
} from "../../components/HelpTooltip/HelpTooltip"
import { Margins } from "../../components/Margins/Margins"
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import { PageHeader, PageHeaderText, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import { Stack } from "../../components/Stack/Stack"
import { TableLoader } from "../../components/TableLoader/TableLoader"
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
Expand All @@ -41,7 +41,7 @@ import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
dayjs.extend(relativeTime)

export const Language = {
createWorkspaceButton: "Create workspace",
createFromTemplateButton: "Create from template",
emptyCreateWorkspaceMessage: "Create your first workspace",
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
emptyResultsMessage: "No results matched your search",
Expand Down Expand Up @@ -132,11 +132,13 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
<Margins>
<PageHeader
actions={
<Link underline="none" component={RouterLink} to="/workspaces/new">
<Button startIcon={<AddCircleOutline />} style={{ height: "44px" }}>
{Language.createWorkspaceButton}
</Button>
</Link>
<PageHeaderText>
Create a new workspace from a{" "}
<Link component={RouterLink} to="/templates">
Template
</Link>
.
</PageHeaderText>
}
>
<PageHeaderTitle>
Expand Down Expand Up @@ -213,7 +215,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
description={Language.emptyCreateWorkspaceDescription}
cta={
<Link underline="none" component={RouterLink} to="/workspaces/new">
<Button startIcon={<AddCircleOutline />}>{Language.createWorkspaceButton}</Button>
<Button startIcon={<AddCircleOutline />}>{Language.createFromTemplateButton}</Button>
</Link>
}
/>
Expand Down
Loading