Fix GPU inventory typing and user role mapping
This commit is contained in:
parent
42611df0f5
commit
4f812a2e4c
3 changed files with 183 additions and 84 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { api } from "@/convex/_generated/api"
|
|
||||||
import type { Id } from "@/convex/_generated/dataModel"
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
|
import type { UserRole } from "@prisma/client"
|
||||||
|
import { api } from "@/convex/_generated/api"
|
||||||
import { ConvexHttpClient } from "convex/browser"
|
import { ConvexHttpClient } from "convex/browser"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
|
|
@ -12,12 +13,11 @@ function normalizeRole(input: string | null | undefined): RoleOption {
|
||||||
return ((ROLE_OPTIONS as readonly string[]).includes(candidate) ? candidate : "agent") as RoleOption
|
return ((ROLE_OPTIONS as readonly string[]).includes(candidate) ? candidate : "agent") as RoleOption
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapToUserRole(role: RoleOption) {
|
const USER_ROLE_OPTIONS: ReadonlyArray<UserRole> = ["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]
|
||||||
const value = role.toUpperCase()
|
|
||||||
if (["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"].includes(value)) {
|
function mapToUserRole(role: RoleOption): UserRole {
|
||||||
return value
|
const candidate = role.toUpperCase() as UserRole
|
||||||
}
|
return USER_ROLE_OPTIONS.includes(candidate) ? candidate : "AGENT"
|
||||||
return "AGENT"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const runtime = "nodejs"
|
export const runtime = "nodejs"
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ import { Skeleton } from "@/components/ui/skeleton"
|
||||||
export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: string; machineId: string }) {
|
export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: string; machineId: string }) {
|
||||||
const queryResult = useQuery(api.machines.listByTenant, { tenantId, includeMetadata: true }) as MachinesQueryItem[] | undefined
|
const queryResult = useQuery(api.machines.listByTenant, { tenantId, includeMetadata: true }) as MachinesQueryItem[] | undefined
|
||||||
const isLoading = queryResult === undefined
|
const isLoading = queryResult === undefined
|
||||||
const machines = queryResult ?? []
|
const machine = useMemo(() => {
|
||||||
const machine = useMemo(() => machines.find((m) => m.id === machineId) ?? null, [machines, machineId])
|
if (!queryResult) return null
|
||||||
|
return queryResult.find((m) => m.id === machineId) ?? null
|
||||||
|
}, [queryResult, machineId])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -199,22 +199,97 @@ function readNumber(record: Record<string, unknown>, ...keys: string[]): number
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
if (typeof raw === "string") {
|
if (typeof raw === "string") {
|
||||||
const parsed = Number(raw)
|
const trimmed = raw.trim()
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!trimmed) continue
|
||||||
return parsed
|
const parsed = Number(trimmed)
|
||||||
|
if (!Number.isNaN(parsed)) return parsed
|
||||||
|
const digits = trimmed.replace(/[^0-9.]/g, "")
|
||||||
|
if (digits) {
|
||||||
|
const fallback = Number(digits)
|
||||||
|
if (!Number.isNaN(fallback)) return fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBytesLike(value: unknown): number | undefined {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return undefined
|
||||||
|
const normalized = trimmed.replace(",", ".")
|
||||||
|
const match = normalized.match(/^([\d.]+)\s*(ti|tb|tib|gb|gib|mb|mib|kb|kib|b)?$/i)
|
||||||
|
if (match) {
|
||||||
|
const amount = Number(match[1])
|
||||||
|
if (Number.isNaN(amount)) return undefined
|
||||||
|
const unit = match[2]?.toLowerCase()
|
||||||
|
const base = 1024
|
||||||
|
const unitMap: Record<string, number> = {
|
||||||
|
b: 1,
|
||||||
|
kb: base,
|
||||||
|
kib: base,
|
||||||
|
mb: base ** 2,
|
||||||
|
mib: base ** 2,
|
||||||
|
gb: base ** 3,
|
||||||
|
gib: base ** 3,
|
||||||
|
tb: base ** 4,
|
||||||
|
tib: base ** 4,
|
||||||
|
ti: base ** 4,
|
||||||
|
}
|
||||||
|
if (unit) {
|
||||||
|
const multiplier = unitMap[unit]
|
||||||
|
if (multiplier) {
|
||||||
|
return amount * multiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
const digits = normalized.replace(/[^0-9.]/g, "")
|
||||||
|
if (digits) {
|
||||||
|
const fallback = Number(digits)
|
||||||
|
if (!Number.isNaN(fallback)) return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveVendor(record: Record<string, unknown>): string | undefined {
|
||||||
|
const direct = readString(record, "vendor", "Vendor", "AdapterCompatibility")
|
||||||
|
if (direct) return direct
|
||||||
|
const pnp = readString(record, "PNPDeviceID")
|
||||||
|
if (!pnp) return undefined
|
||||||
|
const match = pnp.match(/VEN_([0-9A-F]{4})/i)
|
||||||
|
if (match) {
|
||||||
|
const vendorCode = match[1].toUpperCase()
|
||||||
|
const vendorMap: Record<string, string> = {
|
||||||
|
"10DE": "NVIDIA",
|
||||||
|
"1002": "AMD",
|
||||||
|
"1022": "AMD",
|
||||||
|
"8086": "Intel",
|
||||||
|
"8087": "Intel",
|
||||||
|
"1AF4": "Red Hat",
|
||||||
|
}
|
||||||
|
return vendorMap[vendorCode] ?? `VEN_${vendorCode}`
|
||||||
|
}
|
||||||
|
const segments = pnp.split("\\")
|
||||||
|
const last = segments.pop()
|
||||||
|
return last && last.trim().length > 0 ? last : pnp
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeGpuSource(value: unknown): GpuAdapter | null {
|
function normalizeGpuSource(value: unknown): GpuAdapter | null {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const name = value.trim()
|
||||||
|
return name ? { name } : null
|
||||||
|
}
|
||||||
const record = toRecord(value)
|
const record = toRecord(value)
|
||||||
if (!record) return null
|
if (!record) return null
|
||||||
const name = readString(record, "name", "Name")
|
const name = readString(record, "name", "Name", "_name", "AdapterCompatibility")
|
||||||
const vendor = readString(record, "vendor", "Vendor", "PNPDeviceID")
|
const vendor = deriveVendor(record)
|
||||||
const driver = readString(record, "driver", "DriverVersion")
|
const driver = readString(record, "driver", "DriverVersion", "driverVersion")
|
||||||
const memoryBytes = readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM")
|
const memoryBytes =
|
||||||
|
readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM", "VRAM", "vramBytes") ??
|
||||||
|
parseBytesLike(record["AdapterRAM"] ?? record["VRAM"] ?? record["vram"])
|
||||||
if (!name && !vendor && !driver && memoryBytes === undefined) {
|
if (!name && !vendor && !driver && memoryBytes === undefined) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -399,7 +474,7 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
return hay.includes(text)
|
return hay.includes(text)
|
||||||
})
|
})
|
||||||
}, [machines, q, statusFilter, osFilter, companyQuery, onlyAlerts])
|
}, [machines, q, statusFilter, osFilter, companyQuery, onlyAlerts, companyNameBySlug])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
|
|
@ -556,6 +631,9 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
const linuxExt = extended?.linux ?? null
|
const linuxExt = extended?.linux ?? null
|
||||||
const windowsExt = extended?.windows ?? null
|
const windowsExt = extended?.windows ?? null
|
||||||
const macosExt = extended?.macos ?? null
|
const macosExt = extended?.macos ?? null
|
||||||
|
const windowsMemoryModules = Array.isArray(windowsExt?.memoryModules) ? windowsExt.memoryModules : []
|
||||||
|
const windowsVideoControllers = Array.isArray(windowsExt?.videoControllers) ? windowsExt.videoControllers : []
|
||||||
|
const windowsDiskEntries = Array.isArray(windowsExt?.disks) ? windowsExt.disks : []
|
||||||
const linuxLsblk = linuxExt?.lsblk ?? []
|
const linuxLsblk = linuxExt?.lsblk ?? []
|
||||||
const linuxSmartEntries = linuxExt?.smart ?? []
|
const linuxSmartEntries = linuxExt?.smart ?? []
|
||||||
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
||||||
|
|
@ -563,24 +641,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
: []
|
: []
|
||||||
const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null
|
const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null
|
||||||
|
|
||||||
type WinCpuInfo = {
|
const windowsCpuRaw = windowsExt?.cpu
|
||||||
Name?: string
|
const winCpu = windowsCpuRaw
|
||||||
Manufacturer?: string
|
? (Array.isArray(windowsCpuRaw) ? windowsCpuRaw[0] ?? null : windowsCpuRaw)
|
||||||
SocketDesignation?: string
|
: null
|
||||||
NumberOfCores?: number
|
const winMemTotal = windowsMemoryModules.reduce((acc, module) => acc + (parseBytesLike(module?.Capacity) ?? 0), 0)
|
||||||
NumberOfLogicalProcessors?: number
|
|
||||||
L2CacheSize?: number
|
|
||||||
L3CacheSize?: number
|
|
||||||
MaxClockSpeed?: number
|
|
||||||
}
|
|
||||||
const winCpu = (() => {
|
|
||||||
const cpuInfo = windowsExt?.cpu
|
|
||||||
if (!cpuInfo) return null
|
|
||||||
return Array.isArray(cpuInfo) ? cpuInfo[0] ?? null : cpuInfo
|
|
||||||
})()
|
|
||||||
const winMemModules = windowsExt?.memoryModules ?? []
|
|
||||||
const winMemTotal = winMemModules.reduce((acc, module) => acc + Number(module?.Capacity ?? 0), 0)
|
|
||||||
const windowsVideoControllers = windowsExt?.videoControllers ?? []
|
|
||||||
const normalizedWindowsGpus = windowsVideoControllers
|
const normalizedWindowsGpus = windowsVideoControllers
|
||||||
.map((controller) => normalizeGpuSource(controller))
|
.map((controller) => normalizeGpuSource(controller))
|
||||||
.filter((gpu): gpu is GpuAdapter => Boolean(gpu))
|
.filter((gpu): gpu is GpuAdapter => Boolean(gpu))
|
||||||
|
|
@ -592,21 +657,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
],
|
],
|
||||||
(gpu) => `${gpu.name ?? ""}|${gpu.vendor ?? ""}|${gpu.driver ?? ""}`
|
(gpu) => `${gpu.name ?? ""}|${gpu.vendor ?? ""}|${gpu.driver ?? ""}`
|
||||||
)
|
)
|
||||||
const primaryGpu = combinedGpus[0] ?? null
|
const displayGpus = [...combinedGpus].sort(
|
||||||
const displayGpus = combinedGpus
|
(a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0)
|
||||||
const windowsPrimaryGpu = normalizedWindowsGpus[0] ?? null
|
)
|
||||||
const windowsCpuDetails = windowsExt?.cpu
|
const primaryGpu = hardwarePrimaryGpu ?? displayGpus[0] ?? null
|
||||||
? Array.isArray(windowsExt.cpu)
|
const windowsPrimaryGpu = [...normalizedWindowsGpus].sort(
|
||||||
? windowsExt.cpu
|
(a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0)
|
||||||
: [windowsExt.cpu]
|
)[0] ?? null
|
||||||
|
const windowsCpuDetails = windowsCpuRaw
|
||||||
|
? Array.isArray(windowsCpuRaw)
|
||||||
|
? windowsCpuRaw
|
||||||
|
: [windowsCpuRaw]
|
||||||
: []
|
: []
|
||||||
const windowsServices = windowsExt?.services ?? []
|
const windowsServices = windowsExt?.services ?? []
|
||||||
const windowsSoftware = windowsExt?.software ?? []
|
const windowsSoftware = windowsExt?.software ?? []
|
||||||
const windowsDisks = windowsExt?.disks ?? []
|
const winDiskStats = windowsDiskEntries.length > 0
|
||||||
const winDiskStats = windowsDisks.length > 0
|
|
||||||
? {
|
? {
|
||||||
count: windowsDisks.length,
|
count: windowsDiskEntries.length,
|
||||||
total: windowsDisks.reduce((acc, d) => acc + Number(d?.Size ?? 0), 0),
|
total: windowsDiskEntries.reduce((acc, disk) => acc + (parseBytesLike(disk?.Size) ?? 0), 0),
|
||||||
}
|
}
|
||||||
: { count: 0, total: 0 }
|
: { count: 0, total: 0 }
|
||||||
|
|
||||||
|
|
@ -1295,7 +1363,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Array.isArray(windowsExt.memoryModules) && windowsExt.memoryModules.length > 0 ? (
|
{windowsMemoryModules.length > 0 ? (
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">Módulos de memória</p>
|
<p className="text-xs font-semibold uppercase text-slate-500">Módulos de memória</p>
|
||||||
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
||||||
|
|
@ -1310,34 +1378,64 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(windowsExt.memoryModules as Array<any>).map((m, idx) => (
|
{windowsMemoryModules.map((module, idx) => {
|
||||||
<TableRow key={`mem-${idx}`} className="border-slate-100">
|
const record = toRecord(module) ?? {}
|
||||||
<TableCell className="text-sm">{m?.BankLabel ?? "—"}</TableCell>
|
const bank = readString(record, "BankLabel", "bankLabel") ?? "—"
|
||||||
<TableCell className="text-sm text-muted-foreground">{formatBytes(Number(m?.Capacity ?? 0))}</TableCell>
|
const capacityBytes =
|
||||||
<TableCell className="text-sm text-muted-foreground">{m?.Manufacturer ?? "—"}</TableCell>
|
parseBytesLike(record["Capacity"]) ?? parseBytesLike(record["capacity"]) ?? 0
|
||||||
<TableCell className="text-sm text-muted-foreground">{m?.PartNumber ?? "—"}</TableCell>
|
const manufacturer = readString(record, "Manufacturer", "manufacturer") ?? "—"
|
||||||
<TableCell className="text-sm text-muted-foreground">{m?.ConfiguredClockSpeed ?? m?.Speed ? `${m?.ConfiguredClockSpeed ?? m?.Speed} MHz` : "—"}</TableCell>
|
const partNumber = readString(record, "PartNumber", "partNumber") ?? "—"
|
||||||
</TableRow>
|
const clockValue = readNumber(
|
||||||
))}
|
record,
|
||||||
|
"ConfiguredClockSpeed",
|
||||||
|
"configuredClockSpeed",
|
||||||
|
"Speed",
|
||||||
|
"speed"
|
||||||
|
)
|
||||||
|
const clockLabel =
|
||||||
|
typeof clockValue === "number" && Number.isFinite(clockValue)
|
||||||
|
? `${clockValue} MHz`
|
||||||
|
: "—"
|
||||||
|
return (
|
||||||
|
<TableRow key={`mem-${idx}`} className="border-slate-100">
|
||||||
|
<TableCell className="text-sm">{bank}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{capacityBytes > 0 ? formatBytes(capacityBytes) : "—"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{manufacturer}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{partNumber}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{clockLabel}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Array.isArray(windowsExt.videoControllers) && windowsExt.videoControllers.length > 0 ? (
|
{windowsVideoControllers.length > 0 ? (
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">Adaptadores de vídeo</p>
|
<p className="text-xs font-semibold uppercase text-slate-500">Adaptadores de vídeo</p>
|
||||||
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
||||||
{(windowsExt.videoControllers as Array<unknown>).map((vRaw, idx) => {
|
{windowsVideoControllers.map((controller, idx) => {
|
||||||
const v = (vRaw && typeof vRaw === "object") ? (vRaw as Record<string, unknown>) : undefined
|
const record = toRecord(controller) ?? {}
|
||||||
const name = typeof v?.["Name"] === "string" ? (v["Name"] as string) : "—"
|
const normalized = normalizeGpuSource(record)
|
||||||
const ram = typeof v?.["AdapterRAM"] === "number" ? (v["AdapterRAM"] as number) : undefined
|
const name =
|
||||||
const driver = typeof v?.["DriverVersion"] === "string" ? (v["DriverVersion"] as string) : undefined
|
normalized?.name ??
|
||||||
|
readString(record, "Name", "name") ??
|
||||||
|
"—"
|
||||||
|
const ram =
|
||||||
|
normalized?.memoryBytes ??
|
||||||
|
parseBytesLike(record["AdapterRAM"]) ??
|
||||||
|
undefined
|
||||||
|
const driver = normalized?.driver ?? readString(record, "DriverVersion", "driverVersion")
|
||||||
|
const vendor = normalized?.vendor ?? readString(record, "AdapterCompatibility")
|
||||||
return (
|
return (
|
||||||
<li key={`vid-${idx}`}>
|
<li key={`vid-${idx}`}>
|
||||||
<span className="font-medium text-foreground">{name}</span>
|
<span className="font-medium text-foreground">{name}</span>
|
||||||
{typeof ram === "number" ? <span className="ml-1">{formatBytes(ram)}</span> : null}
|
{typeof ram === "number" && ram > 0 ? <span className="ml-1">{formatBytes(ram)}</span> : null}
|
||||||
|
{vendor ? <span className="ml-1 text-muted-foreground">· {vendor}</span> : null}
|
||||||
{driver ? <span className="ml-1">· Driver {driver}</span> : null}
|
{driver ? <span className="ml-1">· Driver {driver}</span> : null}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
@ -1346,7 +1444,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Array.isArray(windowsExt.disks) && windowsExt.disks.length > 0 ? (
|
{windowsDiskEntries.length > 0 ? (
|
||||||
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">Discos físicos</p>
|
<p className="text-xs font-semibold uppercase text-slate-500">Discos físicos</p>
|
||||||
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
<div className="mt-2 overflow-hidden rounded-md border border-slate-200">
|
||||||
|
|
@ -1361,15 +1459,25 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(windowsExt.disks as Array<any>).map((d, idx) => (
|
{windowsDiskEntries.map((disk, idx) => {
|
||||||
<TableRow key={`diskp-${idx}`} className="border-slate-100">
|
const record = toRecord(disk) ?? {}
|
||||||
<TableCell className="text-sm">{d?.Model ?? "—"}</TableCell>
|
const model = readString(record, "Model", "model")
|
||||||
<TableCell className="text-sm text-muted-foreground">{formatBytes(Number(d?.Size ?? 0))}</TableCell>
|
const serial = readString(record, "SerialNumber", "serialNumber")
|
||||||
<TableCell className="text-sm text-muted-foreground">{d?.InterfaceType ?? "—"}</TableCell>
|
const size = parseBytesLike(record["Size"])
|
||||||
<TableCell className="text-sm text-muted-foreground">{d?.MediaType ?? "—"}</TableCell>
|
const iface = readString(record, "InterfaceType", "interfaceType")
|
||||||
<TableCell className="text-sm text-muted-foreground">{d?.SerialNumber ?? "—"}</TableCell>
|
const media = readString(record, "MediaType", "mediaType")
|
||||||
</TableRow>
|
return (
|
||||||
))}
|
<TableRow key={`diskp-${idx}`} className="border-slate-100">
|
||||||
|
<TableCell className="text-sm">{model ?? serial ?? "—"}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{typeof size === "number" && size > 0 ? formatBytes(size) : "—"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{iface ?? "—"}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{media ?? "—"}</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">{serial ?? "—"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1717,17 +1825,6 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNumberLike(value: unknown): number | null {
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) return value
|
|
||||||
if (typeof value === "string") {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return null
|
|
||||||
const numeric = Number(trimmed.replace(/[^0-9.]/g, ""))
|
|
||||||
if (Number.isFinite(numeric)) return numeric
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function DetailLine({ label, value, classNameValue }: DetailLineProps) {
|
function DetailLine({ label, value, classNameValue }: DetailLineProps) {
|
||||||
if (value === null || value === undefined) return null
|
if (value === null || value === undefined) return null
|
||||||
if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) {
|
if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue