import { useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Play, Square, RotateCw, DownloadCloud, ArrowUpCircle, Pencil, Power, Trash2, LayoutTemplate, } from "lucide-react"; import { toast } from "sonner"; 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"; import { ActionStatusList } from "@/components/stacks/ActionStatusBanner"; 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"; import type { ContainerInfo } from "@/types"; const TABS = ["Overview", "Logs", "Environment", "Compose", "Secrets"] as const; type Tab = (typeof TABS)[number]; export function StackDetail() { const { id = "" } = useParams(); const isAdmin = useAuthStore((s) => s.user?.role === "admin"); const [tab, setTab] = useState("Overview"); const actions = useStackActions(); const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["stack", id], queryFn: () => stacksApi.get(id), // Container state arrives over /ws/events; this is the fallback. refetchInterval: 30000, }); if (isLoading || !data) return ; const busy = actions.isBusy(id); return (

{data.name}

{data.status}
{data.description && (

{data.description}

)}
{isAdmin && (
)}
{/* Tabs */}
{TABS.map((t) => ( ))}
{tab === "Overview" && ( queryClient.invalidateQueries({ queryKey: ["stack", id] })} /> )} {tab === "Logs" && } {tab === "Environment" && } {tab === "Compose" && } {tab === "Secrets" && ( queryClient.invalidateQueries({ queryKey: ["stack", id] })} /> )}
); } function Overview({ data, isAdmin, onChanged, }: { data: ReturnType & any; isAdmin: boolean; onChanged: () => void; }) { return (
{data.containers.length === 0 && (

No containers running. Start the stack to see services.

)} {data.containers.map((c: ContainerInfo) => ( ))}
); } function EnvView({ env }: { env: string }) { return ( {env ? (
{env}
) : (

No .env file for this stack.

)}
); } function ComposeView({ yaml }: { yaml: string }) { return (
{yaml}
); } function DeleteStackButton({ stackId }: { stackId: string }) { const navigate = useNavigate(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const [deleteFiles, setDeleteFiles] = useState(true); const [busy, setBusy] = useState(false); const remove = async () => { setBusy(true); const t = toast.loading(`Deleting ${stackId}…`); try { await stacksApi.remove(stackId, deleteFiles); toast.success(`Deleted ${stackId}`, { id: t }); qc.invalidateQueries({ queryKey: ["stacks"] }); navigate("/stacks"); } catch (e) { toast.error(apiErrorMessage(e), { id: t }); setBusy(false); } }; return ( <> {open && ( setOpen(false)} > )} ); } 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 ( <> {open && ( setOpen(false)} > )} ); }