From 6e239fdcf3411014bc2c13970466b6a4fdf32f05 Mon Sep 17 00:00:00 2001 From: Vignesh Nayak Manel Date: Sun, 5 Apr 2026 15:58:41 +0530 Subject: [PATCH] feat(Console): Add quick setup feature --- console/package.json | 2 + .../src/components/forms/QuickSetupWizard.tsx | 190 ++++++++ .../components/forms/quick-setup/YamlStep.tsx | 147 +++++++ .../forms/quick-setup/progress-steps.tsx | 199 +++++++++ .../forms/quick-setup/setup-steps.tsx | 262 +++++++++++ console/src/lib/setup-executor.ts | 405 ++++++++++++++++++ console/src/lib/setup-presets.ts | 181 ++++++++ console/src/pages/Home.tsx | 29 +- console/src/types/setup-config.ts | 71 +++ 9 files changed, 1485 insertions(+), 1 deletion(-) create mode 100644 console/src/components/forms/QuickSetupWizard.tsx create mode 100644 console/src/components/forms/quick-setup/YamlStep.tsx create mode 100644 console/src/components/forms/quick-setup/progress-steps.tsx create mode 100644 console/src/components/forms/quick-setup/setup-steps.tsx create mode 100644 console/src/lib/setup-executor.ts create mode 100644 console/src/lib/setup-presets.ts create mode 100644 console/src/types/setup-config.ts diff --git a/console/package.json b/console/package.json index 8207f3e3..bb5dda57 100644 --- a/console/package.json +++ b/console/package.json @@ -30,6 +30,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "js-yaml": "^4.1.1", "lucide-react": "^0.548.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -42,6 +43,7 @@ }, "devDependencies": { "@eslint/js": "^9.36.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.9.2", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", diff --git a/console/src/components/forms/QuickSetupWizard.tsx b/console/src/components/forms/QuickSetupWizard.tsx new file mode 100644 index 00000000..04e3715f --- /dev/null +++ b/console/src/components/forms/QuickSetupWizard.tsx @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState } from "react" +import { useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { buildSteps, executeSetup, type SetupStep, type SetupResult } from "@/lib/setup-executor" +import { PRESETS, type Preset } from "@/lib/setup-presets" +import type { SetupConfig } from "@/types/setup-config" +import { ModeStep, PresetStep, ParamsStep, ReviewStep } from "./quick-setup/setup-steps" +import { YamlStep } from "./quick-setup/YamlStep" +import { ApplyStep, DoneStep } from "./quick-setup/progress-steps" + +type WizardStep = "mode" | "preset" | "params" | "yaml" | "review" | "applying" | "done" + +const DIALOG_TITLES: Record = { + mode: "Quick Setup", + preset: "Choose a Preset", + params: "Configure Storage", + yaml: "Paste Custom YAML", + review: "Review Actions", + applying: "Applying Configuration", + done: "Setup Complete", +} + +interface QuickSetupWizardProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function QuickSetupWizard({ open, onOpenChange }: QuickSetupWizardProps) { + const queryClient = useQueryClient() + + const [step, setStep] = useState("mode") + const [mode, setMode] = useState<"preset" | "yaml">("preset") + const [selectedPreset, setSelectedPreset] = useState(PRESETS[0]) + const [params, setParams] = useState>({}) + const [config, setConfig] = useState(null) + const [applySteps, setApplySteps] = useState([]) + const [result, setResult] = useState(null) + const [isApplying, setIsApplying] = useState(false) + + const reset = () => { + setStep("mode") + setMode("preset") + setSelectedPreset(PRESETS[0]) + setParams({}) + setConfig(null) + setApplySteps([]) + setResult(null) + setIsApplying(false) + } + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen && isApplying) return + if (!nextOpen) reset() + onOpenChange(nextOpen) + } + + const handleSelectMode = (m: "preset" | "yaml") => { + setMode(m) + setStep(m === "preset" ? "preset" : "yaml") + } + + const loadConfig = (preset: Preset, p: Record) => { + const resolved = preset.buildConfig(p) + setConfig(resolved) + setApplySteps(buildSteps(resolved)) + setStep("review") + } + + const handleNextFromPreset = () => { + if (selectedPreset.params.length > 0) { + setStep("params") + return + } + loadConfig(selectedPreset, {}) + } + + const handleNextFromParams = () => loadConfig(selectedPreset, params) + + const handleNextFromYaml = (parsed: SetupConfig) => { + setConfig(parsed) + setApplySteps(buildSteps(parsed)) + setStep("review") + } + + const handleApply = async () => { + if (!config) return + setStep("applying") + setIsApplying(true) + try { + const setupResult = await executeSetup(config, setApplySteps) + setResult(setupResult) + queryClient.invalidateQueries() + } catch (e) { + toast.error("Setup failed", { description: e instanceof Error ? e.message : String(e) }) + } finally { + setIsApplying(false) + setStep("done") + } + } + + return ( + + + + {DIALOG_TITLES[step]} + {step === "mode" && ( + + Bootstrap a Polaris environment using a preset or your own YAML config. + + )} + + +
+ {step === "mode" && } + + {step === "preset" && ( + setStep("mode")} + onNext={handleNextFromPreset} + /> + )} + + {step === "params" && ( + setStep("preset")} + onNext={handleNextFromParams} + /> + )} + + {step === "yaml" && ( + setStep("mode")} onNext={handleNextFromYaml} /> + )} + + {step === "review" && applySteps.length > 0 && ( + + setStep( + mode === "yaml" ? "yaml" : selectedPreset.params.length > 0 ? "params" : "preset" + ) + } + onApply={handleApply} + /> + )} + + {step === "applying" && } + + {(step === "done" || (step === "applying" && !isApplying)) && ( + handleOpenChange(false)} + /> + )} +
+
+
+ ) +} diff --git a/console/src/components/forms/quick-setup/YamlStep.tsx b/console/src/components/forms/quick-setup/YamlStep.tsx new file mode 100644 index 00000000..e14495c0 --- /dev/null +++ b/console/src/components/forms/quick-setup/YamlStep.tsx @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useEffect, useRef } from "react" +import { load as parseYaml } from "js-yaml" +import { CheckCircle2, AlertCircle, Upload } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { DialogFooter } from "@/components/ui/dialog" +import type { SetupConfig } from "@/types/setup-config" + +type ParseResult = { config: SetupConfig; entityCount: number } | { error: string } + +interface Props { + onBack: () => void + onNext: (config: SetupConfig) => void +} + +function parseConfig(text: string): ParseResult { + try { + const parsed = parseYaml(text) + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { error: "YAML must be a mapping object (not a list or scalar)" } + } + const cfg = parsed as SetupConfig + const entityCount = + (cfg.principal_roles?.length ?? 0) + + Object.keys(cfg.principals ?? {}).length + + (cfg.catalogs?.length ?? 0) + if (entityCount === 0) { + return { error: "No principal_roles, principals, or catalogs found" } + } + return { config: cfg, entityCount } + } catch (e) { + return { error: e instanceof Error ? e.message : String(e) } + } +} + +export function YamlStep({ onBack, onNext }: Props) { + const [text, setText] = useState("") + const fileInputRef = useRef(null) + const [parseResult, setParseResult] = useState(null) + + useEffect(() => { + if (!text.trim()) { + setParseResult(null) + return + } + setParseResult(parseConfig(text)) + }, [text]) + + const handleFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = () => setText(reader.result as string) + reader.readAsText(file) + e.target.value = "" + } + + const valid = parseResult && "config" in parseResult + + return ( + <> +
+
+

Paste a YAML config.

+ + +
+ +
+