Added ability to delete devices from WebUI.

Add device deletion dialog
This commit is contained in:
2025-08-09 18:00:15 -06:00
committed by GitHub
3 changed files with 93 additions and 5 deletions

View File

@@ -10,8 +10,13 @@ import {
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
TableSortLabel TableSortLabel,
IconButton,
Menu,
MenuItem
} from "@mui/material"; } from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import { DeleteDeviceDialog } from "./Dialogs.jsx";
function timeSince(tsSec) { function timeSince(tsSec) {
if (!tsSec) return "unknown"; if (!tsSec) return "unknown";
@@ -34,13 +39,17 @@ export default function DeviceList() {
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
const [orderBy, setOrderBy] = useState("status"); const [orderBy, setOrderBy] = useState("status");
const [order, setOrder] = useState("desc"); const [order, setOrder] = useState("desc");
const [menuAnchor, setMenuAnchor] = useState(null);
const [selected, setSelected] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const fetchAgents = useCallback(async () => { const fetchAgents = useCallback(async () => {
try { try {
const res = await fetch("/api/agents"); const res = await fetch("/api/agents");
const data = await res.json(); const data = await res.json();
const arr = Object.values(data || {}).map((a) => ({ const arr = Object.entries(data || {}).map(([id, a]) => ({
hostname: a.hostname || a.agent_id || "unknown", id,
hostname: a.hostname || id || "unknown",
status: statusFromHeartbeat(a.last_seen), status: statusFromHeartbeat(a.last_seen),
lastSeen: a.last_seen || 0, lastSeen: a.last_seen || 0,
os: a.agent_operating_system || a.os || "-" os: a.agent_operating_system || a.os || "-"
@@ -78,6 +87,30 @@ export default function DeviceList() {
const statusColor = (s) => (s === "Online" ? "#00d18c" : "#ff4f4f"); 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 ( return (
<Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}> <Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}>
<Box sx={{ p: 2, pb: 1 }}> <Box sx={{ p: 2, pb: 1 }}>
@@ -127,11 +160,12 @@ export default function DeviceList() {
OS OS
</TableSortLabel> </TableSortLabel>
</TableCell> </TableCell>
<TableCell />
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sorted.map((r, i) => ( {sorted.map((r, i) => (
<TableRow key={i} hover> <TableRow key={r.id || i} hover>
<TableCell> <TableCell>
<span <span
style={{ style={{
@@ -149,17 +183,39 @@ export default function DeviceList() {
<TableCell>{r.hostname}</TableCell> <TableCell>{r.hostname}</TableCell>
<TableCell>{timeSince(r.lastSeen)}</TableCell> <TableCell>{timeSince(r.lastSeen)}</TableCell>
<TableCell>{r.os}</TableCell> <TableCell>{r.os}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={(e) => openMenu(e, r)}
sx={{ color: "#ccc" }}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow> </TableRow>
))} ))}
{sorted.length === 0 && ( {sorted.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={4} sx={{ color: "#888" }}> <TableCell colSpan={5} sx={{ color: "#888" }}>
No agents connected. No agents connected.
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
</TableBody> </TableBody>
</Table> </Table>
<Menu
anchorEl={menuAnchor}
open={Boolean(menuAnchor)}
onClose={closeMenu}
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
>
<MenuItem onClick={confirmDelete}>Delete</MenuItem>
</Menu>
<DeleteDeviceDialog
open={confirmOpen}
onCancel={() => setConfirmOpen(false)}
onConfirm={handleDelete}
/>
</Paper> </Paper>
); );
} }

View File

@@ -95,6 +95,28 @@ export function RenameTabDialog({ open, value, onChange, onCancel, onSave }) {
); );
} }
export function DeleteDeviceDialog({ open, onCancel, onConfirm }) {
return (
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
<DialogTitle>Remove Device</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: "#ccc" }}>
Are you sure you want to remove this device? If the agent is still running, it will automatically re-enroll the device.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
<Button
onClick={onConfirm}
sx={{ bgcolor: "#ff4f4f", color: "#fff", "&:hover": { bgcolor: "#e04444" } }}
>
Remove
</Button>
</DialogActions>
</Dialog>
);
}
export function TabContextMenu({ anchor, onClose, onRename, onCloseTab }) { export function TabContextMenu({ anchor, onClose, onRename, onCloseTab }) {
return ( return (
<Menu <Menu

View File

@@ -187,6 +187,16 @@ def get_agents():
""" """
return jsonify(registered_agents) return jsonify(registered_agents)
@app.route("/api/agent/<agent_id>", 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"]) @app.route("/api/agent/provision", methods=["POST"])
def provision_agent(): def provision_agent():
data = request.json data = request.json