diff --git a/web_src/src/components/CreateCanvasModal/index.tsx b/web_src/src/components/CreateCanvasModal/index.tsx index 8a625950db..8ea8145298 100644 --- a/web_src/src/components/CreateCanvasModal/index.tsx +++ b/web_src/src/components/CreateCanvasModal/index.tsx @@ -1,16 +1,28 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Upload } from "lucide-react"; import { showErrorToast } from "../../utils/toast"; +import { parseCanvasYaml, readFileAsText, type ParsedCanvas } from "../../utils/parseCanvasYaml"; +import type { ComponentsNode, ComponentsEdge } from "@/api-client"; import { Dialog, DialogActions, DialogBody, DialogDescription, DialogTitle } from "../Dialog/dialog"; import { Field, Label } from "../Fieldset/fieldset"; import { Icon } from "../Icon"; import { Input } from "../Input/input"; import { Textarea } from "../ui/textarea"; import { Button } from "../ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; + +export interface CreateCanvasSubmitData { + name: string; + description?: string; + templateId?: string; + nodes?: ComponentsNode[]; + edges?: ComponentsEdge[]; +} interface CreateCanvasModalProps { isOpen: boolean; onClose: () => void; - onSubmit: (data: { name: string; description?: string; templateId?: string }) => Promise; + onSubmit: (data: CreateCanvasSubmitData) => Promise; isLoading?: boolean; initialData?: { name: string; description?: string }; templates?: { id: string; name: string; description?: string }[]; @@ -37,11 +49,21 @@ export function CreateCanvasModal({ const [nameError, setNameError] = useState(""); const [templateId, setTemplateId] = useState(""); + const [activeTab, setActiveTab] = useState("manual"); + const [yamlText, setYamlText] = useState(""); + const [yamlError, setYamlError] = useState(""); + const [importedSpec, setImportedSpec] = useState<{ nodes: ComponentsNode[]; edges: ComponentsEdge[] } | null>(null); + const fileInputRef = useRef(null); + useEffect(() => { if (isOpen) { setName(initialData?.name ?? ""); setDescription(initialData?.description ?? ""); setNameError(""); + setActiveTab("manual"); + setYamlText(""); + setYamlError(""); + setImportedSpec(null); } if (isOpen && mode === "create") { setTemplateId(defaultTemplateId || ""); @@ -56,9 +78,62 @@ export function CreateCanvasModal({ setDescription(""); setNameError(""); setTemplateId(""); + setYamlText(""); + setYamlError(""); + setImportedSpec(null); onClose(); }; + const applyParsedYaml = useCallback((parsed: ParsedCanvas) => { + setName(parsed.name.slice(0, MAX_CANVAS_NAME_LENGTH)); + setDescription((parsed.description ?? "").slice(0, MAX_CANVAS_DESCRIPTION_LENGTH)); + setImportedSpec({ nodes: parsed.nodes, edges: parsed.edges }); + setYamlError(""); + setNameError(""); + }, []); + + const handleYamlParse = useCallback(() => { + if (!yamlText.trim()) { + setYamlError("Paste or upload a YAML file first."); + setImportedSpec(null); + return; + } + + try { + const parsed = parseCanvasYaml(yamlText); + applyParsedYaml(parsed); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setYamlError(message); + setImportedSpec(null); + } + }, [yamlText, applyParsedYaml]); + + const handleFileUpload = useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + const text = await readFileAsText(file); + setYamlText(text); + + const parsed = parseCanvasYaml(text); + applyParsedYaml(parsed); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setYamlError(message); + setImportedSpec(null); + } + + // Reset file input so re-uploading the same file triggers onChange + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }, + [applyParsedYaml], + ); + const handleSubmit = async () => { setNameError(""); @@ -77,6 +152,8 @@ export function CreateCanvasModal({ name: name.trim(), description: description.trim() || undefined, templateId: templateId || undefined, + nodes: importedSpec?.nodes, + edges: importedSpec?.edges, }); // Reset form and close modal @@ -84,6 +161,9 @@ export function CreateCanvasModal({ setDescription(""); setNameError(""); setTemplateId(""); + setYamlText(""); + setYamlError(""); + setImportedSpec(null); onClose(); } catch (error) { const errorMessage = (error as Error)?.message || error?.toString() || "Failed to create canvas"; @@ -96,6 +176,8 @@ export function CreateCanvasModal({ } }; + const showYamlTab = mode === "create" && !fromTemplate; + return ( @@ -106,66 +188,113 @@ export function CreateCanvasModal({ ? "Create a canvas from this template. Give it a name and optional description to get started." : mode === "edit" ? "Update the canvas details to keep things clear for your teammates." - : "Create a new canvas to orchestrate your DevOps work. You can tweak the details any time."} + : "Create a new canvas or import one from a YAML file."} -
- - - { - if (e.target.value.length <= MAX_CANVAS_DESCRIPTION_LENGTH) { - setName(e.target.value); - } - if (nameError) { - setNameError(""); - } - }} - placeholder="" - className={`w-full ${nameError ? "border-red-500" : ""}`} - autoFocus - maxLength={MAX_CANVAS_NAME_LENGTH} - /> -
- {name.length}/{MAX_CANVAS_NAME_LENGTH} characters -
- {nameError &&
{nameError}
} -
- - - -