mirror of https://github.com/coder/coder.git
feat: Adjust forms to include Rich Parameters (#5856)
* XService: GetTemplateParameters * Rich parameter input shows up * Render option icons * Icons * WIP * For testing purposes: template * Fix: useState * WIP: dynamic validation * Yup validation * Translations * Remove temporary template * make fmt * WIP * Fix: tests * Fix: fmt * URL param * Refactor * Test: rich param value * Storybook * Fix * Refactor for testing purposes * Typo * test: string validation * Button: build parameters * Full screen page * Fix: navigate * XState done * refactor: postWorkspaceBuild * RichParameterInput rendered * Fix: bad initial value * Validation works * Maybe * Fix * Go back button * GoBack button * Form * Fix * Storybook * Fix: CreateWorkspacePage * fmt * Test * ns * fmt * All tests * feat: WorkspaceActions depend on template parameters * Fix
This commit is contained in:
parent
d5e2454b1b
commit
f9ae105a26
|
@ -7,6 +7,7 @@ import GroupsPage from "pages/GroupsPage/GroupsPage"
|
|||
import LoginPage from "pages/LoginPage/LoginPage"
|
||||
import { SetupPage } from "pages/SetupPage/SetupPage"
|
||||
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage"
|
||||
import { WorkspaceBuildParametersPage } from "pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage"
|
||||
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
|
||||
import UsersPage from "pages/UsersPage/UsersPage"
|
||||
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"
|
||||
|
@ -213,6 +214,10 @@ export const AppRouter: FC = () => {
|
|||
path="change-version"
|
||||
element={<WorkspaceChangeVersionPage />}
|
||||
/>
|
||||
<Route
|
||||
path="build-parameters"
|
||||
element={<WorkspaceBuildParametersPage />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import axios, { AxiosRequestHeaders } from "axios"
|
||||
import dayjs from "dayjs"
|
||||
import * as Types from "./types"
|
||||
import { WorkspaceBuildTransition } from "./types"
|
||||
import * as TypesGen from "./typesGenerated"
|
||||
|
||||
export const hardCodedCSRFCookie = (): string => {
|
||||
|
@ -288,6 +287,15 @@ export const getTemplateVersionParameters = async (
|
|||
return response.data
|
||||
}
|
||||
|
||||
export const getTemplateVersionRichParameters = async (
|
||||
versionId: string,
|
||||
): Promise<TypesGen.TemplateVersionParameter[]> => {
|
||||
const response = await axios.get(
|
||||
`/api/v2/templateversions/${versionId}/rich-parameters`,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const createTemplate = async (
|
||||
organizationId: string,
|
||||
data: TypesGen.CreateTemplateRequest,
|
||||
|
@ -390,26 +398,29 @@ export const getWorkspaceByOwnerAndName = async (
|
|||
return response.data
|
||||
}
|
||||
|
||||
const postWorkspaceBuild =
|
||||
(transition: WorkspaceBuildTransition) =>
|
||||
async (
|
||||
workspaceId: string,
|
||||
template_version_id?: string,
|
||||
): Promise<TypesGen.WorkspaceBuild> => {
|
||||
const payload = {
|
||||
transition,
|
||||
template_version_id,
|
||||
}
|
||||
const response = await axios.post(
|
||||
`/api/v2/workspaces/${workspaceId}/builds`,
|
||||
payload,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
export const postWorkspaceBuild = async (
|
||||
workspaceId: string,
|
||||
data: TypesGen.CreateWorkspaceBuildRequest,
|
||||
): Promise<TypesGen.WorkspaceBuild> => {
|
||||
const response = await axios.post(
|
||||
`/api/v2/workspaces/${workspaceId}/builds`,
|
||||
data,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const startWorkspace = postWorkspaceBuild("start")
|
||||
export const stopWorkspace = postWorkspaceBuild("stop")
|
||||
export const deleteWorkspace = postWorkspaceBuild("delete")
|
||||
export const startWorkspace = (
|
||||
workspaceId: string,
|
||||
templateVersionID: string,
|
||||
) =>
|
||||
postWorkspaceBuild(workspaceId, {
|
||||
transition: "start",
|
||||
template_version_id: templateVersionID,
|
||||
})
|
||||
export const stopWorkspace = (workspaceId: string) =>
|
||||
postWorkspaceBuild(workspaceId, { transition: "stop" })
|
||||
export const deleteWorkspace = (workspaceId: string) =>
|
||||
postWorkspaceBuild(workspaceId, { transition: "delete" })
|
||||
|
||||
export const cancelWorkspaceBuild = async (
|
||||
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
|
||||
|
@ -790,3 +801,12 @@ export const updateWorkspaceVersion = async (
|
|||
const template = await getTemplate(workspace.template_id)
|
||||
return startWorkspace(workspace.id, template.active_version_id)
|
||||
}
|
||||
|
||||
export const getWorkspaceBuildParameters = async (
|
||||
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
|
||||
): Promise<TypesGen.WorkspaceBuildParameter[]> => {
|
||||
const response = await axios.get<TypesGen.WorkspaceBuildParameter[]>(
|
||||
`/api/v2/workspacebuilds/${workspaceBuildId}/parameters`,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ 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 SettingsOutlined from "@material-ui/icons/SettingsOutlined"
|
||||
import CropSquareIcon from "@material-ui/icons/CropSquare"
|
||||
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
|
||||
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
|
||||
|
@ -51,6 +52,23 @@ export const ChangeVersionButton: FC<
|
|||
)
|
||||
}
|
||||
|
||||
export const BuildParametersButton: FC<
|
||||
React.PropsWithChildren<WorkspaceAction>
|
||||
> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
startIcon={<SettingsOutlined />}
|
||||
onClick={handleAction}
|
||||
>
|
||||
{t("actionButton.buildParameters")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||
handleAction,
|
||||
}) => {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Button from "@material-ui/core/Button"
|
||||
|
||||
interface GoBackButtonProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
ariaLabel: "Go back",
|
||||
}
|
||||
|
||||
export const GoBackButton: React.FC<
|
||||
React.PropsWithChildren<GoBackButtonProps>
|
||||
> = ({ onClick }) => {
|
||||
return (
|
||||
<Button onClick={onClick} size="small" aria-label={Language.ariaLabel}>
|
||||
Go back
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { TemplateVersionParameter } from "api/typesGenerated"
|
||||
import {
|
||||
RichParameterInput,
|
||||
RichParameterInputProps,
|
||||
} from "./RichParameterInput"
|
||||
|
||||
export default {
|
||||
title: "components/RichParameterInput",
|
||||
component: RichParameterInput,
|
||||
}
|
||||
|
||||
const Template: Story<RichParameterInputProps> = (
|
||||
args: RichParameterInputProps,
|
||||
) => <RichParameterInput {...args} />
|
||||
|
||||
const createTemplateVersionParameter = (
|
||||
partial: Partial<TemplateVersionParameter>,
|
||||
): TemplateVersionParameter => {
|
||||
return {
|
||||
name: "first_parameter",
|
||||
description: "This is first parameter.",
|
||||
type: "string",
|
||||
mutable: false,
|
||||
default_value: "default string",
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
|
||||
export const Basic = Template.bind({})
|
||||
Basic.args = {
|
||||
initialValue: "initial-value",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "project_name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
}),
|
||||
}
|
||||
|
||||
export const NumberType = Template.bind({})
|
||||
NumberType.args = {
|
||||
initialValue: "4",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "number_parameter",
|
||||
type: "number",
|
||||
description: "Numeric parameter",
|
||||
}),
|
||||
}
|
||||
|
||||
export const BooleanType = Template.bind({})
|
||||
BooleanType.args = {
|
||||
initialValue: "false",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "bool_parameter",
|
||||
type: "bool",
|
||||
description: "Boolean parameter",
|
||||
}),
|
||||
}
|
||||
|
||||
export const OptionsType = Template.bind({})
|
||||
OptionsType.args = {
|
||||
initialValue: "first_option",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "options_parameter",
|
||||
type: "string",
|
||||
description: "Parameter with options",
|
||||
options: [
|
||||
{
|
||||
name: "First option",
|
||||
value: "first_option",
|
||||
description: "This is option 1",
|
||||
icon: "",
|
||||
},
|
||||
{
|
||||
name: "Second option",
|
||||
value: "second_option",
|
||||
description: "This is option 2",
|
||||
icon: "/icon/database.svg",
|
||||
},
|
||||
{
|
||||
name: "Third option",
|
||||
value: "third_option",
|
||||
description: "This is option 3",
|
||||
icon: "/icon/aws.png",
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
import FormControlLabel from "@material-ui/core/FormControlLabel"
|
||||
import Radio from "@material-ui/core/Radio"
|
||||
import RadioGroup from "@material-ui/core/RadioGroup"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FC, useState } from "react"
|
||||
import { TemplateVersionParameter } from "../../api/typesGenerated"
|
||||
import { colors } from "theme/colors"
|
||||
|
||||
const isBoolean = (parameter: TemplateVersionParameter) => {
|
||||
return parameter.type === "bool"
|
||||
}
|
||||
|
||||
export interface ParameterLabelProps {
|
||||
index: number
|
||||
parameter: TemplateVersionParameter
|
||||
}
|
||||
|
||||
const ParameterLabel: FC<ParameterLabelProps> = ({ index, parameter }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.labelNameWithIcon}>
|
||||
{parameter.icon && (
|
||||
<span className={styles.iconWrapper}>
|
||||
<img
|
||||
className={styles.icon}
|
||||
alt="Parameter icon"
|
||||
src={parameter.icon}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.labelName}>
|
||||
<label htmlFor={`rich_parameter_values[${index}].value`}>
|
||||
{parameter.name}
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.labelDescription}>{parameter.description}</span>
|
||||
{!parameter.mutable && (
|
||||
<div className={styles.labelImmutable}>
|
||||
This parameter cannot be changed after creating workspace.
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export interface RichParameterInputProps {
|
||||
index: number
|
||||
disabled?: boolean
|
||||
parameter: TemplateVersionParameter
|
||||
onChange: (value: string) => void
|
||||
initialValue?: string
|
||||
}
|
||||
|
||||
export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
index,
|
||||
disabled,
|
||||
onChange,
|
||||
parameter,
|
||||
initialValue,
|
||||
...props
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={0.75}>
|
||||
<ParameterLabel index={index} parameter={parameter} />
|
||||
<div className={styles.input}>
|
||||
<RichParameterField
|
||||
{...props}
|
||||
index={index}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
parameter={parameter}
|
||||
initialValue={initialValue}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const RichParameterField: React.FC<RichParameterInputProps> = ({
|
||||
disabled,
|
||||
onChange,
|
||||
parameter,
|
||||
initialValue,
|
||||
...props
|
||||
}) => {
|
||||
const [parameterValue, setParameterValue] = useState(initialValue)
|
||||
const styles = useStyles()
|
||||
|
||||
if (isBoolean(parameter)) {
|
||||
return (
|
||||
<RadioGroup
|
||||
defaultValue={parameterValue}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value)
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
disabled={disabled}
|
||||
value="true"
|
||||
control={<Radio color="primary" size="small" disableRipple />}
|
||||
label="True"
|
||||
/>
|
||||
<FormControlLabel
|
||||
disabled={disabled}
|
||||
value="false"
|
||||
control={<Radio color="primary" size="small" disableRipple />}
|
||||
label="False"
|
||||
/>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameter.options.length > 0) {
|
||||
return (
|
||||
<RadioGroup
|
||||
defaultValue={parameterValue}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value)
|
||||
}}
|
||||
>
|
||||
{parameter.options.map((option) => (
|
||||
<FormControlLabel
|
||||
disabled={disabled}
|
||||
key={option.name}
|
||||
value={option.value}
|
||||
control={<Radio color="primary" size="small" disableRipple />}
|
||||
label={
|
||||
<span>
|
||||
{option.icon && (
|
||||
<img
|
||||
className={styles.optionIcon}
|
||||
alt="Parameter icon"
|
||||
src={option.icon}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{option.name}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
// A text field can technically handle all cases!
|
||||
// As other cases become more prominent (like filtering for numbers),
|
||||
// we should break this out into more finely scoped input fields.
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
type={parameter.type}
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
placeholder={parameter.default_value}
|
||||
value={parameterValue}
|
||||
onChange={(event) => {
|
||||
setParameterValue(event.target.value)
|
||||
onChange(event.target.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const iconSize = 20
|
||||
const optionIconSize = 24
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
labelName: {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
display: "block",
|
||||
marginBottom: theme.spacing(1.0),
|
||||
},
|
||||
labelNameWithIcon: {
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
labelDescription: {
|
||||
fontSize: 16,
|
||||
color: theme.palette.text.primary,
|
||||
display: "block",
|
||||
fontWeight: 600,
|
||||
},
|
||||
labelImmutable: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
color: colors.yellow[7],
|
||||
},
|
||||
input: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
checkbox: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
iconWrapper: {
|
||||
float: "left",
|
||||
},
|
||||
icon: {
|
||||
maxHeight: iconSize,
|
||||
width: iconSize,
|
||||
marginRight: theme.spacing(1.0),
|
||||
},
|
||||
optionIcon: {
|
||||
maxHeight: optionIconSize,
|
||||
width: optionIconSize,
|
||||
marginRight: theme.spacing(1.0),
|
||||
float: "left",
|
||||
},
|
||||
}))
|
|
@ -45,6 +45,7 @@ export interface WorkspaceProps {
|
|||
handleUpdate: () => void
|
||||
handleCancel: () => void
|
||||
handleChangeVersion: () => void
|
||||
handleBuildParameters: () => void
|
||||
isUpdating: boolean
|
||||
workspace: TypesGen.Workspace
|
||||
resources?: TypesGen.WorkspaceResource[]
|
||||
|
@ -56,6 +57,7 @@ export interface WorkspaceProps {
|
|||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
applicationsHost?: string
|
||||
template?: TypesGen.Template
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||
quota_budget?: number
|
||||
}
|
||||
|
||||
|
@ -70,6 +72,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
handleUpdate,
|
||||
handleCancel,
|
||||
handleChangeVersion,
|
||||
handleBuildParameters,
|
||||
workspace,
|
||||
isUpdating,
|
||||
resources,
|
||||
|
@ -81,6 +84,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
buildInfo,
|
||||
applicationsHost,
|
||||
template,
|
||||
templateParameters,
|
||||
quota_budget,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
@ -122,7 +126,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
if (template !== undefined) {
|
||||
transitionStats = ActiveTransition(template, workspace)
|
||||
}
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<PageHeader
|
||||
|
@ -138,6 +141,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
/>
|
||||
<WorkspaceActions
|
||||
workspaceStatus={workspace.latest_build.status}
|
||||
hasTemplateParameters={
|
||||
templateParameters ? templateParameters.length > 0 : false
|
||||
}
|
||||
isOutdated={workspace.outdated}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
|
@ -145,6 +151,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
handleChangeVersion={handleChangeVersion}
|
||||
handleBuildParameters={handleBuildParameters}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</Stack>
|
||||
|
|
|
@ -12,6 +12,7 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||
workspaceStatus={
|
||||
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
||||
}
|
||||
hasTemplateParameters={props.hasTemplateParameters ?? false}
|
||||
isOutdated={props.isOutdated ?? false}
|
||||
handleStart={jest.fn()}
|
||||
handleStop={jest.fn()}
|
||||
|
@ -19,6 +20,7 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||
handleUpdate={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
handleChangeVersion={jest.fn()}
|
||||
handleBuildParameters={jest.fn()}
|
||||
isUpdating={false}
|
||||
/>,
|
||||
)
|
||||
|
@ -30,6 +32,7 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||
workspaceStatus={
|
||||
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
||||
}
|
||||
hasTemplateParameters={props.hasTemplateParameters ?? false}
|
||||
isOutdated={props.isOutdated ?? false}
|
||||
handleStart={jest.fn()}
|
||||
handleStop={jest.fn()}
|
||||
|
@ -37,6 +40,7 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||
handleUpdate={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
handleChangeVersion={jest.fn()}
|
||||
handleBuildParameters={jest.fn()}
|
||||
isUpdating={false}
|
||||
/>,
|
||||
)
|
||||
|
@ -74,6 +78,33 @@ describe("WorkspaceActions", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
describe("when the workspace is started", () => {
|
||||
it("primary is stop; secondary is delete", async () => {
|
||||
await renderAndClick({
|
||||
workspaceStatus: Mocks.MockWorkspace.latest_build.status,
|
||||
})
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.stop", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.delete", { ns: "workspacePage" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("when the workspace with rich parameters is started", () => {
|
||||
it("primary is stop; secondary is build parameters", async () => {
|
||||
await renderAndClick({
|
||||
workspaceStatus: Mocks.MockWorkspace.latest_build.status,
|
||||
hasTemplateParameters: true,
|
||||
})
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.stop", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.buildParameters", { ns: "workspacePage" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("when the workspace is stopping", () => {
|
||||
it("primary is stopping; cancel is available; no secondary", async () => {
|
||||
await renderComponent({
|
||||
|
|
|
@ -7,14 +7,16 @@ import {
|
|||
ChangeVersionButton,
|
||||
DeleteButton,
|
||||
DisabledButton,
|
||||
BuildParametersButton,
|
||||
StartButton,
|
||||
StopButton,
|
||||
UpdateButton,
|
||||
} from "../DropdownButton/ActionCtas"
|
||||
import { ButtonMapping, ButtonTypesEnum, statusToAbilities } from "./constants"
|
||||
import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants"
|
||||
|
||||
export interface WorkspaceActionsProps {
|
||||
workspaceStatus: WorkspaceStatus
|
||||
hasTemplateParameters: boolean
|
||||
isOutdated: boolean
|
||||
handleStart: () => void
|
||||
handleStop: () => void
|
||||
|
@ -22,12 +24,14 @@ export interface WorkspaceActionsProps {
|
|||
handleUpdate: () => void
|
||||
handleCancel: () => void
|
||||
handleChangeVersion: () => void
|
||||
handleBuildParameters: () => void
|
||||
isUpdating: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
workspaceStatus,
|
||||
hasTemplateParameters,
|
||||
isOutdated,
|
||||
handleStart,
|
||||
handleStop,
|
||||
|
@ -35,11 +39,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||
handleUpdate,
|
||||
handleCancel,
|
||||
handleChangeVersion,
|
||||
handleBuildParameters,
|
||||
isUpdating,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const { canCancel, canAcceptJobs, actions } =
|
||||
statusToAbilities[workspaceStatus]
|
||||
const { canCancel, canAcceptJobs, actions } = buttonAbilities(
|
||||
workspaceStatus,
|
||||
hasTemplateParameters,
|
||||
)
|
||||
const canBeUpdated = isOutdated && canAcceptJobs
|
||||
|
||||
// A mapping of button type to the corresponding React component
|
||||
|
@ -51,6 +58,9 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||
[ButtonTypesEnum.changeVersion]: (
|
||||
<ChangeVersionButton handleAction={handleChangeVersion} />
|
||||
),
|
||||
[ButtonTypesEnum.buildParameters]: (
|
||||
<BuildParametersButton handleAction={handleBuildParameters} />
|
||||
),
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||
[ButtonTypesEnum.starting]: (
|
||||
<ActionLoadingButton label={t("actionButton.starting")} />
|
||||
|
|
|
@ -12,6 +12,7 @@ export enum ButtonTypesEnum {
|
|||
update = "update",
|
||||
updating = "updating",
|
||||
changeVersion = "changeVersion",
|
||||
buildParameters = "buildParameters",
|
||||
// disabled buttons
|
||||
canceling = "canceling",
|
||||
deleted = "deleted",
|
||||
|
@ -28,7 +29,24 @@ interface WorkspaceAbilities {
|
|||
canAcceptJobs: boolean
|
||||
}
|
||||
|
||||
export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
export const buttonAbilities = (
|
||||
status: WorkspaceStatus,
|
||||
hasTemplateParameters: boolean,
|
||||
): WorkspaceAbilities => {
|
||||
if (hasTemplateParameters) {
|
||||
return statusToAbilities[status]
|
||||
}
|
||||
|
||||
const all = statusToAbilities[status]
|
||||
return {
|
||||
...all,
|
||||
actions: all.actions.filter(
|
||||
(action) => action !== ButtonTypesEnum.buildParameters,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
starting: {
|
||||
actions: [ButtonTypesEnum.starting],
|
||||
canCancel: true,
|
||||
|
@ -37,6 +55,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||
running: {
|
||||
actions: [
|
||||
ButtonTypesEnum.stop,
|
||||
ButtonTypesEnum.buildParameters,
|
||||
ButtonTypesEnum.changeVersion,
|
||||
ButtonTypesEnum.delete,
|
||||
],
|
||||
|
@ -51,6 +70,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||
stopped: {
|
||||
actions: [
|
||||
ButtonTypesEnum.start,
|
||||
ButtonTypesEnum.buildParameters,
|
||||
ButtonTypesEnum.changeVersion,
|
||||
ButtonTypesEnum.delete,
|
||||
],
|
||||
|
@ -61,6 +81,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||
actions: [
|
||||
ButtonTypesEnum.start,
|
||||
ButtonTypesEnum.stop,
|
||||
ButtonTypesEnum.buildParameters,
|
||||
ButtonTypesEnum.changeVersion,
|
||||
ButtonTypesEnum.delete,
|
||||
],
|
||||
|
@ -71,6 +92,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||
failed: {
|
||||
actions: [
|
||||
ButtonTypesEnum.start,
|
||||
ButtonTypesEnum.buildParameters,
|
||||
ButtonTypesEnum.changeVersion,
|
||||
ButtonTypesEnum.delete,
|
||||
],
|
||||
|
|
|
@ -2,5 +2,8 @@
|
|||
"templateLabel": "Template",
|
||||
"nameLabel": "Workspace Name",
|
||||
"ownerLabel": "Owner",
|
||||
"createWorkspace": "Create workspace"
|
||||
"createWorkspace": "Create workspace",
|
||||
"validationRequiredParameter": "Parameter is required.",
|
||||
"validationNumberNotInRange": "Value must be between {{min}} and {{max}}.",
|
||||
"validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}})."
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import usersPage from "./usersPage.json"
|
|||
import templateSettingsPage from "./templateSettingsPage.json"
|
||||
import templateVersionPage from "./templateVersionPage.json"
|
||||
import loginPage from "./loginPage.json"
|
||||
import workspaceBuildParametersPage from "./workspaceBuildParametersPage.json"
|
||||
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
|
||||
import workspaceSchedulePage from "./workspaceSchedulePage.json"
|
||||
import appearanceSettings from "./appearanceSettings.json"
|
||||
|
@ -33,6 +34,7 @@ export const en = {
|
|||
templateSettingsPage,
|
||||
templateVersionPage,
|
||||
loginPage,
|
||||
workspaceBuildParametersPage,
|
||||
workspaceChangeVersionPage,
|
||||
workspaceSchedulePage,
|
||||
appearanceSettings,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"title": "Workspace build parameters",
|
||||
"detail": "Those values were provided by the workspace owner.",
|
||||
"noParametersDefined": "This template does not use any rich parameters.",
|
||||
"validationRequiredParameter": "Parameter is required.",
|
||||
"validationNumberNotInRange": "Value must be between {{min}} and {{max}}.",
|
||||
"validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}}).",
|
||||
"updateWorkspace": "Update workspace"
|
||||
}
|
|
@ -28,7 +28,8 @@
|
|||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"deleting": "Deleting...",
|
||||
"changeVersion": "Change version"
|
||||
"changeVersion": "Change version",
|
||||
"buildParameters": "Build parameters"
|
||||
},
|
||||
"disabledButton": {
|
||||
"canceling": "Canceling",
|
||||
|
|
|
@ -9,6 +9,9 @@ import {
|
|||
MockWorkspace,
|
||||
MockWorkspaceQuota,
|
||||
MockWorkspaceRequest,
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
} from "testHelpers/entities"
|
||||
import { renderWithAuth } from "testHelpers/renderHelpers"
|
||||
import CreateWorkspacePage from "./CreateWorkspacePage"
|
||||
|
@ -17,6 +20,16 @@ const { t } = i18next
|
|||
|
||||
const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" })
|
||||
const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" })
|
||||
const validationNumberNotInRangeText = t("validationNumberNotInRange", {
|
||||
ns: "createWorkspacePage",
|
||||
min: "1",
|
||||
max: "3",
|
||||
})
|
||||
const validationPatternNotMatched = t("validationPatternNotMatched", {
|
||||
ns: "createWorkspacePage",
|
||||
error: MockTemplateVersionParameter3.validation_error,
|
||||
pattern: "^[a-z]{3}$",
|
||||
})
|
||||
|
||||
const renderCreateWorkspacePage = () => {
|
||||
return renderWithAuth(<CreateWorkspacePage />, {
|
||||
|
@ -27,11 +40,29 @@ const renderCreateWorkspacePage = () => {
|
|||
|
||||
describe("CreateWorkspacePage", () => {
|
||||
it("renders", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||
renderCreateWorkspacePage()
|
||||
const element = await screen.findByText("Create workspace")
|
||||
|
||||
const element = await screen.findByText(createWorkspaceText)
|
||||
expect(element).toBeDefined()
|
||||
})
|
||||
|
||||
it("renders with rich parameter", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||
renderCreateWorkspacePage()
|
||||
|
||||
const element = await screen.findByText(createWorkspaceText)
|
||||
expect(element).toBeDefined()
|
||||
const firstParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter1.description,
|
||||
)
|
||||
expect(firstParameter).toBeDefined()
|
||||
})
|
||||
|
||||
it("succeeds with default owner", async () => {
|
||||
jest
|
||||
.spyOn(API, "getUsers")
|
||||
|
@ -40,6 +71,9 @@ describe("CreateWorkspacePage", () => {
|
|||
.spyOn(API, "getWorkspaceQuota")
|
||||
.mockResolvedValueOnce(MockWorkspaceQuota)
|
||||
jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace)
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||
|
||||
renderCreateWorkspacePage()
|
||||
|
||||
|
@ -73,13 +107,106 @@ describe("CreateWorkspacePage", () => {
|
|||
default_source_value: "",
|
||||
}),
|
||||
])
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||
|
||||
renderWithAuth(<CreateWorkspacePage />, {
|
||||
route:
|
||||
"/templates/" +
|
||||
MockTemplate.name +
|
||||
`/workspace?param.${param}=${paramValue}`,
|
||||
path: "/templates/:template/workspace",
|
||||
})
|
||||
}),
|
||||
await screen.findByDisplayValue(paramValue)
|
||||
})
|
||||
|
||||
it("uses default rich param values passed from the URL", async () => {
|
||||
const param = "first_parameter"
|
||||
const paramValue = "It works!"
|
||||
jest.spyOn(API, "getTemplateVersionSchema").mockResolvedValueOnce([
|
||||
mockParameterSchema({
|
||||
name: param,
|
||||
default_source_value: "",
|
||||
}),
|
||||
])
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||
|
||||
await waitFor(() =>
|
||||
renderWithAuth(<CreateWorkspacePage />, {
|
||||
route:
|
||||
"/templates/" +
|
||||
MockTemplate.name +
|
||||
`/workspace?param.${param}=${paramValue}`,
|
||||
path: "/templates/:template/workspace",
|
||||
}),
|
||||
)
|
||||
|
||||
await screen.findByDisplayValue(paramValue)
|
||||
})
|
||||
|
||||
it("rich parameter: number validation fails", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
])
|
||||
|
||||
await waitFor(() => renderCreateWorkspacePage())
|
||||
|
||||
const element = await screen.findByText("Create workspace")
|
||||
expect(element).toBeDefined()
|
||||
const secondParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter2.description,
|
||||
)
|
||||
expect(secondParameter).toBeDefined()
|
||||
|
||||
const secondParameterField = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter2.name,
|
||||
)
|
||||
expect(secondParameterField).toBeDefined()
|
||||
|
||||
fireEvent.change(secondParameterField, {
|
||||
target: { value: "4" },
|
||||
})
|
||||
fireEvent.submit(secondParameter)
|
||||
|
||||
const validationError = await screen.findByText(
|
||||
validationNumberNotInRangeText,
|
||||
)
|
||||
expect(validationError).toBeDefined()
|
||||
})
|
||||
|
||||
it("rich parameter: string validation fails", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter3,
|
||||
])
|
||||
|
||||
await waitFor(() => renderCreateWorkspacePage())
|
||||
|
||||
const element = await screen.findByText(createWorkspaceText)
|
||||
expect(element).toBeDefined()
|
||||
const thirdParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter3.description,
|
||||
)
|
||||
expect(thirdParameter).toBeDefined()
|
||||
|
||||
const thirdParameterField = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter3.name,
|
||||
)
|
||||
expect(thirdParameterField).toBeDefined()
|
||||
fireEvent.change(thirdParameterField, {
|
||||
target: { value: "1234" },
|
||||
})
|
||||
fireEvent.submit(thirdParameterField)
|
||||
|
||||
const validationError = await screen.findByText(validationPatternNotMatched)
|
||||
expect(validationError).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -30,6 +30,7 @@ const CreateWorkspacePage: FC = () => {
|
|||
})
|
||||
const {
|
||||
templates,
|
||||
templateParameters,
|
||||
templateSchema,
|
||||
selectedTemplate,
|
||||
getTemplateSchemaError,
|
||||
|
@ -57,6 +58,7 @@ const CreateWorkspacePage: FC = () => {
|
|||
templateName={templateName}
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
templateParameters={templateParameters}
|
||||
templateSchema={templateSchema}
|
||||
createWorkspaceErrors={{
|
||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
|
||||
|
|
|
@ -3,6 +3,9 @@ import {
|
|||
makeMockApiError,
|
||||
mockParameterSchema,
|
||||
MockTemplate,
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
} from "../../testHelpers/entities"
|
||||
import {
|
||||
CreateWorkspaceErrors,
|
||||
|
@ -108,3 +111,15 @@ CreateWorkspaceError.args = {
|
|||
name: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const RichParameters = Template.bind({})
|
||||
RichParameters.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
templateParameters: [
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
],
|
||||
createWorkspaceErrors: {},
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ import TextField from "@material-ui/core/TextField"
|
|||
import * as TypesGen from "api/typesGenerated"
|
||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { i18n } from "i18n"
|
||||
import { FC, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
|
||||
|
@ -30,6 +30,8 @@ export interface CreateWorkspacePageViewProps {
|
|||
templateName: string
|
||||
templates?: TypesGen.Template[]
|
||||
selectedTemplate?: TypesGen.Template
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||
|
||||
templateSchema?: TypesGen.ParameterSchema[]
|
||||
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
|
||||
canCreateForUser?: boolean
|
||||
|
@ -42,30 +44,36 @@ export interface CreateWorkspacePageViewProps {
|
|||
defaultParameterValues?: Record<string, string>
|
||||
}
|
||||
|
||||
const { t } = i18n
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
|
||||
})
|
||||
|
||||
export const CreateWorkspacePageView: FC<
|
||||
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
||||
> = (props) => {
|
||||
const { t } = useTranslation("createWorkspacePage")
|
||||
const styles = useStyles()
|
||||
const formFooterStyles = useFormFooterStyles()
|
||||
const [parameterValues, setParameterValues] = useState<
|
||||
Record<string, string>
|
||||
>(props.defaultParameterValues ?? {})
|
||||
const initialRichParameterValues = selectInitialRichParametersValues(
|
||||
props.templateParameters,
|
||||
props.defaultParameterValues,
|
||||
)
|
||||
|
||||
const { t } = useTranslation("createWorkspacePage")
|
||||
|
||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
||||
useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
template_id: props.selectedTemplate ? props.selectedTemplate.id : "",
|
||||
rich_parameter_values: initialRichParameterValues,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
|
||||
rich_parameter_values: ValidationSchemaForRichParameters(
|
||||
"createWorkspacePage",
|
||||
props.templateParameters,
|
||||
),
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
validationSchema,
|
||||
initialTouched: props.initialTouched,
|
||||
onSubmit: (request) => {
|
||||
if (!props.templateSchema) {
|
||||
|
@ -249,6 +257,48 @@ export const CreateWorkspacePageView: FC<
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Rich parameters */}
|
||||
{props.templateParameters && props.templateParameters.length > 0 && (
|
||||
<div className={styles.formSection}>
|
||||
<div className={styles.formSectionInfo}>
|
||||
<h2 className={styles.formSectionInfoTitle}>
|
||||
Rich template params
|
||||
</h2>
|
||||
<p className={styles.formSectionInfoDescription}>
|
||||
Those values are provided by your template‘s Terraform
|
||||
configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={4} // Spacing here is diff because the fields here don't have the MUI floating label spacing
|
||||
className={styles.formSectionFields}
|
||||
>
|
||||
{props.templateParameters.map((parameter, index) => (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={form.isSubmitting}
|
||||
index={index}
|
||||
key={parameter.name}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
})
|
||||
}}
|
||||
parameter={parameter}
|
||||
initialValue={workspaceBuildParameterValue(
|
||||
initialRichParameterValues,
|
||||
parameter,
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
<FormFooter
|
||||
styles={formFooterStyles}
|
||||
onCancel={props.onCancel}
|
||||
|
@ -332,3 +382,122 @@ const useFormFooterStyles = 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,
|
||||
): string => {
|
||||
const buildParameter = workspaceBuildParameters.find((buildParameter) => {
|
||||
return buildParameter.name === parameter.name
|
||||
})
|
||||
return (buildParameter && buildParameter.value) || ""
|
||||
}
|
||||
|
||||
export const ValidationSchemaForRichParameters = (
|
||||
ns: string,
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[],
|
||||
): 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()
|
||||
.required(t("validationRequiredParameter"))
|
||||
.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 === 0 &&
|
||||
templateParameter.validation_max === 0
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
break
|
||||
case "string":
|
||||
{
|
||||
if (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()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import {
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockWorkspace,
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
renderWithAuth,
|
||||
} from "testHelpers/renderHelpers"
|
||||
import * as API from "api/api"
|
||||
import i18next from "i18next"
|
||||
import { WorkspaceBuildParametersPage } from "./WorkspaceBuildParametersPage"
|
||||
|
||||
const { t } = i18next
|
||||
|
||||
const pageTitleText = t("title", { ns: "workspaceBuildParametersPage" })
|
||||
const validationNumberNotInRangeText = t("validationNumberNotInRange", {
|
||||
ns: "workspaceBuildParametersPage",
|
||||
min: "1",
|
||||
max: "3",
|
||||
})
|
||||
|
||||
const renderWorkspaceBuildParametersPage = () => {
|
||||
return renderWithAuth(<WorkspaceBuildParametersPage />, {
|
||||
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/build-parameters`,
|
||||
path: `/@:ownerName/:workspaceName/build-parameters`,
|
||||
})
|
||||
}
|
||||
|
||||
describe("WorkspaceBuildParametersPage", () => {
|
||||
it("renders without rich parameters", async () => {
|
||||
jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace)
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([])
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
])
|
||||
renderWorkspaceBuildParametersPage()
|
||||
|
||||
const element = await screen.findByText(pageTitleText)
|
||||
expect(element).toBeDefined()
|
||||
|
||||
const goBackButton = await screen.findByText("Go back")
|
||||
expect(goBackButton).toBeDefined()
|
||||
})
|
||||
|
||||
it("renders with rich parameter", async () => {
|
||||
jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace)
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
])
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
])
|
||||
|
||||
renderWorkspaceBuildParametersPage()
|
||||
|
||||
const element = await screen.findByText(pageTitleText)
|
||||
expect(element).toBeDefined()
|
||||
|
||||
const firstParameter = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter1.name,
|
||||
)
|
||||
expect(firstParameter).toBeDefined()
|
||||
|
||||
const secondParameter = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter2.name,
|
||||
)
|
||||
expect(secondParameter).toBeDefined()
|
||||
})
|
||||
|
||||
it("rich parameter: number validation fails", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
])
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
])
|
||||
renderWorkspaceBuildParametersPage()
|
||||
|
||||
const element = await screen.findByText(pageTitleText)
|
||||
expect(element).toBeDefined()
|
||||
const secondParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter2.description,
|
||||
)
|
||||
expect(secondParameter).toBeDefined()
|
||||
|
||||
const secondParameterField = await screen.findByLabelText(
|
||||
MockTemplateVersionParameter2.name,
|
||||
)
|
||||
expect(secondParameterField).toBeDefined()
|
||||
|
||||
fireEvent.change(secondParameterField, {
|
||||
target: { value: "4" },
|
||||
})
|
||||
fireEvent.submit(secondParameter)
|
||||
|
||||
const validationError = await screen.findByText(
|
||||
validationNumberNotInRangeText,
|
||||
)
|
||||
expect(validationError).toBeDefined()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,76 @@
|
|||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { pageTitle } from "util/page"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
import { workspaceBuildParametersMachine } from "xServices/workspace/workspaceBuildParametersXService"
|
||||
import {
|
||||
UpdateWorkspaceErrors,
|
||||
WorkspaceBuildParametersPageView,
|
||||
} from "./WorkspaceBuildParametersPageView"
|
||||
|
||||
export const WorkspaceBuildParametersPage: FC = () => {
|
||||
const { t } = useTranslation("workspaceBuildParametersPage")
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { owner: workspaceOwner, workspace: workspaceName } = useParams() as {
|
||||
owner: string
|
||||
workspace: string
|
||||
}
|
||||
const [state, send] = useMachine(workspaceBuildParametersMachine, {
|
||||
context: {
|
||||
workspaceOwner,
|
||||
workspaceName,
|
||||
},
|
||||
actions: {
|
||||
onUpdateWorkspace: (_, event) => {
|
||||
navigate(
|
||||
`/@${event.data.workspace_owner_name}/${event.data.workspace_name}`,
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
const {
|
||||
selectedWorkspace,
|
||||
templateParameters,
|
||||
workspaceBuildParameters,
|
||||
getWorkspaceError,
|
||||
getTemplateParametersError,
|
||||
getWorkspaceBuildParametersError,
|
||||
updateWorkspaceError,
|
||||
} = state.context
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(t("title"))}</title>
|
||||
</Helmet>
|
||||
<WorkspaceBuildParametersPageView
|
||||
workspace={selectedWorkspace}
|
||||
templateParameters={templateParameters}
|
||||
workspaceBuildParameters={workspaceBuildParameters}
|
||||
updatingWorkspace={state.matches("updatingWorkspace")}
|
||||
hasErrors={state.matches("error")}
|
||||
updateWorkspaceErrors={{
|
||||
[UpdateWorkspaceErrors.GET_WORKSPACE_ERROR]: getWorkspaceError,
|
||||
[UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR]:
|
||||
getTemplateParametersError,
|
||||
[UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR]:
|
||||
getWorkspaceBuildParametersError,
|
||||
[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR]: updateWorkspaceError,
|
||||
}}
|
||||
onCancel={() => {
|
||||
// Go back
|
||||
navigate(-1)
|
||||
}}
|
||||
onSubmit={(request) => {
|
||||
send({
|
||||
type: "UPDATE_WORKSPACE",
|
||||
request,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
MockTemplateVersionParameter4,
|
||||
MockWorkspace,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
WorkspaceBuildParametersPageView,
|
||||
WorkspaceBuildParametersPageViewProps,
|
||||
} from "./WorkspaceBuildParametersPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/WorkspaceBuildParametersPageView",
|
||||
component: WorkspaceBuildParametersPageView,
|
||||
} as ComponentMeta<typeof WorkspaceBuildParametersPageView>
|
||||
|
||||
const Template: Story<WorkspaceBuildParametersPageViewProps> = (args) => (
|
||||
<WorkspaceBuildParametersPageView {...args} />
|
||||
)
|
||||
|
||||
export const NoRichParametersDefined = Template.bind({})
|
||||
NoRichParametersDefined.args = {
|
||||
workspace: MockWorkspace,
|
||||
templateParameters: [],
|
||||
workspaceBuildParameters: [],
|
||||
updateWorkspaceErrors: {},
|
||||
initialTouched: {
|
||||
name: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const RichParametersDefined = Template.bind({})
|
||||
RichParametersDefined.args = {
|
||||
workspace: MockWorkspace,
|
||||
templateParameters: [
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockTemplateVersionParameter3,
|
||||
MockTemplateVersionParameter4,
|
||||
],
|
||||
workspaceBuildParameters: [],
|
||||
updateWorkspaceErrors: {},
|
||||
initialTouched: {
|
||||
name: true,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,317 @@
|
|||
import { FC } from "react"
|
||||
import { FullPageForm } from "components/FullPageForm/FullPageForm"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as TypesGen from "api/typesGenerated"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
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 { FormFooter } from "components/FormFooter/FormFooter"
|
||||
import * as Yup from "yup"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { GoBackButton } from "components/GoBackButton/GoBackButton"
|
||||
|
||||
export enum UpdateWorkspaceErrors {
|
||||
GET_WORKSPACE_ERROR = "getWorkspaceError",
|
||||
GET_TEMPLATE_PARAMETERS_ERROR = "getTemplateParametersError",
|
||||
GET_WORKSPACE_BUILD_PARAMETERS_ERROR = "getWorkspaceBuildParametersError",
|
||||
UPDATE_WORKSPACE_ERROR = "updateWorkspaceError",
|
||||
}
|
||||
|
||||
export interface WorkspaceBuildParametersPageViewProps {
|
||||
workspace?: TypesGen.Workspace
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||
workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[]
|
||||
|
||||
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
|
||||
updatingWorkspace: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (req: TypesGen.CreateWorkspaceBuildRequest) => void
|
||||
|
||||
hasErrors: boolean
|
||||
updateWorkspaceErrors: Partial<Record<UpdateWorkspaceErrors, Error | unknown>>
|
||||
}
|
||||
|
||||
export const WorkspaceBuildParametersPageView: FC<
|
||||
React.PropsWithChildren<WorkspaceBuildParametersPageViewProps>
|
||||
> = (props) => {
|
||||
const { t } = useTranslation("workspaceBuildParametersPage")
|
||||
const styles = useStyles()
|
||||
const formFooterStyles = useFormFooterStyles()
|
||||
|
||||
const initialRichParameterValues = selectInitialRichParametersValues(
|
||||
props.templateParameters,
|
||||
props.workspaceBuildParameters,
|
||||
)
|
||||
|
||||
const form: FormikContextType<TypesGen.CreateWorkspaceBuildRequest> =
|
||||
useFormik<TypesGen.CreateWorkspaceBuildRequest>({
|
||||
initialValues: {
|
||||
template_version_id: props.workspace
|
||||
? props.workspace.latest_build.template_version_id
|
||||
: "",
|
||||
transition: "start",
|
||||
rich_parameter_values: initialRichParameterValues,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
rich_parameter_values: ValidationSchemaForRichParameters(
|
||||
"workspaceBuildParametersPage",
|
||||
props.templateParameters,
|
||||
),
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
initialTouched: props.initialTouched,
|
||||
onSubmit: (request) => {
|
||||
props.onSubmit(
|
||||
stripImmutableParameters(request, props.templateParameters),
|
||||
)
|
||||
form.setSubmitting(false)
|
||||
},
|
||||
})
|
||||
|
||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceBuildRequest>(
|
||||
form,
|
||||
props.updateWorkspaceErrors[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR],
|
||||
)
|
||||
|
||||
{
|
||||
props.hasErrors && (
|
||||
<Stack>
|
||||
{Boolean(
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.GET_WORKSPACE_ERROR
|
||||
],
|
||||
) && (
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.GET_WORKSPACE_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{Boolean(
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR
|
||||
],
|
||||
) && (
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{Boolean(
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR
|
||||
],
|
||||
) && (
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FullPageForm title={t("title")} detail={t("detail")}>
|
||||
<Maybe
|
||||
condition={Boolean(
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR
|
||||
],
|
||||
)}
|
||||
>
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={
|
||||
props.updateWorkspaceErrors[
|
||||
UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Maybe>
|
||||
|
||||
<Maybe
|
||||
condition={Boolean(
|
||||
props.templateParameters && props.templateParameters.length === 0,
|
||||
)}
|
||||
>
|
||||
<div className={styles.formSection}>
|
||||
<AlertBanner severity="info" text={t("noParametersDefined")} />
|
||||
<div className={styles.goBackSection}>
|
||||
<GoBackButton onClick={props.onCancel} />
|
||||
</div>
|
||||
</div>
|
||||
</Maybe>
|
||||
|
||||
{props.templateParameters &&
|
||||
props.templateParameters.length > 0 &&
|
||||
props.workspaceBuildParameters && (
|
||||
<div className={styles.formSection}>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={4} // Spacing here is diff because the fields here don't have the MUI floating label spacing
|
||||
className={styles.formSectionFields}
|
||||
>
|
||||
{props.templateParameters.map((parameter, index) => (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={!parameter.mutable || form.isSubmitting}
|
||||
index={index}
|
||||
key={parameter.name}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
})
|
||||
}}
|
||||
parameter={parameter}
|
||||
initialValue={workspaceBuildParameterValue(
|
||||
initialRichParameterValues,
|
||||
parameter,
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<FormFooter
|
||||
styles={formFooterStyles}
|
||||
onCancel={props.onCancel}
|
||||
isLoading={props.updatingWorkspace}
|
||||
submitLabel={t("updateWorkspace")}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</FullPageForm>
|
||||
)
|
||||
}
|
||||
|
||||
const selectInitialRichParametersValues = (
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[],
|
||||
workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[],
|
||||
): 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 (workspaceBuildParameters) {
|
||||
const foundBuildParameter = workspaceBuildParameters.find(
|
||||
(buildParameter) => {
|
||||
return buildParameter.name === parameter.name
|
||||
},
|
||||
)
|
||||
if (foundBuildParameter) {
|
||||
parameterValue = foundBuildParameter.value
|
||||
}
|
||||
}
|
||||
|
||||
const buildParameter: TypesGen.WorkspaceBuildParameter = {
|
||||
name: parameter.name,
|
||||
value: parameterValue,
|
||||
}
|
||||
defaults.push(buildParameter)
|
||||
return
|
||||
}
|
||||
|
||||
let parameterValue = parameter.default_value
|
||||
if (workspaceBuildParameters) {
|
||||
const foundBuildParameter = workspaceBuildParameters.find(
|
||||
(buildParameter) => {
|
||||
return buildParameter.name === parameter.name
|
||||
},
|
||||
)
|
||||
if (foundBuildParameter) {
|
||||
parameterValue = foundBuildParameter.value
|
||||
}
|
||||
}
|
||||
|
||||
const buildParameter: TypesGen.WorkspaceBuildParameter = {
|
||||
name: parameter.name,
|
||||
value: parameterValue || "",
|
||||
}
|
||||
defaults.push(buildParameter)
|
||||
})
|
||||
return defaults
|
||||
}
|
||||
|
||||
const stripImmutableParameters = (
|
||||
request: TypesGen.CreateWorkspaceBuildRequest,
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[],
|
||||
): TypesGen.CreateWorkspaceBuildRequest => {
|
||||
if (!templateParameters || !request.rich_parameter_values) {
|
||||
return request
|
||||
}
|
||||
|
||||
const mutableBuildParameters = request.rich_parameter_values.filter(
|
||||
(buildParameter) =>
|
||||
templateParameters.find(
|
||||
(templateParameter) => templateParameter.name === buildParameter.name,
|
||||
)?.mutable,
|
||||
)
|
||||
|
||||
return {
|
||||
...request,
|
||||
rich_parameter_values: mutableBuildParameters,
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
goBackSection: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
marginTop: 32,
|
||||
},
|
||||
formSection: {
|
||||
marginTop: 20,
|
||||
},
|
||||
|
||||
formSectionFields: {
|
||||
width: "100%",
|
||||
},
|
||||
}))
|
||||
|
||||
const useFormFooterStyles = makeStyles((theme) => ({
|
||||
button: {
|
||||
minWidth: theme.spacing(23),
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
flexDirection: "row-reverse",
|
||||
gap: theme.spacing(2),
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}))
|
|
@ -30,6 +30,7 @@ const { t } = i18next
|
|||
// It renders the workspace page and waits for it be loaded
|
||||
const renderWorkspacePage = async () => {
|
||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
||||
jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([])
|
||||
renderWithAuth(<WorkspacePage />, {
|
||||
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
|
||||
path: "/@:username/:workspace",
|
||||
|
|
|
@ -20,6 +20,7 @@ export const WorkspacePage: FC = () => {
|
|||
workspace,
|
||||
getWorkspaceError,
|
||||
getTemplateWarning,
|
||||
getTemplateParametersWarning,
|
||||
checkPermissionsError,
|
||||
} = workspaceState.context
|
||||
const [quotaState, quotaSend] = useMachine(quotaMachine)
|
||||
|
@ -50,6 +51,12 @@ export const WorkspacePage: FC = () => {
|
|||
{Boolean(getTemplateWarning) && (
|
||||
<AlertBanner severity="error" error={getTemplateWarning} />
|
||||
)}
|
||||
{Boolean(getTemplateParametersWarning) && (
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={getTemplateParametersWarning}
|
||||
/>
|
||||
)}
|
||||
{Boolean(checkPermissionsError) && (
|
||||
<AlertBanner severity="error" error={checkPermissionsError} />
|
||||
)}
|
||||
|
|
|
@ -45,6 +45,7 @@ export const WorkspaceReadyPage = ({
|
|||
const {
|
||||
workspace,
|
||||
template,
|
||||
templateParameters,
|
||||
refreshWorkspaceWarning,
|
||||
builds,
|
||||
getBuildsError,
|
||||
|
@ -111,6 +112,7 @@ export const WorkspaceReadyPage = ({
|
|||
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
|
||||
handleCancel={() => workspaceSend({ type: "CANCEL" })}
|
||||
handleChangeVersion={() => navigate("change-version")}
|
||||
handleBuildParameters={() => navigate("build-parameters")}
|
||||
resources={workspace.latest_build.resources}
|
||||
builds={builds}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
|
@ -125,6 +127,7 @@ export const WorkspaceReadyPage = ({
|
|||
buildInfo={buildInfo}
|
||||
applicationsHost={applicationsHost}
|
||||
template={template}
|
||||
templateParameters={templateParameters}
|
||||
quota_budget={quotaState.context.quota?.budget}
|
||||
/>
|
||||
<DeleteDialog
|
||||
|
|
|
@ -631,11 +631,77 @@ export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = {
|
|||
count: 26,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter =
|
||||
{
|
||||
name: "first_parameter",
|
||||
type: "string",
|
||||
description: "This is first parameter",
|
||||
default_value: "abc",
|
||||
mutable: true,
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter =
|
||||
{
|
||||
name: "second_parameter",
|
||||
type: "number",
|
||||
description: "This is second parameter",
|
||||
default_value: "2",
|
||||
mutable: true,
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 1,
|
||||
validation_max: 3,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter =
|
||||
{
|
||||
name: "third_parameter",
|
||||
type: "string",
|
||||
description: "This is third parameter",
|
||||
default_value: "aaa",
|
||||
mutable: true,
|
||||
icon: "/icon/database.svg",
|
||||
options: [],
|
||||
validation_error: "No way!",
|
||||
validation_regex: "^[a-z]{3}$",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter =
|
||||
{
|
||||
name: "fourth_parameter",
|
||||
type: "string",
|
||||
description: "This is fourth parameter",
|
||||
default_value: "def",
|
||||
mutable: false,
|
||||
icon: "/icon/database.svg",
|
||||
options: [],
|
||||
validation_error: "",
|
||||
validation_regex: "",
|
||||
validation_min: 0,
|
||||
validation_max: 0,
|
||||
}
|
||||
|
||||
// requests the MockWorkspace
|
||||
export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = {
|
||||
name: "test",
|
||||
parameter_values: [],
|
||||
template_id: "test-template",
|
||||
rich_parameter_values: [
|
||||
{
|
||||
name: MockTemplateVersionParameter1.name,
|
||||
value: MockTemplateVersionParameter1.default_value,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const MockUserAgent: Types.UserAgent = {
|
||||
|
@ -1185,6 +1251,16 @@ export const MockAppearance: TypesGen.AppearanceConfig = {
|
|||
},
|
||||
}
|
||||
|
||||
export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = {
|
||||
name: MockTemplateVersionParameter1.name,
|
||||
value: "mock-abc",
|
||||
}
|
||||
|
||||
export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = {
|
||||
name: MockTemplateVersionParameter2.name,
|
||||
value: "3",
|
||||
}
|
||||
|
||||
export const mockParameterSchema = (
|
||||
partial: Partial<TypesGen.ParameterSchema>,
|
||||
): TypesGen.ParameterSchema => {
|
||||
|
|
|
@ -36,7 +36,7 @@ interface FormHelpers {
|
|||
export const getFormHelpers =
|
||||
<T>(form: FormikContextType<T>, error?: Error | unknown) =>
|
||||
(
|
||||
name: keyof T,
|
||||
name: string,
|
||||
HelperText: ReactNode = "",
|
||||
backendErrorName?: string,
|
||||
): FormHelpers => {
|
||||
|
|
|
@ -2,12 +2,14 @@ import {
|
|||
checkAuthorization,
|
||||
createWorkspace,
|
||||
getTemplates,
|
||||
getTemplateVersionRichParameters,
|
||||
getTemplateVersionSchema,
|
||||
} from "api/api"
|
||||
import {
|
||||
CreateWorkspaceRequest,
|
||||
ParameterSchema,
|
||||
Template,
|
||||
TemplateVersionParameter,
|
||||
User,
|
||||
Workspace,
|
||||
} from "api/typesGenerated"
|
||||
|
@ -19,11 +21,13 @@ type CreateWorkspaceContext = {
|
|||
templateName: string
|
||||
templates?: Template[]
|
||||
selectedTemplate?: Template
|
||||
templateParameters?: TemplateVersionParameter[]
|
||||
templateSchema?: ParameterSchema[]
|
||||
createWorkspaceRequest?: CreateWorkspaceRequest
|
||||
createdWorkspace?: Workspace
|
||||
createWorkspaceError?: Error | unknown
|
||||
getTemplatesError?: Error | unknown
|
||||
getTemplateParametersError?: Error | unknown
|
||||
getTemplateSchemaError?: Error | unknown
|
||||
permissions?: Record<string, boolean>
|
||||
checkPermissionsError?: Error | unknown
|
||||
|
@ -52,6 +56,9 @@ export const createWorkspaceMachine = createMachine(
|
|||
getTemplates: {
|
||||
data: Template[]
|
||||
}
|
||||
getTemplateParameters: {
|
||||
data: TemplateVersionParameter[]
|
||||
}
|
||||
getTemplateSchema: {
|
||||
data: ParameterSchema[]
|
||||
}
|
||||
|
@ -88,7 +95,7 @@ export const createWorkspaceMachine = createMachine(
|
|||
src: "getTemplateSchema",
|
||||
onDone: {
|
||||
actions: ["assignTemplateSchema"],
|
||||
target: "checkingPermissions",
|
||||
target: "gettingTemplateParameters",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignGetTemplateSchemaError"],
|
||||
|
@ -96,6 +103,20 @@ export const createWorkspaceMachine = createMachine(
|
|||
},
|
||||
},
|
||||
},
|
||||
gettingTemplateParameters: {
|
||||
entry: "clearGetTemplateParametersError",
|
||||
invoke: {
|
||||
src: "getTemplateParameters",
|
||||
onDone: {
|
||||
actions: ["assignTemplateParameters"],
|
||||
target: "checkingPermissions",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignGetTemplateParametersError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
checkingPermissions: {
|
||||
entry: "clearCheckPermissionsError",
|
||||
invoke: {
|
||||
|
@ -145,6 +166,17 @@ export const createWorkspaceMachine = createMachine(
|
|||
{
|
||||
services: {
|
||||
getTemplates: (context) => getTemplates(context.organizationId),
|
||||
getTemplateParameters: (context) => {
|
||||
const { selectedTemplate } = context
|
||||
|
||||
if (!selectedTemplate) {
|
||||
throw new Error("No selected template")
|
||||
}
|
||||
|
||||
return getTemplateVersionRichParameters(
|
||||
selectedTemplate.active_version_id,
|
||||
)
|
||||
},
|
||||
getTemplateSchema: (context) => {
|
||||
const { selectedTemplate } = context
|
||||
|
||||
|
@ -206,11 +238,13 @@ export const createWorkspaceMachine = createMachine(
|
|||
return templates.length > 0 ? templates[0] : undefined
|
||||
},
|
||||
}),
|
||||
assignTemplateParameters: assign({
|
||||
templateParameters: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateSchema: assign({
|
||||
// Only show parameters that are allowed to be overridden.
|
||||
// CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155
|
||||
templateSchema: (_, event) =>
|
||||
event.data.filter((param) => param.allow_override_source),
|
||||
templateSchema: (_, event) => event.data,
|
||||
}),
|
||||
assignPermissions: assign({
|
||||
permissions: (_, event) => event.data as Record<string, boolean>,
|
||||
|
@ -239,6 +273,12 @@ export const createWorkspaceMachine = createMachine(
|
|||
clearGetTemplatesError: assign({
|
||||
getTemplatesError: (_) => undefined,
|
||||
}),
|
||||
assignGetTemplateParametersError: assign({
|
||||
getTemplateParametersError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetTemplateParametersError: assign({
|
||||
getTemplateParametersError: (_) => undefined,
|
||||
}),
|
||||
assignGetTemplateSchemaError: assign({
|
||||
getTemplateSchemaError: (_, event) => event.data,
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
import {
|
||||
getTemplateVersionRichParameters,
|
||||
getWorkspaceByOwnerAndName,
|
||||
getWorkspaceBuildParameters,
|
||||
postWorkspaceBuild,
|
||||
} from "api/api"
|
||||
import {
|
||||
CreateWorkspaceBuildRequest,
|
||||
Template,
|
||||
TemplateVersionParameter,
|
||||
Workspace,
|
||||
WorkspaceBuild,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated"
|
||||
import { assign, createMachine } from "xstate"
|
||||
|
||||
type WorkspaceBuildParametersContext = {
|
||||
workspaceOwner: string
|
||||
workspaceName: string
|
||||
|
||||
selectedWorkspace?: Workspace
|
||||
selectedTemplate?: Template
|
||||
templateParameters?: TemplateVersionParameter[]
|
||||
workspaceBuildParameters?: WorkspaceBuildParameter[]
|
||||
|
||||
createWorkspaceBuildRequest?: CreateWorkspaceBuildRequest
|
||||
|
||||
getWorkspaceError?: Error | unknown
|
||||
getTemplateParametersError?: Error | unknown
|
||||
getWorkspaceBuildParametersError?: Error | unknown
|
||||
updateWorkspaceError?: Error | unknown
|
||||
}
|
||||
|
||||
type UpdateWorkspaceEvent = {
|
||||
type: "UPDATE_WORKSPACE"
|
||||
request: CreateWorkspaceBuildRequest
|
||||
}
|
||||
|
||||
export const workspaceBuildParametersMachine = createMachine(
|
||||
{
|
||||
id: "workspaceBuildParametersState",
|
||||
predictableActionArguments: true,
|
||||
tsTypes:
|
||||
{} as import("./workspaceBuildParametersXService.typegen").Typegen0,
|
||||
schema: {
|
||||
context: {} as WorkspaceBuildParametersContext,
|
||||
events: {} as UpdateWorkspaceEvent,
|
||||
services: {} as {
|
||||
getWorkspace: {
|
||||
data: Workspace
|
||||
}
|
||||
getTemplateParameters: {
|
||||
data: TemplateVersionParameter[]
|
||||
}
|
||||
getWorkspaceBuildParameters: {
|
||||
data: WorkspaceBuildParameter[]
|
||||
}
|
||||
updateWorkspace: {
|
||||
data: WorkspaceBuild
|
||||
}
|
||||
},
|
||||
},
|
||||
initial: "gettingWorkspace",
|
||||
states: {
|
||||
gettingWorkspace: {
|
||||
entry: "clearGetWorkspaceError",
|
||||
invoke: {
|
||||
src: "getWorkspace",
|
||||
onDone: [
|
||||
{
|
||||
actions: ["assignWorkspace"],
|
||||
target: "gettingTemplateParameters",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: ["assignGetWorkspaceError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingTemplateParameters: {
|
||||
entry: "clearGetTemplateParametersError",
|
||||
invoke: {
|
||||
src: "getTemplateParameters",
|
||||
onDone: [
|
||||
{
|
||||
actions: ["assignTemplateParameters"],
|
||||
target: "gettingWorkspaceBuildParameters",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: ["assignGetTemplateParametersError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingWorkspaceBuildParameters: {
|
||||
entry: "clearGetWorkspaceBuildParametersError",
|
||||
invoke: {
|
||||
src: "getWorkspaceBuildParameters",
|
||||
onDone: {
|
||||
actions: ["assignWorkspaceBuildParameters"],
|
||||
target: "fillingParams",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignGetWorkspaceBuildParametersError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
fillingParams: {
|
||||
on: {
|
||||
UPDATE_WORKSPACE: {
|
||||
actions: ["assignCreateWorkspaceBuildRequest"],
|
||||
target: "updatingWorkspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
updatingWorkspace: {
|
||||
entry: "clearUpdateWorkspaceError",
|
||||
invoke: {
|
||||
src: "updateWorkspace",
|
||||
onDone: {
|
||||
actions: ["onUpdateWorkspace"],
|
||||
target: "updated",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignUpdateWorkspaceError"],
|
||||
target: "fillingParams",
|
||||
},
|
||||
},
|
||||
},
|
||||
updated: {
|
||||
entry: "onUpdateWorkspace",
|
||||
type: "final",
|
||||
},
|
||||
error: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
services: {
|
||||
getWorkspace: (context) => {
|
||||
const { workspaceOwner, workspaceName } = context
|
||||
return getWorkspaceByOwnerAndName(workspaceOwner, workspaceName)
|
||||
},
|
||||
getTemplateParameters: (context) => {
|
||||
const { selectedWorkspace } = context
|
||||
|
||||
if (!selectedWorkspace) {
|
||||
throw new Error("No workspace selected")
|
||||
}
|
||||
|
||||
return getTemplateVersionRichParameters(
|
||||
selectedWorkspace.latest_build.template_version_id,
|
||||
)
|
||||
},
|
||||
getWorkspaceBuildParameters: (context) => {
|
||||
const { selectedWorkspace } = context
|
||||
|
||||
if (!selectedWorkspace) {
|
||||
throw new Error("No workspace selected")
|
||||
}
|
||||
|
||||
return getWorkspaceBuildParameters(selectedWorkspace.latest_build.id)
|
||||
},
|
||||
updateWorkspace: (context) => {
|
||||
const { selectedWorkspace, createWorkspaceBuildRequest } = context
|
||||
|
||||
if (!selectedWorkspace) {
|
||||
throw new Error("No workspace selected")
|
||||
}
|
||||
|
||||
if (!createWorkspaceBuildRequest) {
|
||||
throw new Error("No workspace build request")
|
||||
}
|
||||
|
||||
return postWorkspaceBuild(
|
||||
selectedWorkspace.id,
|
||||
createWorkspaceBuildRequest,
|
||||
)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
assignWorkspace: assign({
|
||||
selectedWorkspace: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateParameters: assign({
|
||||
templateParameters: (_, event) => event.data,
|
||||
}),
|
||||
assignWorkspaceBuildParameters: assign({
|
||||
workspaceBuildParameters: (_, event) => event.data,
|
||||
}),
|
||||
|
||||
assignCreateWorkspaceBuildRequest: assign({
|
||||
createWorkspaceBuildRequest: (_, event) => event.request,
|
||||
}),
|
||||
assignGetWorkspaceError: assign({
|
||||
getWorkspaceError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetWorkspaceError: assign({
|
||||
getWorkspaceError: (_) => undefined,
|
||||
}),
|
||||
assignGetTemplateParametersError: assign({
|
||||
getTemplateParametersError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetTemplateParametersError: assign({
|
||||
getTemplateParametersError: (_) => undefined,
|
||||
}),
|
||||
clearGetWorkspaceBuildParametersError: assign({
|
||||
getWorkspaceBuildParametersError: (_) => undefined,
|
||||
}),
|
||||
assignGetWorkspaceBuildParametersError: assign({
|
||||
getWorkspaceBuildParametersError: (_, event) => event.data,
|
||||
}),
|
||||
clearUpdateWorkspaceError: assign({
|
||||
updateWorkspaceError: (_) => undefined,
|
||||
}),
|
||||
assignUpdateWorkspaceError: assign({
|
||||
updateWorkspaceError: (_, event) => event.data,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
|
@ -43,6 +43,8 @@ const moreBuildsAvailable = (
|
|||
const Language = {
|
||||
getTemplateWarning:
|
||||
"Error updating workspace: latest template could not be fetched.",
|
||||
getTemplateParametersWarning:
|
||||
"Error updating workspace: template parameters could not be fetched.",
|
||||
buildError: "Workspace action failed.",
|
||||
}
|
||||
|
||||
|
@ -53,11 +55,13 @@ export interface WorkspaceContext {
|
|||
eventSource?: EventSource
|
||||
workspace?: TypesGen.Workspace
|
||||
template?: TypesGen.Template
|
||||
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||
build?: TypesGen.WorkspaceBuild
|
||||
getWorkspaceError?: Error | unknown
|
||||
// these are labeled as warnings because they don't make the page unusable
|
||||
refreshWorkspaceWarning?: Error | unknown
|
||||
getTemplateWarning: Error | unknown
|
||||
getTemplateParametersWarning: Error | unknown
|
||||
// Builds
|
||||
builds?: TypesGen.WorkspaceBuild[]
|
||||
getBuildsError?: Error | unknown
|
||||
|
@ -130,6 +134,9 @@ export const workspaceMachine = createMachine(
|
|||
getTemplate: {
|
||||
data: TypesGen.Template
|
||||
}
|
||||
getTemplateParameters: {
|
||||
data: TypesGen.TemplateVersionParameter[]
|
||||
}
|
||||
startWorkspaceWithLatestTemplate: {
|
||||
data: TypesGen.WorkspaceBuild
|
||||
}
|
||||
|
@ -191,14 +198,14 @@ export const workspaceMachine = createMachine(
|
|||
tags: "loading",
|
||||
},
|
||||
gettingTemplate: {
|
||||
entry: "clearGettingTemplateWarning",
|
||||
entry: "clearGetTemplateWarning",
|
||||
invoke: {
|
||||
src: "getTemplate",
|
||||
id: "getTemplate",
|
||||
onDone: [
|
||||
{
|
||||
actions: "assignTemplate",
|
||||
target: "gettingPermissions",
|
||||
target: "gettingTemplateParameters",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
|
@ -213,6 +220,29 @@ export const workspaceMachine = createMachine(
|
|||
},
|
||||
tags: "loading",
|
||||
},
|
||||
gettingTemplateParameters: {
|
||||
entry: "clearGetTemplateParametersWarning",
|
||||
invoke: {
|
||||
src: "getTemplateParameters",
|
||||
id: "getTemplateParameters",
|
||||
onDone: [
|
||||
{
|
||||
actions: "assignTemplateParameters",
|
||||
target: "gettingPermissions",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
actions: [
|
||||
"assignGetTemplateParametersWarning",
|
||||
"displayGetTemplateParametersWarning",
|
||||
],
|
||||
target: "error",
|
||||
},
|
||||
],
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
gettingPermissions: {
|
||||
entry: "clearGetPermissionsError",
|
||||
invoke: {
|
||||
|
@ -506,6 +536,9 @@ export const workspaceMachine = createMachine(
|
|||
assignTemplate: assign({
|
||||
template: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplateParameters: assign({
|
||||
templateParameters: (_, event) => event.data,
|
||||
}),
|
||||
assignPermissions: assign({
|
||||
// Setting event.data as Permissions to be more stricted. So we know
|
||||
// what permissions we asked for.
|
||||
|
@ -566,9 +599,18 @@ export const workspaceMachine = createMachine(
|
|||
displayGetTemplateWarning: () => {
|
||||
displayError(Language.getTemplateWarning)
|
||||
},
|
||||
clearGettingTemplateWarning: assign({
|
||||
clearGetTemplateWarning: assign({
|
||||
getTemplateWarning: (_) => undefined,
|
||||
}),
|
||||
assignGetTemplateParametersWarning: assign({
|
||||
getTemplateParametersWarning: (_, event) => event.data,
|
||||
}),
|
||||
displayGetTemplateParametersWarning: () => {
|
||||
displayError(Language.getTemplateParametersWarning)
|
||||
},
|
||||
clearGetTemplateParametersWarning: assign({
|
||||
getTemplateParametersWarning: (_) => undefined,
|
||||
}),
|
||||
// Timeline
|
||||
assignBuilds: assign({
|
||||
builds: (_, event) => event.data,
|
||||
|
@ -629,6 +671,15 @@ export const workspaceMachine = createMachine(
|
|||
throw Error("Cannot get template without workspace")
|
||||
}
|
||||
},
|
||||
getTemplateParameters: async (context) => {
|
||||
if (context.workspace) {
|
||||
return await API.getTemplateVersionRichParameters(
|
||||
context.workspace.latest_build.template_version_id,
|
||||
)
|
||||
} else {
|
||||
throw Error("Cannot get template parameters without workspace")
|
||||
}
|
||||
},
|
||||
startWorkspaceWithLatestTemplate: (context) => async (send) => {
|
||||
if (context.workspace && context.template) {
|
||||
const startWorkspacePromise = await API.startWorkspace(
|
||||
|
|
Loading…
Reference in New Issue