mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add BaseInfraSelectStep (#26424)
Implement the first wizard step for selecting a base infrastructure template. - Add `getTemplateBuilderBases` API client method and react-query wrapper - Render a responsive grid of `TemplateCard` components fetched from `GET /api/v2/templatebuilder/bases` - Map `TemplateBuilderBase` to `SelectedBaseMeta` with `hasParameters` derived from the base variables - Single-choice selection that dispatches `SET_BASE` to wizard state, preserving selection on back navigation Relates to [DEVEX-283](https://linear.app/codercom/issue/DEVEX-283). > [!NOTE] > This PR was authored with Coder Agents.
This commit is contained in:
@@ -2446,6 +2446,12 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getTemplateBuilderBases =
|
||||
async (): Promise<TypesGen.TemplateBuilderBasesResponse> => {
|
||||
const response = await this.axios.get("/api/v2/templatebuilder/bases");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
uploadFile = async (file: File): Promise<TypesGen.UploadResponse> => {
|
||||
const response = await this.axios.post("/api/v2/files", file, {
|
||||
headers: { "Content-Type": file.type },
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { API } from "#/api/api";
|
||||
|
||||
export const templateBuilderBases = () => ({
|
||||
queryKey: ["templateBuilder", "bases"],
|
||||
queryFn: API.getTemplateBuilderBases,
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { templateBuilderBases } from "#/api/queries/templateBuilder";
|
||||
import type { TemplateBuilderBase } from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { TemplateCard } from "./TemplateCard";
|
||||
import type { SelectedBaseMeta } from "./wizardState";
|
||||
|
||||
interface BaseInfraSelectStepProps {
|
||||
selectedBaseId: string | null;
|
||||
onSelectBase: (base: SelectedBaseMeta) => void;
|
||||
}
|
||||
|
||||
function toSelectedBaseMeta(base: TemplateBuilderBase): SelectedBaseMeta {
|
||||
return {
|
||||
id: base.id,
|
||||
name: base.name,
|
||||
iconUrl: base.icon,
|
||||
os: base.os,
|
||||
hasParameters: false,
|
||||
};
|
||||
}
|
||||
|
||||
function detailsUrl(baseId: string): string {
|
||||
return `https://registry.coder.com/templates/${baseId}`;
|
||||
}
|
||||
|
||||
export const BaseInfraSelectStep: FC<BaseInfraSelectStepProps> = ({
|
||||
selectedBaseId,
|
||||
onSelectBase,
|
||||
}) => {
|
||||
const { data, error, isLoading } = useQuery(templateBuilderBases());
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorAlert error={error} />;
|
||||
}
|
||||
|
||||
const bases = data?.bases ?? [];
|
||||
|
||||
return (
|
||||
<div role="radiogroup" aria-label="Base infrastructure templates">
|
||||
<h2 className="text-lg font-semibold mb-1">Pick a base template</h2>
|
||||
<p className="text-sm text-content-secondary mb-4">
|
||||
Select your infrastructure foundation.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{bases.map((base) => (
|
||||
<TemplateCard
|
||||
key={base.id}
|
||||
name={base.name}
|
||||
description={base.description}
|
||||
iconUrl={base.icon}
|
||||
detailsUrl={detailsUrl(base.id)}
|
||||
selected={base.id === selectedBaseId}
|
||||
onSelect={() => onSelectBase(toSelectedBaseMeta(base))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { BaseInfraSelectStep } from "./BaseInfraSelectStep";
|
||||
import { SelectionSummary } from "./SelectionSummary";
|
||||
import {
|
||||
findNextVisibleIndex,
|
||||
@@ -56,7 +57,7 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<Margins className="pb-12">
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>Create new template</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
@@ -72,11 +73,18 @@ export const TemplateBuilderPageView: FC<TemplateBuilderPageViewProps> = ({
|
||||
<div className="flex gap-8">
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="rounded-lg border border-solid border-border bg-surface-primary p-6 min-h-[400px]">
|
||||
<p className="text-sm text-content-secondary">
|
||||
Step: {currentStep.id}
|
||||
</p>
|
||||
</div>
|
||||
{currentStep.id === "base-infra" ? (
|
||||
<BaseInfraSelectStep
|
||||
selectedBaseId={state.selectedBase?.id ?? null}
|
||||
onSelectBase={(base) => dispatch({ type: "SET_BASE", base })}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-lg border border-solid border-border bg-surface-primary p-6 min-h-[400px]">
|
||||
<p className="text-sm text-content-secondary">
|
||||
Step: {currentStep.id}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation controls */}
|
||||
<div className="flex justify-end mt-6 gap-2">
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
* UI-only metadata for the selected base template.
|
||||
* Kept separate from the API request payload.
|
||||
*/
|
||||
type SelectedBaseMeta = {
|
||||
export type SelectedBaseMeta = {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl?: string;
|
||||
|
||||
Reference in New Issue
Block a user