8000 fix: push create workspace UX to templates page by f0ssel · Pull Request #2142 · coder/coder · GitHub
[go: up one dir, main page]

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 1 commit
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
Prev Previous commit
Next Next commit
remove template selector
  • Loading branch information
f0ssel committed Jun 7, 2022
commit b774c50e790b7e4ae14c10a30c0c5bf2fb28af79
13 changes: 4 additions & 9 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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 { useOrganizationId } from "../../hooks/useOrganizationId"
import { pageTitle } from "../../util/page"
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
Expand All @@ -11,10 +10,11 @@ import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
const CreateWorkspacePage: FC = () => {
const organizationId = useOrganizationId()
const [searchParams] = useSearchParams()
const preSelectedTemplateName = searchParams.get("template")
const templateParam = searchParams.get("template")
const templateName = templateParam ? templateParam : ""
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,6 +31,7 @@ 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}
Expand All @@ -43,12 +44,6 @@ const CreateWorkspacePage: FC = () => {
request,
})
}}
onSelectTemplate={(template: Template) => {
send({
type: "SELECT_TEMPLATE",
template,
})
}}
/>
</>
)
Expand Down
91 changes: 11 additions & 80 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
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 +15,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 +35,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,74 +69,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.templateName}
variant="outlined"
/>

{props.selectedTemplate && props.templateSchema && (
<>
Expand Down
76 changes: 9 additions & 67 deletions site/src/xServices/createWorkspace/createWorkspaceXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@ import { CreateWorkspaceRequest, ParameterSchema, Template, Workspace } from "..

type CreateWorkspaceContext = {
organizationId: string
templateName: string
templates?: Template[]
selectedTemplate?: Template
templateSchema?: ParameterSchema[]
createWorkspaceRequest?: CreateWorkspaceRequest
createdWorkspace?: Workspace
// This is useful when the user wants to create a workspace from the template
// page having it pre selected. It is string or null because of the
// useSearchQuery
preSelectedTemplateName: string | null
}

type CreateWorkspaceEvent =
| {
type: "SELECT_TEMPLATE"
template: Template
}
| {
type: "CREATE_WORKSPACE"
request: CreateWorkspaceRequest
Expand All @@ -45,64 +38,25 @@ export const createWorkspaceMachine = createMachine(
},
},
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
on: {
SELECT_TEMPLATE: {
actions: ["assignSelectedTemplate"],
target: "gettingTemplateSchema",
},
},
states: {
gettingTemplates: {
invoke: {
src: "getTemplates",
onDone: [
{
actions: ["assignTemplates"],
target: "waitingForTemplateGetCreated",
cond: "areTemplatesEmpty",
},
{
actions: ["assignTemplates", "assignPreSelectedTemplate"],
actions: ["assignTemplates", "assignSelectedTemplate"],
target: "gettingTemplateSchema",
cond: "hasValidPreSelectedTemplate",
},
{
actions: ["assignTemplates"],
target: "selectingTemplate",
},
],
onError: {
target: "error",
},
},
},
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: {
actions: ["assignSelectedTemplate"],
target: "gettingTemplateSchema",
},
},
},
gettingTemplateSchema: {
invoke: {
src: "getTemplateSchema",
Expand Down Expand Up @@ -164,21 +118,20 @@ export const createWorkspaceMachine = createMachine(
},
},
guards: {
hasValidPreSelectedTemplate: (ctx, event) => {
if (!ctx.preSelectedTemplateName) {
return false
}
const template = event.data.find((template) => template.name === ctx.preSelectedTemplateName)
return !!template
},
areTemplatesEmpty: (_, event) => event.data.length === 0,
},
actions: {
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignSelectedTemplate: assign({
selectedTemplate: (_, event) => event.template,
selectedTemplate: (ctx, event) => {
for (const template of event.data) {
if (template.name === ctx.templateName) {
return template
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use Array's filter here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for loops are gonna die hard for me lol

}
},
}),
assignTemplateSchema: assign({
// Only show parameters that are allowed to be overridden.
Expand All @@ -188,17 +141,6 @@ export const createWorkspaceMachine = createMachine(
assignCreateWorkspaceRequest: assign({
createWorkspaceRequest: (_, event) => event.request,
}),
assignPreSelectedTemplate: assign({
selectedTemplate: (ctx, event) => {
const selectedTemplate = event.data.find((template) => template.name === ctx.preSelectedTemplateName)
// The proper validation happens on hasValidPreSelectedTemplate
if (!selectedTemplate) {
throw new Error("Invalid template selected")
}

return selectedTemplate
},
}),
},
},
)
0