diff --git a/app/src/components/detail/ConfigPanel.tsx b/app/src/components/detail/ConfigPanel.tsx index 0178796..9b03526 100644 --- a/app/src/components/detail/ConfigPanel.tsx +++ b/app/src/components/detail/ConfigPanel.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { useNavigate } from "react-router-dom" import { useQueryClient } from "@tanstack/react-query" -import { Check } from "lucide-react" +import { Check, RefreshCw } from "lucide-react" import { apiClient } from "@/services/api/client" import type { AnyDetail, ProgramDetail, ServiceDetail, JobDetail } from "@/types" import { ProgramFields } from "./ProgramFields" @@ -18,12 +18,23 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane const navigate = useNavigate() const qc = useQueryClient() const [message, setMessage] = useState<{ type: "ok" | "error"; text: string } | null>(null) + // A deployment edit only persists to castle.yaml; the unit/process still need + // a deploy (regenerate) + restart to actually take effect. + const [pendingApply, setPendingApply] = useState(false) + const [applying, setApplying] = useState(false) + const isDeployment = configSection !== "programs" const handleSave = async (compName: string, config: Record) => { setMessage(null) try { await apiClient.put(`/config/${configSection}/${compName}`, { config }) - setMessage({ type: "ok", text: "Saved to castle.yaml" }) + setMessage({ + type: "ok", + text: isDeployment + ? "Saved to castle.yaml — not live yet; apply to deploy & restart." + : "Saved to castle.yaml", + }) + if (isDeployment) setPendingApply(true) onRefetch() qc.invalidateQueries({ queryKey: [configSection] }) } catch (e: unknown) { @@ -32,6 +43,32 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane } } + const handleApply = async () => { + setApplying(true) + setMessage(null) + try { + await apiClient.post(`/deploy`, { name: deployment.id }) + if (configSection === "services") { + await apiClient.post(`/services/${deployment.id}/restart`, {}) + } + setPendingApply(false) + setMessage({ type: "ok", text: "Applied — the change is now live." }) + qc.invalidateQueries({ queryKey: ["status"] }) + qc.invalidateQueries({ queryKey: [configSection] }) + onRefetch() + } catch (e: unknown) { + let msg = e instanceof Error ? e.message : String(e) + try { + msg = JSON.parse((e as Error).message).detail ?? msg + } catch { + /* keep msg */ + } + setMessage({ type: "error", text: `Apply failed: ${msg}` }) + } finally { + setApplying(false) + } + } + const handleDelete = async (compName: string) => { try { await apiClient.delete(`/config/${configSection}/${compName}`) @@ -45,18 +82,34 @@ export function ConfigPanel({ deployment, configSection, onRefetch }: ConfigPane return ( <> - {message && ( + {(message || pendingApply) && (
- {message.type === "ok" && } - {message.text} + {message?.type === "ok" && !pendingApply && } + {message?.text} + {pendingApply && ( + + )}
)}