diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index d01b798..a7c4152 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -15,6 +15,7 @@ const NAV_ITEMS: { to: string; label: string; icon: IconName; badge?: "sessions" { to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" }, { to: "/projects", label: "Projects", icon: "projects" }, { to: "/tool-workshop", label: "Tool Workshop", icon: "settings" }, + { to: "/config-profiles", label: "Config Profiles", icon: "folder" }, { to: "/settings", label: "Settings", icon: "settings" } ]; diff --git a/apps/web/src/pages/config-profiles.tsx b/apps/web/src/pages/config-profiles.tsx index 11aedca..10fea6d 100644 --- a/apps/web/src/pages/config-profiles.tsx +++ b/apps/web/src/pages/config-profiles.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { Icon } from "../components/icon"; import { createConfigProfile, deleteConfigProfile, @@ -10,15 +10,23 @@ import { type CreateConfigProfileRequest, type ResolvedProfile, } from "../api/config_profiles"; -import { Icon } from "../components/icon"; +import { listProjects } from "../api/projects"; +import type { Project } from "../types"; +import { listToolTypes, type ToolType } from "../api/tool_types"; + +type Status = "loading" | "ready" | "error"; export const ConfigProfilesPage = () => { - const navigate = useNavigate(); + const [status, setStatus] = useState("loading"); const [profiles, setProfiles] = useState([]); - const [loading, setLoading] = useState(true); + const [projects, setProjects] = useState([]); + const [toolTypes, setToolTypes] = useState([]); + + const [selectedProfileId, setSelectedProfileId] = useState(null); + const [isCreating, setIsCreating] = useState(false); + const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle"); const [error, setError] = useState(null); - const [showForm, setShowForm] = useState(false); - const [editingProfile, setEditingProfile] = useState(null); + const [previewData, setPreviewData] = useState(null); const [previewingId, setPreviewingId] = useState(null); @@ -32,22 +40,28 @@ export const ConfigProfilesPage = () => { is_default: false, }); - const loadProfiles = useCallback(async () => { + const selectedProfile = profiles.find((p) => p.id === selectedProfileId) || null; + + const loadData = useCallback(async () => { + setStatus("loading"); try { - setLoading(true); - const data = await listConfigProfiles(); - setProfiles(data); - setError(null); + const [profs, projs, types] = await Promise.all([ + listConfigProfiles(), + listProjects(), + listToolTypes(), + ]); + setProfiles(profs || []); + setProjects(projs || []); + setToolTypes(types || []); + setStatus("ready"); } catch { - setError("Failed to load config profiles"); - } finally { - setLoading(false); + setStatus("error"); } }, []); useEffect(() => { - void loadProfiles(); - }, [loadProfiles]); + void loadData(); + }, [loadData]); const resetForm = () => { setFormData({ @@ -59,49 +73,12 @@ export const ConfigProfilesPage = () => { files: {}, is_default: false, }); - setEditingProfile(null); - setShowForm(false); + setError(null); + setSaveStatus("idle"); + setPreviewData(null); }; - const handleCreate = async (e: React.FormEvent) => { - e.preventDefault(); - if (!formData.name?.trim()) return; - - try { - await createConfigProfile(formData); - resetForm(); - await loadProfiles(); - } catch { - setError("Failed to create config profile"); - } - }; - - const handleUpdate = async (e: React.FormEvent) => { - e.preventDefault(); - if (!editingProfile || !formData.name?.trim()) return; - - try { - await updateConfigProfile(editingProfile.id, formData); - resetForm(); - await loadProfiles(); - } catch { - setError("Failed to update config profile"); - } - }; - - const handleDelete = async (id: string) => { - if (!confirm("Are you sure you want to delete this config profile?")) return; - - try { - await deleteConfigProfile(id); - await loadProfiles(); - } catch { - setError("Failed to delete config profile"); - } - }; - - const startEdit = (profile: ConfigProfile) => { - setEditingProfile(profile); + const populateForm = (profile: ConfigProfile) => { setFormData({ name: profile.name, description: profile.description || undefined, @@ -113,9 +90,86 @@ export const ConfigProfilesPage = () => { files: profile.files, is_default: profile.is_default, }); - setShowForm(true); + setError(null); + setSaveStatus("idle"); setPreviewData(null); - setPreviewingId(null); + }; + + const handleSelectProfile = (profile: ConfigProfile | null) => { + if (profile) { + setSelectedProfileId(profile.id); + setIsCreating(false); + populateForm(profile); + } else { + setSelectedProfileId(null); + } + }; + + const handleCreateNew = () => { + setSelectedProfileId(null); + setIsCreating(true); + resetForm(); + }; + + const extractErrorMessage = (err: unknown): string => { + const axiosError = err as { response?: { data?: { detail?: string | Array<{msg?: string}> } } }; + const detail = axiosError?.response?.data?.detail; + if (typeof detail === "string") return detail; + if (Array.isArray(detail)) { + return detail.map((d) => typeof d === "string" ? d : d.msg || JSON.stringify(d)).join(", "); + } + return "Failed to save"; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSaveStatus("saving"); + + if (!formData.name?.trim()) { + setError("Name is required"); + setSaveStatus("error"); + return; + } + + try { + if (isCreating) { + const newProfile = await createConfigProfile(formData); + setIsCreating(false); + setSelectedProfileId(newProfile.id); + setSaveStatus("saved"); + await loadData(); + // Re-populate with the new profile data + const refreshed = (await listConfigProfiles()).find((p) => p.id === newProfile.id); + if (refreshed) populateForm(refreshed); + } else if (selectedProfile) { + await updateConfigProfile(selectedProfile.id, formData); + setSaveStatus("saved"); + await loadData(); + // Refresh the selected profile data + const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id); + if (refreshed) populateForm(refreshed); + } + } catch (err) { + setError(extractErrorMessage(err)); + setSaveStatus("error"); + } + }; + + const handleDelete = async (id: string) => { + if (!window.confirm("Are you sure you want to delete this config profile?")) return; + + try { + await deleteConfigProfile(id); + if (selectedProfileId === id) { + setSelectedProfileId(null); + setIsCreating(false); + resetForm(); + } + await loadData(); + } catch { + alert("Failed to delete config profile"); + } }; const handlePreview = async (id: string) => { @@ -135,6 +189,7 @@ export const ConfigProfilesPage = () => { value: CreateConfigProfileRequest[K] ) => { setFormData((prev) => ({ ...prev, [key]: value })); + setSaveStatus("idle"); }; const addEnvVar = () => { @@ -142,6 +197,7 @@ export const ConfigProfilesPage = () => { ...prev, env_vars: { ...prev.env_vars, "": "" }, })); + setSaveStatus("idle"); }; const updateEnvVar = (oldKey: string, newKey: string, value: string) => { @@ -153,6 +209,7 @@ export const ConfigProfilesPage = () => { envVars[newKey] = value; return { ...prev, env_vars: envVars }; }); + setSaveStatus("idle"); }; const removeEnvVar = (key: string) => { @@ -161,6 +218,7 @@ export const ConfigProfilesPage = () => { delete envVars[key]; return { ...prev, env_vars: envVars }; }); + setSaveStatus("idle"); }; const addFile = () => { @@ -168,6 +226,7 @@ export const ConfigProfilesPage = () => { ...prev, files: { ...prev.files, "": "" }, })); + setSaveStatus("idle"); }; const updateFile = (oldPath: string, newPath: string, content: string) => { @@ -179,6 +238,7 @@ export const ConfigProfilesPage = () => { files[newPath] = content; return { ...prev, files }; }); + setSaveStatus("idle"); }; const removeFile = (path: string) => { @@ -187,6 +247,7 @@ export const ConfigProfilesPage = () => { delete files[path]; return { ...prev, files }; }); + setSaveStatus("idle"); }; const addMount = () => { @@ -194,6 +255,7 @@ export const ConfigProfilesPage = () => { ...prev, mounts: [...(prev.mounts || []), { target: "/", mode: "rw", files: {} }], })); + setSaveStatus("idle"); }; const updateMount = (index: number, updates: Partial) => { @@ -202,6 +264,7 @@ export const ConfigProfilesPage = () => { mounts[index] = { ...mounts[index], ...updates }; return { ...prev, mounts }; }); + setSaveStatus("idle"); }; const removeMount = (index: number) => { @@ -210,6 +273,7 @@ export const ConfigProfilesPage = () => { mounts.splice(index, 1); return { ...prev, mounts }; }); + setSaveStatus("idle"); }; const addMountFile = (mountIndex: number) => { @@ -221,6 +285,7 @@ export const ConfigProfilesPage = () => { }; return { ...prev, mounts }; }); + setSaveStatus("idle"); }; const updateMountFile = ( @@ -239,6 +304,7 @@ export const ConfigProfilesPage = () => { mounts[mountIndex] = { ...mounts[mountIndex], files }; return { ...prev, mounts }; }); + setSaveStatus("idle"); }; const removeMountFile = (mountIndex: number, path: string) => { @@ -249,340 +315,508 @@ export const ConfigProfilesPage = () => { mounts[mountIndex] = { ...mounts[mountIndex], files }; return { ...prev, mounts }; }); + setSaveStatus("idle"); }; - if (loading) return
Loading config profiles...
; + if (status === "loading") { + return ( +
+

Loading Config Profiles...

+
+ ); + } - return ( -
-
-
-

Settings

-

Config Profiles

-
-
+ ); + } - {error &&
{error}
} + return ( +
+ {/* Left Sidebar - Profile List */} +
+
+

Config Profiles

+

+ {profiles.length} profile{profiles.length !== 1 ? "s" : ""} +

+
- {!showForm && ( - - )} - - {showForm && ( -
-

{editingProfile ? "Edit Profile" : "Create Profile"}

- -
- - updateFormField("name", e.target.value)} - placeholder="e.g., Development Environment" - required - /> -
- -
- - updateFormField("description", e.target.value || undefined)} - placeholder="Optional description" - /> -
- -
- - updateFormField("project_id", e.target.value || undefined)} - placeholder="Optional project UUID" - /> -
- -
- - updateFormField("tool_type_id", e.target.value || undefined)} - placeholder="Optional tool type UUID" - /> -
- -
- -
- -
-

Environment Variables

- {Object.entries(formData.env_vars || {}).map(([key, value], idx) => ( -
- updateEnvVar(key, e.target.value, value)} - placeholder="VAR_NAME" - /> - updateEnvVar(key, key, e.target.value)} - placeholder="value" - /> - -
- ))} - -
- -
-

Runtime Hints

-