mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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:
@ -7,6 +7,7 @@ import GroupsPage from "pages/GroupsPage/GroupsPage"
|
|||||||
import LoginPage from "pages/LoginPage/LoginPage"
|
import LoginPage from "pages/LoginPage/LoginPage"
|
||||||
import { SetupPage } from "pages/SetupPage/SetupPage"
|
import { SetupPage } from "pages/SetupPage/SetupPage"
|
||||||
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage"
|
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage"
|
||||||
|
import { WorkspaceBuildParametersPage } from "pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage"
|
||||||
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
|
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
|
||||||
import UsersPage from "pages/UsersPage/UsersPage"
|
import UsersPage from "pages/UsersPage/UsersPage"
|
||||||
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"
|
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"
|
||||||
@ -213,6 +214,10 @@ export const AppRouter: FC = () => {
|
|||||||
path="change-version"
|
path="change-version"
|
||||||
element={<WorkspaceChangeVersionPage />}
|
element={<WorkspaceChangeVersionPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="build-parameters"
|
||||||
|
element={<WorkspaceBuildParametersPage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import axios, { AxiosRequestHeaders } from "axios"
|
import axios, { AxiosRequestHeaders } from "axios"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import * as Types from "./types"
|
import * as Types from "./types"
|
||||||
import { WorkspaceBuildTransition } from "./types"
|
|
||||||
import * as TypesGen from "./typesGenerated"
|
import * as TypesGen from "./typesGenerated"
|
||||||
|
|
||||||
export const hardCodedCSRFCookie = (): string => {
|
export const hardCodedCSRFCookie = (): string => {
|
||||||
@ -288,6 +287,15 @@ export const getTemplateVersionParameters = async (
|
|||||||
return response.data
|
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 (
|
export const createTemplate = async (
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
data: TypesGen.CreateTemplateRequest,
|
data: TypesGen.CreateTemplateRequest,
|
||||||
@ -390,26 +398,29 @@ export const getWorkspaceByOwnerAndName = async (
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const postWorkspaceBuild =
|
export const postWorkspaceBuild = async (
|
||||||
(transition: WorkspaceBuildTransition) =>
|
workspaceId: string,
|
||||||
async (
|
data: TypesGen.CreateWorkspaceBuildRequest,
|
||||||
workspaceId: string,
|
): Promise<TypesGen.WorkspaceBuild> => {
|
||||||
template_version_id?: string,
|
const response = await axios.post(
|
||||||
): Promise<TypesGen.WorkspaceBuild> => {
|
`/api/v2/workspaces/${workspaceId}/builds`,
|
||||||
const payload = {
|
data,
|
||||||
transition,
|
)
|
||||||
template_version_id,
|
return response.data
|
||||||
}
|
}
|
||||||
const response = await axios.post(
|
|
||||||
`/api/v2/workspaces/${workspaceId}/builds`,
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
export const startWorkspace = postWorkspaceBuild("start")
|
export const startWorkspace = (
|
||||||
export const stopWorkspace = postWorkspaceBuild("stop")
|
workspaceId: string,
|
||||||
export const deleteWorkspace = postWorkspaceBuild("delete")
|
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 (
|
export const cancelWorkspaceBuild = async (
|
||||||
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
|
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
|
||||||
@ -790,3 +801,12 @@ export const updateWorkspaceVersion = async (
|
|||||||
const template = await getTemplate(workspace.template_id)
|
const template = await getTemplate(workspace.template_id)
|
||||||
return startWorkspace(workspace.id, template.active_version_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 BlockIcon from "@material-ui/icons/Block"
|
||||||
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
|
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
|
||||||
import UpdateOutlined from "@material-ui/icons/UpdateOutlined"
|
import UpdateOutlined from "@material-ui/icons/UpdateOutlined"
|
||||||
|
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
|
||||||
import CropSquareIcon from "@material-ui/icons/CropSquare"
|
import CropSquareIcon from "@material-ui/icons/CropSquare"
|
||||||
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
|
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
|
||||||
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
|
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>> = ({
|
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||||
handleAction,
|
handleAction,
|
||||||
}) => {
|
}) => {
|
||||||
|
19
site/src/components/GoBackButton/GoBackButton.tsx
Normal file
19
site/src/components/GoBackButton/GoBackButton.tsx
Normal file
@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}
|
224
site/src/components/RichParameterInput/RichParameterInput.tsx
Normal file
224
site/src/components/RichParameterInput/RichParameterInput.tsx
Normal file
@ -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
|
handleUpdate: () => void
|
||||||
handleCancel: () => void
|
handleCancel: () => void
|
||||||
handleChangeVersion: () => void
|
handleChangeVersion: () => void
|
||||||
|
handleBuildParameters: () => void
|
||||||
isUpdating: boolean
|
isUpdating: boolean
|
||||||
workspace: TypesGen.Workspace
|
workspace: TypesGen.Workspace
|
||||||
resources?: TypesGen.WorkspaceResource[]
|
resources?: TypesGen.WorkspaceResource[]
|
||||||
@ -56,6 +57,7 @@ export interface WorkspaceProps {
|
|||||||
buildInfo?: TypesGen.BuildInfoResponse
|
buildInfo?: TypesGen.BuildInfoResponse
|
||||||
applicationsHost?: string
|
applicationsHost?: string
|
||||||
template?: TypesGen.Template
|
template?: TypesGen.Template
|
||||||
|
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||||
quota_budget?: number
|
quota_budget?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +72,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
handleUpdate,
|
handleUpdate,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleChangeVersion,
|
handleChangeVersion,
|
||||||
|
handleBuildParameters,
|
||||||
workspace,
|
workspace,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
resources,
|
resources,
|
||||||
@ -81,6 +84,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
buildInfo,
|
buildInfo,
|
||||||
applicationsHost,
|
applicationsHost,
|
||||||
template,
|
template,
|
||||||
|
templateParameters,
|
||||||
quota_budget,
|
quota_budget,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("workspacePage")
|
const { t } = useTranslation("workspacePage")
|
||||||
@ -122,7 +126,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
if (template !== undefined) {
|
if (template !== undefined) {
|
||||||
transitionStats = ActiveTransition(template, workspace)
|
transitionStats = ActiveTransition(template, workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Margins>
|
<Margins>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@ -138,6 +141,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
/>
|
/>
|
||||||
<WorkspaceActions
|
<WorkspaceActions
|
||||||
workspaceStatus={workspace.latest_build.status}
|
workspaceStatus={workspace.latest_build.status}
|
||||||
|
hasTemplateParameters={
|
||||||
|
templateParameters ? templateParameters.length > 0 : false
|
||||||
|
}
|
||||||
isOutdated={workspace.outdated}
|
isOutdated={workspace.outdated}
|
||||||
handleStart={handleStart}
|
handleStart={handleStart}
|
||||||
handleStop={handleStop}
|
handleStop={handleStop}
|
||||||
@ -145,6 +151,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
handleUpdate={handleUpdate}
|
handleUpdate={handleUpdate}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleChangeVersion={handleChangeVersion}
|
handleChangeVersion={handleChangeVersion}
|
||||||
|
handleBuildParameters={handleBuildParameters}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -12,6 +12,7 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||||||
workspaceStatus={
|
workspaceStatus={
|
||||||
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
||||||
}
|
}
|
||||||
|
hasTemplateParameters={props.hasTemplateParameters ?? false}
|
||||||
isOutdated={props.isOutdated ?? false}
|
isOutdated={props.isOutdated ?? false}
|
||||||
handleStart={jest.fn()}
|
handleStart={jest.fn()}
|
||||||
handleStop={jest.fn()}
|
handleStop={jest.fn()}
|
||||||
@ -19,6 +20,7 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||||||
handleUpdate={jest.fn()}
|
handleUpdate={jest.fn()}
|
||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
handleChangeVersion={jest.fn()}
|
handleChangeVersion={jest.fn()}
|
||||||
|
handleBuildParameters={jest.fn()}
|
||||||
isUpdating={false}
|
isUpdating={false}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
@ -30,6 +32,7 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||||||
workspaceStatus={
|
workspaceStatus={
|
||||||
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status
|
||||||
}
|
}
|
||||||
|
hasTemplateParameters={props.hasTemplateParameters ?? false}
|
||||||
isOutdated={props.isOutdated ?? false}
|
isOutdated={props.isOutdated ?? false}
|
||||||
handleStart={jest.fn()}
|
handleStart={jest.fn()}
|
||||||
handleStop={jest.fn()}
|
handleStop={jest.fn()}
|
||||||
@ -37,6 +40,7 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
|||||||
handleUpdate={jest.fn()}
|
handleUpdate={jest.fn()}
|
||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
handleChangeVersion={jest.fn()}
|
handleChangeVersion={jest.fn()}
|
||||||
|
handleBuildParameters={jest.fn()}
|
||||||
isUpdating={false}
|
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", () => {
|
describe("when the workspace is stopping", () => {
|
||||||
it("primary is stopping; cancel is available; no secondary", async () => {
|
it("primary is stopping; cancel is available; no secondary", async () => {
|
||||||
await renderComponent({
|
await renderComponent({
|
||||||
|
@ -7,14 +7,16 @@ import {
|
|||||||
ChangeVersionButton,
|
ChangeVersionButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
DisabledButton,
|
DisabledButton,
|
||||||
|
BuildParametersButton,
|
||||||
StartButton,
|
StartButton,
|
||||||
StopButton,
|
StopButton,
|
||||||
UpdateButton,
|
UpdateButton,
|
||||||
} from "../DropdownButton/ActionCtas"
|
} from "../DropdownButton/ActionCtas"
|
||||||
import { ButtonMapping, ButtonTypesEnum, statusToAbilities } from "./constants"
|
import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants"
|
||||||
|
|
||||||
export interface WorkspaceActionsProps {
|
export interface WorkspaceActionsProps {
|
||||||
workspaceStatus: WorkspaceStatus
|
workspaceStatus: WorkspaceStatus
|
||||||
|
hasTemplateParameters: boolean
|
||||||
isOutdated: boolean
|
isOutdated: boolean
|
||||||
handleStart: () => void
|
handleStart: () => void
|
||||||
handleStop: () => void
|
handleStop: () => void
|
||||||
@ -22,12 +24,14 @@ export interface WorkspaceActionsProps {
|
|||||||
handleUpdate: () => void
|
handleUpdate: () => void
|
||||||
handleCancel: () => void
|
handleCancel: () => void
|
||||||
handleChangeVersion: () => void
|
handleChangeVersion: () => void
|
||||||
|
handleBuildParameters: () => void
|
||||||
isUpdating: boolean
|
isUpdating: boolean
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||||
workspaceStatus,
|
workspaceStatus,
|
||||||
|
hasTemplateParameters,
|
||||||
isOutdated,
|
isOutdated,
|
||||||
handleStart,
|
handleStart,
|
||||||
handleStop,
|
handleStop,
|
||||||
@ -35,11 +39,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||||||
handleUpdate,
|
handleUpdate,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleChangeVersion,
|
handleChangeVersion,
|
||||||
|
handleBuildParameters,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("workspacePage")
|
const { t } = useTranslation("workspacePage")
|
||||||
const { canCancel, canAcceptJobs, actions } =
|
const { canCancel, canAcceptJobs, actions } = buttonAbilities(
|
||||||
statusToAbilities[workspaceStatus]
|
workspaceStatus,
|
||||||
|
hasTemplateParameters,
|
||||||
|
)
|
||||||
const canBeUpdated = isOutdated && canAcceptJobs
|
const canBeUpdated = isOutdated && canAcceptJobs
|
||||||
|
|
||||||
// A mapping of button type to the corresponding React component
|
// A mapping of button type to the corresponding React component
|
||||||
@ -51,6 +58,9 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||||||
[ButtonTypesEnum.changeVersion]: (
|
[ButtonTypesEnum.changeVersion]: (
|
||||||
<ChangeVersionButton handleAction={handleChangeVersion} />
|
<ChangeVersionButton handleAction={handleChangeVersion} />
|
||||||
),
|
),
|
||||||
|
[ButtonTypesEnum.buildParameters]: (
|
||||||
|
<BuildParametersButton handleAction={handleBuildParameters} />
|
||||||
|
),
|
||||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||||
[ButtonTypesEnum.starting]: (
|
[ButtonTypesEnum.starting]: (
|
||||||
<ActionLoadingButton label={t("actionButton.starting")} />
|
<ActionLoadingButton label={t("actionButton.starting")} />
|
||||||
|
@ -12,6 +12,7 @@ export enum ButtonTypesEnum {
|
|||||||
update = "update",
|
update = "update",
|
||||||
updating = "updating",
|
updating = "updating",
|
||||||
changeVersion = "changeVersion",
|
changeVersion = "changeVersion",
|
||||||
|
buildParameters = "buildParameters",
|
||||||
// disabled buttons
|
// disabled buttons
|
||||||
canceling = "canceling",
|
canceling = "canceling",
|
||||||
deleted = "deleted",
|
deleted = "deleted",
|
||||||
@ -28,7 +29,24 @@ interface WorkspaceAbilities {
|
|||||||
canAcceptJobs: boolean
|
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: {
|
starting: {
|
||||||
actions: [ButtonTypesEnum.starting],
|
actions: [ButtonTypesEnum.starting],
|
||||||
canCancel: true,
|
canCancel: true,
|
||||||
@ -37,6 +55,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||||||
running: {
|
running: {
|
||||||
actions: [
|
actions: [
|
||||||
ButtonTypesEnum.stop,
|
ButtonTypesEnum.stop,
|
||||||
|
ButtonTypesEnum.buildParameters,
|
||||||
ButtonTypesEnum.changeVersion,
|
ButtonTypesEnum.changeVersion,
|
||||||
ButtonTypesEnum.delete,
|
ButtonTypesEnum.delete,
|
||||||
],
|
],
|
||||||
@ -51,6 +70,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||||||
stopped: {
|
stopped: {
|
||||||
actions: [
|
actions: [
|
||||||
ButtonTypesEnum.start,
|
ButtonTypesEnum.start,
|
||||||
|
ButtonTypesEnum.buildParameters,
|
||||||
ButtonTypesEnum.changeVersion,
|
ButtonTypesEnum.changeVersion,
|
||||||
ButtonTypesEnum.delete,
|
ButtonTypesEnum.delete,
|
||||||
],
|
],
|
||||||
@ -61,6 +81,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||||||
actions: [
|
actions: [
|
||||||
ButtonTypesEnum.start,
|
ButtonTypesEnum.start,
|
||||||
ButtonTypesEnum.stop,
|
ButtonTypesEnum.stop,
|
||||||
|
ButtonTypesEnum.buildParameters,
|
||||||
ButtonTypesEnum.changeVersion,
|
ButtonTypesEnum.changeVersion,
|
||||||
ButtonTypesEnum.delete,
|
ButtonTypesEnum.delete,
|
||||||
],
|
],
|
||||||
@ -71,6 +92,7 @@ export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
|||||||
failed: {
|
failed: {
|
||||||
actions: [
|
actions: [
|
||||||
ButtonTypesEnum.start,
|
ButtonTypesEnum.start,
|
||||||
|
ButtonTypesEnum.buildParameters,
|
||||||
ButtonTypesEnum.changeVersion,
|
ButtonTypesEnum.changeVersion,
|
||||||
ButtonTypesEnum.delete,
|
ButtonTypesEnum.delete,
|
||||||
],
|
],
|
||||||
|
@ -2,5 +2,8 @@
|
|||||||
"templateLabel": "Template",
|
"templateLabel": "Template",
|
||||||
"nameLabel": "Workspace Name",
|
"nameLabel": "Workspace Name",
|
||||||
"ownerLabel": "Owner",
|
"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 templateSettingsPage from "./templateSettingsPage.json"
|
||||||
import templateVersionPage from "./templateVersionPage.json"
|
import templateVersionPage from "./templateVersionPage.json"
|
||||||
import loginPage from "./loginPage.json"
|
import loginPage from "./loginPage.json"
|
||||||
|
import workspaceBuildParametersPage from "./workspaceBuildParametersPage.json"
|
||||||
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
|
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
|
||||||
import workspaceSchedulePage from "./workspaceSchedulePage.json"
|
import workspaceSchedulePage from "./workspaceSchedulePage.json"
|
||||||
import appearanceSettings from "./appearanceSettings.json"
|
import appearanceSettings from "./appearanceSettings.json"
|
||||||
@ -33,6 +34,7 @@ export const en = {
|
|||||||
templateSettingsPage,
|
templateSettingsPage,
|
||||||
templateVersionPage,
|
templateVersionPage,
|
||||||
loginPage,
|
loginPage,
|
||||||
|
workspaceBuildParametersPage,
|
||||||
workspaceChangeVersionPage,
|
workspaceChangeVersionPage,
|
||||||
workspaceSchedulePage,
|
workspaceSchedulePage,
|
||||||
appearanceSettings,
|
appearanceSettings,
|
||||||
|
9
site/src/i18n/en/workspaceBuildParametersPage.json
Normal file
9
site/src/i18n/en/workspaceBuildParametersPage.json
Normal file
@ -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...",
|
"starting": "Starting...",
|
||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
"deleting": "Deleting...",
|
"deleting": "Deleting...",
|
||||||
"changeVersion": "Change version"
|
"changeVersion": "Change version",
|
||||||
|
"buildParameters": "Build parameters"
|
||||||
},
|
},
|
||||||
"disabledButton": {
|
"disabledButton": {
|
||||||
"canceling": "Canceling",
|
"canceling": "Canceling",
|
||||||
|
@ -9,6 +9,9 @@ import {
|
|||||||
MockWorkspace,
|
MockWorkspace,
|
||||||
MockWorkspaceQuota,
|
MockWorkspaceQuota,
|
||||||
MockWorkspaceRequest,
|
MockWorkspaceRequest,
|
||||||
|
MockTemplateVersionParameter1,
|
||||||
|
MockTemplateVersionParameter2,
|
||||||
|
MockTemplateVersionParameter3,
|
||||||
} from "testHelpers/entities"
|
} from "testHelpers/entities"
|
||||||
import { renderWithAuth } from "testHelpers/renderHelpers"
|
import { renderWithAuth } from "testHelpers/renderHelpers"
|
||||||
import CreateWorkspacePage from "./CreateWorkspacePage"
|
import CreateWorkspacePage from "./CreateWorkspacePage"
|
||||||
@ -17,6 +20,16 @@ const { t } = i18next
|
|||||||
|
|
||||||
const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" })
|
const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" })
|
||||||
const createWorkspaceText = t("createWorkspace", { 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 = () => {
|
const renderCreateWorkspacePage = () => {
|
||||||
return renderWithAuth(<CreateWorkspacePage />, {
|
return renderWithAuth(<CreateWorkspacePage />, {
|
||||||
@ -27,11 +40,29 @@ const renderCreateWorkspacePage = () => {
|
|||||||
|
|
||||||
describe("CreateWorkspacePage", () => {
|
describe("CreateWorkspacePage", () => {
|
||||||
it("renders", async () => {
|
it("renders", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(API, "getTemplateVersionRichParameters")
|
||||||
|
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||||
renderCreateWorkspacePage()
|
renderCreateWorkspacePage()
|
||||||
const element = await screen.findByText("Create workspace")
|
|
||||||
|
const element = await screen.findByText(createWorkspaceText)
|
||||||
expect(element).toBeDefined()
|
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 () => {
|
it("succeeds with default owner", async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(API, "getUsers")
|
.spyOn(API, "getUsers")
|
||||||
@ -40,6 +71,9 @@ describe("CreateWorkspacePage", () => {
|
|||||||
.spyOn(API, "getWorkspaceQuota")
|
.spyOn(API, "getWorkspaceQuota")
|
||||||
.mockResolvedValueOnce(MockWorkspaceQuota)
|
.mockResolvedValueOnce(MockWorkspaceQuota)
|
||||||
jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace)
|
jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace)
|
||||||
|
jest
|
||||||
|
.spyOn(API, "getTemplateVersionRichParameters")
|
||||||
|
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||||
|
|
||||||
renderCreateWorkspacePage()
|
renderCreateWorkspacePage()
|
||||||
|
|
||||||
@ -73,13 +107,106 @@ describe("CreateWorkspacePage", () => {
|
|||||||
default_source_value: "",
|
default_source_value: "",
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
jest
|
||||||
|
.spyOn(API, "getTemplateVersionRichParameters")
|
||||||
|
.mockResolvedValueOnce([MockTemplateVersionParameter1])
|
||||||
|
|
||||||
renderWithAuth(<CreateWorkspacePage />, {
|
renderWithAuth(<CreateWorkspacePage />, {
|
||||||
route:
|
route:
|
||||||
"/templates/" +
|
"/templates/" +
|
||||||
MockTemplate.name +
|
MockTemplate.name +
|
||||||
`/workspace?param.${param}=${paramValue}`,
|
`/workspace?param.${param}=${paramValue}`,
|
||||||
path: "/templates/:template/workspace",
|
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)
|
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 {
|
const {
|
||||||
templates,
|
templates,
|
||||||
|
templateParameters,
|
||||||
templateSchema,
|
templateSchema,
|
||||||
selectedTemplate,
|
selectedTemplate,
|
||||||
getTemplateSchemaError,
|
getTemplateSchemaError,
|
||||||
@ -57,6 +58,7 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
templateName={templateName}
|
templateName={templateName}
|
||||||
templates={templates}
|
templates={templates}
|
||||||
selectedTemplate={selectedTemplate}
|
selectedTemplate={selectedTemplate}
|
||||||
|
templateParameters={templateParameters}
|
||||||
templateSchema={templateSchema}
|
templateSchema={templateSchema}
|
||||||
createWorkspaceErrors={{
|
createWorkspaceErrors={{
|
||||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
|
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
|
||||||
|
@ -3,6 +3,9 @@ import {
|
|||||||
makeMockApiError,
|
makeMockApiError,
|
||||||
mockParameterSchema,
|
mockParameterSchema,
|
||||||
MockTemplate,
|
MockTemplate,
|
||||||
|
MockTemplateVersionParameter1,
|
||||||
|
MockTemplateVersionParameter2,
|
||||||
|
MockTemplateVersionParameter3,
|
||||||
} from "../../testHelpers/entities"
|
} from "../../testHelpers/entities"
|
||||||
import {
|
import {
|
||||||
CreateWorkspaceErrors,
|
CreateWorkspaceErrors,
|
||||||
@ -108,3 +111,15 @@ CreateWorkspaceError.args = {
|
|||||||
name: true,
|
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 * as TypesGen from "api/typesGenerated"
|
||||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||||
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
||||||
|
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
||||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||||
import { i18n } from "i18n"
|
|
||||||
import { FC, useState } from "react"
|
import { FC, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
|
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
|
||||||
@ -30,6 +30,8 @@ export interface CreateWorkspacePageViewProps {
|
|||||||
templateName: string
|
templateName: string
|
||||||
templates?: TypesGen.Template[]
|
templates?: TypesGen.Template[]
|
||||||
selectedTemplate?: TypesGen.Template
|
selectedTemplate?: TypesGen.Template
|
||||||
|
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||||
|
|
||||||
templateSchema?: TypesGen.ParameterSchema[]
|
templateSchema?: TypesGen.ParameterSchema[]
|
||||||
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
|
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
|
||||||
canCreateForUser?: boolean
|
canCreateForUser?: boolean
|
||||||
@ -42,30 +44,36 @@ export interface CreateWorkspacePageViewProps {
|
|||||||
defaultParameterValues?: Record<string, string>
|
defaultParameterValues?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = i18n
|
|
||||||
|
|
||||||
export const validationSchema = Yup.object({
|
|
||||||
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const CreateWorkspacePageView: FC<
|
export const CreateWorkspacePageView: FC<
|
||||||
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { t } = useTranslation("createWorkspacePage")
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const formFooterStyles = useFormFooterStyles()
|
const formFooterStyles = useFormFooterStyles()
|
||||||
const [parameterValues, setParameterValues] = useState<
|
const [parameterValues, setParameterValues] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
>(props.defaultParameterValues ?? {})
|
>(props.defaultParameterValues ?? {})
|
||||||
|
const initialRichParameterValues = selectInitialRichParametersValues(
|
||||||
|
props.templateParameters,
|
||||||
|
props.defaultParameterValues,
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation("createWorkspacePage")
|
||||||
|
|
||||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
||||||
useFormik<TypesGen.CreateWorkspaceRequest>({
|
useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
template_id: props.selectedTemplate ? props.selectedTemplate.id : "",
|
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,
|
enableReinitialize: true,
|
||||||
validationSchema,
|
|
||||||
initialTouched: props.initialTouched,
|
initialTouched: props.initialTouched,
|
||||||
onSubmit: (request) => {
|
onSubmit: (request) => {
|
||||||
if (!props.templateSchema) {
|
if (!props.templateSchema) {
|
||||||
@ -249,6 +257,48 @@ export const CreateWorkspacePageView: FC<
|
|||||||
</div>
|
</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
|
<FormFooter
|
||||||
styles={formFooterStyles}
|
styles={formFooterStyles}
|
||||||
onCancel={props.onCancel}
|
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
|
// It renders the workspace page and waits for it be loaded
|
||||||
const renderWorkspacePage = async () => {
|
const renderWorkspacePage = async () => {
|
||||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
||||||
|
jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([])
|
||||||
renderWithAuth(<WorkspacePage />, {
|
renderWithAuth(<WorkspacePage />, {
|
||||||
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
|
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
|
||||||
path: "/@:username/:workspace",
|
path: "/@:username/:workspace",
|
||||||
|
@ -20,6 +20,7 @@ export const WorkspacePage: FC = () => {
|
|||||||
workspace,
|
workspace,
|
||||||
getWorkspaceError,
|
getWorkspaceError,
|
||||||
getTemplateWarning,
|
getTemplateWarning,
|
||||||
|
getTemplateParametersWarning,
|
||||||
checkPermissionsError,
|
checkPermissionsError,
|
||||||
} = workspaceState.context
|
} = workspaceState.context
|
||||||
const [quotaState, quotaSend] = useMachine(quotaMachine)
|
const [quotaState, quotaSend] = useMachine(quotaMachine)
|
||||||
@ -50,6 +51,12 @@ export const WorkspacePage: FC = () => {
|
|||||||
{Boolean(getTemplateWarning) && (
|
{Boolean(getTemplateWarning) && (
|
||||||
<AlertBanner severity="error" error={getTemplateWarning} />
|
<AlertBanner severity="error" error={getTemplateWarning} />
|
||||||
)}
|
)}
|
||||||
|
{Boolean(getTemplateParametersWarning) && (
|
||||||
|
<AlertBanner
|
||||||
|
severity="error"
|
||||||
|
error={getTemplateParametersWarning}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{Boolean(checkPermissionsError) && (
|
{Boolean(checkPermissionsError) && (
|
||||||
<AlertBanner severity="error" error={checkPermissionsError} />
|
<AlertBanner severity="error" error={checkPermissionsError} />
|
||||||
)}
|
)}
|
||||||
|
@ -45,6 +45,7 @@ export const WorkspaceReadyPage = ({
|
|||||||
const {
|
const {
|
||||||
workspace,
|
workspace,
|
||||||
template,
|
template,
|
||||||
|
templateParameters,
|
||||||
refreshWorkspaceWarning,
|
refreshWorkspaceWarning,
|
||||||
builds,
|
builds,
|
||||||
getBuildsError,
|
getBuildsError,
|
||||||
@ -111,6 +112,7 @@ export const WorkspaceReadyPage = ({
|
|||||||
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
|
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
|
||||||
handleCancel={() => workspaceSend({ type: "CANCEL" })}
|
handleCancel={() => workspaceSend({ type: "CANCEL" })}
|
||||||
handleChangeVersion={() => navigate("change-version")}
|
handleChangeVersion={() => navigate("change-version")}
|
||||||
|
handleBuildParameters={() => navigate("build-parameters")}
|
||||||
resources={workspace.latest_build.resources}
|
resources={workspace.latest_build.resources}
|
||||||
builds={builds}
|
builds={builds}
|
||||||
canUpdateWorkspace={canUpdateWorkspace}
|
canUpdateWorkspace={canUpdateWorkspace}
|
||||||
@ -125,6 +127,7 @@ export const WorkspaceReadyPage = ({
|
|||||||
buildInfo={buildInfo}
|
buildInfo={buildInfo}
|
||||||
applicationsHost={applicationsHost}
|
applicationsHost={applicationsHost}
|
||||||
template={template}
|
template={template}
|
||||||
|
templateParameters={templateParameters}
|
||||||
quota_budget={quotaState.context.quota?.budget}
|
quota_budget={quotaState.context.quota?.budget}
|
||||||
/>
|
/>
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
|
@ -631,11 +631,77 @@ export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = {
|
|||||||
count: 26,
|
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
|
// requests the MockWorkspace
|
||||||
export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = {
|
export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = {
|
||||||
name: "test",
|
name: "test",
|
||||||
parameter_values: [],
|
parameter_values: [],
|
||||||
template_id: "test-template",
|
template_id: "test-template",
|
||||||
|
rich_parameter_values: [
|
||||||
|
{
|
||||||
|
name: MockTemplateVersionParameter1.name,
|
||||||
|
value: MockTemplateVersionParameter1.default_value,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockUserAgent: Types.UserAgent = {
|
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 = (
|
export const mockParameterSchema = (
|
||||||
partial: Partial<TypesGen.ParameterSchema>,
|
partial: Partial<TypesGen.ParameterSchema>,
|
||||||
): TypesGen.ParameterSchema => {
|
): TypesGen.ParameterSchema => {
|
||||||
|
@ -36,7 +36,7 @@ interface FormHelpers {
|
|||||||
export const getFormHelpers =
|
export const getFormHelpers =
|
||||||
<T>(form: FormikContextType<T>, error?: Error | unknown) =>
|
<T>(form: FormikContextType<T>, error?: Error | unknown) =>
|
||||||
(
|
(
|
||||||
name: keyof T,
|
name: string,
|
||||||
HelperText: ReactNode = "",
|
HelperText: ReactNode = "",
|
||||||
backendErrorName?: string,
|
backendErrorName?: string,
|
||||||
): FormHelpers => {
|
): FormHelpers => {
|
||||||
|
@ -2,12 +2,14 @@ import {
|
|||||||
checkAuthorization,
|
checkAuthorization,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
getTemplates,
|
getTemplates,
|
||||||
|
getTemplateVersionRichParameters,
|
||||||
getTemplateVersionSchema,
|
getTemplateVersionSchema,
|
||||||
} from "api/api"
|
} from "api/api"
|
||||||
import {
|
import {
|
||||||
CreateWorkspaceRequest,
|
CreateWorkspaceRequest,
|
||||||
ParameterSchema,
|
ParameterSchema,
|
||||||
Template,
|
Template,
|
||||||
|
TemplateVersionParameter,
|
||||||
User,
|
User,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from "api/typesGenerated"
|
} from "api/typesGenerated"
|
||||||
@ -19,11 +21,13 @@ type CreateWorkspaceContext = {
|
|||||||
templateName: string
|
templateName: string
|
||||||
templates?: Template[]
|
templates?: Template[]
|
||||||
selectedTemplate?: Template
|
selectedTemplate?: Template
|
||||||
|
templateParameters?: TemplateVersionParameter[]
|
||||||
templateSchema?: ParameterSchema[]
|
templateSchema?: ParameterSchema[]
|
||||||
createWorkspaceRequest?: CreateWorkspaceRequest
|
createWorkspaceRequest?: CreateWorkspaceRequest
|
||||||
createdWorkspace?: Workspace
|
createdWorkspace?: Workspace
|
||||||
createWorkspaceError?: Error | unknown
|
createWorkspaceError?: Error | unknown
|
||||||
getTemplatesError?: Error | unknown
|
getTemplatesError?: Error | unknown
|
||||||
|
getTemplateParametersError?: Error | unknown
|
||||||
getTemplateSchemaError?: Error | unknown
|
getTemplateSchemaError?: Error | unknown
|
||||||
permissions?: Record<string, boolean>
|
permissions?: Record<string, boolean>
|
||||||
checkPermissionsError?: Error | unknown
|
checkPermissionsError?: Error | unknown
|
||||||
@ -52,6 +56,9 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
getTemplates: {
|
getTemplates: {
|
||||||
data: Template[]
|
data: Template[]
|
||||||
}
|
}
|
||||||
|
getTemplateParameters: {
|
||||||
|
data: TemplateVersionParameter[]
|
||||||
|
}
|
||||||
getTemplateSchema: {
|
getTemplateSchema: {
|
||||||
data: ParameterSchema[]
|
data: ParameterSchema[]
|
||||||
}
|
}
|
||||||
@ -88,7 +95,7 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
src: "getTemplateSchema",
|
src: "getTemplateSchema",
|
||||||
onDone: {
|
onDone: {
|
||||||
actions: ["assignTemplateSchema"],
|
actions: ["assignTemplateSchema"],
|
||||||
target: "checkingPermissions",
|
target: "gettingTemplateParameters",
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
actions: ["assignGetTemplateSchemaError"],
|
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: {
|
checkingPermissions: {
|
||||||
entry: "clearCheckPermissionsError",
|
entry: "clearCheckPermissionsError",
|
||||||
invoke: {
|
invoke: {
|
||||||
@ -145,6 +166,17 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
{
|
{
|
||||||
services: {
|
services: {
|
||||||
getTemplates: (context) => getTemplates(context.organizationId),
|
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) => {
|
getTemplateSchema: (context) => {
|
||||||
const { selectedTemplate } = context
|
const { selectedTemplate } = context
|
||||||
|
|
||||||
@ -206,11 +238,13 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
return templates.length > 0 ? templates[0] : undefined
|
return templates.length > 0 ? templates[0] : undefined
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
assignTemplateParameters: assign({
|
||||||
|
templateParameters: (_, event) => event.data,
|
||||||
|
}),
|
||||||
assignTemplateSchema: assign({
|
assignTemplateSchema: assign({
|
||||||
// Only show parameters that are allowed to be overridden.
|
// Only show parameters that are allowed to be overridden.
|
||||||
// CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155
|
// CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155
|
||||||
templateSchema: (_, event) =>
|
templateSchema: (_, event) => event.data,
|
||||||
event.data.filter((param) => param.allow_override_source),
|
|
||||||
}),
|
}),
|
||||||
assignPermissions: assign({
|
assignPermissions: assign({
|
||||||
permissions: (_, event) => event.data as Record<string, boolean>,
|
permissions: (_, event) => event.data as Record<string, boolean>,
|
||||||
@ -239,6 +273,12 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
clearGetTemplatesError: assign({
|
clearGetTemplatesError: assign({
|
||||||
getTemplatesError: (_) => undefined,
|
getTemplatesError: (_) => undefined,
|
||||||
}),
|
}),
|
||||||
|
assignGetTemplateParametersError: assign({
|
||||||
|
getTemplateParametersError: (_, event) => event.data,
|
||||||
|
}),
|
||||||
|
clearGetTemplateParametersError: assign({
|
||||||
|
getTemplateParametersError: (_) => undefined,
|
||||||
|
}),
|
||||||
assignGetTemplateSchemaError: assign({
|
assignGetTemplateSchemaError: assign({
|
||||||
getTemplateSchemaError: (_, event) => event.data,
|
getTemplateSchemaError: (_, event) => event.data,
|
||||||
}),
|
}),
|
||||||
|
223
site/src/xServices/workspace/workspaceBuildParametersXService.ts
Normal file
223
site/src/xServices/workspace/workspaceBuildParametersXService.ts
Normal file
@ -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 = {
|
const Language = {
|
||||||
getTemplateWarning:
|
getTemplateWarning:
|
||||||
"Error updating workspace: latest template could not be fetched.",
|
"Error updating workspace: latest template could not be fetched.",
|
||||||
|
getTemplateParametersWarning:
|
||||||
|
"Error updating workspace: template parameters could not be fetched.",
|
||||||
buildError: "Workspace action failed.",
|
buildError: "Workspace action failed.",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +55,13 @@ export interface WorkspaceContext {
|
|||||||
eventSource?: EventSource
|
eventSource?: EventSource
|
||||||
workspace?: TypesGen.Workspace
|
workspace?: TypesGen.Workspace
|
||||||
template?: TypesGen.Template
|
template?: TypesGen.Template
|
||||||
|
templateParameters?: TypesGen.TemplateVersionParameter[]
|
||||||
build?: TypesGen.WorkspaceBuild
|
build?: TypesGen.WorkspaceBuild
|
||||||
getWorkspaceError?: Error | unknown
|
getWorkspaceError?: Error | unknown
|
||||||
// these are labeled as warnings because they don't make the page unusable
|
// these are labeled as warnings because they don't make the page unusable
|
||||||
refreshWorkspaceWarning?: Error | unknown
|
refreshWorkspaceWarning?: Error | unknown
|
||||||
getTemplateWarning: Error | unknown
|
getTemplateWarning: Error | unknown
|
||||||
|
getTemplateParametersWarning: Error | unknown
|
||||||
// Builds
|
// Builds
|
||||||
builds?: TypesGen.WorkspaceBuild[]
|
builds?: TypesGen.WorkspaceBuild[]
|
||||||
getBuildsError?: Error | unknown
|
getBuildsError?: Error | unknown
|
||||||
@ -130,6 +134,9 @@ export const workspaceMachine = createMachine(
|
|||||||
getTemplate: {
|
getTemplate: {
|
||||||
data: TypesGen.Template
|
data: TypesGen.Template
|
||||||
}
|
}
|
||||||
|
getTemplateParameters: {
|
||||||
|
data: TypesGen.TemplateVersionParameter[]
|
||||||
|
}
|
||||||
startWorkspaceWithLatestTemplate: {
|
startWorkspaceWithLatestTemplate: {
|
||||||
data: TypesGen.WorkspaceBuild
|
data: TypesGen.WorkspaceBuild
|
||||||
}
|
}
|
||||||
@ -191,14 +198,14 @@ export const workspaceMachine = createMachine(
|
|||||||
tags: "loading",
|
tags: "loading",
|
||||||
},
|
},
|
||||||
gettingTemplate: {
|
gettingTemplate: {
|
||||||
entry: "clearGettingTemplateWarning",
|
entry: "clearGetTemplateWarning",
|
||||||
invoke: {
|
invoke: {
|
||||||
src: "getTemplate",
|
src: "getTemplate",
|
||||||
id: "getTemplate",
|
id: "getTemplate",
|
||||||
onDone: [
|
onDone: [
|
||||||
{
|
{
|
||||||
actions: "assignTemplate",
|
actions: "assignTemplate",
|
||||||
target: "gettingPermissions",
|
target: "gettingTemplateParameters",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onError: [
|
onError: [
|
||||||
@ -213,6 +220,29 @@ export const workspaceMachine = createMachine(
|
|||||||
},
|
},
|
||||||
tags: "loading",
|
tags: "loading",
|
||||||
},
|
},
|
||||||
|
gettingTemplateParameters: {
|
||||||
|
entry: "clearGetTemplateParametersWarning",
|
||||||
|
invoke: {
|
||||||
|
src: "getTemplateParameters",
|
||||||
|
id: "getTemplateParameters",
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: "assignTemplateParameters",
|
||||||
|
target: "gettingPermissions",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
"assignGetTemplateParametersWarning",
|
||||||
|
"displayGetTemplateParametersWarning",
|
||||||
|
],
|
||||||
|
target: "error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tags: "loading",
|
||||||
|
},
|
||||||
gettingPermissions: {
|
gettingPermissions: {
|
||||||
entry: "clearGetPermissionsError",
|
entry: "clearGetPermissionsError",
|
||||||
invoke: {
|
invoke: {
|
||||||
@ -506,6 +536,9 @@ export const workspaceMachine = createMachine(
|
|||||||
assignTemplate: assign({
|
assignTemplate: assign({
|
||||||
template: (_, event) => event.data,
|
template: (_, event) => event.data,
|
||||||
}),
|
}),
|
||||||
|
assignTemplateParameters: assign({
|
||||||
|
templateParameters: (_, event) => event.data,
|
||||||
|
}),
|
||||||
assignPermissions: assign({
|
assignPermissions: assign({
|
||||||
// Setting event.data as Permissions to be more stricted. So we know
|
// Setting event.data as Permissions to be more stricted. So we know
|
||||||
// what permissions we asked for.
|
// what permissions we asked for.
|
||||||
@ -566,9 +599,18 @@ export const workspaceMachine = createMachine(
|
|||||||
displayGetTemplateWarning: () => {
|
displayGetTemplateWarning: () => {
|
||||||
displayError(Language.getTemplateWarning)
|
displayError(Language.getTemplateWarning)
|
||||||
},
|
},
|
||||||
clearGettingTemplateWarning: assign({
|
clearGetTemplateWarning: assign({
|
||||||
getTemplateWarning: (_) => undefined,
|
getTemplateWarning: (_) => undefined,
|
||||||
}),
|
}),
|
||||||
|
assignGetTemplateParametersWarning: assign({
|
||||||
|
getTemplateParametersWarning: (_, event) => event.data,
|
||||||
|
}),
|
||||||
|
displayGetTemplateParametersWarning: () => {
|
||||||
|
displayError(Language.getTemplateParametersWarning)
|
||||||
|
},
|
||||||
|
clearGetTemplateParametersWarning: assign({
|
||||||
|
getTemplateParametersWarning: (_) => undefined,
|
||||||
|
}),
|
||||||
// Timeline
|
// Timeline
|
||||||
assignBuilds: assign({
|
assignBuilds: assign({
|
||||||
builds: (_, event) => event.data,
|
builds: (_, event) => event.data,
|
||||||
@ -629,6 +671,15 @@ export const workspaceMachine = createMachine(
|
|||||||
throw Error("Cannot get template without workspace")
|
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) => {
|
startWorkspaceWithLatestTemplate: (context) => async (send) => {
|
||||||
if (context.workspace && context.template) {
|
if (context.workspace && context.template) {
|
||||||
const startWorkspacePromise = await API.startWorkspace(
|
const startWorkspacePromise = await API.startWorkspace(
|
||||||
|
Reference in New Issue
Block a user