mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(site): Ask for parameter values when updating a workspace (#6586)
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import axios from "axios"
|
||||
import { getApiKey, getURLWithSearchParams, login, logout } from "./api"
|
||||
import {
|
||||
MockTemplate,
|
||||
MockTemplateVersionParameter1,
|
||||
MockWorkspace,
|
||||
MockWorkspaceBuild,
|
||||
} from "testHelpers/entities"
|
||||
import * as api from "./api"
|
||||
import * as TypesGen from "./typesGenerated"
|
||||
|
||||
describe("api.ts", () => {
|
||||
@@ -12,7 +18,7 @@ describe("api.ts", () => {
|
||||
jest.spyOn(axios, "post").mockResolvedValueOnce({ data: loginResponse })
|
||||
|
||||
// when
|
||||
const result = await login("test", "123")
|
||||
const result = await api.login("test", "123")
|
||||
|
||||
// then
|
||||
expect(axios.post).toHaveBeenCalled()
|
||||
@@ -33,7 +39,7 @@ describe("api.ts", () => {
|
||||
axios.post = axiosMockPost
|
||||
|
||||
try {
|
||||
await login("test", "123")
|
||||
await api.login("test", "123")
|
||||
} catch (error) {
|
||||
expect(error).toStrictEqual(expectedError)
|
||||
}
|
||||
@@ -49,7 +55,7 @@ describe("api.ts", () => {
|
||||
axios.post = axiosMockPost
|
||||
|
||||
// when
|
||||
await logout()
|
||||
await api.logout()
|
||||
|
||||
// then
|
||||
expect(axiosMockPost).toHaveBeenCalled()
|
||||
@@ -68,7 +74,7 @@ describe("api.ts", () => {
|
||||
axios.post = axiosMockPost
|
||||
|
||||
try {
|
||||
await logout()
|
||||
await api.logout()
|
||||
} catch (error) {
|
||||
expect(error).toStrictEqual(expectedError)
|
||||
}
|
||||
@@ -87,7 +93,7 @@ describe("api.ts", () => {
|
||||
axios.post = axiosMockPost
|
||||
|
||||
// when
|
||||
const result = await getApiKey()
|
||||
const result = await api.getApiKey()
|
||||
|
||||
// then
|
||||
expect(axiosMockPost).toHaveBeenCalled()
|
||||
@@ -107,7 +113,7 @@ describe("api.ts", () => {
|
||||
axios.post = axiosMockPost
|
||||
|
||||
try {
|
||||
await getApiKey()
|
||||
await api.getApiKey()
|
||||
} catch (error) {
|
||||
expect(error).toStrictEqual(expectedError)
|
||||
}
|
||||
@@ -133,7 +139,7 @@ describe("api.ts", () => {
|
||||
])(
|
||||
`Workspaces - getURLWithSearchParams(%p, %p) returns %p`,
|
||||
(basePath, filter, expected) => {
|
||||
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
|
||||
expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected)
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -150,8 +156,38 @@ describe("api.ts", () => {
|
||||
])(
|
||||
`Users - getURLWithSearchParams(%p, %p) returns %p`,
|
||||
(basePath, filter, expected) => {
|
||||
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
|
||||
expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("creates a build with start and the latest template", async () => {
|
||||
jest
|
||||
.spyOn(api, "postWorkspaceBuild")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
||||
await api.updateWorkspace(MockWorkspace)
|
||||
expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||
transition: "start",
|
||||
template_version_id: MockTemplate.active_version_id,
|
||||
rich_parameter_values: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("fails when having missing parameters", async () => {
|
||||
jest
|
||||
.spyOn(api, "postWorkspaceBuild")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
||||
jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValueOnce([])
|
||||
jest
|
||||
.spyOn(api, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||
|
||||
await expect(api.updateWorkspace(MockWorkspace)).rejects.toThrow(
|
||||
api.MissingBuildParameters,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -897,3 +897,82 @@ export const getWorkspaceBuildParameters = async (
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export class MissingBuildParameters extends Error {
|
||||
parameters: TypesGen.TemplateVersionParameter[] = []
|
||||
|
||||
constructor(parameters: TypesGen.TemplateVersionParameter[]) {
|
||||
super("Missing build parameters.")
|
||||
this.parameters = parameters
|
||||
}
|
||||
}
|
||||
|
||||
/** Steps to update the workspace
|
||||
* - Get the latest template to access the latest active version
|
||||
* - Get the current build parameters
|
||||
* - Get the template parameters
|
||||
* - Update the build parameters and check if there are missed parameters for the newest version
|
||||
* - If there are missing parameters raise an error
|
||||
* - Create a build with the latest version and updated build parameters
|
||||
*/
|
||||
export const updateWorkspace = async (
|
||||
workspace: TypesGen.Workspace,
|
||||
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
|
||||
): Promise<TypesGen.WorkspaceBuild> => {
|
||||
const [template, oldBuildParameters] = await Promise.all([
|
||||
getTemplate(workspace.template_id),
|
||||
getWorkspaceBuildParameters(workspace.latest_build.id),
|
||||
])
|
||||
const activeVersionId = template.active_version_id
|
||||
const templateParameters = await getTemplateVersionRichParameters(
|
||||
activeVersionId,
|
||||
)
|
||||
const [updatedBuildParameters, missingParameters] = updateBuildParameters(
|
||||
oldBuildParameters,
|
||||
newBuildParameters,
|
||||
templateParameters,
|
||||
)
|
||||
if (missingParameters.length > 0) {
|
||||
throw new MissingBuildParameters(missingParameters)
|
||||
}
|
||||
|
||||
return postWorkspaceBuild(workspace.id, {
|
||||
transition: "start",
|
||||
template_version_id: activeVersionId,
|
||||
rich_parameter_values: updatedBuildParameters,
|
||||
})
|
||||
}
|
||||
|
||||
const updateBuildParameters = (
|
||||
oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
|
||||
newBuildParameters: TypesGen.WorkspaceBuildParameter[],
|
||||
templateParameters: TypesGen.TemplateVersionParameter[],
|
||||
) => {
|
||||
const missingParameters: TypesGen.TemplateVersionParameter[] = []
|
||||
const updatedBuildParameters: TypesGen.WorkspaceBuildParameter[] = []
|
||||
|
||||
for (const parameter of templateParameters) {
|
||||
// Check if there is a new value
|
||||
let buildParameter = newBuildParameters.find(
|
||||
(p) => p.name === parameter.name,
|
||||
)
|
||||
|
||||
// If not, get the old one
|
||||
if (!buildParameter) {
|
||||
buildParameter = oldBuildParameters.find((p) => p.name === parameter.name)
|
||||
}
|
||||
|
||||
// If there is a value from the new or old one, add it to the list
|
||||
if (buildParameter) {
|
||||
updatedBuildParameters.push(buildParameter)
|
||||
continue
|
||||
}
|
||||
|
||||
// If there is no value and it is required, add it to the list of missing parameters
|
||||
if (parameter.required) {
|
||||
missingParameters.push(parameter)
|
||||
}
|
||||
}
|
||||
|
||||
return [updatedBuildParameters, missingParameters] as const
|
||||
}
|
||||
|
||||
+77
-19
@@ -4,36 +4,84 @@ import {
|
||||
FormFooter as BaseFormFooter,
|
||||
} from "components/FormFooter/FormFooter"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FC, HTMLProps, PropsWithChildren } from "react"
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
HTMLProps,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
} from "react"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
|
||||
export const HorizontalForm: FC<
|
||||
PropsWithChildren & HTMLProps<HTMLFormElement>
|
||||
> = ({ children, ...formProps }) => {
|
||||
type FormContextValue = { direction?: "horizontal" | "vertical" }
|
||||
|
||||
const FormContext = createContext<FormContextValue>({
|
||||
direction: "horizontal",
|
||||
})
|
||||
|
||||
type FormProps = HTMLProps<HTMLFormElement> & {
|
||||
direction?: FormContextValue["direction"]
|
||||
}
|
||||
|
||||
export const Form: FC<FormProps> = ({ direction, className, ...formProps }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<form {...formProps}>
|
||||
<Stack direction="column" spacing={10} className={styles.formSections}>
|
||||
{children}
|
||||
</Stack>
|
||||
</form>
|
||||
<FormContext.Provider value={{ direction }}>
|
||||
<form
|
||||
{...formProps}
|
||||
className={combineClasses([styles.form, className])}
|
||||
/>
|
||||
</FormContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const HorizontalForm: FC<HTMLProps<HTMLFormElement>> = ({
|
||||
children,
|
||||
...formProps
|
||||
}) => {
|
||||
return (
|
||||
<Form direction="horizontal" {...formProps}>
|
||||
{children}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const VerticalForm: FC<HTMLProps<HTMLFormElement>> = ({
|
||||
children,
|
||||
...formProps
|
||||
}) => {
|
||||
return (
|
||||
<Form direction="vertical" {...formProps}>
|
||||
{children}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const FormSection: FC<
|
||||
PropsWithChildren & {
|
||||
title: string
|
||||
title: string | JSX.Element
|
||||
description: string | JSX.Element
|
||||
className?: string
|
||||
classes?: {
|
||||
root?: string
|
||||
infoTitle?: string
|
||||
}
|
||||
}
|
||||
> = ({ children, title, description, className }) => {
|
||||
const styles = useStyles()
|
||||
> = ({ children, title, description, classes = {} }) => {
|
||||
const formContext = useContext(FormContext)
|
||||
const styles = useStyles(formContext)
|
||||
|
||||
return (
|
||||
<div className={combineClasses([styles.formSection, className])}>
|
||||
<div className={combineClasses([styles.formSection, classes.root])}>
|
||||
<div className={styles.formSectionInfo}>
|
||||
<h2 className={styles.formSectionInfoTitle}>{title}</h2>
|
||||
<h2
|
||||
className={combineClasses([
|
||||
styles.formSectionInfoTitle,
|
||||
classes.infoTitle,
|
||||
])}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<div className={styles.formSectionInfoDescription}>{description}</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +110,12 @@ export const FormFooter: FC<BaseFormFooterProps> = (props) => {
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
formSections: {
|
||||
form: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: ({ direction }: FormContextValue = {}) =>
|
||||
direction === "horizontal" ? theme.spacing(10) : theme.spacing(5),
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
gap: theme.spacing(8),
|
||||
},
|
||||
@@ -71,7 +124,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
formSection: {
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing(15),
|
||||
gap: ({ direction }: FormContextValue = {}) =>
|
||||
direction === "horizontal" ? theme.spacing(15) : theme.spacing(3),
|
||||
flexDirection: ({ direction }: FormContextValue = {}) =>
|
||||
direction === "horizontal" ? "row" : "column",
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
flexDirection: "column",
|
||||
@@ -80,9 +136,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
|
||||
formSectionInfo: {
|
||||
width: 312,
|
||||
maxWidth: ({ direction }: FormContextValue = {}) =>
|
||||
direction === "horizontal" ? 312 : undefined,
|
||||
flexShrink: 0,
|
||||
position: "sticky",
|
||||
position: ({ direction }: FormContextValue = {}) =>
|
||||
direction === "horizontal" ? "sticky" : undefined,
|
||||
top: theme.spacing(3),
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
@@ -65,5 +65,8 @@
|
||||
"agentVersionLabel": "Agent version",
|
||||
"serverVersionLabel": "Server version",
|
||||
"updateWorkspaceLabel": "Update workspace"
|
||||
},
|
||||
"askParametersDialog": {
|
||||
"message": "It looks like the new version has new parameters that need to be filled in to update the workspace."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@ import { LazyIconField } from "components/IconField/LazyIconField"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import i18next from "i18next"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||
import {
|
||||
HorizontalForm,
|
||||
FormSection,
|
||||
FormFields,
|
||||
} from "components/HorizontalForm/HorizontalForm"
|
||||
FormFooter,
|
||||
} from "components/Form/Form"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import capitalize from "lodash/capitalize"
|
||||
import { VariableInput } from "./VariableInput"
|
||||
|
||||
@@ -19,8 +19,12 @@ import {
|
||||
FormSection,
|
||||
FormFooter,
|
||||
HorizontalForm,
|
||||
} from "components/HorizontalForm/HorizontalForm"
|
||||
} from "components/Form/Form"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import {
|
||||
selectInitialRichParametersValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "util/richParameters"
|
||||
|
||||
export enum CreateWorkspaceErrors {
|
||||
GET_TEMPLATES_ERROR = "getTemplatesError",
|
||||
@@ -82,7 +86,7 @@ export const CreateWorkspacePageView: FC<
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
|
||||
rich_parameter_values: ValidationSchemaForRichParameters(
|
||||
rich_parameter_values: useValidationSchemaForRichParameters(
|
||||
"createWorkspacePage",
|
||||
props.templateParameters,
|
||||
),
|
||||
@@ -334,7 +338,7 @@ export const CreateWorkspacePageView: FC<
|
||||
props.templateParameters.filter((p) => !p.mutable).length > 0 && (
|
||||
<FormSection
|
||||
title="Immutable parameters"
|
||||
className={styles.warningSection}
|
||||
classes={{ root: styles.warningSection }}
|
||||
description={
|
||||
<>
|
||||
Those values are also parameters provided from your Terraform
|
||||
@@ -398,44 +402,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const selectInitialRichParametersValues = (
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[],
|
||||
defaultValuesFromQuery?: Record<string, string>,
|
||||
): TypesGen.WorkspaceBuildParameter[] => {
|
||||
const defaults: TypesGen.WorkspaceBuildParameter[] = []
|
||||
if (!templateParameters) {
|
||||
return defaults
|
||||
}
|
||||
|
||||
templateParameters.forEach((parameter) => {
|
||||
if (parameter.options.length > 0) {
|
||||
let parameterValue = parameter.options[0].value
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
}
|
||||
|
||||
const buildParameter: TypesGen.WorkspaceBuildParameter = {
|
||||
name: parameter.name,
|
||||
value: parameterValue,
|
||||
}
|
||||
defaults.push(buildParameter)
|
||||
return
|
||||
}
|
||||
|
||||
let parameterValue = parameter.default_value
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
}
|
||||
|
||||
const buildParameter: TypesGen.WorkspaceBuildParameter = {
|
||||
name: parameter.name,
|
||||
value: parameterValue || "",
|
||||
}
|
||||
defaults.push(buildParameter)
|
||||
})
|
||||
return defaults
|
||||
}
|
||||
|
||||
export const workspaceBuildParameterValue = (
|
||||
workspaceBuildParameters: TypesGen.WorkspaceBuildParameter[],
|
||||
parameter: TypesGen.TemplateVersionParameter,
|
||||
@@ -445,107 +411,3 @@ export const workspaceBuildParameterValue = (
|
||||
})
|
||||
return (buildParameter && buildParameter.value) || ""
|
||||
}
|
||||
|
||||
export const ValidationSchemaForRichParameters = (
|
||||
ns: string,
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[],
|
||||
lastBuildParameters?: TypesGen.WorkspaceBuildParameter[],
|
||||
): Yup.AnySchema => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
if (!templateParameters) {
|
||||
return Yup.object()
|
||||
}
|
||||
|
||||
return Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
value: Yup.string().test("verify with template", (val, ctx) => {
|
||||
const name = ctx.parent.name
|
||||
const templateParameter = templateParameters.find(
|
||||
(parameter) => parameter.name === name,
|
||||
)
|
||||
if (templateParameter) {
|
||||
switch (templateParameter.type) {
|
||||
case "number":
|
||||
if (
|
||||
templateParameter.validation_min &&
|
||||
templateParameter.validation_max
|
||||
) {
|
||||
if (
|
||||
Number(val) < templateParameter.validation_min ||
|
||||
templateParameter.validation_max < Number(val)
|
||||
) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotInRange", {
|
||||
min: templateParameter.validation_min,
|
||||
max: templateParameter.validation_max,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
templateParameter.validation_monotonic &&
|
||||
lastBuildParameters
|
||||
) {
|
||||
const lastBuildParameter = lastBuildParameters.find(
|
||||
(last) => last.name === name,
|
||||
)
|
||||
if (lastBuildParameter) {
|
||||
switch (templateParameter.validation_monotonic) {
|
||||
case "increasing":
|
||||
if (Number(lastBuildParameter.value) > Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotIncreasing", {
|
||||
last: lastBuildParameter.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
case "decreasing":
|
||||
if (Number(lastBuildParameter.value) < Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotDecreasing", {
|
||||
last: lastBuildParameter.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case "string":
|
||||
{
|
||||
if (
|
||||
!templateParameter.validation_regex ||
|
||||
templateParameter.validation_regex.length === 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const regex = new RegExp(templateParameter.validation_regex)
|
||||
if (val && !regex.test(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationPatternNotMatched", {
|
||||
error: templateParameter.validation_error,
|
||||
pattern: templateParameter.validation_regex,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.required()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
FormFooter,
|
||||
} from "components/HorizontalForm/HorizontalForm"
|
||||
} from "components/Form/Form"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import Checkbox from "@material-ui/core/Checkbox"
|
||||
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
FormFooter,
|
||||
} from "components/HorizontalForm/HorizontalForm"
|
||||
} from "components/Form/Form"
|
||||
import {
|
||||
SensitiveVariableHelperText,
|
||||
TemplateVariableField,
|
||||
|
||||
@@ -8,14 +8,12 @@ import { makeStyles } from "@material-ui/core/styles"
|
||||
import { getFormHelpers } from "util/formUtils"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
|
||||
import {
|
||||
ValidationSchemaForRichParameters,
|
||||
workspaceBuildParameterValue,
|
||||
} from "pages/CreateWorkspacePage/CreateWorkspacePageView"
|
||||
import { workspaceBuildParameterValue } from "pages/CreateWorkspacePage/CreateWorkspacePageView"
|
||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||
import * as Yup from "yup"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { GoBackButton } from "components/GoBackButton/GoBackButton"
|
||||
import { useValidationSchemaForRichParameters } from "util/richParameters"
|
||||
|
||||
export enum UpdateWorkspaceErrors {
|
||||
GET_WORKSPACE_ERROR = "getWorkspaceError",
|
||||
@@ -61,7 +59,7 @@ export const WorkspaceBuildParametersPageView: FC<
|
||||
rich_parameter_values: initialRichParameterValues,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
rich_parameter_values: ValidationSchemaForRichParameters(
|
||||
rich_parameter_values: useValidationSchemaForRichParameters(
|
||||
"workspaceBuildParametersPage",
|
||||
props.templateParameters,
|
||||
initialRichParameterValues,
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Dialog from "@material-ui/core/Dialog"
|
||||
import DialogContent from "@material-ui/core/DialogContent"
|
||||
import DialogContentText from "@material-ui/core/DialogContentText"
|
||||
import DialogTitle from "@material-ui/core/DialogTitle"
|
||||
import { DialogProps } from "components/Dialogs/Dialog"
|
||||
import { FC } from "react"
|
||||
import { getFormHelpers } from "util/formUtils"
|
||||
import { FormFields, VerticalForm } from "components/Form/Form"
|
||||
import {
|
||||
TemplateVersionParameter,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated"
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
|
||||
import { useFormik } from "formik"
|
||||
import {
|
||||
selectInitialRichParametersValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "util/richParameters"
|
||||
import * as Yup from "yup"
|
||||
import DialogActions from "@material-ui/core/DialogActions"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export type UpdateBuildParametersDialogProps = DialogProps & {
|
||||
onClose: () => void
|
||||
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void
|
||||
parameters?: TemplateVersionParameter[]
|
||||
}
|
||||
|
||||
export const UpdateBuildParametersDialog: FC<
|
||||
UpdateBuildParametersDialogProps
|
||||
> = ({ parameters, onUpdate, ...dialogProps }) => {
|
||||
const styles = useStyles()
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
rich_parameter_values: selectInitialRichParametersValues(parameters),
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
rich_parameter_values: useValidationSchemaForRichParameters(
|
||||
"createWorkspacePage",
|
||||
parameters,
|
||||
),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values.rich_parameter_values)
|
||||
},
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers(form)
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
scroll="body"
|
||||
aria-labelledby="update-build-parameters-title"
|
||||
maxWidth="xs"
|
||||
data-testid="dialog"
|
||||
>
|
||||
<DialogTitle
|
||||
id="update-build-parameters-title"
|
||||
classes={{ root: styles.title }}
|
||||
>
|
||||
Workspace parameters
|
||||
</DialogTitle>
|
||||
<DialogContent className={styles.content}>
|
||||
<DialogContentText className={styles.info}>
|
||||
{t("askParametersDialog.message")}
|
||||
</DialogContentText>
|
||||
<VerticalForm
|
||||
className={styles.form}
|
||||
onSubmit={form.handleSubmit}
|
||||
id="updateParameters"
|
||||
>
|
||||
{parameters && parameters.filter((p) => p.mutable).length > 0 && (
|
||||
<FormFields>
|
||||
{parameters.map((parameter, index) => {
|
||||
if (!parameter.mutable) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
initialValue=""
|
||||
index={index}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue(
|
||||
"rich_parameter_values." + index,
|
||||
{
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</FormFields>
|
||||
)}
|
||||
</VerticalForm>
|
||||
</DialogContent>
|
||||
<DialogActions disableSpacing className={styles.dialogActions}>
|
||||
<Button
|
||||
fullWidth
|
||||
type="button"
|
||||
variant="outlined"
|
||||
onClick={dialogProps.onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" fullWidth type="submit" form="updateParameters">
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
padding: theme.spacing(3, 5),
|
||||
|
||||
"& h2": {
|
||||
fontSize: theme.spacing(2.5),
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
|
||||
content: {
|
||||
padding: theme.spacing(0, 5, 0, 5),
|
||||
},
|
||||
|
||||
info: {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
form: {
|
||||
paddingTop: theme.spacing(4),
|
||||
},
|
||||
|
||||
infoTitle: {
|
||||
fontSize: theme.spacing(2),
|
||||
fontWeight: 600,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
|
||||
warningIcon: {
|
||||
color: theme.palette.warning.light,
|
||||
fontSize: theme.spacing(1.5),
|
||||
},
|
||||
|
||||
formFooter: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
|
||||
dialogActions: {
|
||||
padding: theme.spacing(5),
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}))
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
MockStoppedWorkspace,
|
||||
MockStoppingWorkspace,
|
||||
MockTemplate,
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockWorkspace,
|
||||
MockWorkspaceBuild,
|
||||
renderWithAuth,
|
||||
@@ -171,52 +173,79 @@ describe("WorkspacePage", () => {
|
||||
|
||||
expect(cancelWorkspaceMock).toBeCalled()
|
||||
})
|
||||
it("requests a template when the user presses Update", async () => {
|
||||
const getTemplateMock = jest
|
||||
.spyOn(api, "getTemplate")
|
||||
.mockResolvedValueOnce(MockTemplate)
|
||||
server.use(
|
||||
rest.get(
|
||||
`/api/v2/users/:userId/workspace/:workspaceName`,
|
||||
(req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(MockOutdatedWorkspace))
|
||||
},
|
||||
),
|
||||
|
||||
it("requests an update when the user presses Update", async () => {
|
||||
jest
|
||||
.spyOn(api, "getWorkspaceByOwnerAndName")
|
||||
.mockResolvedValueOnce(MockOutdatedWorkspace)
|
||||
const updateWorkspaceMock = jest
|
||||
.spyOn(api, "updateWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
|
||||
await testButton(
|
||||
t("actionButton.update", { ns: "workspacePage" }),
|
||||
updateWorkspaceMock,
|
||||
)
|
||||
|
||||
await renderWorkspacePage()
|
||||
const buttonText = t("actionButton.update", { ns: "workspacePage" })
|
||||
const button = await screen.findByText(buttonText, { exact: true })
|
||||
await userEvent.setup().click(button)
|
||||
|
||||
// getTemplate is called twice: once when the machine starts, and once after the user requests to update
|
||||
expect(getTemplateMock).toBeCalledTimes(2)
|
||||
})
|
||||
it("after an update postWorkspaceBuild is called with the latest template active version id", async () => {
|
||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) // active_version_id = "test-template-version"
|
||||
jest.spyOn(api, "startWorkspace").mockResolvedValueOnce({
|
||||
...MockWorkspaceBuild,
|
||||
|
||||
it("updates the parameters when they are missing during update", async () => {
|
||||
// Setup mocks
|
||||
const user = userEvent.setup()
|
||||
jest
|
||||
.spyOn(api, "getWorkspaceByOwnerAndName")
|
||||
.mockResolvedValueOnce(MockOutdatedWorkspace)
|
||||
const updateWorkspaceSpy = jest
|
||||
.spyOn(api, "updateWorkspace")
|
||||
.mockRejectedValueOnce(
|
||||
new api.MissingBuildParameters([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
]),
|
||||
)
|
||||
// Render page and wait for it to be loaded
|
||||
renderWithAuth(<WorkspacePage />, {
|
||||
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
|
||||
path: "/@:username/:workspace",
|
||||
})
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`/api/v2/users/:userId/workspace/:workspaceName`,
|
||||
(req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(MockOutdatedWorkspace))
|
||||
await waitForLoaderToBeRemoved()
|
||||
// Click on the update button
|
||||
const workspaceActions = screen.getByTestId("workspace-actions")
|
||||
await user.click(
|
||||
within(workspaceActions).getByRole("button", { name: "Update" }),
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(api.updateWorkspace).toBeCalled()
|
||||
// We want to clear this mock to use it later
|
||||
updateWorkspaceSpy.mockClear()
|
||||
})
|
||||
// Fill the parameters and send the form
|
||||
const dialog = await screen.findByTestId("dialog")
|
||||
const firstParameterInput = within(dialog).getByLabelText(
|
||||
MockTemplateVersionParameter1.name,
|
||||
{ exact: false },
|
||||
)
|
||||
await user.clear(firstParameterInput)
|
||||
await user.type(firstParameterInput, "some-value")
|
||||
const secondParameterInput = within(dialog).getByLabelText(
|
||||
MockTemplateVersionParameter2.name,
|
||||
{ exact: false },
|
||||
)
|
||||
await user.clear(secondParameterInput)
|
||||
await user.type(secondParameterInput, "2")
|
||||
await user.click(within(dialog).getByRole("button", { name: "Update" }))
|
||||
// Check if the update was called using the values from the form
|
||||
await waitFor(() => {
|
||||
expect(api.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [
|
||||
{
|
||||
name: MockTemplateVersionParameter1.name,
|
||||
value: "some-value",
|
||||
},
|
||||
),
|
||||
)
|
||||
await renderWorkspacePage()
|
||||
const buttonText = t("actionButton.update", { ns: "workspacePage" })
|
||||
const button = await screen.findByText(buttonText, { exact: true })
|
||||
await userEvent.setup().click(button)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.startWorkspace).toBeCalledWith(
|
||||
"test-outdated-workspace",
|
||||
"test-template-version",
|
||||
),
|
||||
)
|
||||
{
|
||||
name: MockTemplateVersionParameter2.name,
|
||||
value: "2",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it("shows the Stopping status when the workspace is stopping", async () => {
|
||||
@@ -225,48 +254,56 @@ describe("WorkspacePage", () => {
|
||||
t("workspaceStatus.stopping", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Stopped status when the workspace is stopped", async () => {
|
||||
await testStatus(
|
||||
MockStoppedWorkspace,
|
||||
t("workspaceStatus.stopped", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Building status when the workspace is starting", async () => {
|
||||
await testStatus(
|
||||
MockStartingWorkspace,
|
||||
t("workspaceStatus.starting", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Running status when the workspace is running", async () => {
|
||||
await testStatus(
|
||||
MockWorkspace,
|
||||
t("workspaceStatus.running", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Failed status when the workspace is failed or canceled", async () => {
|
||||
await testStatus(
|
||||
MockFailedWorkspace,
|
||||
t("workspaceStatus.failed", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Canceling status when the workspace is canceling", async () => {
|
||||
await testStatus(
|
||||
MockCancelingWorkspace,
|
||||
t("workspaceStatus.canceling", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Canceled status when the workspace is canceling", async () => {
|
||||
await testStatus(
|
||||
MockCanceledWorkspace,
|
||||
t("workspaceStatus.canceled", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Deleting status when the workspace is deleting", async () => {
|
||||
await testStatus(
|
||||
MockDeletingWorkspace,
|
||||
t("workspaceStatus.deleting", { ns: "common" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the Deleted status when the workspace is deleted", async () => {
|
||||
await testStatus(
|
||||
MockDeletedWorkspace,
|
||||
@@ -274,17 +311,15 @@ describe("WorkspacePage", () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe("Timeline", () => {
|
||||
it("shows the timeline build", async () => {
|
||||
await renderWorkspacePage()
|
||||
const table = await screen.findByTestId("builds-table")
|
||||
it("shows the timeline build", async () => {
|
||||
await renderWorkspacePage()
|
||||
const table = await screen.findByTestId("builds-table")
|
||||
|
||||
// Wait for the results to be loaded
|
||||
await waitFor(async () => {
|
||||
const rows = table.querySelectorAll("tbody > tr")
|
||||
// Added +1 because of the date row
|
||||
expect(rows).toHaveLength(MockBuilds.length + 1)
|
||||
})
|
||||
// Wait for the results to be loaded
|
||||
await waitFor(async () => {
|
||||
const rows = table.querySelectorAll("tbody > tr")
|
||||
// Added +1 because of the date row
|
||||
expect(rows).toHaveLength(MockBuilds.length + 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
WorkspaceEvent,
|
||||
workspaceMachine,
|
||||
} from "../../xServices/workspace/workspaceXService"
|
||||
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"
|
||||
|
||||
interface WorkspaceReadyPageProps {
|
||||
workspaceState: StateFrom<typeof workspaceMachine>
|
||||
@@ -52,6 +53,7 @@ export const WorkspaceReadyPage = ({
|
||||
cancellationError,
|
||||
applicationsHost,
|
||||
permissions,
|
||||
missingParameters,
|
||||
} = workspaceState.context
|
||||
if (workspace === undefined) {
|
||||
throw Error("Workspace is undefined")
|
||||
@@ -103,7 +105,7 @@ export const WorkspaceReadyPage = ({
|
||||
deadline,
|
||||
),
|
||||
}}
|
||||
isUpdating={workspaceState.hasTag("updating")}
|
||||
isUpdating={workspaceState.matches("ready.build.requestingUpdate")}
|
||||
workspace={workspace}
|
||||
handleStart={() => workspaceSend({ type: "START" })}
|
||||
handleStop={() => workspaceSend({ type: "STOP" })}
|
||||
@@ -140,6 +142,18 @@ export const WorkspaceReadyPage = ({
|
||||
workspaceSend({ type: "DELETE" })
|
||||
}}
|
||||
/>
|
||||
<UpdateBuildParametersDialog
|
||||
parameters={missingParameters}
|
||||
open={workspaceState.matches(
|
||||
"ready.build.askingForMissedBuildParameters",
|
||||
)}
|
||||
onClose={() => {
|
||||
workspaceSend({ type: "CANCEL" })
|
||||
}}
|
||||
onUpdate={(buildParameters) => {
|
||||
workspaceSend({ type: "UPDATE", buildParameters })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -311,4 +311,11 @@ export const handlers = [
|
||||
rest.get("/api/v2/deployment/stats", (_, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockDeploymentStats))
|
||||
}),
|
||||
|
||||
rest.get(
|
||||
"/api/v2/workspacebuilds/:workspaceBuildId/parameters",
|
||||
(_, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([M.MockWorkspaceBuildParameter1]))
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
TemplateVersionParameter,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as Yup from "yup"
|
||||
|
||||
export const selectInitialRichParametersValues = (
|
||||
templateParameters?: TemplateVersionParameter[],
|
||||
defaultValuesFromQuery?: Record<string, string>,
|
||||
): WorkspaceBuildParameter[] => {
|
||||
const defaults: WorkspaceBuildParameter[] = []
|
||||
if (!templateParameters) {
|
||||
return defaults
|
||||
}
|
||||
|
||||
templateParameters.forEach((parameter) => {
|
||||
if (parameter.options.length > 0) {
|
||||
let parameterValue = parameter.options[0].value
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
}
|
||||
|
||||
const buildParameter: WorkspaceBuildParameter = {
|
||||
name: parameter.name,
|
||||
value: parameterValue,
|
||||
}
|
||||
defaults.push(buildParameter)
|
||||
return
|
||||
}
|
||||
|
||||
let parameterValue = parameter.default_value
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
}
|
||||
|
||||
const buildParameter: WorkspaceBuildParameter = {
|
||||
name: parameter.name,
|
||||
value: parameterValue || "",
|
||||
}
|
||||
defaults.push(buildParameter)
|
||||
})
|
||||
return defaults
|
||||
}
|
||||
|
||||
export const useValidationSchemaForRichParameters = (
|
||||
ns: string,
|
||||
templateParameters?: TemplateVersionParameter[],
|
||||
lastBuildParameters?: WorkspaceBuildParameter[],
|
||||
): Yup.AnySchema => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
if (!templateParameters) {
|
||||
return Yup.object()
|
||||
}
|
||||
|
||||
return Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
value: Yup.string().test("verify with template", (val, ctx) => {
|
||||
const name = ctx.parent.name
|
||||
const templateParameter = templateParameters.find(
|
||||
(parameter) => parameter.name === name,
|
||||
)
|
||||
if (templateParameter) {
|
||||
switch (templateParameter.type) {
|
||||
case "number":
|
||||
if (
|
||||
templateParameter.validation_min &&
|
||||
templateParameter.validation_max
|
||||
) {
|
||||
if (
|
||||
Number(val) < templateParameter.validation_min ||
|
||||
templateParameter.validation_max < Number(val)
|
||||
) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotInRange", {
|
||||
min: templateParameter.validation_min,
|
||||
max: templateParameter.validation_max,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
templateParameter.validation_monotonic &&
|
||||
lastBuildParameters
|
||||
) {
|
||||
const lastBuildParameter = lastBuildParameters.find(
|
||||
(last) => last.name === name,
|
||||
)
|
||||
if (lastBuildParameter) {
|
||||
switch (templateParameter.validation_monotonic) {
|
||||
case "increasing":
|
||||
if (Number(lastBuildParameter.value) > Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotIncreasing", {
|
||||
last: lastBuildParameter.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
case "decreasing":
|
||||
if (Number(lastBuildParameter.value) < Number(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationNumberNotDecreasing", {
|
||||
last: lastBuildParameter.value,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case "string":
|
||||
{
|
||||
if (
|
||||
!templateParameter.validation_regex ||
|
||||
templateParameter.validation_regex.length === 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const regex = new RegExp(templateParameter.validation_regex)
|
||||
if (val && !regex.test(val)) {
|
||||
return ctx.createError({
|
||||
path: ctx.path,
|
||||
message: t("validationPatternNotMatched", {
|
||||
error: templateParameter.validation_error,
|
||||
pattern: templateParameter.validation_regex,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.required()
|
||||
}
|
||||
@@ -63,6 +63,7 @@ export interface WorkspaceContext {
|
||||
// Builds
|
||||
builds?: TypesGen.WorkspaceBuild[]
|
||||
getBuildsError?: Error | unknown
|
||||
missingParameters?: TypesGen.TemplateVersionParameter[]
|
||||
// error creating a new WorkspaceBuild
|
||||
buildError?: Error | unknown
|
||||
cancellationMessage?: Types.Message
|
||||
@@ -82,7 +83,7 @@ export type WorkspaceEvent =
|
||||
| { type: "ASK_DELETE" }
|
||||
| { type: "DELETE" }
|
||||
| { type: "CANCEL_DELETE" }
|
||||
| { type: "UPDATE" }
|
||||
| { type: "UPDATE"; buildParameters?: TypesGen.WorkspaceBuildParameter[] }
|
||||
| { type: "CANCEL" }
|
||||
| {
|
||||
type: "REFRESH_TIMELINE"
|
||||
@@ -135,7 +136,7 @@ export const workspaceMachine = createMachine(
|
||||
getTemplateParameters: {
|
||||
data: TypesGen.TemplateVersionParameter[]
|
||||
}
|
||||
startWorkspaceWithLatestTemplate: {
|
||||
updateWorkspace: {
|
||||
data: TypesGen.WorkspaceBuild
|
||||
}
|
||||
startWorkspace: {
|
||||
@@ -301,7 +302,7 @@ export const workspaceMachine = createMachine(
|
||||
START: "requestingStart",
|
||||
STOP: "requestingStop",
|
||||
ASK_DELETE: "askingDelete",
|
||||
UPDATE: "updatingWorkspace",
|
||||
UPDATE: "requestingUpdate",
|
||||
CANCEL: "requestingCancel",
|
||||
},
|
||||
},
|
||||
@@ -315,38 +316,31 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
},
|
||||
updatingWorkspace: {
|
||||
tags: "updating",
|
||||
initial: "refreshingTemplate",
|
||||
states: {
|
||||
refreshingTemplate: {
|
||||
invoke: {
|
||||
id: "refreshTemplate",
|
||||
src: "getTemplate",
|
||||
onDone: {
|
||||
target: "startingWithLatestTemplate",
|
||||
actions: ["assignTemplate"],
|
||||
},
|
||||
onError: {
|
||||
target: "#workspaceState.ready.build.idle",
|
||||
actions: ["assignGetTemplateWarning"],
|
||||
},
|
||||
},
|
||||
requestingUpdate: {
|
||||
entry: ["clearBuildError"],
|
||||
invoke: {
|
||||
src: "updateWorkspace",
|
||||
onDone: {
|
||||
target: "idle",
|
||||
actions: ["assignBuild"],
|
||||
},
|
||||
startingWithLatestTemplate: {
|
||||
invoke: {
|
||||
id: "startWorkspaceWithLatestTemplate",
|
||||
src: "startWorkspaceWithLatestTemplate",
|
||||
onDone: {
|
||||
target: "#workspaceState.ready.build.idle",
|
||||
actions: ["assignBuild"],
|
||||
},
|
||||
onError: {
|
||||
target: "#workspaceState.ready.build.idle",
|
||||
actions: ["assignBuildError"],
|
||||
},
|
||||
onError: [
|
||||
{
|
||||
target: "askingForMissedBuildParameters",
|
||||
cond: "isMissingBuildParameterError",
|
||||
actions: ["assignMissingParameters"],
|
||||
},
|
||||
},
|
||||
{
|
||||
target: "idle",
|
||||
actions: ["assignBuildError"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
askingForMissedBuildParameters: {
|
||||
on: {
|
||||
CANCEL: "idle",
|
||||
UPDATE: "requestingUpdate",
|
||||
},
|
||||
},
|
||||
requestingStart: {
|
||||
@@ -641,9 +635,20 @@ export const workspaceMachine = createMachine(
|
||||
}
|
||||
},
|
||||
}),
|
||||
assignMissingParameters: assign({
|
||||
missingParameters: (_, { data }) => {
|
||||
if (!(data instanceof API.MissingBuildParameters)) {
|
||||
throw new Error("data is not a MissingBuildParameters error")
|
||||
}
|
||||
return data.parameters
|
||||
},
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
moreBuildsAvailable,
|
||||
isMissingBuildParameterError: (_, { data }) => {
|
||||
return data instanceof API.MissingBuildParameters
|
||||
},
|
||||
},
|
||||
services: {
|
||||
getWorkspace: async (_, event) => {
|
||||
@@ -671,18 +676,16 @@ export const workspaceMachine = createMachine(
|
||||
throw Error("Cannot get template parameters without workspace")
|
||||
}
|
||||
},
|
||||
startWorkspaceWithLatestTemplate: (context) => async (send) => {
|
||||
if (context.workspace && context.template) {
|
||||
const startWorkspacePromise = await API.startWorkspace(
|
||||
context.workspace.id,
|
||||
context.template.active_version_id,
|
||||
)
|
||||
updateWorkspace:
|
||||
({ workspace }, { buildParameters }) =>
|
||||
async (send) => {
|
||||
if (!workspace) {
|
||||
throw new Error("Workspace is not set")
|
||||
}
|
||||
const build = await API.updateWorkspace(workspace, buildParameters)
|
||||
send({ type: "REFRESH_TIMELINE" })
|
||||
return startWorkspacePromise
|
||||
} else {
|
||||
throw Error("Cannot start workspace without workspace id")
|
||||
}
|
||||
},
|
||||
return build
|
||||
},
|
||||
startWorkspace: (context) => async (send) => {
|
||||
if (context.workspace) {
|
||||
const startWorkspacePromise = await API.startWorkspace(
|
||||
|
||||
Reference in New Issue
Block a user