mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 04:38:42 -06:00
Added Customizable Device Column Chooser
This commit is contained in:
@@ -209,7 +209,7 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
|
|
||||||
const summary = details.summary || {};
|
const summary = details.summary || {};
|
||||||
const summaryItems = [
|
const summaryItems = [
|
||||||
{ label: "Device Name", value: summary.hostname || agent.hostname || device?.hostname || "unknown" },
|
{ label: "Hostname", value: summary.hostname || agent.hostname || device?.hostname || "unknown" },
|
||||||
{ label: "Operating System", value: summary.operating_system || agent.agent_operating_system || "unknown" },
|
{ label: "Operating System", value: summary.operating_system || agent.agent_operating_system || "unknown" },
|
||||||
{ label: "Device Type", value: summary.device_type || "unknown" },
|
{ label: "Device Type", value: summary.device_type || "unknown" },
|
||||||
{ label: "Last User", value: (
|
{ label: "Last User", value: (
|
||||||
|
@@ -17,10 +17,12 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Popover,
|
Popover,
|
||||||
TextField
|
TextField,
|
||||||
|
Tooltip
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||||
|
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
|
||||||
import { DeleteDeviceDialog } from "../Dialogs.jsx";
|
import { DeleteDeviceDialog } from "../Dialogs.jsx";
|
||||||
import QuickJob from "../Scheduling/Quick_Job.jsx";
|
import QuickJob from "../Scheduling/Quick_Job.jsx";
|
||||||
|
|
||||||
@@ -59,26 +61,44 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
const [quickJobOpen, setQuickJobOpen] = useState(false);
|
const [quickJobOpen, setQuickJobOpen] = useState(false);
|
||||||
|
|
||||||
// Column configuration and rearranging state
|
// Column configuration and rearranging state
|
||||||
|
const COL_LABELS = useMemo(
|
||||||
|
() => ({
|
||||||
|
status: "Status",
|
||||||
|
hostname: "Hostname",
|
||||||
|
description: "Description",
|
||||||
|
lastUser: "Last User",
|
||||||
|
type: "Type",
|
||||||
|
os: "OS",
|
||||||
|
internalIp: "Internal IP",
|
||||||
|
externalIp: "External IP",
|
||||||
|
lastReboot: "Last Reboot",
|
||||||
|
created: "Created",
|
||||||
|
lastSeen: "Last Seen",
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const defaultColumns = useMemo(
|
const defaultColumns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ id: "status", label: "Status" },
|
{ id: "status", label: COL_LABELS.status },
|
||||||
{ id: "hostname", label: "Hostname" },
|
{ id: "hostname", label: COL_LABELS.hostname },
|
||||||
{ id: "lastUser", label: "Last User" },
|
{ id: "description", label: COL_LABELS.description },
|
||||||
{ id: "type", label: "Type" },
|
{ id: "lastUser", label: COL_LABELS.lastUser },
|
||||||
{ id: "os", label: "OS" },
|
{ id: "type", label: COL_LABELS.type },
|
||||||
{ id: "created", label: "Created" }
|
{ id: "os", label: COL_LABELS.os },
|
||||||
],
|
],
|
||||||
[]
|
[COL_LABELS]
|
||||||
);
|
);
|
||||||
const [columns, setColumns] = useState(defaultColumns);
|
const [columns, setColumns] = useState(defaultColumns);
|
||||||
const dragColId = useRef(null);
|
const dragColId = useRef(null);
|
||||||
|
const [colChooserAnchor, setColChooserAnchor] = useState(null);
|
||||||
|
|
||||||
// Per-column filters
|
// Per-column filters
|
||||||
const [filters, setFilters] = useState({});
|
const [filters, setFilters] = useState({});
|
||||||
const [filterAnchor, setFilterAnchor] = useState(null); // { id, anchorEl }
|
const [filterAnchor, setFilterAnchor] = useState(null); // { id, anchorEl }
|
||||||
|
|
||||||
// Cache device details to avoid re-fetching every refresh
|
// Cache device details to avoid re-fetching every refresh
|
||||||
const [detailsByHost, setDetailsByHost] = useState({}); // hostname -> { lastUser, created, createdTs }
|
const [detailsByHost, setDetailsByHost] = useState({}); // hostname -> cached fields
|
||||||
|
|
||||||
const fetchAgents = useCallback(async () => {
|
const fetchAgents = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -98,6 +118,10 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
type: a.device_type || details.type || "",
|
type: a.device_type || details.type || "",
|
||||||
created: details.created || "",
|
created: details.created || "",
|
||||||
createdTs: details.createdTs || 0,
|
createdTs: details.createdTs || 0,
|
||||||
|
internalIp: details.internalIp || "",
|
||||||
|
externalIp: details.externalIp || "",
|
||||||
|
lastReboot: details.lastReboot || "",
|
||||||
|
description: details.description || "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setRows(arr);
|
setRows(arr);
|
||||||
@@ -129,9 +153,22 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
createdTs = isNaN(parsed) ? 0 : Math.floor(parsed / 1000);
|
createdTs = isNaN(parsed) ? 0 : Math.floor(parsed / 1000);
|
||||||
}
|
}
|
||||||
const deviceType = (summary.device_type || "").trim();
|
const deviceType = (summary.device_type || "").trim();
|
||||||
|
const internalIp = summary.internal_ip || "";
|
||||||
|
const externalIp = summary.external_ip || "";
|
||||||
|
const lastReboot = summary.last_reboot || "";
|
||||||
|
const description = summary.description || "";
|
||||||
setDetailsByHost((prev) => ({
|
setDetailsByHost((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[h]: { lastUser, created: createdRaw, createdTs, type: deviceType },
|
[h]: {
|
||||||
|
lastUser,
|
||||||
|
created: createdRaw,
|
||||||
|
createdTs,
|
||||||
|
type: deviceType,
|
||||||
|
internalIp,
|
||||||
|
externalIp,
|
||||||
|
lastReboot,
|
||||||
|
description,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
// ignore per-host failure
|
// ignore per-host failure
|
||||||
@@ -150,6 +187,10 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
type: det.type || r.type,
|
type: det.type || r.type,
|
||||||
created: det.created || r.created,
|
created: det.created || r.created,
|
||||||
createdTs: det.createdTs || r.createdTs,
|
createdTs: det.createdTs || r.createdTs,
|
||||||
|
internalIp: det.internalIp || r.internalIp,
|
||||||
|
externalIp: det.externalIp || r.externalIp,
|
||||||
|
lastReboot: det.lastReboot || r.lastReboot,
|
||||||
|
description: det.description || r.description,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -176,14 +217,24 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
return row.status || "";
|
return row.status || "";
|
||||||
case "hostname":
|
case "hostname":
|
||||||
return row.hostname || "";
|
return row.hostname || "";
|
||||||
|
case "description":
|
||||||
|
return row.description || "";
|
||||||
case "lastUser":
|
case "lastUser":
|
||||||
return row.lastUser || "";
|
return row.lastUser || "";
|
||||||
case "type":
|
case "type":
|
||||||
return row.type || "";
|
return row.type || "";
|
||||||
case "os":
|
case "os":
|
||||||
return row.os || "";
|
return row.os || "";
|
||||||
|
case "internalIp":
|
||||||
|
return row.internalIp || "";
|
||||||
|
case "externalIp":
|
||||||
|
return row.externalIp || "";
|
||||||
|
case "lastReboot":
|
||||||
|
return row.lastReboot || "";
|
||||||
case "created":
|
case "created":
|
||||||
return formatCreated(row.created, row.createdTs);
|
return formatCreated(row.created, row.createdTs);
|
||||||
|
case "lastSeen":
|
||||||
|
return formatLastSeen(row.lastSeen);
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -310,7 +361,16 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
||||||
Devices
|
Devices
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Tooltip title="Column Chooser">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => setColChooserAnchor(e.currentTarget)}
|
||||||
|
sx={{ color: "#bbb", mr: 1 }}
|
||||||
|
>
|
||||||
|
<ViewColumnIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -415,16 +475,28 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
{r.hostname}
|
{r.hostname}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
|
case "description":
|
||||||
|
return <TableCell key={col.id}>{r.description || ""}</TableCell>;
|
||||||
case "lastUser":
|
case "lastUser":
|
||||||
return <TableCell key={col.id}>{r.lastUser || ""}</TableCell>;
|
return <TableCell key={col.id}>{r.lastUser || ""}</TableCell>;
|
||||||
case "type":
|
case "type":
|
||||||
return <TableCell key={col.id}>{r.type || ""}</TableCell>;
|
return <TableCell key={col.id}>{r.type || ""}</TableCell>;
|
||||||
case "os":
|
case "os":
|
||||||
return <TableCell key={col.id}>{r.os}</TableCell>;
|
return <TableCell key={col.id}>{r.os}</TableCell>;
|
||||||
|
case "internalIp":
|
||||||
|
return <TableCell key={col.id}>{r.internalIp || ""}</TableCell>;
|
||||||
|
case "externalIp":
|
||||||
|
return <TableCell key={col.id}>{r.externalIp || ""}</TableCell>;
|
||||||
|
case "lastReboot":
|
||||||
|
return <TableCell key={col.id}>{r.lastReboot || ""}</TableCell>;
|
||||||
case "created":
|
case "created":
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.id}>{formatCreated(r.created, r.createdTs)}</TableCell>
|
<TableCell key={col.id}>{formatCreated(r.created, r.createdTs)}</TableCell>
|
||||||
);
|
);
|
||||||
|
case "lastSeen":
|
||||||
|
return (
|
||||||
|
<TableCell key={col.id}>{formatLastSeen(r.lastSeen)}</TableCell>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <TableCell key={col.id} />;
|
return <TableCell key={col.id} />;
|
||||||
}
|
}
|
||||||
@@ -452,6 +524,63 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{/* Column chooser popover */}
|
||||||
|
<Popover
|
||||||
|
open={Boolean(colChooserAnchor)}
|
||||||
|
anchorEl={colChooserAnchor}
|
||||||
|
onClose={() => setColChooserAnchor(null)}
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||||
|
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: '#fff', p: 1 } }}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 1 }}>
|
||||||
|
{[
|
||||||
|
{ id: 'hostname', label: 'Hostname' },
|
||||||
|
{ id: 'os', label: 'Operating System' },
|
||||||
|
{ id: 'type', label: 'Device Type' },
|
||||||
|
{ id: 'lastUser', label: 'Last User' },
|
||||||
|
{ id: 'internalIp', label: 'Internal IP' },
|
||||||
|
{ id: 'externalIp', label: 'External IP' },
|
||||||
|
{ id: 'lastReboot', label: 'Last Reboot' },
|
||||||
|
{ id: 'created', label: 'Created' },
|
||||||
|
{ id: 'lastSeen', label: 'Last Seen' },
|
||||||
|
{ id: 'description', label: 'Description' },
|
||||||
|
].map((opt) => (
|
||||||
|
<MenuItem key={opt.id} disableRipple onClick={(e) => e.stopPropagation()} sx={{ gap: 1 }}>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={columns.some((c) => c.id === opt.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
setColumns((prev) => {
|
||||||
|
// Keep 'status' always present; manage others per toggle
|
||||||
|
const exists = prev.some((c) => c.id === opt.id);
|
||||||
|
if (checked) {
|
||||||
|
if (exists) return prev;
|
||||||
|
// Append new column at the end with canonical label
|
||||||
|
const label = COL_LABELS[opt.id] || opt.label || opt.id;
|
||||||
|
return [...prev, { id: opt.id, label }];
|
||||||
|
}
|
||||||
|
// Remove column
|
||||||
|
return prev.filter((c) => c.id !== opt.id);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
sx={{ p: 0.3, color: '#bbb' }}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" sx={{ color: '#ddd' }}>{opt.label}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, pt: 0.5 }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setColumns(defaultColumns)}
|
||||||
|
sx={{ textTransform: 'none', borderColor: '#555', color: '#bbb' }}
|
||||||
|
>
|
||||||
|
Reset Default
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
{/* Filter popover */}
|
{/* Filter popover */}
|
||||||
<Popover
|
<Popover
|
||||||
open={Boolean(filterAnchor)}
|
open={Boolean(filterAnchor)}
|
||||||
|
Reference in New Issue
Block a user