diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 3159ad8d44324..1b1cf04006a98 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -46,6 +46,9 @@ const WorkspaceBuildPage = lazy( () => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"), ) const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage")) +const WorkspaceChangeVersionPage = lazy( + () => import("./pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage"), +) const WorkspaceSchedulePage = lazy( () => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"), ) @@ -360,6 +363,7 @@ export const AppRouter: FC = () => { } /> + { } /> + + + + + } + /> diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx index 0186797a9703d..1ccbb82ed2b72 100644 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ b/site/src/components/DropdownButton/ActionCtas.tsx @@ -3,6 +3,7 @@ import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" import BlockIcon from "@material-ui/icons/Block" import CloudQueueIcon from "@material-ui/icons/CloudQueue" +import UpdateOutlined from "@material-ui/icons/UpdateOutlined" import CropSquareIcon from "@material-ui/icons/CropSquare" import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" @@ -33,6 +34,23 @@ export const UpdateButton: FC> = ({ ) } +export const ChangeVersionButton: FC< + React.PropsWithChildren +> = ({ handleAction }) => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + + ) +} + export const StartButton: FC> = ({ handleAction, }) => { diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index edd59eb123669..0634f10a07a4e 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -45,6 +45,7 @@ export interface WorkspaceProps { handleDelete: () => void handleUpdate: () => void handleCancel: () => void + handleChangeVersion: () => void isUpdating: boolean workspace: TypesGen.Workspace resources?: TypesGen.WorkspaceResource[] @@ -68,6 +69,7 @@ export const Workspace: FC> = ({ handleDelete, handleUpdate, handleCancel, + handleChangeVersion, workspace, isUpdating, resources, @@ -143,6 +145,7 @@ export const Workspace: FC> = ({ handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} + handleChangeVersion={handleChangeVersion} isUpdating={isUpdating} /> diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index e8d4368f03053..27d59496fbf06 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -18,6 +18,7 @@ const renderComponent = async (props: Partial = {}) => { handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} + handleChangeVersion={jest.fn()} isUpdating={false} />, ) @@ -35,6 +36,7 @@ const renderAndClick = async (props: Partial = {}) => { handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} + handleChangeVersion={jest.fn()} isUpdating={false} />, ) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 503ae6e7947d6..0f5064fe82229 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next" import { WorkspaceStatus } from "../../api/typesGenerated" import { ActionLoadingButton, + ChangeVersionButton, DeleteButton, DisabledButton, StartButton, @@ -20,6 +21,7 @@ export interface WorkspaceActionsProps { handleDelete: () => void handleUpdate: () => void handleCancel: () => void + handleChangeVersion: () => void isUpdating: boolean children?: ReactNode } @@ -32,6 +34,7 @@ export const WorkspaceActions: FC = ({ handleDelete, handleUpdate, handleCancel, + handleChangeVersion, isUpdating, }) => { const { t } = useTranslation("workspacePage") @@ -45,6 +48,9 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.updating]: ( ), + [ButtonTypesEnum.changeVersion]: ( + + ), [ButtonTypesEnum.start]: , [ButtonTypesEnum.starting]: ( diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index fb3cb13dd0e5c..5d5c59434d48b 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -11,6 +11,7 @@ export enum ButtonTypesEnum { deleting = "deleting", update = "update", updating = "updating", + changeVersion = "changeVersion", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -34,7 +35,11 @@ export const statusToAbilities: Record = { canAcceptJobs: false, }, running: { - actions: [ButtonTypesEnum.stop, ButtonTypesEnum.delete], + actions: [ + ButtonTypesEnum.stop, + ButtonTypesEnum.changeVersion, + ButtonTypesEnum.delete, + ], canCancel: false, canAcceptJobs: true, }, @@ -44,7 +49,11 @@ export const statusToAbilities: Record = { canAcceptJobs: false, }, stopped: { - actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete], + actions: [ + ButtonTypesEnum.start, + ButtonTypesEnum.changeVersion, + ButtonTypesEnum.delete, + ], canCancel: false, canAcceptJobs: true, }, @@ -52,6 +61,7 @@ export const statusToAbilities: Record = { actions: [ ButtonTypesEnum.start, ButtonTypesEnum.stop, + ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, ], canCancel: false, @@ -59,7 +69,11 @@ export const statusToAbilities: Record = { }, // in the case of an error failed: { - actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete], + actions: [ + ButtonTypesEnum.start, + ButtonTypesEnum.changeVersion, + ButtonTypesEnum.delete, + ], canCancel: false, canAcceptJobs: true, }, diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index e37f8c6337760..a8146f03aa316 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -10,6 +10,7 @@ import workspacesPage from "./workspacesPage.json" import usersPage from "./usersPage.json" import templateVersionPage from "./templateVersionPage.json" import loginPage from "./loginPage.json" +import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json" export const en = { common, @@ -24,4 +25,5 @@ export const en = { usersPage, templateVersionPage, loginPage, + workspaceChangeVersionPage, } diff --git a/site/src/i18n/en/workspaceChangeVersionPage.json b/site/src/i18n/en/workspaceChangeVersionPage.json new file mode 100644 index 0000000000000..08e58705655ec --- /dev/null +++ b/site/src/i18n/en/workspaceChangeVersionPage.json @@ -0,0 +1,9 @@ +{ + "title": "", + "labels": { + "workspaceVersion": "Workspace version", + "submit": "Update version", + "createdBy": "Created by", + "active": "Active" + } +} diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index 1c486a3275855..2ee997dccb439 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -27,7 +27,8 @@ "updating": "Updating", "starting": "Starting...", "stopping": "Stopping...", - "deleting": "Deleting..." + "deleting": "Deleting...", + "changeVersion": "Change version" }, "disabledButton": { "canceling": "Canceling", diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx new file mode 100644 index 0000000000000..bcca66b5a9364 --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionForm.tsx @@ -0,0 +1,155 @@ +import { makeStyles } from "@material-ui/core/styles" +import TextField from "@material-ui/core/TextField" +import Autocomplete from "@material-ui/lab/Autocomplete" +import { Template, TemplateVersion, Workspace } from "api/typesGenerated" +import { FormFooter } from "components/FormFooter/FormFooter" +import { Pill } from "components/Pill/Pill" +import { Stack } from "components/Stack/Stack" +import { useFormik } from "formik" +import { FC } from "react" +import { useTranslation } from "react-i18next" +import { createDayString } from "util/createDayString" +import * as Yup from "yup" + +const validationSchema = Yup.object({ + versionId: Yup.string().required(), +}) + +export const WorkspaceChangeVersionForm: FC<{ + isLoading: boolean + workspace: Workspace + template: Template + versions: TemplateVersion[] + onSubmit: (versionId: string) => void + onCancel: () => void +}> = ({ isLoading, workspace, template, versions, onSubmit, onCancel }) => { + const styles = useStyles() + const { t } = useTranslation("workspaceChangeVersionPage") + const formik = useFormik({ + initialValues: { + versionId: workspace.latest_build.template_version_id, + }, + validationSchema, + onSubmit: ({ versionId }) => onSubmit(versionId), + }) + const autocompleteValue = versions.find( + (version) => version.id === formik.values.versionId, + ) + + return ( +
+ + +
+ +
+ + {workspace.name} + + + {workspace.template_display_name.length > 0 + ? workspace.template_display_name + : workspace.template_name} + + +
+ + { + if (value) { + await formik.setFieldValue("versionId", value.id) + } + }} + renderInput={(params) => ( + + )} + getOptionLabel={(version: TemplateVersion) => version.name} + renderOption={(version: TemplateVersion) => ( +
+
+
{version.name}
+
+ {t("labels.createdBy")} {version.created_by.username}{" "} + {createDayString(version.created_at)} +
+
+ + {template.active_version_id === version.id && ( + + )} +
+ )} + /> +
+ + + + ) +} + +const useStyles = makeStyles((theme) => ({ + workspace: { + padding: theme.spacing(2.5, 3), + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + }, + + workspaceName: { + fontSize: theme.spacing(2), + }, + + workspaceDescription: { + fontSize: theme.spacing(1.75), + color: theme.palette.text.secondary, + }, + + workspaceIcon: { + width: theme.spacing(5), + lineHeight: 1, + + "& img": { + width: "100%", + }, + }, + + menuItem: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + position: "relative", + width: "100%", + }, + + versionDescription: { + fontSize: theme.spacing(1.5), + color: theme.palette.text.secondary, + }, + + activePill: { + position: "absolute", + top: theme.spacing(2), + right: theme.spacing(2), + }, +})) diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx new file mode 100644 index 0000000000000..9d398dce6d3bd --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.test.tsx @@ -0,0 +1,64 @@ +import { + MockTemplateVersion2, + MockUser, + MockWorkspace, + renderWithAuth, + waitForLoaderToBeRemoved, +} from "testHelpers/renderHelpers" +import WorkspaceChangeVersionPage from "./WorkspaceChangeVersionPage" +import { screen, waitFor } from "@testing-library/react" +import * as API from "api/api" +import userEvent from "@testing-library/user-event" +import * as CreateDayString from "util/createDayString" +import i18next from "i18next" + +const t = (path: string) => { + return i18next.t(path, { ns: "workspaceChangeVersionPage" }) +} + +const renderPage = async () => { + renderWithAuth(, { + path: "/@:username/:workspace/change-version", + route: `/@${MockUser.username}/${MockWorkspace.name}/change-version`, + }) + await waitForLoaderToBeRemoved() +} + +describe("WorkspaceChangeVersionPage", () => { + beforeEach(() => { + jest + .spyOn(CreateDayString, "createDayString") + .mockImplementation(() => "a minute ago") + }) + + it("sends the update request with the right version", async () => { + const user = userEvent.setup() + const updateSpy = jest.spyOn(API, "startWorkspace") + await renderPage() + + // Type the version name and select it + const autocompleteInput = screen.getByLabelText( + t("labels.workspaceVersion"), + ) + await user.clear(autocompleteInput) + await user.type(autocompleteInput, MockTemplateVersion2.name) + const newOption = screen.getByRole("option", { + // Using RegExp so we can match a substring + name: new RegExp(MockTemplateVersion2.name), + }) + await user.click(newOption) + + // Submit the form + const submitButton = screen.getByRole("button", { + name: t("labels.submit"), + }) + await user.click(submitButton) + + await waitFor(() => { + expect(updateSpy).toBeCalledWith( + MockWorkspace.id, + MockTemplateVersion2.id, + ) + }) + }) +}) diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx new file mode 100644 index 0000000000000..14a1ebfdf4e07 --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPage.tsx @@ -0,0 +1,48 @@ +import { useMachine } from "@xstate/react" +import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" +import { useNavigate, useParams } from "react-router-dom" +import { changeWorkspaceVersionMachine } from "xServices/workspace/changeWorkspaceVersionXService" +import { WorkspaceChangeVersionPageView } from "./WorkspaceChangeVersionPageView" + +export const WorkspaceChangeVersionPage: FC = () => { + const navigate = useNavigate() + const { t } = useTranslation("workspaceChangeVersionPage") + const { username: owner, workspace: workspaceName } = useParams() as { + username: string + workspace: string + } + const [state, send] = useMachine(changeWorkspaceVersionMachine, { + context: { + owner, + workspaceName, + }, + actions: { + onUpdateVersion: () => { + navigate(-1) + }, + }, + }) + + return ( + <> + + Codestin Search App + + + { + send({ + type: "UPDATE_VERSION", + versionId, + }) + }} + /> + + ) +} + +export default WorkspaceChangeVersionPage diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx new file mode 100644 index 0000000000000..ed6cbb279fa93 --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.stories.tsx @@ -0,0 +1,51 @@ +import { action } from "@storybook/addon-actions" +import { ComponentMeta, Story } from "@storybook/react" +import { + makeMockApiError, + MockTemplate, + MockTemplateVersion, + MockTemplateVersion2, + MockUser, + MockWorkspace, +} from "testHelpers/entities" +import { + WorkspaceChangeVersionPageView, + WorkspaceChangeVersionPageViewProps, +} from "./WorkspaceChangeVersionPageView" + +export default { + title: "pages/WorkspaceChangeVersionPageView", + component: WorkspaceChangeVersionPageView, +} as ComponentMeta + +const Template: Story = (args) => ( + +) + +export const Example = Template.bind({}) +Example.args = { + isUpdating: false, + onSubmit: action("submit"), + context: { + error: undefined, + owner: MockUser.username, + workspaceName: MockWorkspace.name, + template: MockTemplate, + templateVersions: [MockTemplateVersion2, MockTemplateVersion], + workspace: MockWorkspace, + }, +} + +export const Error = Template.bind({}) +Error.args = { + isUpdating: false, + onSubmit: action("submit"), + context: { + error: makeMockApiError({ message: "Error on updating the version." }), + owner: MockUser.username, + workspaceName: MockWorkspace.name, + template: MockTemplate, + templateVersions: [MockTemplateVersion2, MockTemplateVersion], + workspace: MockWorkspace, + }, +} diff --git a/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx new file mode 100644 index 0000000000000..0d0511d67be5a --- /dev/null +++ b/site/src/pages/WorkspaceChangeVersionPage/WorkspaceChangeVersionPageView.tsx @@ -0,0 +1,46 @@ +import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { Maybe } from "components/Conditionals/Maybe" +import { FullPageForm } from "components/FullPageForm/FullPageForm" +import { Loader } from "components/Loader/Loader" +import { Stack } from "components/Stack/Stack" +import { FC } from "react" +import { useNavigate } from "react-router-dom" +import { ChangeWorkspaceVersionContext } from "xServices/workspace/changeWorkspaceVersionXService" +import { WorkspaceChangeVersionForm } from "./WorkspaceChangeVersionForm" + +export interface WorkspaceChangeVersionPageViewProps { + isUpdating: boolean + context: ChangeWorkspaceVersionContext + onSubmit: (versionId: string) => void +} + +export const WorkspaceChangeVersionPageView: FC< + WorkspaceChangeVersionPageViewProps +> = ({ context, onSubmit, isUpdating }) => { + const navigate = useNavigate() + const { workspace, templateVersions, template, error } = context + + return ( + navigate(-1)}> + + + + + {workspace && template && templateVersions ? ( + { + navigate(-1) + }} + /> + ) : ( + + )} + + + ) +} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ec0c96b402ca8..ea87ad679a8c4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -4,6 +4,7 @@ import dayjs from "dayjs" import { useContext } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" import { getMaxDeadline, getMaxDeadlineChange, @@ -63,6 +64,7 @@ export const WorkspaceReadyPage = ({ const canUpdateWorkspace = Boolean(permissions?.updateWorkspace) const { t } = useTranslation("workspacePage") const favicon = getFaviconByStatus(workspace.latest_build) + const navigate = useNavigate() return ( <> @@ -114,6 +116,7 @@ export const WorkspaceReadyPage = ({ handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleUpdate={() => workspaceSend({ type: "UPDATE" })} handleCancel={() => workspaceSend({ type: "CANCEL" })} + handleChangeVersion={() => navigate("change-version")} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 482f6e1d47b29..327b274798578 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -181,6 +181,23 @@ You can add instructions here created_by: MockUser, } +export const MockTemplateVersion2: TypesGen.TemplateVersion = { + id: "test-template-version-2", + created_at: "2022-05-17T17:39:01.382927298Z", + updated_at: "2022-05-17T17:39:01.382927298Z", + template_id: "test-template", + job: MockProvisionerJob, + name: "test-version-2", + readme: `--- +name:Template test 2 +--- +## Instructions +You can add instructions here + +[Some link info](https://coder.com)`, + created_by: MockUser, +} + export const MockTemplate: TypesGen.Template = { id: "test-template", created_at: "2022-05-17T17:39:01.382927298Z", @@ -383,7 +400,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { initiator_id: MockUser.id, initiator_name: MockUser.username, job: MockProvisionerJob, - template_version_id: "", + template_version_id: MockTemplateVersion.id, transition: "start", updated_at: "2022-05-17T17:39:01.382927298Z", workspace_name: "test-workspace", @@ -406,7 +423,7 @@ export const MockFailedWorkspaceBuild = ( initiator_id: MockUser.id, initiator_name: MockUser.username, job: MockFailedProvisionerJob, - template_version_id: "", + template_version_id: MockTemplateVersion.id, transition: transition, updated_at: "2022-05-17T17:39:01.382927298Z", workspace_name: "test-workspace", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 30e45fa727a2b..32382402d64f7 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -37,7 +37,10 @@ export const handlers = [ return res(ctx.status(200), ctx.json(M.MockTemplate)) }), rest.get("/api/v2/templates/:templateId/versions", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockTemplateVersion])) + return res( + ctx.status(200), + ctx.json([M.MockTemplateVersion2, M.MockTemplateVersion]), + ) }), rest.patch("/api/v2/templates/:templateId", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockTemplate)) diff --git a/site/src/xServices/workspace/changeWorkspaceVersionXService.ts b/site/src/xServices/workspace/changeWorkspaceVersionXService.ts new file mode 100644 index 0000000000000..4031287453c5b --- /dev/null +++ b/site/src/xServices/workspace/changeWorkspaceVersionXService.ts @@ -0,0 +1,144 @@ +import { + getTemplate, + getTemplateVersions, + getWorkspaceByOwnerAndName, + startWorkspace, +} from "api/api" +import { + Template, + TemplateVersion, + Workspace, + WorkspaceBuild, +} from "api/typesGenerated" +import { assign, createMachine } from "xstate" + +export interface ChangeWorkspaceVersionContext { + owner: string + workspaceName: string + workspace?: Workspace + template?: Template + templateVersions?: TemplateVersion[] + error?: unknown +} + +interface ChangeWorkspaceVersionSchema { + context: ChangeWorkspaceVersionContext + + services: { + getWorkspace: { + data: Workspace + } + getTemplateData: { + data: { + template: Template + versions: TemplateVersion[] + } + } + updateVersion: { + data: WorkspaceBuild + } + } + + events: { + type: "UPDATE_VERSION" + versionId: string + } +} + +export const changeWorkspaceVersionMachine = createMachine( + { + id: "changeWorkspaceVersion", + predictableActionArguments: true, + schema: {} as ChangeWorkspaceVersionSchema, + tsTypes: {} as import("./changeWorkspaceVersionXService.typegen").Typegen0, + initial: "loadingWorkspace", + states: { + loadingWorkspace: { + invoke: { + src: "getWorkspace", + onDone: { + target: "loadingTemplateData", + actions: "assignWorkspace", + }, + onError: { + target: "idle", + actions: "assignError", + }, + }, + }, + loadingTemplateData: { + invoke: { + src: "getTemplateData", + onDone: { + target: "idle", + actions: "assignTemplateData", + }, + onError: { + target: "idle", + actions: "assignError", + }, + }, + }, + idle: { + on: { + UPDATE_VERSION: "updatingVersion", + }, + }, + updatingVersion: { + invoke: { + src: "updateVersion", + onDone: { + target: "idle", + actions: "onUpdateVersion", + }, + onError: { + target: "idle", + actions: "assignError", + }, + }, + }, + }, + }, + { + services: { + getWorkspace: ({ owner, workspaceName }) => + getWorkspaceByOwnerAndName(owner, workspaceName), + + getTemplateData: async ({ workspace }) => { + if (!workspace) { + throw new Error("Workspace not defined.") + } + + const [template, versions] = await Promise.all([ + getTemplate(workspace.template_id), + getTemplateVersions(workspace.template_id), + ]) + + return { template, versions } + }, + + updateVersion: ({ workspace }, { versionId }) => { + if (!workspace) { + throw new Error("Workspace not defined.") + } + + return startWorkspace(workspace.id, versionId) + }, + }, + + actions: { + assignError: assign({ + error: (_, { data }) => data, + }), + + assignWorkspace: assign({ + workspace: (_, { data }) => data, + }), + + assignTemplateData: assign({ + template: (_, { data }) => data.template, + templateVersions: (_, { data }) => data.versions, + }), + }, + }, +)