diff --git a/client/src/components/ui-kit.tsx b/client/src/components/ui-kit.tsx index 847a48d..1885d34 100644 --- a/client/src/components/ui-kit.tsx +++ b/client/src/components/ui-kit.tsx @@ -47,6 +47,8 @@ const ICON_MAP = { download: 'download', upload: 'upload', database: 'database', + trash: 'trash', + check: 'check', folder: 'folder', docker: 'boxes-stacked', package: 'box-open', diff --git a/client/src/features/machines/MachineTile.tsx b/client/src/features/machines/MachineTile.tsx index c14cc07..7030c95 100644 --- a/client/src/features/machines/MachineTile.tsx +++ b/client/src/features/machines/MachineTile.tsx @@ -1,7 +1,8 @@ // client/src/features/machines/MachineTile.tsx -import { useState } from "react"; -import type { MachineStatus, MachineView } from "@shared/types.js"; -import { Button, Icon, IconButton, StatusLed } from "../../components/ui-kit.js"; +import { useEffect, useState } from "react"; +import type { ActionType, MachineStatus, MachineView } from "@shared/types.js"; +import { Button, Icon, IconButton, Popup, StatusLed } from "../../components/ui-kit.js"; +import { api, type DockerSettingsView, type DockerStackRow, type StackStatus } from "../../lib/api.js"; interface Props { machine: MachineView; @@ -114,7 +115,7 @@ export function MachineTile({ open={dockerOpen} onToggle={() => setDockerOpen((value) => !value)} /> - {dockerOpen && } + {dockerOpen && } void }) { + const [settings, setSettings] = useState(null); + const [stacks, setStacks] = useState(null); + const [rootsInput, setRootsInput] = useState(""); + const [showRoots, setShowRoots] = useState(false); + const [busy, setBusy] = useState(null); + const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null); + const [confirm, setConfirm] = useState(null); + + async function load() { + try { + const [s, st] = await Promise.all([api.dockerSettings(machineId), api.dockerStacks(machineId)]); + setSettings(s); + setStacks(st); + if (s.roots.length) setRootsInput(s.roots.map((r) => r.path).join("\n")); + } catch (err) { + setMsg({ kind: "err", text: (err as Error).message }); + } + } + useEffect(() => { + void load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [machineId]); + + async function withBusy(key: string, fn: () => Promise) { + setBusy(key); + setMsg(null); + try { + await fn(); + } catch (err) { + setMsg({ kind: "err", text: (err as Error).message }); + } finally { + setBusy(null); + } + } + + const saveRoots = () => + withBusy("roots", async () => { + const paths = rootsInput.split("\n").map((s) => s.trim()).filter(Boolean); + if (!paths.length) throw new Error("Indique au moins une racine Compose."); + await api.dockerSetRoots(machineId, paths); + await load(); + setShowRoots(false); + setMsg({ kind: "ok", text: "Racines enregistrées." }); + }); + + const scan = () => + withBusy("scan", async () => { + onSelect(machineId); // bascule le terminal de droite sur cette machine + await api.dockerScan(machineId); + setMsg({ kind: "ok", text: "Scan lancé… (voir le terminal de droite)" }); + window.setTimeout(() => void load(), 6000); + }); + + const setStatus = (stackId: string, status: StackStatus) => + withBusy(`stack:${stackId}`, async () => { + await api.setStackStatus(machineId, stackId, status); + await load(); + }); + + const pullCheck = (stackId: string) => + withBusy(`pull:${stackId}`, async () => { + onSelect(machineId); // bascule le terminal de droite sur cette machine + await api.runAction(machineId, "docker_pull_check", stackId); + setMsg({ kind: "ok", text: "Pull-check lancé… (voir le terminal de droite)" }); + window.setTimeout(() => void load(), 8000); + }); + + const runConfirmed = () => + withBusy("confirm", async () => { + if (!confirm) return; + onSelect(machineId); // bascule le terminal de droite sur cette machine + const req = await api.createActionRequest(machineId, { + action: confirm.action, + stackId: confirm.stackId, + aggressive: confirm.aggressive, + }); + await api.approveActionRequest(req.id); + setConfirm(null); + setMsg({ kind: "ok", text: `${confirm.label} : demande approuvée et lancée.` }); + window.setTimeout(() => void load(), 8000); + }); + + const enabledExists = (stacks ?? []).some((s) => s.status === "enabled"); + const lastScan = settings?.settings?.lastScanAt; + return (
-
- Docker non scanné - -
-
- Roots compose, stacks, upgrades image et prune seront affichés ici dès que le backend Docker sera disponible. +
+ + + {enabledExists && ( + + )} + {lastScan && scan {formatDate(lastScan)}}
+ + {showRoots && ( +
+ Racines Compose (une par ligne) +