Skip to content
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
Next Next commit
Refactor create workspace machine
  • Loading branch information
BrunoQuaresma committed May 23, 2022
commit c90c22648498cd1750234c2cde76d7aa1ddc95d2
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"drpcserver",
"Dsts",
"fatih",
"Formik",
"goarch",
"gographviz",
"goleak",
"gossh",
"gsyslog",
"hashicorp",
"hclsyntax",
"httpapi",
"httpmw",
"idtoken",
"Iflag",
Expand Down Expand Up @@ -63,6 +65,7 @@
"tfjson",
"tfstate",
"trimprefix",
"typegen",
"unconvert",
"Untar",
"VMID",
Expand Down
67 changes: 43 additions & 24 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,55 @@
import { useMachine } from "@xstate/react"
import React from "react"
import { useNavigate } from "react-router"
import { useParams } from "react-router-dom"
import { createWorkspace } from "../../api/api"
import { templateMachine } from "../../xServices/template/templateXService"
import { useActor, useMachine } from "@xstate/react"
import React, { useContext } from "react"
import { useNavigate } from "react-router-dom"
import { Template } from "../../api/typesGenerated"
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
import { XServiceContext } from "../../xServices/StateContext"
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"

const useOrganizationId = () => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const organizationId = authState.context.me?.organization_ids[0]

if (!organizationId) {
throw new Error("No organization ID found")
}

return organizationId
}

