diff --git a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx index ffda44a0bf4..592d69050a7 100644 --- a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx +++ b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx @@ -62,6 +62,7 @@ const TemplateBuilderPage: FC = () => { onCreateTemplate={handleCreate} createError={createMutation.error} isCreating={createMutation.isPending} + onClearCreateError={() => createMutation.reset()} /> ); diff --git a/site/src/pages/TemplateBuilder/TemplateBuilderPageView.tsx b/site/src/pages/TemplateBuilder/TemplateBuilderPageView.tsx index 099cb94a896..94e54359638 100644 --- a/site/src/pages/TemplateBuilder/TemplateBuilderPageView.tsx +++ b/site/src/pages/TemplateBuilder/TemplateBuilderPageView.tsx @@ -1,4 +1,10 @@ -import { type FC, type ReactNode, useReducer, useState } from "react"; +import { + type FC, + type ReactNode, + useCallback, + useReducer, + useState, +} from "react"; import { useQuery } from "react-query"; import { templateBuilderModules } from "#/api/queries/templateBuilder"; @@ -51,6 +57,7 @@ interface TemplateBuilderPageViewProps { onCreateTemplate: (state: TemplateBuilderWizardState) => void; createError: Error | null; isCreating: boolean; + onClearCreateError?: () => void; } export const TemplateBuilderPageView: FC = ({ @@ -59,6 +66,7 @@ export const TemplateBuilderPageView: FC = ({ onCreateTemplate, createError, isCreating, + onClearCreateError, }) => { const [state, dispatch] = useReducer(wizardReducer, initialWizardState); const [stepIndex, setStepIndex] = useState(0); @@ -85,6 +93,10 @@ export const TemplateBuilderPageView: FC = ({ ); const handleBack = () => { + if (currentStep.id === "customizations") { + dispatch({ type: "RESET_CUSTOMIZATIONS" }); + onClearCreateError?.(); + } window.scrollTo(0, 0); setStepIndex(prevIndex); }; @@ -98,6 +110,13 @@ export const TemplateBuilderPageView: FC = ({ setStepIndex(nextIndex); }; + const handleProvisionerStatusChange = useCallback( + (value: boolean | undefined) => { + dispatch({ type: "SET_HAS_PROVISIONERS", value }); + }, + [], + ); + const handleDeselectModule = (moduleId: string) => { // If the only module gets deselected, go back to module selection if (state.modules.length === 1) { @@ -142,6 +161,7 @@ export const TemplateBuilderPageView: FC = ({ dispatch, moduleVarMap, createError, + handleProvisionerStatusChange, )} @@ -193,6 +213,7 @@ function renderStepContent( dispatch: (action: WizardAction) => void, moduleVarMap: Record>, createError: Error | null, + onProvisionerStatusChange: (value: boolean | undefined) => void, ): ReactNode { switch (stepId) { case "base-infra": @@ -253,6 +274,7 @@ function renderStepContent( value, }) } + onProvisionerStatusChange={onProvisionerStatusChange} /> ); @@ -284,7 +306,7 @@ function computeCanContinue( moduleVarMap, ); case "customizations": - return state.name.trim() !== ""; + return state.name.trim() !== "" && state.hasProvisioners !== false; default: return true; } diff --git a/site/src/pages/TemplateBuilder/TemplateCustomizationsStep.tsx b/site/src/pages/TemplateBuilder/TemplateCustomizationsStep.tsx index 16b433cfe4d..7f193fb2142 100644 --- a/site/src/pages/TemplateBuilder/TemplateCustomizationsStep.tsx +++ b/site/src/pages/TemplateBuilder/TemplateCustomizationsStep.tsx @@ -28,11 +28,12 @@ interface TemplateCustomizationsStepProps { field: "organizationId" | "name" | "displayName" | "description" | "icon", value: string, ) => void; + onProvisionerStatusChange: (hasProvisioners: boolean | undefined) => void; } export const TemplateCustomizationsStep: FC< TemplateCustomizationsStepProps -> = ({ state, onChangeField }) => { +> = ({ state, onChangeField, onProvisionerStatusChange }) => { const permittedOrgsQuery = useQuery( permittedOrganizations({ object: { resource_type: "template" }, @@ -47,7 +48,14 @@ export const TemplateCustomizationsStep: FC< ...provisionerDaemons(selectedOrg?.id ?? ""), enabled: Boolean(selectedOrg), }); - const showProvisionerWarning = provisioners ? provisioners.length < 1 : false; + const hasProvisioners = provisioners ? provisioners.length > 0 : undefined; + const showProvisionerWarning = hasProvisioners === false; + + // Notify parent when provisioner status changes so the wizard can + // disable the create button when no provisioners are available. + useEffect(() => { + onProvisionerStatusChange(hasProvisioners); + }, [hasProvisioners, onProvisionerStatusChange]); // Auto-select when exactly one org is available. useEffect(() => { diff --git a/site/src/pages/TemplateBuilder/wizardState.ts b/site/src/pages/TemplateBuilder/wizardState.ts index a2bf362b31d..45cd9a337b1 100644 --- a/site/src/pages/TemplateBuilder/wizardState.ts +++ b/site/src/pages/TemplateBuilder/wizardState.ts @@ -34,6 +34,7 @@ export type TemplateBuilderWizardState = { baseVariableValues: Record; modules: TemplateBuilderComposeModule[]; organizationId?: string; + hasProvisioners: boolean | undefined; name: string; displayName: string; description: string; @@ -46,6 +47,7 @@ export const initialWizardState: TemplateBuilderWizardState = { baseTemplateId: null, baseVariableValues: {}, modules: [], + hasProvisioners: undefined, name: "", displayName: "", description: "", @@ -72,6 +74,8 @@ export type WizardAction = field: "organizationId" | "name" | "displayName" | "description" | "icon"; value: string; } + | { type: "SET_HAS_PROVISIONERS"; value: boolean | undefined } + | { type: "RESET_CUSTOMIZATIONS" } | { type: "RESET" }; export function wizardReducer( @@ -123,6 +127,21 @@ export function wizardReducer( ...state, [action.field]: action.value, }; + case "SET_HAS_PROVISIONERS": + return { + ...state, + hasProvisioners: action.value, + }; + case "RESET_CUSTOMIZATIONS": + return { + ...state, + organizationId: undefined, + hasProvisioners: undefined, + name: "", + displayName: "", + description: "", + icon: "", + }; case "RESET": return initialWizardState; default: