eh
This commit is contained in:
1 parent
2eb9fa4ebe
commit
d5fb07ee9f
5 files changed
+376
-41
No files matched your search
@@ -213,11 +213,37 @@ export function DevicesPage({
|
||||
async function copyValue(label: string, value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
async function copyWithFallback(text: string) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "0";
|
||||
textarea.style.left = "0";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
if (!ok) throw new Error("fallback-copy-failed");
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(trimmed);
|
||||
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(trimmed);
|
||||
} else {
|
||||
await copyWithFallback(trimmed);
|
||||
}
|
||||
setCopyStatus(`Copied ${label}`);
|
||||
} catch {
|
||||
setCopyStatus(`Copy failed for ${label}`);
|
||||
try {
|
||||
await copyWithFallback(trimmed);
|
||||
setCopyStatus(`Copied ${label}`);
|
||||
} catch {
|
||||
setCopyStatus(`Copy failed for ${label}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,8 +314,8 @@ export function DevicesPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="grid gap-3 xl:grid-cols-[2fr_1fr]">
|
||||
<Card>
|
||||
<section className="grid gap-3 xl:grid-cols-[minmax(0,2fr)_minmax(20rem,1fr)]">
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0">
|
||||
<CardTitle>Devices</CardTitle>
|
||||
<Button
|
||||
@@ -303,8 +329,8 @@ export function DevicesPage({
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-3">
|
||||
<label className="grid gap-1 text-sm text-muted-foreground">
|
||||
<div className="mt-2 grid gap-2 md:grid-cols-3">
|
||||
<label className="grid min-w-0 gap-1 text-sm text-muted-foreground">
|
||||
<span>Agent</span>
|
||||
<Select
|
||||
value={selectedAgentId}
|
||||
@@ -312,7 +338,7 @@ export function DevicesPage({
|
||||
if (value) onSelectAgent(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectTrigger className="w-full min-w-0">
|
||||
<SelectValue placeholder="Select agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -326,9 +352,10 @@ export function DevicesPage({
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1 text-sm text-muted-foreground sm:col-span-2">
|
||||
<label className="grid min-w-0 gap-1 text-sm text-muted-foreground md:col-span-2">
|
||||
<span>Search</span>
|
||||
<Input
|
||||
className="w-full min-w-0"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="name, ip, mac, interface"
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DeviceRow, SortDir } from "@/pages/devices/types";
|
||||
import {
|
||||
chooseWakeTarget,
|
||||
@@ -129,33 +136,40 @@ export function DeviceInventoryTable({
|
||||
wakeBusyId === row.id || !selectedAgentId || bulkWakeBusy
|
||||
}
|
||||
size="sm"
|
||||
className="device-btn"
|
||||
>
|
||||
{wakeBusyId === row.id ? "Waking..." : "Wake"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => onCopyValue("name", chooseWakeTarget(row))}
|
||||
>
|
||||
Copy name
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => onCopyValue("ip", row.ips[0] || "")}
|
||||
>
|
||||
Copy IP
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => onCopyValue("mac", row.macs[0] || "")}
|
||||
>
|
||||
Copy MAC
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"device-copy-trigger",
|
||||
)}
|
||||
>
|
||||
Copy
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-32">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCopyValue("target", chooseWakeTarget(row))}
|
||||
>
|
||||
Copy target
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onCopyValue("name", row.name)}>
|
||||
Copy name
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCopyValue("ip", row.ips[0] || "")}
|
||||
>
|
||||
Copy IP
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCopyValue("mac", row.macs[0] || "")}
|
||||
>
|
||||
Copy MAC
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -44,15 +44,19 @@
|
||||
|
||||
.device-list {
|
||||
margin-top: 0.8rem;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.device-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
36px minmax(110px, 1.1fr) minmax(160px, 2fr) minmax(130px, 1.5fr)
|
||||
minmax(110px, 0.9fr) minmax(120px, 1fr) minmax(220px, auto);
|
||||
minmax(100px, 0.85fr) minmax(100px, 0.9fr) minmax(170px, max-content);
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
column-gap: 0.4rem;
|
||||
row-gap: 0.55rem;
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
.device-header {
|
||||
@@ -77,11 +81,19 @@
|
||||
.device-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.35rem;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.device-btn {
|
||||
min-width: 4.25rem;
|
||||
}
|
||||
|
||||
.device-copy-trigger {
|
||||
min-width: 6.2rem;
|
||||
}
|
||||
|
||||
.device-select {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -102,6 +114,7 @@
|
||||
"mac mac"
|
||||
"interfaces interfaces";
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-row .device-cell {
|
||||
@@ -148,6 +161,7 @@
|
||||
grid-area: action;
|
||||
justify-self: end;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-wake {
|
||||
|
||||
Reference in new issue
Block a user