From c79c3e5ccbe873f8f0f7ff918a83a3d92c9939ec Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 6 Jun 2026 07:09:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20section=20Docker=20interactive=20su?= =?UTF-8?q?r=20la=20tuile=20machine=20(t=C3=A2che=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branche le frontend sur le backend Docker (SJ-4/5/6) : - scan, configuration des racines Compose, liste stacks + services avec badges de statut (candidat/activé/maj dispo/à jour) - activer/ignorer/désactiver un stack ; pull-check (non destructif) - apply/down/prune via action_request + confirmation Popup (design system) - toute action streamée auto-sélectionne la machine → flux visible dans le terminal de droite (outputHub rejoue le buffer) - api client : docker settings/roots/scan/stacks/status + action-requests - icônes trash/check, styles docker-* (variables CSS uniquement) Co-Authored-By: Claude Opus 4.8 --- client/src/components/ui-kit.tsx | 2 + client/src/features/machines/MachineTile.tsx | 299 ++++++++++++++++++- client/src/lib/api.ts | 71 ++++- client/src/styles/app.css | 61 ++++ 4 files changed, 420 insertions(+), 13 deletions(-) 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) +