Phase 25: templates as stack folders (0.31.0)
Templates are now stack-shaped folders (compose.yaml + .env.example +
template.json) instead of DB rows + manifest.json + {{VAR}} rendering.
Pull copies the folder into a new stack; custom templates persist under
DATA_DIR/templates. Adds POST /api/templates/from-stack and a one-time
startup migration for pre-0.31 DB templates (drops the template table).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
34cb215266
commit
1609b8bcc3
@@ -10,9 +10,10 @@ import {
|
||||
Pencil,
|
||||
Power,
|
||||
Trash2,
|
||||
LayoutTemplate,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge, Button, Card, Spinner, StatusDot } from "@/components/ui";
|
||||
import { Badge, Button, Card, Input, Spinner, StatusDot } from "@/components/ui";
|
||||
import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
|
||||
import { LogViewer } from "@/components/stacks/LogViewer";
|
||||
import { ContainerCard } from "@/components/stacks/ContainerCard";
|
||||
@@ -20,6 +21,7 @@ import { AutoUpdatePanel } from "@/components/stacks/AutoUpdatePanel";
|
||||
import { SecretsPanel } from "@/components/stacks/SecretsPanel";
|
||||
import { BackupButton } from "@/components/stacks/BackupRestore";
|
||||
import { stacksApi } from "@/api/stacks";
|
||||
import { templatesApi } from "@/api/templates";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import { useStackActions } from "@/hooks/useStackActions";
|
||||
@@ -78,6 +80,7 @@ export function StackDetail() {
|
||||
<Power className="h-4 w-4" /> Down
|
||||
</Button>
|
||||
<BackupButton stackId={id} />
|
||||
<SaveAsTemplateButton stackId={id} defaultName={data.name} />
|
||||
<Link to={`/stacks/${id}/edit`}>
|
||||
<Button>
|
||||
<Pencil className="h-4 w-4" /> Edit
|
||||
@@ -230,3 +233,49 @@ function DeleteStackButton({ stackId }: { stackId: string }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SaveAsTemplateButton({ stackId, defaultName }: { stackId: string; defaultName: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState(defaultName);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const save = async () => {
|
||||
if (!name.trim()) {
|
||||
toast.error("Template name required");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await templatesApi.saveFromStack({ stack_id: stackId, name: name.trim() });
|
||||
toast.success(`Saved template “${name.trim()}”`);
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
toast.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setOpen(true)}>
|
||||
<LayoutTemplate className="h-4 w-4" /> Save as template
|
||||
</Button>
|
||||
{open && (
|
||||
<ConfirmDialog
|
||||
title="Save as template"
|
||||
message="Snapshots this stack's compose and .env into a reusable custom template."
|
||||
confirmLabel="Save template"
|
||||
busy={busy}
|
||||
onConfirm={save}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs font-medium text-slate-500">Template name</span>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</label>
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user