mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 06:08:42 -06:00
feat: add device deletion dialog
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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?
|
||||||
|
</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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user