mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site/src/pages/TemplateBuilder): disable create button when no provisioners (#26938)
## Summary Add provisioner awareness to the Template Builder wizard: disable the Create Template button when the selected organization has no provisioners, and reset the customizations step when navigating back. ## Changes - Query provisioner daemons for the selected org in `TemplateCustomizationsStep` and show a warning alert when none are found - Track `hasProvisioners` in wizard state via `SET_HAS_PROVISIONERS` action; `computeCanContinue` disables the Create Template button when `hasProvisioners === false` - Add `RESET_CUSTOMIZATIONS` action to clear customization fields (name, displayName, description, icon, organizationId, hasProvisioners) when navigating back from the customizations step - Clear the create mutation error on back navigation via `onClearCreateError` callback Follows up on #26935 which added the provisioner warning alert to the Template Builder. > 🤖 Generated by Coder Agents on behalf of @jeremyruppel
This commit is contained in:
@@ -62,6 +62,7 @@ const TemplateBuilderPage: FC = () => {
|
||||
onCreateTemplate={handleCreate}
|
||||
createError={createMutation.error}
|
||||
isCreating={createMutation.isPending}
|
||||
onClearCreateError={() => createMutation.reset()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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<TemplateBuilderPageViewProps> = ({
|
||||
@@ -59,6 +66,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
|
||||
onCreateTemplate,
|
||||
createError,
|
||||
isCreating,
|
||||
onClearCreateError,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(wizardReducer, initialWizardState);
|
||||
const [stepIndex, setStepIndex] = useState(0);
|
||||
@@ -85,6 +93,10 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
|
||||
);
|
||||
|
||||
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<TemplateBuilderPageViewProps> = ({
|
||||
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<TemplateBuilderPageViewProps> = ({
|
||||
dispatch,
|
||||
moduleVarMap,
|
||||
createError,
|
||||
handleProvisionerStatusChange,
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -193,6 +213,7 @@ function renderStepContent(
|
||||
dispatch: (action: WizardAction) => void,
|
||||
moduleVarMap: Record<string, Record<string, string>>,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -34,6 +34,7 @@ export type TemplateBuilderWizardState = {
|
||||
baseVariableValues: Record<string, string>;
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user