mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
feat(site): add build parameters option when starting or restarting a workspace (#8524)
This commit is contained in:
@ -519,11 +519,13 @@ export const startWorkspace = (
|
||||
workspaceId: string,
|
||||
templateVersionId: string,
|
||||
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
|
||||
buildParameters?: TypesGen.WorkspaceBuildParameter[],
|
||||
) =>
|
||||
postWorkspaceBuild(workspaceId, {
|
||||
transition: "start",
|
||||
template_version_id: templateVersionId,
|
||||
log_level: logLevel,
|
||||
rich_parameter_values: buildParameters,
|
||||
})
|
||||
export const stopWorkspace = (
|
||||
workspaceId: string,
|
||||
@ -552,7 +554,13 @@ export const cancelWorkspaceBuild = async (
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const restartWorkspace = async (workspace: TypesGen.Workspace) => {
|
||||
export const restartWorkspace = async ({
|
||||
workspace,
|
||||
buildParameters,
|
||||
}: {
|
||||
workspace: TypesGen.Workspace
|
||||
buildParameters?: TypesGen.WorkspaceBuildParameter[]
|
||||
}) => {
|
||||
const stopBuild = await stopWorkspace(workspace.id)
|
||||
const awaitedStopBuild = await waitForBuild(stopBuild)
|
||||
|
||||
@ -564,6 +572,8 @@ export const restartWorkspace = async (workspace: TypesGen.Workspace) => {
|
||||
const startBuild = await startWorkspace(
|
||||
workspace.id,
|
||||
workspace.latest_build.template_version_id,
|
||||
undefined,
|
||||
buildParameters,
|
||||
)
|
||||
await waitForBuild(startBuild)
|
||||
}
|
||||
@ -1346,3 +1356,15 @@ export const issueReconnectingPTYSignedToken = async (
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => {
|
||||
const latestBuild = workspace.latest_build
|
||||
const [templateVersionRichParameters, buildParameters] = await Promise.all([
|
||||
getTemplateVersionRichParameters(latestBuild.template_version_id),
|
||||
getWorkspaceBuildParameters(latestBuild.id),
|
||||
])
|
||||
return {
|
||||
templateVersionRichParameters,
|
||||
buildParameters,
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
import { FC } from "react"
|
||||
import { forwardRef } from "react"
|
||||
import MuiLoadingButton, {
|
||||
LoadingButtonProps as MuiLoadingButtonProps,
|
||||
} from "@mui/lab/LoadingButton"
|
||||
|
||||
export type LoadingButtonProps = MuiLoadingButtonProps
|
||||
|
||||
export const LoadingButton: FC<LoadingButtonProps> = ({
|
||||
children,
|
||||
loadingIndicator,
|
||||
...buttonProps
|
||||
}) => {
|
||||
export const LoadingButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
MuiLoadingButtonProps
|
||||
>(({ children, loadingIndicator, ...buttonProps }, ref) => {
|
||||
return (
|
||||
<MuiLoadingButton variant="outlined" color="neutral" {...buttonProps}>
|
||||
<MuiLoadingButton
|
||||
variant="outlined"
|
||||
color="neutral"
|
||||
ref={ref}
|
||||
{...buttonProps}
|
||||
>
|
||||
{/* known issue: https://github.com/mui/material-ui/issues/27853 */}
|
||||
<span>
|
||||
{buttonProps.loading && loadingIndicator ? loadingIndicator : children}
|
||||
</span>
|
||||
</MuiLoadingButton>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -12,17 +12,21 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import gfm from "remark-gfm"
|
||||
import { colors } from "theme/colors"
|
||||
import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism"
|
||||
import { combineClasses } from "utils/combineClasses"
|
||||
|
||||
export interface MarkdownProps {
|
||||
children: string
|
||||
}
|
||||
|
||||
export const Markdown: FC<{ children: string }> = ({ children }) => {
|
||||
export const Markdown: FC<{ children: string; className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={styles.markdown}
|
||||
className={combineClasses([styles.markdown, className])}
|
||||
remarkPlugins={[gfm]}
|
||||
components={{
|
||||
a: ({ href, target, children }) => (
|
||||
|
@ -1,18 +1,14 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { TemplateVersionParameter } from "api/typesGenerated"
|
||||
import {
|
||||
RichParameterInput,
|
||||
RichParameterInputProps,
|
||||
} from "./RichParameterInput"
|
||||
import { RichParameterInput } from "./RichParameterInput"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
export default {
|
||||
const meta: Meta<typeof RichParameterInput> = {
|
||||
title: "components/RichParameterInput",
|
||||
component: RichParameterInput,
|
||||
}
|
||||
|
||||
const Template: Story<RichParameterInputProps> = (
|
||||
args: RichParameterInputProps,
|
||||
) => <RichParameterInput {...args} />
|
||||
export default meta
|
||||
type Story = StoryObj<typeof RichParameterInput>
|
||||
|
||||
const createTemplateVersionParameter = (
|
||||
partial: Partial<TemplateVersionParameter>,
|
||||
@ -37,154 +33,221 @@ const createTemplateVersionParameter = (
|
||||
}
|
||||
}
|
||||
|
||||
export const Basic = Template.bind({})
|
||||
Basic.args = {
|
||||
initialValue: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "project_name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
}),
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
initialValue: "initial-value",
|
||||
id: "project_name",
|
||||
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",
|
||||
id: "number_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "number_parameter",
|
||||
type: "number",
|
||||
description: "Numeric parameter",
|
||||
}),
|
||||
export const NumberType: Story = {
|
||||
args: {
|
||||
initialValue: "4",
|
||||
id: "number_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "number_parameter",
|
||||
type: "number",
|
||||
description: "Numeric parameter",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const BooleanType = Template.bind({})
|
||||
BooleanType.args = {
|
||||
initialValue: "false",
|
||||
id: "bool_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "bool_parameter",
|
||||
type: "bool",
|
||||
description: "Boolean parameter",
|
||||
}),
|
||||
export const BooleanType: Story = {
|
||||
args: {
|
||||
initialValue: "false",
|
||||
id: "bool_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "bool_parameter",
|
||||
type: "bool",
|
||||
description: "Boolean parameter",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const OptionsType = Template.bind({})
|
||||
OptionsType.args = {
|
||||
initialValue: "first_option",
|
||||
id: "options_parameter",
|
||||
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",
|
||||
},
|
||||
],
|
||||
}),
|
||||
export const OptionsType: Story = {
|
||||
args: {
|
||||
initialValue: "first_option",
|
||||
id: "options_parameter",
|
||||
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",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const ListStringType = Template.bind({})
|
||||
ListStringType.args = {
|
||||
initialValue: JSON.stringify(["first", "second", "third"]),
|
||||
id: "list_string_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "list_string_parameter",
|
||||
type: "list(string)",
|
||||
description: "List string parameter",
|
||||
}),
|
||||
export const ListStringType: Story = {
|
||||
args: {
|
||||
initialValue: JSON.stringify(["first", "second", "third"]),
|
||||
id: "list_string_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "list_string_parameter",
|
||||
type: "list(string)",
|
||||
description: "List string parameter",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const IconLabel = Template.bind({})
|
||||
IconLabel.args = {
|
||||
initialValue: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "project_name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
icon: "/emojis/1f30e.png",
|
||||
}),
|
||||
export const IconLabel: Story = {
|
||||
args: {
|
||||
initialValue: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "project_name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
icon: "/emojis/1f30e.png",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const NoDescription = Template.bind({})
|
||||
NoDescription.args = {
|
||||
initialValue: "",
|
||||
id: "region",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "Region",
|
||||
description: "",
|
||||
description_plaintext: "",
|
||||
type: "string",
|
||||
mutable: false,
|
||||
default_value: "",
|
||||
icon: "/emojis/1f30e.png",
|
||||
options: [
|
||||
{
|
||||
name: "Pittsburgh",
|
||||
description: "",
|
||||
value: "us-pittsburgh",
|
||||
icon: "/emojis/1f1fa-1f1f8.png",
|
||||
},
|
||||
{
|
||||
name: "Helsinki",
|
||||
description: "",
|
||||
value: "eu-helsinki",
|
||||
icon: "/emojis/1f1eb-1f1ee.png",
|
||||
},
|
||||
{
|
||||
name: "Sydney",
|
||||
description: "",
|
||||
value: "ap-sydney",
|
||||
icon: "/emojis/1f1e6-1f1fa.png",
|
||||
},
|
||||
],
|
||||
}),
|
||||
export const NoDescription: Story = {
|
||||
args: {
|
||||
initialValue: "",
|
||||
id: "region",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "Region",
|
||||
description: "",
|
||||
description_plaintext: "",
|
||||
type: "string",
|
||||
mutable: false,
|
||||
default_value: "",
|
||||
icon: "/emojis/1f30e.png",
|
||||
options: [
|
||||
{
|
||||
name: "Pittsburgh",
|
||||
description: "",
|
||||
value: "us-pittsburgh",
|
||||
icon: "/emojis/1f1fa-1f1f8.png",
|
||||
},
|
||||
{
|
||||
name: "Helsinki",
|
||||
description: "",
|
||||
value: "eu-helsinki",
|
||||
icon: "/emojis/1f1eb-1f1ee.png",
|
||||
},
|
||||
{
|
||||
name: "Sydney",
|
||||
description: "",
|
||||
value: "ap-sydney",
|
||||
icon: "/emojis/1f1e6-1f1fa.png",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const DescriptionWithLinks = Template.bind({})
|
||||
DescriptionWithLinks.args = {
|
||||
initialValue: "",
|
||||
id: "coder-repository-directory",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "Coder Repository Directory",
|
||||
description:
|
||||
"The directory specified will be created and [coder/coder](https://github.com/coder/coder) will be automatically cloned into it 🪄.",
|
||||
description_plaintext:
|
||||
"The directory specified will be created and coder/coder (https://github.com/coder/coder) will be automatically cloned into it 🪄.",
|
||||
type: "string",
|
||||
mutable: true,
|
||||
default_value: "~/coder",
|
||||
icon: "",
|
||||
options: [],
|
||||
}),
|
||||
export const DescriptionWithLinks: Story = {
|
||||
args: {
|
||||
initialValue: "",
|
||||
id: "coder-repository-directory",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "Coder Repository Directory",
|
||||
description:
|
||||
"The directory specified will be created and [coder/coder](https://github.com/coder/coder) will be automatically cloned into it 🪄.",
|
||||
description_plaintext:
|
||||
"The directory specified will be created and coder/coder (https://github.com/coder/coder) will be automatically cloned into it 🪄.",
|
||||
type: "string",
|
||||
mutable: true,
|
||||
default_value: "~/coder",
|
||||
icon: "",
|
||||
options: [],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const BasicWithDisplayName = Template.bind({})
|
||||
BasicWithDisplayName.args = {
|
||||
initialValue: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "project_name",
|
||||
display_name: "Project Name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
}),
|
||||
export const BasicWithDisplayName: Story = {
|
||||
args: {
|
||||
initialValue: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "project_name",
|
||||
display_name: "Project Name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
// Smaller version of the components. Used in popovers.
|
||||
|
||||
export const SmallBasic: Story = {
|
||||
args: {
|
||||
...Basic.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallNumberType: Story = {
|
||||
args: {
|
||||
...NumberType.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallBooleanType: Story = {
|
||||
args: {
|
||||
...BooleanType.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallOptionsType: Story = {
|
||||
args: {
|
||||
...OptionsType.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallListStringType: Story = {
|
||||
args: {
|
||||
...ListStringType.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallIconLabel: Story = {
|
||||
args: {
|
||||
...IconLabel.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallNoDescription: Story = {
|
||||
args: {
|
||||
...NoDescription.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallBasicWithDisplayName: Story = {
|
||||
args: {
|
||||
...BasicWithDisplayName.args,
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import { TemplateVersionParameter } from "../../api/typesGenerated"
|
||||
import { colors } from "theme/colors"
|
||||
import { MemoizedMarkdown } from "components/Markdown/Markdown"
|
||||
import { MultiTextField } from "components/MultiTextField/MultiTextField"
|
||||
import Box from "@mui/material/Box"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
|
||||
const isBoolean = (parameter: TemplateVersionParameter) => {
|
||||
return parameter.type === "bool"
|
||||
@ -42,9 +44,9 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ id, parameter }) => {
|
||||
{hasDescription ? (
|
||||
<Stack spacing={0}>
|
||||
<span className={styles.labelCaption}>{displayName}</span>
|
||||
<span className={styles.labelPrimary}>
|
||||
<MemoizedMarkdown>{parameter.description}</MemoizedMarkdown>
|
||||
</span>
|
||||
<MemoizedMarkdown className={styles.labelPrimary}>
|
||||
{parameter.description}
|
||||
</MemoizedMarkdown>
|
||||
</Stack>
|
||||
) : (
|
||||
<span className={styles.labelPrimary}>{displayName}</span>
|
||||
@ -54,12 +56,18 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ id, parameter }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export type RichParameterInputProps = Omit<TextFieldProps, "onChange"> & {
|
||||
type Size = "medium" | "small"
|
||||
|
||||
export type RichParameterInputProps = Omit<
|
||||
TextFieldProps,
|
||||
"onChange" | "size"
|
||||
> & {
|
||||
index: number
|
||||
parameter: TemplateVersionParameter
|
||||
onChange: (value: string) => void
|
||||
initialValue?: string
|
||||
id: string
|
||||
size?: Size
|
||||
}
|
||||
|
||||
export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
@ -68,14 +76,17 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
onChange,
|
||||
parameter,
|
||||
initialValue,
|
||||
size = "medium",
|
||||
...fieldProps
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={2}>
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={size === "small" ? 1.25 : 2}
|
||||
className={size}
|
||||
>
|
||||
<ParameterLabel id={fieldProps.id} parameter={parameter} />
|
||||
<div className={styles.input}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<RichParameterField
|
||||
{...fieldProps}
|
||||
index={index}
|
||||
@ -84,7 +95,7 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
parameter={parameter}
|
||||
initialValue={initialValue}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@ -94,6 +105,7 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
|
||||
onChange,
|
||||
parameter,
|
||||
initialValue,
|
||||
size,
|
||||
...props
|
||||
}) => {
|
||||
const [parameterValue, setParameterValue] = useState(initialValue)
|
||||
@ -102,6 +114,7 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
|
||||
if (isBoolean(parameter)) {
|
||||
return (
|
||||
<RadioGroup
|
||||
className={styles.radioGroup}
|
||||
defaultValue={parameterValue}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value)
|
||||
@ -126,6 +139,7 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
|
||||
if (parameter.options.length > 0) {
|
||||
return (
|
||||
<RadioGroup
|
||||
className={styles.radioGroup}
|
||||
defaultValue={parameterValue}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value)
|
||||
@ -192,6 +206,7 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
className={styles.textField}
|
||||
type={parameter.type}
|
||||
disabled={disabled}
|
||||
required={parameter.required}
|
||||
@ -205,15 +220,18 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const optionIconSize = 20
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const useStyles = makeStyles<Theme>((theme) => ({
|
||||
label: {
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
labelCaption: {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
".small &": {
|
||||
fontSize: 13,
|
||||
lineHeight: "140%",
|
||||
},
|
||||
},
|
||||
labelPrimary: {
|
||||
fontSize: 16,
|
||||
@ -224,15 +242,34 @@ const useStyles = makeStyles((theme) => ({
|
||||
margin: 0,
|
||||
lineHeight: "24px", // Keep the same as ParameterInput
|
||||
},
|
||||
|
||||
".small &": {
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
labelImmutable: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
color: colors.yellow[7],
|
||||
},
|
||||
input: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
textField: {
|
||||
".small & .MuiInputBase-root": {
|
||||
height: 36,
|
||||
fontSize: 14,
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
radioGroup: {
|
||||
".small & .MuiFormControlLabel-label": {
|
||||
fontSize: 14,
|
||||
},
|
||||
".small & .MuiRadio-root": {
|
||||
padding: theme.spacing(0.75, "9px"), // 8px + 1px border
|
||||
},
|
||||
".small & .MuiRadio-root svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
},
|
||||
checkbox: {
|
||||
display: "flex",
|
||||
@ -243,6 +280,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
width: theme.spacing(2.5),
|
||||
height: theme.spacing(2.5),
|
||||
display: "block",
|
||||
|
||||
".small &": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
labelIcon: {
|
||||
width: "100%",
|
||||
@ -255,7 +296,12 @@ const useStyles = makeStyles((theme) => ({
|
||||
gap: theme.spacing(1.5),
|
||||
},
|
||||
optionIcon: {
|
||||
maxHeight: optionIconSize,
|
||||
width: optionIconSize,
|
||||
maxHeight: 20,
|
||||
width: 20,
|
||||
|
||||
".small &": {
|
||||
maxHeight: 16,
|
||||
width: 16,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
VariableValue,
|
||||
WorkspaceResource,
|
||||
} from "api/typesGenerated"
|
||||
import { Alert } from "components/Alert/Alert"
|
||||
import { Alert, AlertDetail } from "components/Alert/Alert"
|
||||
import { Avatar } from "components/Avatar/Avatar"
|
||||
import { AvatarData } from "components/AvatarData/AvatarData"
|
||||
import { bannerHeight } from "components/DeploymentBanner/DeploymentBannerView"
|
||||
@ -47,6 +47,7 @@ import {
|
||||
TemplateVersionStatusBadge,
|
||||
} from "./TemplateVersionStatusBadge"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
import AlertTitle from "@mui/material/AlertTitle"
|
||||
|
||||
export interface TemplateVersionEditorProps {
|
||||
template: Template
|
||||
@ -374,7 +375,12 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
}`}
|
||||
>
|
||||
{templateVersion.job.error && (
|
||||
<Alert severity="error">{templateVersion.job.error}</Alert>
|
||||
<div>
|
||||
<Alert severity="error">
|
||||
<AlertTitle>Error during the build</AlertTitle>
|
||||
<AlertDetail>{templateVersion.job.error}</AlertDetail>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{buildLogs && buildLogs.length > 0 && (
|
||||
|
@ -45,9 +45,9 @@ export interface WorkspaceProps {
|
||||
maxDeadlineIncrease: number
|
||||
maxDeadlineDecrease: number
|
||||
}
|
||||
handleStart: () => void
|
||||
handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void
|
||||
handleStop: () => void
|
||||
handleRestart: () => void
|
||||
handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void
|
||||
handleDelete: () => void
|
||||
handleUpdate: () => void
|
||||
handleCancel: () => void
|
||||
@ -194,8 +194,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
|
||||
<PageHeaderActions>
|
||||
<WorkspaceActions
|
||||
workspaceStatus={workspace.latest_build.status}
|
||||
isOutdated={workspace.outdated}
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleRestart={handleRestart}
|
||||
|
192
site/src/components/WorkspaceActions/BuildParametersPopover.tsx
Normal file
192
site/src/components/WorkspaceActions/BuildParametersPopover.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"
|
||||
import Box from "@mui/material/Box"
|
||||
import Button from "@mui/material/Button"
|
||||
import Popover from "@mui/material/Popover"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getWorkspaceParameters } from "api/api"
|
||||
import {
|
||||
TemplateVersionParameter,
|
||||
Workspace,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated"
|
||||
import { FormFields } from "components/Form/Form"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
|
||||
import {
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "components/Tooltips/HelpTooltip/HelpTooltip"
|
||||
import { useFormik } from "formik"
|
||||
import { useRef, useState } from "react"
|
||||
import { getFormHelpers } from "utils/formUtils"
|
||||
import { getInitialParameterValues } from "utils/richParameters"
|
||||
|
||||
export const BuildParametersPopover = ({
|
||||
workspace,
|
||||
disabled,
|
||||
onSubmit,
|
||||
}: {
|
||||
workspace: Workspace
|
||||
disabled?: boolean
|
||||
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void
|
||||
}) => {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { data: parameters } = useQuery({
|
||||
queryKey: ["workspace", workspace.id, "parameters"],
|
||||
queryFn: () => getWorkspaceParameters(workspace),
|
||||
enabled: isOpen,
|
||||
})
|
||||
const ephemeralParameters = parameters
|
||||
? parameters.templateVersionRichParameters.filter((p) => p.ephemeral)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
color="neutral"
|
||||
sx={{ px: 0 }}
|
||||
ref={anchorRef}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
>
|
||||
<ExpandMoreOutlined sx={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
sx={{
|
||||
".MuiPaper-root": {
|
||||
width: (theme) => theme.spacing(38),
|
||||
marginTop: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{parameters && parameters.buildParameters && ephemeralParameters ? (
|
||||
ephemeralParameters.length > 0 ? (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
p: 2.5,
|
||||
borderBottom: (theme) =>
|
||||
`1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<HelpTooltipTitle>Build Options</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
These parameters only apply for a single workspace start.
|
||||
</HelpTooltipText>
|
||||
</Box>
|
||||
<Box sx={{ p: 2.5 }}>
|
||||
<Form
|
||||
onSubmit={(buildParameters) => {
|
||||
onSubmit(buildParameters)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
ephemeralParameters={ephemeralParameters}
|
||||
buildParameters={parameters.buildParameters}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
p: 2.5,
|
||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<HelpTooltipTitle>Build Options</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
This template has no ephemeral build options.
|
||||
</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href="https://coder.com/docs/v2/latest/templates/parameters#ephemeral-parameters">
|
||||
Read the docs
|
||||
</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Form = ({
|
||||
ephemeralParameters,
|
||||
buildParameters,
|
||||
onSubmit,
|
||||
}: {
|
||||
ephemeralParameters: TemplateVersionParameter[]
|
||||
buildParameters: WorkspaceBuildParameter[]
|
||||
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void
|
||||
}) => {
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
rich_parameter_values: getInitialParameterValues(
|
||||
ephemeralParameters,
|
||||
buildParameters,
|
||||
),
|
||||
},
|
||||
onSubmit: (values) => {
|
||||
onSubmit(values.rich_parameter_values)
|
||||
},
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers(form)
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<FormFields>
|
||||
{ephemeralParameters.map((parameter, index) => {
|
||||
return (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers("rich_parameter_values[" + index + "].value")}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
initialValue={form.values.rich_parameter_values[index]?.value}
|
||||
index={index}
|
||||
size="small"
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue(`rich_parameter_values[${index}]`, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</FormFields>
|
||||
<Box sx={{ py: 3, pb: 1 }}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
Build workspace
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -7,6 +7,9 @@ import ReplayIcon from "@mui/icons-material/Replay"
|
||||
import { LoadingButton } from "components/LoadingButton/LoadingButton"
|
||||
import { FC } from "react"
|
||||
import BlockOutlined from "@mui/icons-material/BlockOutlined"
|
||||
import ButtonGroup from "@mui/material/ButtonGroup"
|
||||
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"
|
||||
import { BuildParametersPopover } from "./BuildParametersPopover"
|
||||
|
||||
interface WorkspaceAction {
|
||||
loading?: boolean
|
||||
@ -31,17 +34,37 @@ export const UpdateButton: FC<WorkspaceAction> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
|
||||
export const StartButton: FC<
|
||||
Omit<WorkspaceAction, "handleAction"> & {
|
||||
workspace: Workspace
|
||||
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void
|
||||
}
|
||||
> = ({ handleAction, workspace, loading }) => {
|
||||
return (
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingIndicator="Starting..."
|
||||
loadingPosition="start"
|
||||
startIcon={<PlayCircleOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
<ButtonGroup
|
||||
variant="outlined"
|
||||
sx={{
|
||||
// Workaround to make the border transitions smmothly on button groups
|
||||
"& > button:hover + button": {
|
||||
borderLeft: "1px solid #FFF",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingIndicator="Starting..."
|
||||
loadingPosition="start"
|
||||
startIcon={<PlayCircleOutlineIcon />}
|
||||
onClick={() => handleAction()}
|
||||
>
|
||||
Start
|
||||
</LoadingButton>
|
||||
<BuildParametersPopover
|
||||
workspace={workspace}
|
||||
disabled={loading}
|
||||
onSubmit={handleAction}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@ -59,21 +82,38 @@ export const StopButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const RestartButton: FC<WorkspaceAction> = ({
|
||||
handleAction,
|
||||
loading,
|
||||
}) => {
|
||||
export const RestartButton: FC<
|
||||
Omit<WorkspaceAction, "handleAction"> & {
|
||||
workspace: Workspace
|
||||
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void
|
||||
}
|
||||
> = ({ handleAction, loading, workspace }) => {
|
||||
return (
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingIndicator="Restarting..."
|
||||
loadingPosition="start"
|
||||
startIcon={<ReplayIcon />}
|
||||
onClick={handleAction}
|
||||
data-testid="workspace-restart-button"
|
||||
<ButtonGroup
|
||||
variant="outlined"
|
||||
sx={{
|
||||
// Workaround to make the border transitions smmothly on button groups
|
||||
"& > button:hover + button": {
|
||||
borderLeft: "1px solid #FFF",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
loadingIndicator="Restarting..."
|
||||
loadingPosition="start"
|
||||
startIcon={<ReplayIcon />}
|
||||
onClick={() => handleAction()}
|
||||
data-testid="workspace-restart-button"
|
||||
>
|
||||
Restart
|
||||
</LoadingButton>
|
||||
<BuildParametersPopover
|
||||
workspace={workspace}
|
||||
disabled={loading}
|
||||
onSubmit={handleAction}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,67 +26,66 @@ const defaultArgs = {
|
||||
export const Starting = Template.bind({})
|
||||
Starting.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockStartingWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockStartingWorkspace,
|
||||
}
|
||||
|
||||
export const Running = Template.bind({})
|
||||
Running.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockWorkspace,
|
||||
}
|
||||
|
||||
export const Stopping = Template.bind({})
|
||||
Stopping.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockStoppingWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockStoppingWorkspace,
|
||||
}
|
||||
|
||||
export const Stopped = Template.bind({})
|
||||
Stopped.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockStoppedWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockStoppedWorkspace,
|
||||
}
|
||||
|
||||
export const Canceling = Template.bind({})
|
||||
Canceling.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockCancelingWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockCancelingWorkspace,
|
||||
}
|
||||
|
||||
export const Canceled = Template.bind({})
|
||||
Canceled.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockCanceledWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockCanceledWorkspace,
|
||||
}
|
||||
|
||||
export const Deleting = Template.bind({})
|
||||
Deleting.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockDeletingWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockDeletingWorkspace,
|
||||
}
|
||||
|
||||
export const Deleted = Template.bind({})
|
||||
Deleted.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockDeletedWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockDeletedWorkspace,
|
||||
}
|
||||
|
||||
export const Outdated = Template.bind({})
|
||||
Outdated.args = {
|
||||
...defaultArgs,
|
||||
isOutdated: true,
|
||||
workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockOutdatedWorkspace,
|
||||
}
|
||||
|
||||
export const Failed = Template.bind({})
|
||||
Failed.args = {
|
||||
...defaultArgs,
|
||||
workspaceStatus: Mocks.MockFailedWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockFailedWorkspace,
|
||||
}
|
||||
|
||||
export const Updating = Template.bind({})
|
||||
Updating.args = {
|
||||
...defaultArgs,
|
||||
isUpdating: true,
|
||||
workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status,
|
||||
workspace: Mocks.MockOutdatedWorkspace,
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import Menu from "@mui/material/Menu"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"
|
||||
import { FC, Fragment, ReactNode, useRef, useState } from "react"
|
||||
import { WorkspaceStatus } from "api/typesGenerated"
|
||||
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"
|
||||
import {
|
||||
ActionLoadingButton,
|
||||
CancelButton,
|
||||
@ -24,11 +24,10 @@ import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
|
||||
import IconButton from "@mui/material/IconButton"
|
||||
|
||||
export interface WorkspaceActionsProps {
|
||||
workspaceStatus: WorkspaceStatus
|
||||
isOutdated: boolean
|
||||
handleStart: () => void
|
||||
workspace: Workspace
|
||||
handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void
|
||||
handleStop: () => void
|
||||
handleRestart: () => void
|
||||
handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void
|
||||
handleDelete: () => void
|
||||
handleUpdate: () => void
|
||||
handleCancel: () => void
|
||||
@ -41,8 +40,7 @@ export interface WorkspaceActionsProps {
|
||||
}
|
||||
|
||||
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
workspaceStatus,
|
||||
isOutdated,
|
||||
workspace,
|
||||
handleStart,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
@ -60,8 +58,8 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
canCancel,
|
||||
canAcceptJobs,
|
||||
actions: actionsByStatus,
|
||||
} = actionsByWorkspaceStatus(workspaceStatus)
|
||||
const canBeUpdated = isOutdated && canAcceptJobs
|
||||
} = actionsByWorkspaceStatus(workspace.latest_build.status)
|
||||
const canBeUpdated = workspace.outdated && canAcceptJobs
|
||||
const menuTriggerRef = useRef<HTMLButtonElement>(null)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
|
||||
@ -71,17 +69,25 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
[ButtonTypesEnum.updating]: (
|
||||
<UpdateButton loading handleAction={handleUpdate} />
|
||||
),
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||
[ButtonTypesEnum.start]: (
|
||||
<StartButton workspace={workspace} handleAction={handleStart} />
|
||||
),
|
||||
[ButtonTypesEnum.starting]: (
|
||||
<StartButton loading handleAction={handleStart} />
|
||||
<StartButton loading workspace={workspace} handleAction={handleStart} />
|
||||
),
|
||||
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
|
||||
[ButtonTypesEnum.stopping]: (
|
||||
<StopButton loading handleAction={handleStop} />
|
||||
),
|
||||
[ButtonTypesEnum.restart]: <RestartButton handleAction={handleRestart} />,
|
||||
[ButtonTypesEnum.restart]: (
|
||||
<RestartButton workspace={workspace} handleAction={handleRestart} />
|
||||
),
|
||||
[ButtonTypesEnum.restarting]: (
|
||||
<RestartButton loading handleAction={handleRestart} />
|
||||
<RestartButton
|
||||
loading
|
||||
workspace={workspace}
|
||||
handleAction={handleRestart}
|
||||
/>
|
||||
),
|
||||
[ButtonTypesEnum.deleting]: <ActionLoadingButton label="Deleting" />,
|
||||
[ButtonTypesEnum.canceling]: <DisabledButton label="Canceling..." />,
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
MutableTemplateParametersSection,
|
||||
} from "components/TemplateParameters/TemplateParameters"
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
||||
import { paramUsedToCreateWorkspace } from "utils/workspace"
|
||||
|
||||
export enum CreateWorkspaceErrors {
|
||||
GET_TEMPLATES_ERROR = "getTemplatesError",
|
||||
@ -59,8 +60,11 @@ export interface CreateWorkspacePageViewProps {
|
||||
export const CreateWorkspacePageView: FC<
|
||||
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
||||
> = (props) => {
|
||||
const templateParameters = props.templateParameters?.filter(
|
||||
paramUsedToCreateWorkspace,
|
||||
)
|
||||
const initialRichParameterValues = selectInitialRichParametersValues(
|
||||
props.templateParameters,
|
||||
templateParameters,
|
||||
props.defaultParameterValues,
|
||||
)
|
||||
const [gitAuthErrors, setGitAuthErrors] = useState<Record<string, string>>({})
|
||||
@ -72,20 +76,16 @@ export const CreateWorkspacePageView: FC<
|
||||
// to disappear.
|
||||
setGitAuthErrors({})
|
||||
}, [props.templateGitAuth])
|
||||
|
||||
const workspaceErrors =
|
||||
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]
|
||||
|
||||
// Scroll to top of page if errors are present
|
||||
useEffect(() => {
|
||||
if (props.hasTemplateErrors || Boolean(workspaceErrors)) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}, [props.hasTemplateErrors, workspaceErrors])
|
||||
|
||||
const { t } = useTranslation("createWorkspacePage")
|
||||
const styles = useStyles()
|
||||
|
||||
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
|
||||
useFormik<TypesGen.CreateWorkspaceRequest>({
|
||||
initialValues: {
|
||||
@ -97,7 +97,7 @@ export const CreateWorkspacePageView: FC<
|
||||
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
|
||||
rich_parameter_values: useValidationSchemaForRichParameters(
|
||||
"createWorkspacePage",
|
||||
props.templateParameters,
|
||||
templateParameters,
|
||||
),
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
@ -240,10 +240,10 @@ export const CreateWorkspacePageView: FC<
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{props.templateParameters && (
|
||||
{templateParameters && (
|
||||
<>
|
||||
<MutableTemplateParametersSection
|
||||
templateParameters={props.templateParameters}
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
...getFieldHelpers(
|
||||
@ -264,7 +264,7 @@ export const CreateWorkspacePageView: FC<
|
||||
}}
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={props.templateParameters}
|
||||
templateParameters={templateParameters}
|
||||
classes={{ root: styles.warningSection }}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
selectInitialRichParametersValues,
|
||||
workspaceBuildParameterValue,
|
||||
} from "utils/richParameters"
|
||||
import { paramUsedToCreateWorkspace } from "utils/workspace"
|
||||
|
||||
type ButtonValues = Record<string, string>
|
||||
|
||||
@ -38,7 +39,9 @@ const TemplateEmbedPage = () => {
|
||||
</Helmet>
|
||||
<TemplateEmbedPageView
|
||||
template={template}
|
||||
templateParameters={templateParameters}
|
||||
templateParameters={templateParameters?.filter(
|
||||
paramUsedToCreateWorkspace,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -100,13 +100,11 @@ export const WorkspaceReadyPage = ({
|
||||
["canceling", "deleting", "pending", "starting", "stopping"].includes(
|
||||
workspace.latest_build.status,
|
||||
))
|
||||
|
||||
const {
|
||||
mutate: restartWorkspace,
|
||||
error: restartBuildError,
|
||||
isLoading: isRestarting,
|
||||
} = useRestartWorkspace()
|
||||
|
||||
// keep banner machine in sync with workspace
|
||||
useEffect(() => {
|
||||
bannerSend({ type: "REFRESH_WORKSPACE", workspace })
|
||||
@ -151,12 +149,14 @@ export const WorkspaceReadyPage = ({
|
||||
isUpdating={workspaceState.matches("ready.build.requestingUpdate")}
|
||||
isRestarting={isRestarting}
|
||||
workspace={workspace}
|
||||
handleStart={() => workspaceSend({ type: "START" })}
|
||||
handleStart={(buildParameters) =>
|
||||
workspaceSend({ type: "START", buildParameters })
|
||||
}
|
||||
handleStop={() => workspaceSend({ type: "STOP" })}
|
||||
handleDelete={() => workspaceSend({ type: "ASK_DELETE" })}
|
||||
handleRestart={() => {
|
||||
handleRestart={(buildParameters) => {
|
||||
if (isWarningIgnored("restart")) {
|
||||
restartWorkspace(workspace)
|
||||
restartWorkspace({ workspace, buildParameters })
|
||||
} else {
|
||||
setIsConfirmingRestart(true)
|
||||
}
|
||||
@ -260,7 +260,7 @@ export const WorkspaceReadyPage = ({
|
||||
if (shouldIgnore) {
|
||||
ignoreWarning("restart")
|
||||
}
|
||||
restartWorkspace(workspace)
|
||||
restartWorkspace({ workspace })
|
||||
setIsConfirmingRestart(false)
|
||||
}}
|
||||
onClose={() => setIsConfirmingRestart(false)}
|
||||
|
@ -9,6 +9,7 @@ import { useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
getInitialParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
workspaceBuildParameterValue,
|
||||
} from "utils/richParameters"
|
||||
@ -48,18 +49,10 @@ export const WorkspaceParametersForm: FC<{
|
||||
const form = useFormik<WorkspaceParametersFormValues>({
|
||||
onSubmit,
|
||||
initialValues: {
|
||||
rich_parameter_values: mutableParameters.map((parameter) => {
|
||||
const buildParameter = buildParameters.find(
|
||||
(p) => p.name === parameter.name,
|
||||
)
|
||||
if (!buildParameter) {
|
||||
return {
|
||||
name: parameter.name,
|
||||
value: parameter.default_value,
|
||||
}
|
||||
}
|
||||
return buildParameter
|
||||
}),
|
||||
rich_parameter_values: getInitialParameterValues(
|
||||
mutableParameters,
|
||||
buildParameters,
|
||||
),
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
rich_parameter_values: useValidationSchemaForRichParameters(
|
||||
@ -72,36 +65,80 @@ export const WorkspaceParametersForm: FC<{
|
||||
form,
|
||||
error,
|
||||
)
|
||||
const hasEphemeralParameters = mutableParameters.some(
|
||||
(parameter) => parameter.ephemeral,
|
||||
)
|
||||
const hasNonEphemeralParameters = mutableParameters.some(
|
||||
(parameter) => !parameter.ephemeral,
|
||||
)
|
||||
|
||||
return (
|
||||
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
|
||||
{mutableParameters.length > 0 && (
|
||||
{hasNonEphemeralParameters && (
|
||||
<FormSection
|
||||
title={t("parameters").toString()}
|
||||
description={t("parametersDescription").toString()}
|
||||
>
|
||||
<FormFields>
|
||||
{mutableParameters.map((parameter, index) => (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
index={index}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
})
|
||||
}}
|
||||
parameter={parameter}
|
||||
initialValue={workspaceBuildParameterValue(
|
||||
buildParameters,
|
||||
parameter,
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{mutableParameters.map((parameter, index) =>
|
||||
// Since we are adding the values to the form based on the index
|
||||
// we can't filter them to not loose the right index position
|
||||
parameter.ephemeral ? null : (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
index={index}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
})
|
||||
}}
|
||||
parameter={parameter}
|
||||
initialValue={workspaceBuildParameterValue(
|
||||
buildParameters,
|
||||
parameter,
|
||||
)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
{hasEphemeralParameters && (
|
||||
<FormSection
|
||||
title="Ephemeral Parameters"
|
||||
description="These parameters only apply for a single workspace start."
|
||||
>
|
||||
<FormFields>
|
||||
{mutableParameters.map((parameter, index) =>
|
||||
// Since we are adding the values to the form based on the index
|
||||
// we can't filter them to not loose the right index position
|
||||
parameter.ephemeral ? (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting}
|
||||
index={index}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("rich_parameter_values." + index, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
})
|
||||
}}
|
||||
parameter={parameter}
|
||||
initialValue={workspaceBuildParameterValue(
|
||||
buildParameters,
|
||||
parameter,
|
||||
)}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
|
@ -1,9 +1,4 @@
|
||||
import {
|
||||
getTemplateVersionRichParameters,
|
||||
getWorkspaceBuildParameters,
|
||||
postWorkspaceBuild,
|
||||
} from "api/api"
|
||||
import { Workspace } from "api/typesGenerated"
|
||||
import { getWorkspaceParameters, postWorkspaceBuild } from "api/api"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { pageTitle } from "utils/page"
|
||||
import { useWorkspaceSettingsContext } from "../WorkspaceSettingsLayout"
|
||||
@ -20,22 +15,10 @@ import { FC } from "react"
|
||||
import { isApiValidationError } from "api/errors"
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert"
|
||||
|
||||
const getWorkspaceParameters = async (workspace: Workspace) => {
|
||||
const latestBuild = workspace.latest_build
|
||||
const [templateVersionRichParameters, buildParameters] = await Promise.all([
|
||||
getTemplateVersionRichParameters(latestBuild.template_version_id),
|
||||
getWorkspaceBuildParameters(latestBuild.id),
|
||||
])
|
||||
return {
|
||||
templateVersionRichParameters,
|
||||
buildParameters,
|
||||
}
|
||||
}
|
||||
|
||||
const WorkspaceParametersPage = () => {
|
||||
const { workspace } = useWorkspaceSettingsContext()
|
||||
const query = useQuery({
|
||||
queryKey: ["workspaceSettings", workspace.id],
|
||||
queryKey: ["workspace", workspace.id, "parameters"],
|
||||
queryFn: () => getWorkspaceParameters(workspace),
|
||||
})
|
||||
const navigate = useNavigate()
|
||||
|
@ -32,6 +32,10 @@ export const selectInitialRichParametersValues = (
|
||||
return
|
||||
}
|
||||
|
||||
if (parameter.ephemeral) {
|
||||
parameterValue = parameter.default_value
|
||||
}
|
||||
|
||||
if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) {
|
||||
parameterValue = defaultValuesFromQuery[parameter.name]
|
||||
}
|
||||
@ -182,3 +186,21 @@ export const workspaceBuildParameterValue = (
|
||||
})
|
||||
return (buildParameter && buildParameter.value) || ""
|
||||
}
|
||||
|
||||
export const getInitialParameterValues = (
|
||||
templateParameters: TemplateVersionParameter[],
|
||||
buildParameters: WorkspaceBuildParameter[],
|
||||
) => {
|
||||
return templateParameters.map((parameter) => {
|
||||
const buildParameter = buildParameters.find(
|
||||
(p) => p.name === parameter.name,
|
||||
)
|
||||
if (!buildParameter || parameter.ephemeral) {
|
||||
return {
|
||||
name: parameter.name,
|
||||
value: parameter.default_value,
|
||||
}
|
||||
}
|
||||
return buildParameter
|
||||
})
|
||||
}
|
||||
|
@ -285,3 +285,7 @@ const LoadingIcon = () => {
|
||||
export const hasJobError = (workspace: TypesGen.Workspace) => {
|
||||
return workspace.latest_build.job.error !== undefined
|
||||
}
|
||||
|
||||
export const paramUsedToCreateWorkspace = (
|
||||
param: TypesGen.TemplateVersionParameter,
|
||||
) => !param.ephemeral
|
||||
|
@ -75,7 +75,7 @@ export interface WorkspaceContext {
|
||||
|
||||
export type WorkspaceEvent =
|
||||
| { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] }
|
||||
| { type: "START" }
|
||||
| { type: "START"; buildParameters?: TypesGen.WorkspaceBuildParameter[] }
|
||||
| { type: "STOP" }
|
||||
| { type: "ASK_DELETE" }
|
||||
| { type: "DELETE" }
|
||||
@ -152,9 +152,6 @@ export const workspaceMachine = createMachine(
|
||||
loadInitialWorkspaceData: {
|
||||
data: Awaited<ReturnType<typeof loadInitialWorkspaceData>>
|
||||
}
|
||||
getTemplateParameters: {
|
||||
data: TypesGen.TemplateVersionParameter[]
|
||||
}
|
||||
updateWorkspace: {
|
||||
data: TypesGen.WorkspaceBuild
|
||||
}
|
||||
@ -629,12 +626,13 @@ export const workspaceMachine = createMachine(
|
||||
send({ type: "REFRESH_TIMELINE" })
|
||||
return build
|
||||
},
|
||||
startWorkspace: (context) => async (send) => {
|
||||
startWorkspace: (context, data) => async (send) => {
|
||||
if (context.workspace) {
|
||||
const startWorkspacePromise = await API.startWorkspace(
|
||||
context.workspace.id,
|
||||
context.workspace.latest_build.template_version_id,
|
||||
context.createBuildLogLevel,
|
||||
"buildParameters" in data ? data.buildParameters : undefined,
|
||||
)
|
||||
send({ type: "REFRESH_TIMELINE" })
|
||||
return startWorkspacePromise
|
||||
|
Reference in New Issue
Block a user