diff --git a/site/src/components/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx b/site/src/components/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx new file mode 100644 index 0000000000000..23dfc27be2506 --- /dev/null +++ b/site/src/components/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx @@ -0,0 +1,148 @@ +import { makeStyles } from "@material-ui/core/styles" +import Dialog from "@material-ui/core/Dialog" +import DialogContent from "@material-ui/core/DialogContent" +import DialogContentText from "@material-ui/core/DialogContentText" +import DialogTitle from "@material-ui/core/DialogTitle" +import { DialogProps } from "components/Dialogs/Dialog" +import { FC, useEffect, useState } from "react" +import { FormFields, VerticalForm } from "components/Form/Form" +import { TemplateVersionVariable, VariableValue } from "api/typesGenerated" +import DialogActions from "@material-ui/core/DialogActions" +import Button from "@material-ui/core/Button" +import { VariableInput } from "pages/CreateTemplatePage/VariableInput" +import { Loader } from "components/Loader/Loader" + +export type MissingTemplateVariablesDialogProps = Omit< + DialogProps, + "onSubmit" +> & { + onClose: () => void + onSubmit: (values: VariableValue[]) => void + missingVariables?: TemplateVersionVariable[] +} + +export const MissingTemplateVariablesDialog: FC< + MissingTemplateVariablesDialogProps +> = ({ missingVariables, onSubmit, ...dialogProps }) => { + const styles = useStyles() + const [variableValues, setVariableValues] = useState([]) + + // Pre-fill the form with the default values when missing variables are loaded + useEffect(() => { + if (!missingVariables) { + return + } + setVariableValues( + missingVariables.map((v) => ({ name: v.name, value: v.value })), + ) + }, [missingVariables]) + + return ( + + + Template variables + + + + There are a few missing template variable values. Please fill them in. + + { + e.preventDefault() + onSubmit(variableValues) + }} + > + {missingVariables ? ( + + {missingVariables.map((variable, index) => { + return ( + { + setVariableValues((prev) => { + prev[index] = { + name: variable.name, + value, + } + return [...prev] + }) + }} + /> + ) + })} + + ) : ( + + )} + + + + + + + + ) +} + +const useStyles = makeStyles((theme) => ({ + title: { + padding: theme.spacing(3, 5), + + "& h2": { + fontSize: theme.spacing(2.5), + fontWeight: 400, + }, + }, + + content: { + padding: theme.spacing(0, 5, 0, 5), + }, + + info: { + margin: 0, + }, + + form: { + paddingTop: theme.spacing(4), + }, + + infoTitle: { + fontSize: theme.spacing(2), + fontWeight: 600, + display: "flex", + alignItems: "center", + gap: theme.spacing(1), + }, + + formFooter: { + flexDirection: "column", + }, + + dialogActions: { + padding: theme.spacing(5), + flexDirection: "column", + gap: theme.spacing(1), + }, +})) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index f9eea9478339f..d639a262ff169 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -9,8 +9,11 @@ import { ProvisionerJobLog, Template, TemplateVersion, + TemplateVersionVariable, + VariableValue, WorkspaceResource, } from "api/typesGenerated" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { Avatar } from "components/Avatar/Avatar" import { AvatarData } from "components/AvatarData/AvatarData" import { bannerHeight } from "components/DeploymentBanner/DeploymentBannerView" @@ -36,6 +39,7 @@ import { RenameFileDialog, } from "./FileDialog" import { FileTreeView } from "./FileTreeView" +import { MissingTemplateVariablesDialog } from "./MissingTemplateVariablesDialog" import { MonacoEditor } from "./MonacoEditor" import { PublishTemplateVersionDialog } from "./PublishTemplateVersionDialog" import { @@ -58,7 +62,11 @@ export interface TemplateVersionEditorProps { onCancelPublish: () => void publishingError: unknown isAskingPublishParameters: boolean + isPromptingMissingVariables: boolean isPublishing: boolean + missingVariables?: TemplateVersionVariable[] + onSubmitMissingVariableValues: (values: VariableValue[]) => void + onCancelSubmitMissingVariableValues: () => void } const topbarHeight = 80 @@ -91,6 +99,10 @@ export const TemplateVersionEditor: FC = ({ isPublishing, buildLogs, resources, + isPromptingMissingVariables, + missingVariables, + onSubmitMissingVariableValues, + onCancelSubmitMissingVariableValues, }) => { const [selectedTab, setSelectedTab] = useState(() => { // If resources are provided, show them by default! @@ -143,7 +155,7 @@ export const TemplateVersionEditor: FC = ({ return } if ( - previousVersion.current.job.status === "running" && + ["running", "pending"].includes(previousVersion.current.job.status) && templateVersion.job.status === "succeeded" ) { setSelectedTab(1) @@ -363,22 +375,24 @@ export const TemplateVersionEditor: FC = ({ selectedTab === 0 ? "" : "hidden" }`} > - {buildLogs && ( + {templateVersion.job.error && ( + + )} + + {buildLogs && buildLogs.length > 0 && ( )} - {templateVersion.job.error && ( -
- {templateVersion.job.error} -
- )}
@@ -410,6 +424,13 @@ export const TemplateVersionEditor: FC = ({ isPublishing={isPublishing} defaultName={templateVersion.name} /> + + ) } @@ -570,13 +591,8 @@ const useStyles = makeStyles< }, buildLogs: { display: "flex", - flexDirection: "column-reverse", + flexDirection: "column", overflowY: "auto", - }, - buildLogError: { - whiteSpace: "pre-wrap", - }, - resources: { - // padding: 16, + gap: theme.spacing(1), }, })) diff --git a/site/src/pages/CreateTemplatePage/VariableInput.tsx b/site/src/pages/CreateTemplatePage/VariableInput.tsx index 25ce27541fee3..83978dc30a7ab 100644 --- a/site/src/pages/CreateTemplatePage/VariableInput.tsx +++ b/site/src/pages/CreateTemplatePage/VariableInput.tsx @@ -90,6 +90,7 @@ const VariableField: React.FC = ({ return ( { } resources={editorState.context.resources} buildLogs={editorState.context.buildLogs} + isPromptingMissingVariables={editorState.matches("promptVariables")} + missingVariables={editorState.context.missingVariables} + onSubmitMissingVariableValues={(values) => { + sendEvent({ + type: "SET_MISSING_VARIABLE_VALUES", + values, + }) + }} + onCancelSubmitMissingVariableValues={() => { + sendEvent({ + type: "CANCEL_MISSING_VARIABLE_VALUES", + }) + }} /> )} diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index b9a873f11bf07..067107bcbc9c4 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -2,7 +2,9 @@ import { ProvisionerJobLog, ProvisionerJobStatus, TemplateVersion, + TemplateVersionVariable, UploadResponse, + VariableValue, WorkspaceResource, } from "api/typesGenerated" import { assign, createMachine } from "xstate" @@ -26,6 +28,8 @@ export interface TemplateVersionEditorMachineContext { buildLogs?: ProvisionerJobLog[] tarReader?: TarReader publishingError?: unknown + missingVariables?: TemplateVersionVariable[] + missingVariableValues?: VariableValue[] } export const templateVersionEditorMachine = createMachine( @@ -42,6 +46,8 @@ export const templateVersionEditorMachine = createMachine( templateId: string } | { type: "CANCEL_VERSION" } + | { type: "SET_MISSING_VARIABLE_VALUES"; values: VariableValue[] } + | { type: "CANCEL_MISSING_VARIABLE_VALUES" } | { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog } | { type: "PUBLISH" } | ({ type: "CONFIRM_PUBLISH" } & PublishVersionData) @@ -66,6 +72,9 @@ export const templateVersionEditorMachine = createMachine( publishingVersion: { data: void } + loadMissingVariables: { + data: TemplateVersionVariable[] + } }, }, tsTypes: {} as import("./templateVersionEditorXService.typegen").Typegen0, @@ -170,9 +179,41 @@ export const templateVersionEditorMachine = createMachine( invoke: { id: "fetchVersion", src: "fetchVersion", - onDone: { - actions: ["assignBuild"], - target: "fetchResources", + onDone: [ + { + actions: ["assignBuild"], + target: "promptVariables", + cond: "jobFailedWithMissingVariables", + }, + { + actions: ["assignBuild"], + target: "fetchResources", + }, + ], + }, + }, + promptVariables: { + initial: "loadingMissingVariables", + states: { + loadingMissingVariables: { + invoke: { + src: "loadMissingVariables", + onDone: { + actions: "assignMissingVariables", + target: "idle", + }, + }, + }, + idle: { + on: { + SET_MISSING_VARIABLE_VALUES: { + actions: "assignMissingVariableValues", + target: "#templateVersionEditor.creatingBuild", + }, + CANCEL_MISSING_VARIABLE_VALUES: { + target: "#templateVersionEditor.idle", + }, + }, }, }, }, @@ -235,6 +276,12 @@ export const templateVersionEditorMachine = createMachine( publishingError: (_, event) => event.data, }), clearPublishingError: assign({ publishingError: (_) => undefined }), + assignMissingVariables: assign({ + missingVariables: (_, event) => event.data, + }), + assignMissingVariableValues: assign({ + missingVariableValues: (_, event) => event.values, + }), }, services: { uploadTar: async ({ fileTree, tarReader }) => { @@ -297,6 +344,7 @@ export const templateVersionEditorMachine = createMachine( tags: {}, template_id: ctx.templateId, file_id: ctx.uploadResponse.hash, + user_variable_values: ctx.missingVariableValues, }) }, fetchVersion: (ctx) => { @@ -352,6 +400,18 @@ export const templateVersionEditorMachine = createMachine( : Promise.resolve(), ]) }, + loadMissingVariables: ({ version }) => { + if (!version) { + throw new Error("Version is not set") + } + const variables = API.getTemplateVersionVariables(version.id) + return variables + }, + }, + guards: { + jobFailedWithMissingVariables: (_, { data }) => { + return data.job.error_code === "REQUIRED_TEMPLATE_VARIABLES" + }, }, }, )