ui: small refactoring / auto formatting

This commit is contained in:
henrygd
2026-02-12 18:40:16 -05:00
parent e816ea143a
commit 1f1a448aef
6 changed files with 90 additions and 101 deletions

View File

@@ -54,36 +54,34 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
fields: "id,name,image,cpu,memory,net,health,status,system,updated", fields: "id,name,image,cpu,memory,net,health,status,system,updated",
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined, filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
}) })
.then( .then(({ items }) => {
({ items }) => { if (items.length === 0) {
if (items.length === 0) {
setData((curItems) => {
if (systemId) {
return curItems?.filter((item) => item.system !== systemId) ?? []
}
return []
})
return
}
setData((curItems) => { setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0) if (systemId) {
const containerIds = new Set() return curItems?.filter((item) => item.system !== systemId) ?? []
const newItems = []
for (const item of items) {
if (Math.abs(lastUpdated - item.updated) < 70_000) {
containerIds.add(item.id)
newItems.push(item)
}
} }
for (const item of curItems ?? []) { return []
if (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
}) })
return
} }
) setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const containerIds = new Set()
const newItems = []
for (const item of items) {
if (Math.abs(lastUpdated - item.updated) < 70_000) {
containerIds.add(item.id)
newItems.push(item)
}
}
for (const item of curItems ?? []) {
if (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
})
})
} }
// initial load // initial load
@@ -285,7 +283,7 @@ async function getInfoHtml(container: ContainerRecord): Promise<string> {
]) ])
try { try {
info = JSON.stringify(JSON.parse(info), null, 2) info = JSON.stringify(JSON.parse(info), null, 2)
} catch (_) { } } catch (_) {}
return info ? highlighter.codeToHtml(info, { lang: "json", theme: syntaxTheme }) : t`No results.` return info ? highlighter.codeToHtml(info, { lang: "json", theme: syntaxTheme }) : t`No results.`
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -342,12 +340,12 @@ function ContainerSheet({
setLogsDisplay("") setLogsDisplay("")
setInfoDisplay("") setInfoDisplay("")
if (!container) return if (!container) return
; (async () => { ;(async () => {
const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)]) const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])
setLogsDisplay(logsHtml) setLogsDisplay(logsHtml)
setInfoDisplay(infoHtml) setInfoDisplay(infoHtml)
setTimeout(scrollLogsToBottom, 20) setTimeout(scrollLogsToBottom, 20)
})() })()
}, [container]) }, [container])
return ( return (
@@ -473,7 +471,7 @@ const ContainerTableRow = memo(function ContainerTableRow({
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
key={cell.id} key={cell.id}
className="py-0" className="py-0 ps-4.5"
style={{ style={{
height: virtualRow.size, height: virtualRow.size,
}} }}

View File

@@ -19,7 +19,7 @@ import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/u
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums" import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils" import { cn, formatBytes, getHostDisplayValue, secondsToUptimeString, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types" import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
export default function InfoBar({ export default function InfoBar({
@@ -77,14 +77,6 @@ export default function InfoBar({
}, },
} }
let uptime: string
if (system.info.u < 3600) {
uptime = secondsToString(system.info.u, "minute")
} else if (system.info.u < 360000) {
uptime = secondsToString(system.info.u, "hour")
} else {
uptime = secondsToString(system.info.u, "day")
}
const info = [ const info = [
{ value: getHostDisplayValue(system), Icon: GlobeIcon }, { value: getHostDisplayValue(system), Icon: GlobeIcon },
{ {
@@ -94,7 +86,7 @@ export default function InfoBar({
// hide if hostname is same as host or name // hide if hostname is same as host or name
hide: hostname === system.host || hostname === system.name, hide: hostname === system.host || hostname === system.name,
}, },
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u }, { value: secondsToUptimeString(system.info.u), Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
osInfo[os], osInfo[os],
{ {
value: cpuModel, value: cpuModel,

View File

@@ -174,8 +174,8 @@ export const columns: ColumnDef<SmartDeviceRecord>[] = [
<HeaderButton column={column} name={t({ message: "Power On", comment: "Power On Time" })} Icon={Clock} /> <HeaderButton column={column} name={t({ message: "Power On", comment: "Power On Time" })} Icon={Clock} />
), ),
cell: ({ getValue }) => { cell: ({ getValue }) => {
const hours = (getValue() ?? 0) as number const hours = getValue() as number | undefined
if (!hours && hours !== 0) { if (hours == null) {
return <div className="text-sm text-muted-foreground ms-1.5">N/A</div> return <div className="text-sm text-muted-foreground ms-1.5">N/A</div>
} }
const seconds = hours * 3600 const seconds = hours * 3600
@@ -195,7 +195,7 @@ export const columns: ColumnDef<SmartDeviceRecord>[] = [
), ),
cell: ({ getValue }) => { cell: ({ getValue }) => {
const cycles = getValue() as number | undefined const cycles = getValue() as number | undefined
if (!cycles && cycles !== 0) { if (cycles == null) {
return <div className="text-muted-foreground ms-1.5">N/A</div> return <div className="text-muted-foreground ms-1.5">N/A</div>
} }
return <span className="ms-1.5">{cycles.toLocaleString()}</span> return <span className="ms-1.5">{cycles.toLocaleString()}</span>
@@ -206,9 +206,8 @@ export const columns: ColumnDef<SmartDeviceRecord>[] = [
invertSorting: true, invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Temp`} Icon={ThermometerIcon} />, header: ({ column }) => <HeaderButton column={column} name={t`Temp`} Icon={ThermometerIcon} />,
cell: ({ getValue }) => { cell: ({ getValue }) => {
const temp = getValue() as number | undefined | null const temp = getValue() as number | null | undefined
// Most devices won't report a real 0C temperature; treat 0 as "unknown". if (!temp) {
if (temp == null || temp === 0) {
return <div className="text-muted-foreground ms-1.5">N/A</div> return <div className="text-muted-foreground ms-1.5">N/A</div>
} }
const { value, unit } = formatTemperature(temp) const { value, unit } = formatTemperature(temp)
@@ -309,41 +308,41 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
? { fields: SMART_DEVICE_FIELDS, filter: pb.filter("system = {:system}", { system: systemId }) } ? { fields: SMART_DEVICE_FIELDS, filter: pb.filter("system = {:system}", { system: systemId }) }
: { fields: SMART_DEVICE_FIELDS } : { fields: SMART_DEVICE_FIELDS }
; (async () => { ;(async () => {
try { try {
unsubscribe = await pb.collection("smart_devices").subscribe( unsubscribe = await pb.collection("smart_devices").subscribe(
"*", "*",
(event) => { (event) => {
const record = event.record as SmartDeviceRecord const record = event.record as SmartDeviceRecord
setSmartDevices((currentDevices) => { setSmartDevices((currentDevices) => {
const devices = currentDevices ?? [] const devices = currentDevices ?? []
const matchesSystemScope = !systemId || record.system === systemId const matchesSystemScope = !systemId || record.system === systemId
if (event.action === "delete") { if (event.action === "delete") {
return devices.filter((device) => device.id !== record.id) return devices.filter((device) => device.id !== record.id)
} }
if (!matchesSystemScope) { if (!matchesSystemScope) {
// Record moved out of scope; ensure it disappears locally. // Record moved out of scope; ensure it disappears locally.
return devices.filter((device) => device.id !== record.id) return devices.filter((device) => device.id !== record.id)
} }
const existingIndex = devices.findIndex((device) => device.id === record.id) const existingIndex = devices.findIndex((device) => device.id === record.id)
if (existingIndex === -1) { if (existingIndex === -1) {
return [record, ...devices] return [record, ...devices]
} }
const next = [...devices] const next = [...devices]
next[existingIndex] = record next[existingIndex] = record
return next return next
}) })
}, },
pbOptions pbOptions
) )
} catch (error) { } catch (error) {
console.error("Failed to subscribe to SMART device updates:", error) console.error("Failed to subscribe to SMART device updates:", error)
} }
})() })()
return () => { return () => {
unsubscribe?.() unsubscribe?.()

View File

@@ -35,7 +35,7 @@ import {
formatTemperature, formatTemperature,
getMeterState, getMeterState,
parseSemVer, parseSemVer,
secondsToString, secondsToUptimeString,
} from "@/lib/utils" } from "@/lib/utils"
import { batteryStateTranslations } from "@/lib/i18n" import { batteryStateTranslations } from "@/lib/i18n"
import type { SystemRecord } from "@/types" import type { SystemRecord } from "@/types"
@@ -154,11 +154,7 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
{name} {name}
</Link> </Link>
</span> </span>
<Link <Link href={linkUrl} className="inset-0 absolute size-full" aria-label={name}></Link>
href={linkUrl}
className="inset-0 absolute size-full"
aria-label={name}
></Link>
</> </>
) )
}, },
@@ -382,20 +378,13 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
size: 50, size: 50,
Icon: ClockArrowUp, Icon: ClockArrowUp,
header: sortableHeader, header: sortableHeader,
hideSort: true,
cell(info) { cell(info) {
const uptime = info.getValue() as number const uptime = info.getValue() as number
if (!uptime) { if (!uptime) {
return null return null
} }
let formatted: string return <span className="tabular-nums whitespace-nowrap">{secondsToUptimeString(uptime)}</span>
if (uptime < 3600) {
formatted = secondsToString(uptime, "minute")
} else if (uptime < 360000) {
formatted = secondsToString(uptime, "hour")
} else {
formatted = secondsToString(uptime, "day")
}
return <span className="tabular-nums whitespace-nowrap">{formatted}</span>
}, },
}, },
{ {
@@ -479,9 +468,9 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const meterClass = cn( const meterClass = cn(
"h-full", "h-full",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) || (info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) || (threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) || (threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down STATUS_COLORS.down
) )
return ( return (
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full"> <div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
@@ -593,7 +582,7 @@ export function IndicatorDot({ system, className }: { system: SystemRecord; clas
return ( return (
<span <span
className={cn("shrink-0 size-2 rounded-full", className)} className={cn("shrink-0 size-2 rounded-full", className)}
// style={{ marginBottom: "-1px" }} // style={{ marginBottom: "-1px" }}
/> />
) )
} }

View File

@@ -434,7 +434,7 @@ const SystemTableRow = memo(
width: cell.column.getSize(), width: cell.column.getSize(),
height: virtualRow.size, height: virtualRow.size,
}} }}
className="py-0" className="py-0 ps-4.5"
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>

View File

@@ -465,4 +465,15 @@ export function secondsToString(seconds: number, unit: "hour" | "minute" | "day"
case "day": case "day":
return plural(count, { one: `${countString} day`, other: `${countString} days` }) return plural(count, { one: `${countString} day`, other: `${countString} days` })
} }
}
/** Format seconds to uptime string - "X minutes", "X hours", "X days" */
export function secondsToUptimeString(seconds: number): string {
if (seconds < 3600) {
return secondsToString(seconds, "minute")
} else if (seconds < 360000) {
return secondsToString(seconds, "hour")
} else {
return secondsToString(seconds, "day")
}
} }