mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
refactor: rm diskinfo abstraction from smart-table.tsx
This commit is contained in:
@@ -93,53 +93,15 @@ export const smartColumns: ColumnDef<SmartAttribute>[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export type DiskInfo = {
|
||||
id: string
|
||||
system: string
|
||||
device: string
|
||||
model: string
|
||||
capacity: string
|
||||
capacityBytes: number
|
||||
status: string
|
||||
temperature: number
|
||||
deviceType: string
|
||||
powerOnHours?: number
|
||||
powerCycles?: number
|
||||
attributes?: SmartAttribute[]
|
||||
updated: string
|
||||
}
|
||||
|
||||
// Function to format capacity display
|
||||
function formatCapacity(bytes: number): string {
|
||||
const { value, unit } = formatBytes(bytes)
|
||||
return `${toFixedFloat(value, value >= 10 ? 1 : 2)} ${unit}`
|
||||
}
|
||||
|
||||
// Function to convert SmartDeviceRecord to DiskInfo
|
||||
function convertSmartDeviceRecordToDiskInfo(records: SmartDeviceRecord[]): DiskInfo[] {
|
||||
const unknown = "Unknown"
|
||||
return records.map((record) => ({
|
||||
id: record.id,
|
||||
system: record.system,
|
||||
device: record.name || unknown,
|
||||
model: record.model || unknown,
|
||||
serialNumber: record.serial || unknown,
|
||||
firmwareVersion: record.firmware || unknown,
|
||||
capacity: record.capacity ? formatCapacity(record.capacity) : unknown,
|
||||
capacityBytes: record.capacity || 0,
|
||||
status: record.state || unknown,
|
||||
temperature: record.temp || 0,
|
||||
deviceType: record.type || unknown,
|
||||
attributes: record.attributes,
|
||||
updated: record.updated,
|
||||
powerOnHours: record.hours,
|
||||
powerCycles: record.cycles,
|
||||
}))
|
||||
}
|
||||
|
||||
const SMART_DEVICE_FIELDS = "id,system,name,model,state,capacity,temp,type,hours,cycles,updated"
|
||||
|
||||
export const columns: ColumnDef<DiskInfo>[] = [
|
||||
export const columns: ColumnDef<SmartDeviceRecord>[] = [
|
||||
{
|
||||
id: "system",
|
||||
accessorFn: (record) => record.system,
|
||||
@@ -156,12 +118,12 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "device",
|
||||
sortingFn: (a, b) => a.original.device.localeCompare(b.original.device),
|
||||
accessorKey: "name",
|
||||
sortingFn: (a, b) => a.original.name.localeCompare(b.original.name),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium max-w-40 truncate ms-1.5" title={row.getValue("device")}>
|
||||
{row.getValue("device")}
|
||||
cell: ({ getValue }) => (
|
||||
<div className="font-medium max-w-40 truncate ms-1.5" title={getValue() as string}>
|
||||
{getValue() as string}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -169,20 +131,20 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
accessorKey: "model",
|
||||
sortingFn: (a, b) => a.original.model.localeCompare(b.original.model),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Model`} Icon={Box} />,
|
||||
cell: ({ row }) => (
|
||||
<div className="max-w-48 truncate ms-1.5" title={row.getValue("model")}>
|
||||
{row.getValue("model")}
|
||||
cell: ({ getValue }) => (
|
||||
<div className="max-w-48 truncate ms-1.5" title={getValue() as string}>
|
||||
{getValue() as string}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "capacity",
|
||||
sortingFn: (a, b) => a.original.capacityBytes - b.original.capacityBytes,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Capacity`} Icon={BinaryIcon} />,
|
||||
cell: ({ getValue }) => <span className="ms-1.5">{getValue() as string}</span>,
|
||||
cell: ({ getValue }) => <span className="ms-1.5">{formatCapacity(getValue() as number)}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
accessorKey: "state",
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={Activity} />,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue() as string
|
||||
@@ -194,8 +156,8 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "deviceType",
|
||||
sortingFn: (a, b) => a.original.deviceType.localeCompare(b.original.deviceType),
|
||||
accessorKey: "type",
|
||||
sortingFn: (a, b) => a.original.type.localeCompare(b.original.type),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Type`} Icon={ArrowLeftRightIcon} />,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="ms-1.5">
|
||||
@@ -206,7 +168,7 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "powerOnHours",
|
||||
accessorKey: "hours",
|
||||
invertSorting: true,
|
||||
header: ({ column }) => (
|
||||
<HeaderButton column={column} name={t({ message: "Power On", comment: "Power On Time" })} Icon={Clock} />
|
||||
@@ -226,7 +188,7 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "powerCycles",
|
||||
accessorKey: "cycles",
|
||||
invertSorting: true,
|
||||
header: ({ column }) => (
|
||||
<HeaderButton column={column} name={t({ message: "Cycles", comment: "Power Cycles" })} Icon={RotateCwIcon} />
|
||||
@@ -240,7 +202,7 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "temperature",
|
||||
accessorKey: "temp",
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Temp`} Icon={ThermometerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
@@ -249,14 +211,14 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
},
|
||||
},
|
||||
// {
|
||||
// accessorKey: "serialNumber",
|
||||
// sortingFn: (a, b) => a.original.serialNumber.localeCompare(b.original.serialNumber),
|
||||
// accessorKey: "serial",
|
||||
// sortingFn: (a, b) => a.original.serial.localeCompare(b.original.serial),
|
||||
// header: ({ column }) => <HeaderButton column={column} name={t`Serial Number`} Icon={HashIcon} />,
|
||||
// cell: ({ getValue }) => <span className="ms-1.5">{getValue() as string}</span>,
|
||||
// },
|
||||
// {
|
||||
// accessorKey: "firmwareVersion",
|
||||
// sortingFn: (a, b) => a.original.firmwareVersion.localeCompare(b.original.firmwareVersion),
|
||||
// accessorKey: "firmware",
|
||||
// sortingFn: (a, b) => a.original.firmware.localeCompare(b.original.firmware),
|
||||
// header: ({ column }) => <HeaderButton column={column} name={t`Firmware`} Icon={CpuIcon} />,
|
||||
// cell: ({ getValue }) => <span className="ms-1.5">{getValue() as string}</span>,
|
||||
// },
|
||||
@@ -275,7 +237,15 @@ export const columns: ColumnDef<DiskInfo>[] = [
|
||||
},
|
||||
]
|
||||
|
||||
function HeaderButton({ column, name, Icon }: { column: Column<DiskInfo>; name: string; Icon: React.ElementType }) {
|
||||
function HeaderButton({
|
||||
column,
|
||||
name,
|
||||
Icon,
|
||||
}: {
|
||||
column: Column<SmartDeviceRecord>
|
||||
name: string
|
||||
Icon: React.ElementType
|
||||
}) {
|
||||
const isSorted = column.getIsSorted()
|
||||
return (
|
||||
<Button
|
||||
@@ -293,7 +263,7 @@ function HeaderButton({ column, name, Icon }: { column: Column<DiskInfo>; name:
|
||||
}
|
||||
|
||||
export default function DisksTable({ systemId }: { systemId?: string }) {
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: systemId ? "device" : "system", desc: false }])
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: systemId ? "name" : "system", desc: false }])
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [smartDevices, setSmartDevices] = useState<SmartDeviceRecord[] | undefined>(undefined)
|
||||
@@ -302,96 +272,95 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
|
||||
const [rowActionState, setRowActionState] = useState<{ type: "refresh" | "delete"; id: string } | null>(null)
|
||||
const [globalFilter, setGlobalFilter] = useState("")
|
||||
|
||||
const openSheet = (disk: DiskInfo) => {
|
||||
const openSheet = (disk: SmartDeviceRecord) => {
|
||||
setActiveDiskId(disk.id)
|
||||
setSheetOpen(true)
|
||||
}
|
||||
|
||||
// Fetch smart devices from collection (without attributes to save bandwidth)
|
||||
const fetchSmartDevices = useCallback(() => {
|
||||
// Fetch smart devices
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
pb.collection<SmartDeviceRecord>("smart_devices")
|
||||
.getFullList({
|
||||
filter: systemId ? pb.filter("system = {:system}", { system: systemId }) : undefined,
|
||||
fields: SMART_DEVICE_FIELDS,
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((records) => {
|
||||
setSmartDevices(records)
|
||||
.then(setSmartDevices)
|
||||
.catch((err) => {
|
||||
if (!err.isAbort) {
|
||||
setSmartDevices([])
|
||||
}
|
||||
})
|
||||
.catch(() => setSmartDevices([]))
|
||||
|
||||
return () => controller.abort()
|
||||
}, [systemId])
|
||||
|
||||
// Fetch smart devices when component mounts or systemId changes
|
||||
useEffect(() => {
|
||||
fetchSmartDevices()
|
||||
}, [fetchSmartDevices])
|
||||
|
||||
// Subscribe to live updates so rows add/remove without manual refresh/filtering
|
||||
// Subscribe to updates
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined
|
||||
const pbOptions = systemId
|
||||
? { fields: SMART_DEVICE_FIELDS, filter: pb.filter("system = {:system}", { system: systemId }) }
|
||||
: { fields: SMART_DEVICE_FIELDS }
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
unsubscribe = await pb.collection("smart_devices").subscribe(
|
||||
"*",
|
||||
(event) => {
|
||||
const record = event.record as SmartDeviceRecord
|
||||
setSmartDevices((currentDevices) => {
|
||||
const devices = currentDevices ?? []
|
||||
const matchesSystemScope = !systemId || record.system === systemId
|
||||
; (async () => {
|
||||
try {
|
||||
unsubscribe = await pb.collection("smart_devices").subscribe(
|
||||
"*",
|
||||
(event) => {
|
||||
const record = event.record as SmartDeviceRecord
|
||||
setSmartDevices((currentDevices) => {
|
||||
const devices = currentDevices ?? []
|
||||
const matchesSystemScope = !systemId || record.system === systemId
|
||||
|
||||
if (event.action === "delete") {
|
||||
return devices.filter((device) => device.id !== record.id)
|
||||
}
|
||||
if (event.action === "delete") {
|
||||
return devices.filter((device) => device.id !== record.id)
|
||||
}
|
||||
|
||||
if (!matchesSystemScope) {
|
||||
// Record moved out of scope; ensure it disappears locally.
|
||||
return devices.filter((device) => device.id !== record.id)
|
||||
}
|
||||
if (!matchesSystemScope) {
|
||||
// Record moved out of scope; ensure it disappears locally.
|
||||
return devices.filter((device) => device.id !== record.id)
|
||||
}
|
||||
|
||||
const existingIndex = devices.findIndex((device) => device.id === record.id)
|
||||
if (existingIndex === -1) {
|
||||
return [record, ...devices]
|
||||
}
|
||||
const existingIndex = devices.findIndex((device) => device.id === record.id)
|
||||
if (existingIndex === -1) {
|
||||
return [record, ...devices]
|
||||
}
|
||||
|
||||
const next = [...devices]
|
||||
next[existingIndex] = record
|
||||
return next
|
||||
})
|
||||
},
|
||||
pbOptions
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to subscribe to SMART device updates:", error)
|
||||
}
|
||||
})()
|
||||
const next = [...devices]
|
||||
next[existingIndex] = record
|
||||
return next
|
||||
})
|
||||
},
|
||||
pbOptions
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to subscribe to SMART device updates:", error)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
unsubscribe?.()
|
||||
}
|
||||
}, [systemId])
|
||||
|
||||
const handleRowRefresh = useCallback(
|
||||
async (disk: DiskInfo) => {
|
||||
if (!disk.system) return
|
||||
setRowActionState({ type: "refresh", id: disk.id })
|
||||
try {
|
||||
await pb.send("/api/beszel/smart/refresh", {
|
||||
method: "POST",
|
||||
query: { system: disk.system },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh SMART device:", error)
|
||||
} finally {
|
||||
setRowActionState((state) => (state?.id === disk.id ? null : state))
|
||||
}
|
||||
},
|
||||
[fetchSmartDevices]
|
||||
)
|
||||
const handleRowRefresh = useCallback(async (disk: SmartDeviceRecord) => {
|
||||
if (!disk.system) return
|
||||
setRowActionState({ type: "refresh", id: disk.id })
|
||||
try {
|
||||
await pb.send("/api/beszel/smart/refresh", {
|
||||
method: "POST",
|
||||
query: { system: disk.system },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh SMART device:", error)
|
||||
} finally {
|
||||
setRowActionState((state) => (state?.id === disk.id ? null : state))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDeleteDevice = useCallback(async (disk: DiskInfo) => {
|
||||
const handleDeleteDevice = useCallback(async (disk: SmartDeviceRecord) => {
|
||||
setRowActionState({ type: "delete", id: disk.id })
|
||||
try {
|
||||
await pb.collection("smart_devices").delete(disk.id)
|
||||
@@ -403,7 +372,7 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const actionColumn = useMemo<ColumnDef<DiskInfo>>(
|
||||
const actionColumn = useMemo<ColumnDef<SmartDeviceRecord>>(
|
||||
() => ({
|
||||
id: "actions",
|
||||
enableSorting: false,
|
||||
@@ -471,13 +440,8 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
|
||||
return [...baseColumns, actionColumn]
|
||||
}, [systemId, actionColumn])
|
||||
|
||||
// Convert SmartDeviceRecord to DiskInfo
|
||||
const diskData = useMemo(() => {
|
||||
return smartDevices ? convertSmartDeviceRecordToDiskInfo(smartDevices) : []
|
||||
}, [smartDevices])
|
||||
|
||||
const table = useReactTable({
|
||||
data: diskData,
|
||||
data: smartDevices || ([] as SmartDeviceRecord[]),
|
||||
columns: tableColumns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
@@ -495,10 +459,10 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
|
||||
globalFilterFn: (row, _columnId, filterValue) => {
|
||||
const disk = row.original
|
||||
const systemName = $allSystemsById.get()[disk.system]?.name ?? ""
|
||||
const device = disk.device ?? ""
|
||||
const device = disk.name ?? ""
|
||||
const model = disk.model ?? ""
|
||||
const status = disk.status ?? ""
|
||||
const type = disk.deviceType ?? ""
|
||||
const status = disk.state ?? ""
|
||||
const type = disk.type ?? ""
|
||||
const searchString = `${systemName} ${device} ${model} ${status} ${type}`.toLowerCase()
|
||||
return (filterValue as string)
|
||||
.toLowerCase()
|
||||
@@ -508,7 +472,7 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
|
||||
})
|
||||
|
||||
// Hide the table on system pages if there's no data, but always show on global page
|
||||
if (systemId && !diskData.length && !columnFilters.length) {
|
||||
if (systemId && !smartDevices?.length && !columnFilters.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user