Skip to content

Commit fcd610e

Browse files
refactor: Update create workspace flow to allow creation from the workspaces page (#1684)
1 parent 5f8d0e5 commit fcd610e

File tree

11 files changed

+366
-260
lines changed

11 files changed

+366
-260
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
"drpcserver",
1616
"Dsts",
1717
"fatih",
18+
"Formik",
1819
"goarch",
1920
"gographviz",
2021
"goleak",
2122
"gossh",
2223
"gsyslog",
2324
"hashicorp",
2425
"hclsyntax",
26+
"httpapi",
2527
"httpmw",
2628
"idtoken",
2729
"Iflag",
@@ -63,6 +65,7 @@
6365
"tfjson",
6466
"tfstate",
6567
"trimprefix",
68+
"typegen",
6669
"unconvert",
6770
"Untar",
6871
"VMID",

site/src/AppRouter.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ export const AppRouter: React.FC = () => (
5656
</AuthAndFrame>
5757
}
5858
/>
59+
60+
<Route
61+
path="new"
62+
element={
63+
<RequireAuth>
64+
<CreateWorkspacePage />
65+
</RequireAuth>
66+
}
67+
/>
68+
5969
<Route path=":workspace">
6070
<Route
6171
index
@@ -85,17 +95,6 @@ export const AppRouter: React.FC = () => (
8595
</AuthAndFrame>
8696
}
8797
/>
88-
89-
<Route path=":template">
90-
<Route
91-
path="new"
92-
element={
93-
<RequireAuth>
94-
<CreateWorkspacePage />
95-
</RequireAuth>
96-
}
97-
/>
98-
</Route>
9998
</Route>
10099

101100
<Route path="users">

site/src/components/FormFooter/FormFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const FormFooter: React.FC<FormFooterProps> = ({
3535
const styles = useStyles()
3636
return (
3737
<div className={styles.footer}>
38-
<Button className={styles.button} onClick={onCancel} variant="outlined">
38+
<Button type="button" className={styles.button} onClick={onCancel} variant="outlined">
3939
{Language.cancelLabel}
4040
</Button>
4141
<LoadingButton loading={isLoading} className={styles.button} variant="contained" color="primary" type="submit">

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import { reach, StringSchema } from "yup"
55
import * as API from "../../api/api"
66
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
77
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
8-
import { history, render } from "../../testHelpers/renderHelpers"
8+
import { renderWithAuth } from "../../testHelpers/renderHelpers"
99
import CreateWorkspacePage from "./CreateWorkspacePage"
1010
import { Language, validationSchema } from "./CreateWorkspacePageView"
1111

12+
const renderCreateWorkspacePage = () => {
13+
return renderWithAuth(<CreateWorkspacePage />, {
14+
route: "/workspaces/new?template=" + MockTemplate.name,
15+
path: "/workspaces/new",
16+
})
17+
}
18+
1219
const fillForm = async ({ name = "example" }: { name?: string }) => {
1320
const nameField = await screen.findByLabelText(Language.nameLabel)
1421
await userEvent.type(nameField, name)
@@ -19,25 +26,21 @@ const fillForm = async ({ name = "example" }: { name?: string }) => {
1926
const nameSchema = reach(validationSchema, "name") as StringSchema
2027

2128
describe("CreateWorkspacePage", () => {
22-
beforeEach(() => {
23-
history.replace("/templates/" + MockTemplate.name + "/new")
24-
})
25-
2629
it("renders", async () => {
27-
render(<CreateWorkspacePage />)
30+
renderCreateWorkspacePage()
2831
const element = await screen.findByText("Create workspace")
2932
expect(element).toBeDefined()
3033
})
3134

3235
it("shows validation error message", async () => {
33-
render(<CreateWorkspacePage />)
36+
renderCreateWorkspacePage()
3437
await fillForm({ name: "$$$" })
3538
const errorMessage = await screen.findByText(Language.nameMatches)
3639
expect(errorMessage).toBeDefined()
3740
})
3841

3942
it("succeeds", async () => {
40-
render(<CreateWorkspacePage />)
43+
renderCreateWorkspacePage()
4144
// You have to spy the method before it is used.
4245
jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace)
4346
await fillForm({ name: "test" })

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,59 @@
1-
import { useMachine } from "@xstate/react"
2-
import React from "react"
3-
import { useNavigate } from "react-router"
4-
import { useParams } from "react-router-dom"
5-
import { createWorkspace } from "../../api/api"
6-
import { templateMachine } from "../../xServices/template/templateXService"
1+
import { useActor, useMachine } from "@xstate/react"
2+
import React, { useContext } from "react"
3+
import { useNavigate, useSearchParams } from "react-router-dom"
4+
import { Template } from "../../api/typesGenerated"
5+
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
6+
import { XServiceContext } from "../../xServices/StateContext"
77
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
88

9+
const useOrganizationId = () => {
10+
const xServices = useContext(XServiceContext)
11+
const [authState] = useActor(xServices.authXService)
12+
const organizationId = authState.context.me?.organization_ids[0]
13+
14+
if (!organizationId) {
15+
throw new Error("No organization ID found")
16+
}
17+
18+
return organizationId
19+
}
20+
921
const CreateWorkspacePage: React.FC = () => {
10-
const { template } = useParams()
11-
const [templateState] = useMachine(templateMachine, {
12-
context: {
13-
name: template,
22+
const organizationId = useOrganizationId()
23+
const [searchParams] = useSearchParams()
24+
const preSelectedTemplateName = searchParams.get("template")
25+
const navigate = useNavigate()
26+
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
27+
context: { organizationId, preSelectedTemplateName },
28+
actions: {
29+
onCreateWorkspace: (_, event) => {
30+
navigate("/workspaces/" + event.data.id)
31+
},
1432
},
1533
})
16-
const navigate = useNavigate()
17-
const loading = templateState.hasTag("loading")
18-
if (!templateState.context.template || !templateState.context.templateSchema) {
19-
return null
20-
}
2134

2235
return (
2336
<CreateWorkspacePageView
24-
template={templateState.context.template}
25-
templateSchema={templateState.context.templateSchema}
26-
loading={loading}
27-
onCancel={() => navigate("/templates")}
28-
onSubmit={async (req) => {
29-
if (!templateState.context.template) {
30-
throw new Error("template isn't valid")
31-
}
32-
const workspace = await createWorkspace(templateState.context.template.organization_id, req)
33-
navigate("/workspaces/" + workspace.id)
37+
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
38+
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
39+
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
40+
templates={createWorkspaceState.context.templates}
41+
selectedTemplate={createWorkspaceState.context.selectedTemplate}
42+
templateSchema={createWorkspaceState.context.templateSchema}
43+
onCancel={() => {
44+
navigate(preSelectedTemplateName ? "/templates" : "/workspaces")
45+
}}
46+
onSubmit={(request) => {
47+
send({
48+
type: "CREATE_WORKSPACE",
49+
request,
50+
})
51+
}}
52+
onSelectTemplate={(template: Template) => {
53+
send({
54+
type: "SELECT_TEMPLATE",
55+
template,
56+
})
3457
}}
3558
/>
3659
)

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
import { ComponentMeta, Story } from "@storybook/react"
22
import React from "react"
3-
import { createParameterSchema } from "../../components/ParameterInput/ParameterInput.stories"
3+
import { ParameterSchema } from "../../api/typesGenerated"
44
import { MockTemplate } from "../../testHelpers/entities"
55
import { CreateWorkspacePageView, CreateWorkspacePageViewProps } from "./CreateWorkspacePageView"
66

7+
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
8+
return {
9+
id: "000000",
10+
job_id: "000000",
11+
allow_override_destination: false,
12+
allow_override_source: true,
13+
created_at: "",
14+
default_destination_scheme: "none",
15+
default_refresh: "",
16+
default_source_scheme: "data",
17+
default_source_value: "default-value",
18+
name: "parameter name",
19+
description: "Some description!",
20+
redisplay_value: false,
21+
validation_condition: "",
22+
validation_contains: [],
23+
validation_error: "",
24+
validation_type_system: "",
25+
validation_value_type: "",
26+
...partial,
27+
}
28+
}
29+
730
export default {
831
title: "pages/CreateWorkspacePageView",
932
component: CreateWorkspacePageView,
@@ -13,13 +36,15 @@ const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspace
1336

1437
export const NoParameters = Template.bind({})
1538
NoParameters.args = {
16-
template: MockTemplate,
39+
templates: [MockTemplate],
40+
selectedTemplate: MockTemplate,
1741
templateSchema: [],
1842
}
1943

2044
export const Parameters = Template.bind({})
2145
Parameters.args = {
22-
template: MockTemplate,
46+
templates: [MockTemplate],
47+
selectedTemplate: MockTemplate,
2348
templateSchema: [
2449
createParameterSchema({
2550
name: "region",

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy