diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx index 46d283e..280e33b 100644 --- a/Data/Server/WebUI/src/Device_List.jsx +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -10,8 +10,13 @@ import { TableCell, TableHead, TableRow, - TableSortLabel + TableSortLabel, + IconButton, + Menu, + MenuItem } from "@mui/material"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import { DeleteDeviceDialog } from "./Dialogs.jsx"; function timeSince(tsSec) { if (!tsSec) return "unknown"; @@ -34,13 +39,17 @@ export default function DeviceList() { const [rows, setRows] = useState([]); const [orderBy, setOrderBy] = useState("status"); const [order, setOrder] = useState("desc"); + const [menuAnchor, setMenuAnchor] = useState(null); + const [selected, setSelected] = useState(null); + const [confirmOpen, setConfirmOpen] = useState(false); const fetchAgents = useCallback(async () => { try { const res = await fetch("/api/agents"); const data = await res.json(); - const arr = Object.values(data || {}).map((a) => ({ - hostname: a.hostname || a.agent_id || "unknown", + const arr = Object.entries(data || {}).map(([id, a]) => ({ + id, + hostname: a.hostname || id || "unknown", status: statusFromHeartbeat(a.last_seen), lastSeen: a.last_seen || 0, os: a.agent_operating_system || a.os || "-" @@ -78,6 +87,30 @@ export default function DeviceList() { const statusColor = (s) => (s === "Online" ? "#00d18c" : "#ff4f4f"); + const openMenu = (e, row) => { + setMenuAnchor(e.currentTarget); + setSelected(row); + }; + + const closeMenu = () => setMenuAnchor(null); + + const confirmDelete = () => { + closeMenu(); + setConfirmOpen(true); + }; + + const handleDelete = async () => { + if (!selected) return; + try { + await fetch(`/api/agent/${selected.id}`, { method: "DELETE" }); + } catch (e) { + console.warn("Failed to remove agent", e); + } + setRows((r) => r.filter((x) => x.id !== selected.id)); + setConfirmOpen(false); + setSelected(null); + }; + return ( @@ -127,11 +160,12 @@ export default function DeviceList() { OS + {sorted.map((r, i) => ( - + {r.hostname} {timeSince(r.lastSeen)} {r.os} + + openMenu(e, r)} + sx={{ color: "#ccc" }} + > + + + ))} {sorted.length === 0 && ( - + No agents connected. )} + + Delete + + setConfirmOpen(false)} + onConfirm={handleDelete} + /> ); } diff --git a/Data/Server/WebUI/src/Dialogs.jsx b/Data/Server/WebUI/src/Dialogs.jsx index 73060a0..9da3489 100644 --- a/Data/Server/WebUI/src/Dialogs.jsx +++ b/Data/Server/WebUI/src/Dialogs.jsx @@ -95,6 +95,28 @@ export function RenameTabDialog({ open, value, onChange, onCancel, onSave }) { ); } +export function DeleteDeviceDialog({ open, onCancel, onConfirm }) { + return ( + + Remove Device + + + Are you sure you want to remove this device? If the agent is still running, it will automatically re-enroll the device. + + + + + + + + ); +} + export function TabContextMenu({ anchor, onClose, onRename, onCloseTab }) { return ( ", methods=["DELETE"]) +def delete_agent(agent_id: str): + """Remove an agent from the in-memory registry.""" + if agent_id in registered_agents: + registered_agents.pop(agent_id, None) + agent_configurations.pop(agent_id, None) + return jsonify({"status": "removed"}) + return jsonify({"error": "agent not found"}), 404 + @app.route("/api/agent/provision", methods=["POST"]) def provision_agent(): data = request.json