Skip to content
Merged
Prev Previous commit
Next Next commit
Add base change version flow
  • Loading branch information
BrunoQuaresma committed Nov 23, 2022
commit db02cc016d4e002cd6c0db351b8e2327d614a824
13 changes: 13 additions & 0 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
Expand Down Expand Up @@ -360,6 +363,7 @@ export const AppRouter: FC = () => {
</AuthAndFrame>
}
/>

<Route
path="schedule"
element={
Expand All @@ -386,6 +390,15 @@ export const AppRouter: FC = () => {
</AuthAndFrame>
}
/>

<Route
path="change-version"
element={
<RequireAuth>
<WorkspaceChangeVersionPage />
</RequireAuth>
}
/>
</Route>
</Route>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import MenuItem from "@material-ui/core/MenuItem"
import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"
import { useMachine } from "@xstate/react"
import { Template, TemplateVersion, Workspace } from "api/typesGenerated"
import { FormFooter } from "components/FormFooter/FormFooter"
import { FullPageForm } from "components/FullPageForm/FullPageForm"
import { Loader } from "components/Loader/Loader"
import { Pill } from "components/Pill/Pill"
import { Stack } from "components/Stack/Stack"
import { useFormik } from "formik"
import { FC } from "react"
import { useNavigate, useParams } from "react-router-dom"
import { createDayString } from "util/createDayString"
import { changeWorkspaceVersionMachine } from "xServices/workspace/changeWorkspaceVersionXService"

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 formik = useFormik({
initialValues: {
versionId: workspace.latest_build.template_version_id,
},
onSubmit: ({ versionId }) => onSubmit(versionId),
})

return (
<form onSubmit={formik.handleSubmit}>
<Stack direction="column" spacing={3}>
<Stack
direction="row"
spacing={2}
className={styles.workspace}
alignItems="center"
>
<div className={styles.workspaceIcon}>
<img src={workspace.template_icon} alt="" />
</div>
<Stack direction="column" spacing={0.5}>
<span className={styles.workspaceName}>{workspace.name}</span>

<span className={styles.workspaceDescription}>
{workspace.template_display_name.length > 0
? workspace.template_display_name
: workspace.template_name}
</span>
</Stack>
</Stack>

<TextField
select
label="Workspace version"
variant="outlined"
SelectProps={{
renderValue: (versionId: unknown) => {
const version = versions.find(
(version) => version.id === versionId,
)
if (!version) {
throw new Error(`${versionId} not found.`)
}
return <>{version.name}</>
},
}}
{...formik.getFieldProps("versionId")}
>
{versions
.slice()
.reverse()
.map((version) => (
<MenuItem
key={version.id}
value={version.id}
className={styles.menuItem}
>
<div>
<div>{version.name}</div>
<div className={styles.versionDescription}>
Created by {version.created_by.username}{" "}
{createDayString(version.created_at)}
</div>
</div>

{template.active_version_id === version.id && (
<Pill
type="success"
text="Active"
className={styles.activePill}
/>
)}
</MenuItem>
))}
</TextField>
</Stack>

<FormFooter
onCancel={onCancel}
isLoading={isLoading}
submitLabel="Update version"
/>
</form>
)
}

export const WorkspaceChangeVersionPage: FC = () => {
const navigate = useNavigate()
const { username: owner, workspace: workspaceName } = useParams() as {
username: string
workspace: string
}
const [state, send] = useMachine(changeWorkspaceVersionMachine, {
context: {
owner,
workspaceName,
},
actions: {
onUpdateVersion: () => {
navigate(-1)
},
},
})
const { workspace, templateVersions, template } = state.context

return (
<FullPageForm title="Change version" onCancel={() => navigate(-1)}>
{workspace && template && templateVersions ? (
<WorkspaceChangeVersionForm
isLoading={state.matches("updatingVersion")}
versions={templateVersions}
workspace={workspace}
template={template}
onSubmit={(versionId) => {
send({
type: "UPDATE_VERSION",
versionId,
})
}}
onCancel={() => {
navigate(-1)
}}
/>
) : (
<Loader />
)}
</FullPageForm>
)
}

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: 16,
},

workspaceDescription: {
fontSize: 14,
color: theme.palette.text.secondary,
},

workspaceIcon: {
width: theme.spacing(5),
lineHeight: 1,

"& img": {
width: "100%",
},
},

menuItem: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
position: "relative",
},

versionDescription: {
fontSize: 12,
color: theme.palette.text.secondary,
},

activePill: {
position: "absolute",
top: theme.spacing(2),
right: theme.spacing(2),
},
}))

export default WorkspaceChangeVersionPage
2 changes: 1 addition & 1 deletion site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const WorkspaceReadyPage = ({
handleDelete={() => workspaceSend({ type: "ASK_DELETE" })}
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
handleCancel={() => workspaceSend({ type: "CANCEL" })}
handleChangeVersion={() => navigate("/change-version")}
handleChangeVersion={() => navigate("change-version")}
resources={workspace.latest_build.resources}
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
Expand Down
142 changes: 142 additions & 0 deletions site/src/xServices/workspace/changeWorkspaceVersionXService.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
getTemplate,
getTemplateVersions,
getWorkspaceByOwnerAndName,
startWorkspace,
} from "api/api"
import {
Template,
TemplateVersion,
Workspace,
WorkspaceBuild,
} from "api/typesGenerated"
import { assign, createMachine } from "xstate"

interface ChangeWorkspaceVersionSchema {
context: {
owner: string
workspaceName: string
workspace?: Workspace
template?: Template
templateVersions?: TemplateVersion[]
error?: unknown
}

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,
}),
},
},
)