mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
fix: push create workspace UX to templates page (#2142)
This commit is contained in:
@ -56,15 +56,6 @@ export const AppRouter: FC = () => (
|
|||||||
</AuthAndFrame>
|
</AuthAndFrame>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
|
||||||
path="new"
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<CreateWorkspacePage />
|
|
||||||
</RequireAuth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="templates">
|
<Route path="templates">
|
||||||
@ -77,14 +68,24 @@ export const AppRouter: FC = () => (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route path=":template">
|
||||||
path=":template"
|
<Route
|
||||||
element={
|
index
|
||||||
<AuthAndFrame>
|
element={
|
||||||
<TemplatePage />
|
<AuthAndFrame>
|
||||||
</AuthAndFrame>
|
<TemplatePage />
|
||||||
}
|
</AuthAndFrame>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="workspace"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<CreateWorkspacePage />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="users">
|
<Route path="users">
|
||||||
|
@ -32,6 +32,12 @@ export const PageHeaderSubtitle: React.FC = ({ children }) => {
|
|||||||
return <h2 className={styles.subtitle}>{children}</h2>
|
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) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -58,6 +64,15 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
marginTop: theme.spacing(1),
|
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: {
|
actions: {
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
},
|
},
|
||||||
|
@ -4,14 +4,13 @@ import * as API from "../../api/api"
|
|||||||
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
|
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
|
||||||
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
|
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
|
||||||
import { renderWithAuth } from "../../testHelpers/renderHelpers"
|
import { renderWithAuth } from "../../testHelpers/renderHelpers"
|
||||||
import { Language as FormLanguage } from "../../util/formUtils"
|
|
||||||
import CreateWorkspacePage from "./CreateWorkspacePage"
|
import CreateWorkspacePage from "./CreateWorkspacePage"
|
||||||
import { Language } from "./CreateWorkspacePageView"
|
import { Language } from "./CreateWorkspacePageView"
|
||||||
|
|
||||||
const renderCreateWorkspacePage = () => {
|
const renderCreateWorkspacePage = () => {
|
||||||
return renderWithAuth(<CreateWorkspacePage />, {
|
return renderWithAuth(<CreateWorkspacePage />, {
|
||||||
route: "/workspaces/new?template=" + MockTemplate.name,
|
route: "/templates/" + MockTemplate.name + "/workspace",
|
||||||
path: "/workspaces/new",
|
path: "/templates/:template/workspace",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,13 +28,6 @@ describe("CreateWorkspacePage", () => {
|
|||||||
expect(element).toBeDefined()
|
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 () => {
|
it("succeeds", async () => {
|
||||||
renderCreateWorkspacePage()
|
renderCreateWorkspacePage()
|
||||||
// You have to spy the method before it is used.
|
// You have to spy the method before it is used.
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet"
|
import { Helmet } from "react-helmet"
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
import { useNavigate, useParams } from "react-router-dom"
|
||||||
import { Template } from "../../api/typesGenerated"
|
|
||||||
import { useOrganizationId } from "../../hooks/useOrganizationId"
|
import { useOrganizationId } from "../../hooks/useOrganizationId"
|
||||||
import { pageTitle } from "../../util/page"
|
import { pageTitle } from "../../util/page"
|
||||||
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
|
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
|
||||||
@ -10,11 +9,11 @@ import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
|
|||||||
|
|
||||||
const CreateWorkspacePage: FC = () => {
|
const CreateWorkspacePage: FC = () => {
|
||||||
const organizationId = useOrganizationId()
|
const organizationId = useOrganizationId()
|
||||||
const [searchParams] = useSearchParams()
|
const { template } = useParams()
|
||||||
const preSelectedTemplateName = searchParams.get("template")
|
const templateName = template ? template : ""
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
||||||
context: { organizationId, preSelectedTemplateName },
|
context: { organizationId, templateName },
|
||||||
actions: {
|
actions: {
|
||||||
onCreateWorkspace: (_, event) => {
|
onCreateWorkspace: (_, event) => {
|
||||||
navigate(`/@${event.data.owner_name}/${event.data.name}`)
|
navigate(`/@${event.data.owner_name}/${event.data.name}`)
|
||||||
@ -31,11 +30,12 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
|
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
|
||||||
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
|
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
|
||||||
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
|
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
|
||||||
|
templateName={createWorkspaceState.context.templateName}
|
||||||
templates={createWorkspaceState.context.templates}
|
templates={createWorkspaceState.context.templates}
|
||||||
selectedTemplate={createWorkspaceState.context.selectedTemplate}
|
selectedTemplate={createWorkspaceState.context.selectedTemplate}
|
||||||
templateSchema={createWorkspaceState.context.templateSchema}
|
templateSchema={createWorkspaceState.context.templateSchema}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
navigate(preSelectedTemplateName ? "/templates" : "/workspaces")
|
navigate("/templates")
|
||||||
}}
|
}}
|
||||||
onSubmit={(request) => {
|
onSubmit={(request) => {
|
||||||
send({
|
send({
|
||||||
@ -43,12 +43,6 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
request,
|
request,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onSelectTemplate={(template: Template) => {
|
|
||||||
send({
|
|
||||||
type: "SELECT_TEMPLATE",
|
|
||||||
template,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -33,11 +33,6 @@ export default {
|
|||||||
|
|
||||||
const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />
|
const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />
|
||||||
|
|
||||||
export const NoTemplates = Template.bind({})
|
|
||||||
NoTemplates.args = {
|
|
||||||
templates: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NoParameters = Template.bind({})
|
export const NoParameters = Template.bind({})
|
||||||
NoParameters.args = {
|
NoParameters.args = {
|
||||||
templates: [MockTemplate],
|
templates: [MockTemplate],
|
||||||
|
@ -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 { makeStyles } from "@material-ui/core/styles"
|
||||||
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
|
import TextField from "@material-ui/core/TextField"
|
||||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew"
|
|
||||||
import { FormikContextType, useFormik } from "formik"
|
import { FormikContextType, useFormik } from "formik"
|
||||||
import { FC, useState } from "react"
|
import { FC, useState } from "react"
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
|
||||||
import * as Yup from "yup"
|
import * as Yup from "yup"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
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 { FormFooter } from "../../components/FormFooter/FormFooter"
|
||||||
import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
|
import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
|
||||||
import { Loader } from "../../components/Loader/Loader"
|
import { Loader } from "../../components/Loader/Loader"
|
||||||
@ -20,29 +14,18 @@ import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formU
|
|||||||
export const Language = {
|
export const Language = {
|
||||||
templateLabel: "Template",
|
templateLabel: "Template",
|
||||||
nameLabel: "Name",
|
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 {
|
export interface CreateWorkspacePageViewProps {
|
||||||
loadingTemplates: boolean
|
loadingTemplates: boolean
|
||||||
loadingTemplateSchema: boolean
|
loadingTemplateSchema: boolean
|
||||||
creatingWorkspace: boolean
|
creatingWorkspace: boolean
|
||||||
|
templateName: string
|
||||||
templates?: TypesGen.Template[]
|
templates?: TypesGen.Template[]
|
||||||
selectedTemplate?: TypesGen.Template
|
selectedTemplate?: TypesGen.Template
|
||||||
templateSchema?: TypesGen.ParameterSchema[]
|
templateSchema?: TypesGen.ParameterSchema[]
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
|
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
|
||||||
onSelectTemplate: (template: TypesGen.Template) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validationSchema = Yup.object({
|
export const validationSchema = Yup.object({
|
||||||
@ -51,7 +34,8 @@ export const validationSchema = Yup.object({
|
|||||||
|
|
||||||
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props) => {
|
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props) => {
|
||||||
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
|
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
|
||||||
const styles = useStyles()
|
useStyles()
|
||||||
|
|
||||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
|
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
@ -84,75 +68,20 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
|
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 (
|
return (
|
||||||
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
|
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
|
||||||
<form onSubmit={form.handleSubmit}>
|
<form onSubmit={form.handleSubmit}>
|
||||||
{props.loadingTemplates && <Loader />}
|
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
{props.templates && props.templates.length === 0 && (
|
<TextField
|
||||||
<EmptyState
|
disabled
|
||||||
className={styles.emptyState}
|
fullWidth
|
||||||
message={Language.emptyMessage}
|
label={Language.templateLabel}
|
||||||
description={Language.emptyDescription}
|
value={props.selectedTemplate?.name || props.templateName}
|
||||||
descriptionClassName={styles.emptyStateDescription}
|
variant="outlined"
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{props.loadingTemplateSchema && <Loader />}
|
||||||
{props.selectedTemplate && props.templateSchema && (
|
{props.selectedTemplate && props.templateSchema && (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -39,7 +39,7 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
|
|||||||
<Margins>
|
<Margins>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
actions={
|
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>
|
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
HelpTooltipTitle,
|
HelpTooltipTitle,
|
||||||
} from "../../components/HelpTooltip/HelpTooltip"
|
} from "../../components/HelpTooltip/HelpTooltip"
|
||||||
import { Margins } from "../../components/Margins/Margins"
|
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 { Stack } from "../../components/Stack/Stack"
|
||||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||||
|
|
||||||
@ -84,6 +84,9 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
|
|||||||
<TemplateHelpTooltip />
|
<TemplateHelpTooltip />
|
||||||
</Stack>
|
</Stack>
|
||||||
</PageHeaderTitle>
|
</PageHeaderTitle>
|
||||||
|
{props.templates && props.templates.length > 0 && (
|
||||||
|
<PageHeaderText>Choose a template to create a new workspace.</PageHeaderText>
|
||||||
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
HelpTooltipTitle,
|
HelpTooltipTitle,
|
||||||
} from "../../components/HelpTooltip/HelpTooltip"
|
} from "../../components/HelpTooltip/HelpTooltip"
|
||||||
import { Margins } from "../../components/Margins/Margins"
|
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 { Stack } from "../../components/Stack/Stack"
|
||||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||||
@ -41,7 +41,7 @@ import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
|
|||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
createWorkspaceButton: "Create workspace",
|
createFromTemplateButton: "Create from template",
|
||||||
emptyCreateWorkspaceMessage: "Create your first workspace",
|
emptyCreateWorkspaceMessage: "Create your first workspace",
|
||||||
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
|
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
|
||||||
emptyResultsMessage: "No results matched your search",
|
emptyResultsMessage: "No results matched your search",
|
||||||
@ -132,11 +132,13 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
|
|||||||
<Margins>
|
<Margins>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
actions={
|
actions={
|
||||||
<Link underline="none" component={RouterLink} to="/workspaces/new">
|
<PageHeaderText>
|
||||||
<Button startIcon={<AddCircleOutline />} style={{ height: "44px" }}>
|
Create a new workspace from a{" "}
|
||||||
{Language.createWorkspaceButton}
|
<Link component={RouterLink} to="/templates">
|
||||||
</Button>
|
Template
|
||||||
</Link>
|
</Link>
|
||||||
|
.
|
||||||
|
</PageHeaderText>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PageHeaderTitle>
|
<PageHeaderTitle>
|
||||||
@ -213,7 +215,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
|
|||||||
description={Language.emptyCreateWorkspaceDescription}
|
description={Language.emptyCreateWorkspaceDescription}
|
||||||
cta={
|
cta={
|
||||||
<Link underline="none" component={RouterLink} to="/workspaces/new">
|
<Link underline="none" component={RouterLink} to="/workspaces/new">
|
||||||
<Button startIcon={<AddCircleOutline />}>{Language.createWorkspaceButton}</Button>
|
<Button startIcon={<AddCircleOutline />}>{Language.createFromTemplateButton}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -4,26 +4,18 @@ import { CreateWorkspaceRequest, ParameterSchema, Template, Workspace } from "..
|
|||||||
|
|
||||||
type CreateWorkspaceContext = {
|
type CreateWorkspaceContext = {
|
||||||
organizationId: string
|
organizationId: string
|
||||||
|
templateName: string
|
||||||
templates?: Template[]
|
templates?: Template[]
|
||||||
selectedTemplate?: Template
|
selectedTemplate?: Template
|
||||||
templateSchema?: ParameterSchema[]
|
templateSchema?: ParameterSchema[]
|
||||||
createWorkspaceRequest?: CreateWorkspaceRequest
|
createWorkspaceRequest?: CreateWorkspaceRequest
|
||||||
createdWorkspace?: Workspace
|
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 CreateWorkspaceEvent = {
|
||||||
| {
|
type: "CREATE_WORKSPACE"
|
||||||
type: "SELECT_TEMPLATE"
|
request: CreateWorkspaceRequest
|
||||||
template: Template
|
}
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "CREATE_WORKSPACE"
|
|
||||||
request: CreateWorkspaceRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createWorkspaceMachine = createMachine(
|
export const createWorkspaceMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -45,12 +37,6 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
|
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
|
||||||
on: {
|
|
||||||
SELECT_TEMPLATE: {
|
|
||||||
actions: ["assignSelectedTemplate"],
|
|
||||||
target: "gettingTemplateSchema",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
states: {
|
states: {
|
||||||
gettingTemplates: {
|
gettingTemplates: {
|
||||||
invoke: {
|
invoke: {
|
||||||
@ -58,17 +44,11 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
onDone: [
|
onDone: [
|
||||||
{
|
{
|
||||||
actions: ["assignTemplates"],
|
actions: ["assignTemplates"],
|
||||||
target: "waitingForTemplateGetCreated",
|
|
||||||
cond: "areTemplatesEmpty",
|
cond: "areTemplatesEmpty",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
actions: ["assignTemplates", "assignPreSelectedTemplate"],
|
actions: ["assignTemplates", "assignSelectedTemplate"],
|
||||||
target: "gettingTemplateSchema",
|
target: "gettingTemplateSchema",
|
||||||
cond: "hasValidPreSelectedTemplate",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actions: ["assignTemplates"],
|
|
||||||
target: "selectingTemplate",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onError: {
|
onError: {
|
||||||
@ -76,33 +56,6 @@ 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: {
|
|
||||||
actions: ["assignSelectedTemplate"],
|
|
||||||
target: "gettingTemplateSchema",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gettingTemplateSchema: {
|
gettingTemplateSchema: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: "getTemplateSchema",
|
src: "getTemplateSchema",
|
||||||
@ -164,13 +117,6 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
guards: {
|
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,
|
areTemplatesEmpty: (_, event) => event.data.length === 0,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -178,7 +124,10 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
templates: (_, event) => event.data,
|
templates: (_, event) => event.data,
|
||||||
}),
|
}),
|
||||||
assignSelectedTemplate: assign({
|
assignSelectedTemplate: assign({
|
||||||
selectedTemplate: (_, event) => event.template,
|
selectedTemplate: (ctx, event) => {
|
||||||
|
const templates = event.data.filter((template) => template.name === ctx.templateName)
|
||||||
|
return templates.length ? templates[0] : undefined
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
assignTemplateSchema: assign({
|
assignTemplateSchema: assign({
|
||||||
// Only show parameters that are allowed to be overridden.
|
// Only show parameters that are allowed to be overridden.
|
||||||
@ -188,17 +137,6 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
assignCreateWorkspaceRequest: assign({
|
assignCreateWorkspaceRequest: assign({
|
||||||
createWorkspaceRequest: (_, event) => event.request,
|
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
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user