will this be reverted?
This commit is contained in:
8 files changed
+283
-31
No files matched your search
@@ -12,6 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { DeviceInventoryTable } from "@/pages/devices/DeviceInventoryTable";
|
||||
import { DeviceDetailsDialog } from "@/pages/devices/DeviceDetailsDialog";
|
||||
import { WakeHistoryCard } from "@/pages/devices/WakeHistoryCard";
|
||||
import type {
|
||||
DeviceRow,
|
||||
@@ -112,6 +113,7 @@ export function DevicesPage({
|
||||
const [bulkWakeBusy, setBulkWakeBusy] = useState(false);
|
||||
const [quickWake, setQuickWake] = useState("");
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [detailsDevice, setDetailsDevice] = useState<DeviceRow | null>(null);
|
||||
const [copyStatus, setCopyStatus] = useState("");
|
||||
const [recentWakes, setRecentWakes] = useState<WakeEvent[]>([]);
|
||||
function appendWakeEvent(event: WakeEvent) {
|
||||
@@ -288,9 +290,17 @@ export function DevicesPage({
|
||||
);
|
||||
}
|
||||
// Sort
|
||||
const sorter = sorters[sort.key];
|
||||
result = [...result].sort(sorter);
|
||||
if (sort.dir === "desc") result.reverse();
|
||||
if (sort.key === "name") {
|
||||
result = [...result].sort((a, b) => {
|
||||
if (a.isUnnamed !== b.isUnnamed) return a.isUnnamed ? 1 : -1;
|
||||
const cmp = a.name.localeCompare(b.name);
|
||||
return sort.dir === "desc" ? -cmp : cmp;
|
||||
});
|
||||
} else {
|
||||
const sorter = sorters[sort.key];
|
||||
result = [...result].sort(sorter);
|
||||
if (sort.dir === "desc") result.reverse();
|
||||
}
|
||||
return result;
|
||||
}, [rows, query, sort, presenceFilter]);
|
||||
|
||||
@@ -342,7 +352,7 @@ export function DevicesPage({
|
||||
<span>Agent</span>
|
||||
<Select
|
||||
value={selectedAgentId}
|
||||
onValueChange={(value) => {
|
||||
onValueChange={(value: string) => {
|
||||
if (value) onSelectAgent(value);
|
||||
}}
|
||||
>
|
||||
@@ -505,6 +515,14 @@ export function DevicesPage({
|
||||
onSortChange={setSort}
|
||||
onWakeDevice={(device) => void wakeDevice(device)}
|
||||
onCopyValue={(label, value) => void copyValue(label, value)}
|
||||
onOpenDetails={setDetailsDevice}
|
||||
/>
|
||||
<DeviceDetailsDialog
|
||||
device={detailsDevice}
|
||||
open={Boolean(detailsDevice)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setDetailsDevice(null);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { DeviceRow } from "@/pages/devices/types";
|
||||
|
||||
type Props = {
|
||||
device: DeviceRow | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
function summarizeAll(values: string[]): string {
|
||||
if (!values.length) return "-";
|
||||
return values.join(", ");
|
||||
}
|
||||
|
||||
export function DeviceDetailsDialog({ device, open, onOpenChange }: Props) {
|
||||
if (!device) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl p-0">
|
||||
<div className="grid gap-4 p-4 text-sm">
|
||||
<DialogHeader className="pr-8">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<DialogTitle className="truncate">{device.name}</DialogTitle>
|
||||
<DialogDescription>Device details</DialogDescription>
|
||||
</div>
|
||||
<Badge variant="outline">{device.presence}</Badge>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 font-medium">IP addresses</p>
|
||||
<p className="text-muted-foreground">{summarizeAll(device.ips)}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<p className="mb-1 font-medium">MAC addresses</p>
|
||||
<p className="text-muted-foreground">{summarizeAll(device.macs)}</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<p className="mb-1 font-medium">Interfaces</p>
|
||||
<p className="text-muted-foreground">
|
||||
{summarizeAll(device.interfaces)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ type Props = {
|
||||
onSortChange: (next: { key: SortKey; dir: SortDir }) => void;
|
||||
onWakeDevice: (device: DeviceRow) => void;
|
||||
onCopyValue: (label: string, value: string) => void;
|
||||
onOpenDetails: (device: DeviceRow) => void;
|
||||
};
|
||||
|
||||
function sortArrow(active: boolean, dir: SortDir) {
|
||||
@@ -48,7 +49,13 @@ export function DeviceInventoryTable({
|
||||
onSortChange,
|
||||
onWakeDevice,
|
||||
onCopyValue,
|
||||
onOpenDetails,
|
||||
}: Props) {
|
||||
function shouldIgnoreRowClick(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return Boolean(target.closest("button, input, a, [role='menuitem']"));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="device-list grid gap-2">
|
||||
<div className="device-row device-header rounded-md border bg-muted/60 px-3 py-2 text-sm">
|
||||
@@ -85,14 +92,26 @@ export function DeviceInventoryTable({
|
||||
>
|
||||
Presence {sortArrow(sort.key === "presence", sort.dir)}
|
||||
</span>
|
||||
<span className="device-cell">Interfaces</span>
|
||||
<span className="device-cell device-action">Actions</span>
|
||||
</div>
|
||||
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
className="device-row rounded-md border bg-card px-3 py-2 text-sm"
|
||||
className="device-row cursor-pointer rounded-md border bg-card px-3 py-2 text-sm transition-colors hover:bg-accent/30"
|
||||
key={row.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(event) => {
|
||||
if (shouldIgnoreRowClick(event.target)) return;
|
||||
onOpenDetails(row);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (shouldIgnoreRowClick(event.target)) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onOpenDetails(row);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="device-cell device-select" data-label="Pick">
|
||||
<input
|
||||
@@ -122,13 +141,6 @@ export function DeviceInventoryTable({
|
||||
<span className="device-cell" data-label="Presence">
|
||||
<Badge variant="outline">{row.presence}</Badge>
|
||||
</span>
|
||||
<span
|
||||
className="device-cell"
|
||||
data-label="Interfaces"
|
||||
title={row.interfaces.join(", ") || "-"}
|
||||
>
|
||||
{summarize(row.interfaces)}
|
||||
</span>
|
||||
<span className="device-cell device-action" data-label="">
|
||||
<Button
|
||||
onClick={() => onWakeDevice(row)}
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
36px minmax(110px, 1.1fr) minmax(160px, 2fr) minmax(130px, 1.5fr)
|
||||
minmax(100px, 0.85fr) minmax(100px, 0.9fr) minmax(170px, max-content);
|
||||
minmax(100px, 0.85fr) minmax(170px, max-content);
|
||||
align-items: center;
|
||||
column-gap: 0.4rem;
|
||||
row-gap: 0.55rem;
|
||||
min-width: 980px;
|
||||
min-width: 860px;
|
||||
}
|
||||
|
||||
.device-header {
|
||||
@@ -111,8 +111,7 @@
|
||||
"name name"
|
||||
"presence presence"
|
||||
"ip ip"
|
||||
"mac mac"
|
||||
"interfaces interfaces";
|
||||
"mac mac";
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -162,10 +161,6 @@
|
||||
grid-area: mac;
|
||||
}
|
||||
|
||||
.device-row .device-cell[data-label="Interfaces"] {
|
||||
grid-area: interfaces;
|
||||
}
|
||||
|
||||
.device-row .device-action {
|
||||
grid-area: action;
|
||||
justify-self: end;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type DeviceRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
isUnnamed: boolean;
|
||||
ips: string[];
|
||||
macs: string[];
|
||||
interfaces: string[];
|
||||
|
||||
@@ -50,11 +50,13 @@ export function parseInventoryRows(payload: unknown): DeviceRow[] {
|
||||
: [];
|
||||
const presence =
|
||||
typeof device.presence === "string" ? device.presence : "unknown";
|
||||
const isUnnamed = names.length === 0;
|
||||
|
||||
const id = macs[0] || ips[0] || names[0] || `row-${idx}`;
|
||||
return {
|
||||
id,
|
||||
name: names[0] || "(unnamed)",
|
||||
isUnnamed,
|
||||
ips,
|
||||
macs,
|
||||
interfaces,
|
||||
@@ -122,9 +124,7 @@ export function parseWakeSummary(response: unknown): {
|
||||
}
|
||||
|
||||
export function chooseWakeTarget(device: DeviceRow): string {
|
||||
return device.name !== "(unnamed)"
|
||||
? device.name
|
||||
: device.macs[0] || device.ips[0] || "";
|
||||
return device.isUnnamed ? device.macs[0] || device.ips[0] || "" : device.name;
|
||||
}
|
||||
|
||||
export function summarize(values: string[]): string {
|
||||
|
||||
Reference in new issue
Block a user