will this be reverted?

This commit is contained in:
lda committed 2026-04-18 00:53:12 +07:00 Verified
1 parent 150588d834
commit b4152e84c4
8 files changed
+283 -31

No files matched your search

+22 -4
View File
@@ -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>
);
}
+21 -9
View File
@@ -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)}
+3 -8
View File
@@ -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
View File
@@ -1,6 +1,7 @@
export type DeviceRow = {
id: string;
name: string;
isUnnamed: boolean;
ips: string[];
macs: string[];
interfaces: string[];
+3 -3
View File
@@ -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 {