const CreateWorkspacePage: React.FC = () => {
const { template } = useParams()
const [templateState] = useMachine(templateMachine, {
context: {
name: template,
const organizationId = useOrganizationId()
const navigate = useNavigate()
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
context: { organizationId },
actions: {
onCreateWorkspace: (_, event) => {
navigate("/workspaces/" + event.data.id)
},
},
})
const navigate = useNavigate()
const loading = templateState.hasTag("loading")
if (!templateState.context.template || !templateState.context.templateSchema) {
return null
}

return (
<CreateWorkspacePageView
template={templateState.context.template}
templateSchema={templateState.context.templateSchema}
loading={loading}
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
templates={createWorkspaceState.context.templates}
selectedTemplate={createWorkspaceState.context.selectedTemplate}
templateSchema={createWorkspaceState.context.templateSchema}
onCancel={() => navigate("/templates")}
onSubmit={async (req) => {
if (!templateState.context.template) {
throw new Error("template isn't valid")
}
const workspace = await createWorkspace(templateState.context.template.organization_id, req)
navigate("/workspaces/" + workspace.id)
onSubmit={(request) => {
send({
type: "CREATE_WORKSPACE",
request,
})
}}
onSelectTemplate={(template: Template) => {
send({
type: "SELECT_TEMPLATE",
template,
})
}}
/>
)
Expand Down
108 changes: 76 additions & 32 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MenuItem from "@material-ui/core/MenuItem"
import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
import { FormikContextType, useFormik } from "formik"
import React from "react"
import * as Yup from "yup"
Expand All @@ -11,6 +12,7 @@ import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"

export const Language = {
templateLabel: "Template",
nameLabel: "Name",
nameRequired: "Please enter a name.",
nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -",
Expand All @@ -24,12 +26,15 @@ const maxLenName = 32
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/

export interface CreateWorkspacePageViewProps {
loading?: boolean
template: TypesGen.Template
templateSchema: TypesGen.ParameterSchema[]

loadingTemplates: boolean
loadingTemplateSchema: boolean
creatingWorkspace: boolean
templates?: TypesGen.Template[]
selectedTemplate?: TypesGen.Template
templateSchema?: TypesGen.ParameterSchema[]
onCancel: () => void
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => Promise<void>
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
onSelectTemplate: (template: TypesGen.Template) => void
}

export const validationSchema = Yup.object({
Expand All @@ -45,10 +50,14 @@ export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
initialValues: {
name: "",
template_id: props.template.id,
template_id: "",
},
validationSchema,
onSubmit: (request) => {
if (!props.templateSchema) {
throw new Error("No template schema loaded")
}

const createRequests: TypesGen.CreateParameterRequest[] = []
props.templateSchema.forEach((schema) => {
let value = schema.default_source_value
Expand All @@ -70,38 +79,73 @@ export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (
})
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)

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

// The TextField + MenuItem returns the index of the selected option
const templateIndex = Number(event.currentTarget.value)
const selectedTemplate = props.templates[templateIndex]
form.setFieldValue("template_id", selectedTemplate.id)
props.onSelectTemplate(selectedTemplate)
}

return (
<Margins>
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
<form onSubmit={form.handleSubmit}>
<TextField
{...getFieldHelpers("name")}
disabled={form.isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={Language.nameLabel}
variant="outlined"
/>
{props.templateSchema.length > 0 && (
<div className={styles.parameters}>
{props.templateSchema.map((schema) => (
<ParameterInput
disabled={form.isSubmitting}
key={schema.id}
onChange={(value) => {
setParameterValues({
...parameterValues,
[schema.name]: value,
})
}}
schema={schema}
/>
{props.templates && (
<TextField
{...getFieldHelpers("template_id")}
disabled={form.isSubmitting}
onChange={handleTemplateChange}
autoFocus
fullWidth
label={Language.templateLabel}
variant="outlined"
select
>
{props.templates.map((template) => (
<MenuItem key={template.id} value={template.id}>
{template.name}
</MenuItem>
))}
</div>
</TextField>
)}

{props.selectedTemplate && props.templateSchema && (
<>
<TextField
{...getFieldHelpers("name")}
disabled={form.isSubmitting}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={Language.nameLabel}
variant="outlined"
/>
{props.templateSchema.length > 0 && (
<div className={styles.parameters}>
{props.templateSchema.map((schema) => (
<ParameterInput
disabled={form.isSubmitting}
key={schema.id}
onChange={(value) => {
setParameterValues({
...parameterValues,
[schema.name]: value,
})
}}
schema={schema}
/>
))}
</div>
)}
</>
)}

<FormFooter onCancel={props.onCancel} isLoading={props.loading || form.isSubmitting} />
<FormFooter onCancel={props.onCancel} isLoading={props.creatingWorkspace} />
</form>
</FullPageForm>
</Margins>
Expand Down
140 changes: 140 additions & 0 deletions site/src/xServices/createWorkspace/createWorkspaceXService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { assign, createMachine } from "xstate"
import { createWorkspace, getTemplates, getTemplateVersionSchema } from "../../api/api"
import { CreateWorkspaceRequest, ParameterSchema, Template, Workspace } from "../../api/typesGenerated"

type CreateWorkspaceContext = {
organizationId: string
templates?: Template[]
selectedTemplate?: Template
templateSchema?: ParameterSchema[]
createWorkspaceRequest?: CreateWorkspaceRequest
createdWorkspace?: Workspace
}

type CreateWorkspaceEvent =
| {
type: "SELECT_TEMPLATE"
template: Template
}
| {
type: "CREATE_WORKSPACE"
request: CreateWorkspaceRequest
}

export const createWorkspaceMachine = createMachine(
{
id: "createWorkspaceState",
initial: "gettingTemplates",
schema: {
context: {} as CreateWorkspaceContext,
events: {} as CreateWorkspaceEvent,
services: {} as {
getTemplates: {
data: Template[]
}
getTemplateSchema: {
data: ParameterSchema[]
}
createWorkspace: {
data: Workspace
}
},
},
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
states: {
gettingTemplates: {
invoke: {
src: "getTemplates",
onDone: {
actions: ["assignTemplates"],
target: "selectingTemplate",
},
onError: {
target: "error",
},
},
},
selectingTemplate: {
on: {
SELECT_TEMPLATE: {
actions: ["assignSelectedTemplate"],
target: "gettingTemplateSchema",
},
},
},
gettingTemplateSchema: {
invoke: {
src: "getTemplateSchema",
onDone: {
actions: ["assignTemplateSchema"],
target: "fillingForm",
},
onError: {
target: "error",
},
},
},
fillingForm: {
on: {
CREATE_WORKSPACE: {
actions: ["assignCreateWorkspaceRequest"],
target: "creatingWorkspace",
},
},
},
creatingWorkspace: {
invoke: {
src: "createWorkspace",
onDone: {
actions: ["onCreateWorkspace"],
target: "created",
},
onError: {
target: "error",
},
},
},
created: {
type: "final",
},
error: {},
},
},
{
services: {
getTemplates: (context) => getTemplates(context.organizationId),
getTemplateSchema: (context) => {
const { selectedTemplate } = context

if (!selectedTemplate) {
throw new Error("No selected template")
}

return getTemplateVersionSchema(selectedTemplate.active_version_id)
},
createWorkspace: (context) => {
const { createWorkspaceRequest, organizationId } = context

if (!createWorkspaceRequest) {
throw new Error("No create workspace request")
}

return createWorkspace(organizationId, createWorkspaceRequest)
},
},
actions: {
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignSelectedTemplate: assign({
selectedTemplate: (_, event) => event.template,
}),
assignTemplateSchema: assign({
templateSchema: (_, event) => event.data,
}),
assignCreateWorkspaceRequest: assign({
createWorkspaceRequest: (_, event) => event.request,
}),
},
},
)