Skip to content

feat(site): Ask for parameter values when update a workspace #6586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 14, 2023
54 changes: 45 additions & 9 deletions site/src/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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()
Expand All @@ -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)
}
Expand All @@ -49,7 +55,7 @@ describe("api.ts", () => {
axios.post = axiosMockPost

// when
await logout()
await api.logout()

// then
expect(axiosMockPost).toHaveBeenCalled()
Expand All @@ -68,7 +74,7 @@ describe("api.ts", () => {
axios.post = axiosMockPost

try {
await logout()
await api.logout()
} catch (error) {
expect(error).toStrictEqual(expectedError)
}
Expand All @@ -87,7 +93,7 @@ describe("api.ts", () => {
axios.post = axiosMockPost

// when
const result = await getApiKey()
const result = await api.getApiKey()

// then
expect(axiosMockPost).toHaveBeenCalled()
Expand All @@ -107,7 +113,7 @@ describe("api.ts", () => {
axios.post = axiosMockPost

try {
await getApiKey()
await api.getApiKey()
} catch (error) {
expect(error).toStrictEqual(expectedError)
}
Expand All @@ -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)
},
)
})
Expand All @@ -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,
)
})
})
})
79 changes: 79 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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),
},
Expand All @@ -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",
Expand All @@ -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")]: {
Expand Down
3 changes: 3 additions & 0 deletions site/src/i18n/en/workspacePage.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
4 changes: 2 additions & 2 deletions site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading