mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
feat: Change workspace version using the UI (#5158)
This commit is contained in:
@@ -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 = () => {
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="schedule"
|
||||
element={
|
||||
@@ -386,6 +390,15 @@ export const AppRouter: FC = () => {
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="change-version"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<WorkspaceChangeVersionPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -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<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const ChangeVersionButton: FC<
|
||||
React.PropsWithChildren<WorkspaceAction>
|
||||
> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
startIcon={<UpdateOutlined />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
{t("actionButton.changeVersion")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
|
||||
@@ -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<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleCancel,
|
||||
handleChangeVersion,
|
||||
workspace,
|
||||
isUpdating,
|
||||
resources,
|
||||
@@ -143,6 +145,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
handleChangeVersion={handleChangeVersion}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -18,6 +18,7 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
handleDelete={jest.fn()}
|
||||
handleUpdate={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
handleChangeVersion={jest.fn()}
|
||||
isUpdating={false}
|
||||
/>,
|
||||
)
|
||||
@@ -35,6 +36,7 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
handleDelete={jest.fn()}
|
||||
handleUpdate={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
handleChangeVersion={jest.fn()}
|
||||
isUpdating={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
@@ -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<WorkspaceActionsProps> = ({
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleCancel,
|
||||
handleChangeVersion,
|
||||
isUpdating,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
@@ -45,6 +48,9 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
[ButtonTypesEnum.updating]: (
|
||||
<ActionLoadingButton label={t("actionButton.updating")} />
|
||||
),
|
||||
[ButtonTypesEnum.changeVersion]: (
|
||||
<ChangeVersionButton handleAction={handleChangeVersion} />
|
||||
),
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||
[ButtonTypesEnum.starting]: (
|
||||
<ActionLoadingButton label={t("actionButton.starting")} />
|
||||
|
||||
@@ -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<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
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<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
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<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
actions: [
|
||||
ButtonTypesEnum.start,
|
||||
ButtonTypesEnum.stop,
|
||||
ButtonTypesEnum.changeVersion,
|
||||
ButtonTypesEnum.delete,
|
||||
],
|
||||
canCancel: false,
|
||||
@@ -59,7 +69,11 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
},
|
||||
// in the case of an error
|
||||
failed: {
|
||||
actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete],
|
||||
actions: [
|
||||
ButtonTypesEnum.start,
|
||||
ButtonTypesEnum.changeVersion,
|
||||
ButtonTypesEnum.delete,
|
||||
],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "",
|
||||
"labels": {
|
||||
"workspaceVersion": "Workspace version",
|
||||
"submit": "Update version",
|
||||
"createdBy": "Created by",
|
||||
"active": "Active"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
"updating": "Updating",
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"deleting": "Deleting..."
|
||||
"deleting": "Deleting...",
|
||||
"changeVersion": "Change version"
|
||||
},
|
||||
"disabledButton": {
|
||||
"canceling": "Canceling",
|
||||
|
||||
@@ -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 (
|
||||
<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>
|
||||
|
||||
<Autocomplete
|
||||
id="workspaceVersion"
|
||||
disableClearable
|
||||
options={versions.slice().reverse()}
|
||||
value={autocompleteValue}
|
||||
onChange={async (_event, value) => {
|
||||
if (value) {
|
||||
await formik.setFieldValue("versionId", value.id)
|
||||
}
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t("labels.workspaceVersion")}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
getOptionLabel={(version: TemplateVersion) => version.name}
|
||||
renderOption={(version: TemplateVersion) => (
|
||||
<div className={styles.menuItem}>
|
||||
<div>
|
||||
<div>{version.name}</div>
|
||||
<div className={styles.versionDescription}>
|
||||
{t("labels.createdBy")} {version.created_by.username}{" "}
|
||||
{createDayString(version.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{template.active_version_id === version.id && (
|
||||
<Pill
|
||||
type="success"
|
||||
text={t("labels.active")}
|
||||
className={styles.activePill}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isLoading}
|
||||
submitLabel={t("labels.submit")}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
}))
|
||||
@@ -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(<WorkspaceChangeVersionPage />, {
|
||||
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,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`${t("title")} · ${workspaceName}`}</title>
|
||||
</Helmet>
|
||||
|
||||
<WorkspaceChangeVersionPageView
|
||||
isUpdating={state.matches("updatingVersion")}
|
||||
context={state.context}
|
||||
onSubmit={(versionId) => {
|
||||
send({
|
||||
type: "UPDATE_VERSION",
|
||||
versionId,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceChangeVersionPage
|
||||
@@ -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<typeof WorkspaceChangeVersionPageView>
|
||||
|
||||
const Template: Story<WorkspaceChangeVersionPageViewProps> = (args) => (
|
||||
<WorkspaceChangeVersionPageView {...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,
|
||||
},
|
||||
}
|
||||
@@ -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 (
|
||||
<FullPageForm title="Change version" onCancel={() => navigate(-1)}>
|
||||
<Stack>
|
||||
<Maybe condition={Boolean(error)}>
|
||||
<AlertBanner severity="error" error={error} />
|
||||
</Maybe>
|
||||
{workspace && template && templateVersions ? (
|
||||
<WorkspaceChangeVersionForm
|
||||
isLoading={isUpdating}
|
||||
versions={templateVersions}
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => {
|
||||
navigate(-1)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</Stack>
|
||||
</FullPageForm>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user