mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 01:39:34 +08:00
Virtualize All Systems table to improve performance with hundreds of systems (#1100)
- Also truncate long system names in tables and alerts sheet. (#1104)
This commit is contained in:
Binary file not shown.
@@ -33,6 +33,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
@@ -133,7 +133,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
>
|
||||
<Tabs defaultValue={tab} onValueChange={setTab}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-2">
|
||||
<DialogTitle className="mb-2 max-w-100 truncate pr-8">
|
||||
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
|
||||
</DialogTitle>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
|
@@ -16,7 +16,9 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
|
||||
<Trans>System</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => <span className="ps-2">{row.original.expand?.system?.name || row.original.system}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="ps-2 max-w-60 truncate">{row.original.expand?.system?.name || row.original.system}</div>
|
||||
),
|
||||
filterFn: (row, _, filterValue) => {
|
||||
const display = row.original.expand?.system?.name || row.original.system || ""
|
||||
return display.toLowerCase().includes(filterValue.toLowerCase())
|
||||
|
@@ -96,7 +96,7 @@ export const AlertDialogContent = memo(function AlertDialogContent({ system }: {
|
||||
<TabsList className="mb-1 -mt-0.5">
|
||||
<TabsTrigger value="system">
|
||||
<ServerIcon className="me-2 h-3.5 w-3.5" />
|
||||
{system.name}
|
||||
<span className="truncate max-w-60">{system.name}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="global">
|
||||
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
|
||||
|
@@ -44,7 +44,7 @@ export default memo(function () {
|
||||
<SystemsTable />
|
||||
</Suspense>
|
||||
|
||||
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 text-xs opacity-80">
|
||||
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
|
||||
<a
|
||||
href="https://github.com/henrygd/beszel"
|
||||
target="_blank"
|
||||
|
@@ -273,13 +273,13 @@ export default function AlertsHistoryDataTable() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<tr key={headerGroup.id} className="border-border/50">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead className="px-2" key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</tr>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
@@ -102,7 +102,7 @@ export default function SettingsLayout() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className="pt-5 px-4 pb-8 min-h-96 sm:pt-6 sm:px-7">
|
||||
<Card className="pt-5 px-4 pb-8 min-h-96 mb-14 sm:pt-6 sm:px-7">
|
||||
<CardHeader className="p-0">
|
||||
<CardTitle className="mb-1">
|
||||
<Trans>Settings</Trans>
|
||||
|
@@ -272,7 +272,7 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
|
||||
<div className="rounded-md border overflow-hidden w-full mt-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<tr className="border-border/50">
|
||||
{headerCols.map((col) => (
|
||||
<TableHead key={col.label} style={{ minWidth: col.w }}>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -288,12 +288,14 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
|
||||
</span>
|
||||
</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody className="whitespace-pre">
|
||||
{fingerprints.map((fingerprint, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell className="font-medium ps-5 py-2">{fingerprint.expand.system.name}</TableCell>
|
||||
<TableCell className="font-medium ps-5 py-2 max-w-60 truncate">
|
||||
{fingerprint.expand.system.name}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.token}</TableCell>
|
||||
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.fingerprint}</TableCell>
|
||||
{!isReadOnly && (
|
||||
|
@@ -391,7 +391,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
|
||||
<div id="chartwrap" className="grid gap-4 mb-14 overflow-x-clip">
|
||||
{/* system info */}
|
||||
<Card>
|
||||
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||
|
@@ -27,7 +27,7 @@ import {
|
||||
} from "@/lib/utils"
|
||||
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $userSettings } from "@/lib/stores"
|
||||
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useMemo, useRef, useState } from "react"
|
||||
import { memo } from "react"
|
||||
@@ -111,12 +111,15 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
Icon: ServerIcon,
|
||||
cell: (info) => {
|
||||
const { name } = info.row.original
|
||||
const longestName = useStore($longestSystemNameLen)
|
||||
return (
|
||||
<>
|
||||
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1 md:pe-5">
|
||||
<IndicatorDot system={info.row.original} />
|
||||
<span className="truncate" style={{ width: `${longestName}ch` }}>
|
||||
{name}
|
||||
</span>
|
||||
</span>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { name })}
|
||||
className="inset-0 absolute size-full"
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
Row,
|
||||
Table as TableType,
|
||||
} from "@tanstack/react-table"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
EyeIcon,
|
||||
FilterIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo, useState } from "react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { $systems } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, runOnce, useLocalStorage } from "@/lib/utils"
|
||||
@@ -47,6 +47,7 @@ import { getPagePath } from "@nanostores/router"
|
||||
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
|
||||
import AlertButton from "../alerts/alert-button"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
|
||||
|
||||
type ViewMode = "table" | "grid"
|
||||
type StatusFilter = "all" | "up" | "down" | "paused"
|
||||
@@ -255,7 +256,7 @@ export default function SystemsTable() {
|
||||
<div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
|
||||
{viewMode === "table" ? (
|
||||
// table layout
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
<div className="rounded-md">
|
||||
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
|
||||
</div>
|
||||
) : (
|
||||
@@ -277,36 +278,78 @@ export default function SystemsTable() {
|
||||
)
|
||||
}
|
||||
|
||||
const AllSystemsTable = memo(
|
||||
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
|
||||
const AllSystemsTable = memo(function ({
|
||||
table,
|
||||
rows,
|
||||
colLength,
|
||||
}: {
|
||||
table: TableType<SystemRecord>
|
||||
rows: Row<SystemRecord>[]
|
||||
colLength: number
|
||||
}) {
|
||||
// The virtualizer will need a reference to the scrollable container element
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
||||
count: rows.length,
|
||||
estimateSize: () => (rows.length > 10 ? 56 : 60),
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 5,
|
||||
})
|
||||
const virtualRows = virtualizer.getVirtualItems()
|
||||
|
||||
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
|
||||
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<div
|
||||
className={cn(
|
||||
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
|
||||
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
|
||||
(!rows.length || rows.length > 2) && "min-h-50"
|
||||
)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{/* add header height to table size */}
|
||||
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
|
||||
<table className="text-sm w-full h-full">
|
||||
<SystemsTableHead table={table} colLength={colLength} />
|
||||
<TableBody onMouseEnter={preloadSystemDetail}>
|
||||
{rows.length ? (
|
||||
rows.map((row) => (
|
||||
<SystemTableRow key={row.original.id} row={row} length={rows.length} colLength={colLength} />
|
||||
))
|
||||
virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index] as Row<SystemRecord>
|
||||
return (
|
||||
<SystemTableRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
virtualRow={virtualRow}
|
||||
length={rows.length}
|
||||
colLength={colLength}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={colLength} className="h-24 text-center">
|
||||
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
|
||||
<Trans>No systems found.</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
|
||||
const { i18n } = useLingui()
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<TableHeader>
|
||||
<TableHeader className="sticky top-0 z-20 w-full">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<tr key={headerGroup.id} className="border-border/50">
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead className="px-1.5" key={header.id}>
|
||||
@@ -314,15 +357,23 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
</tr>
|
||||
))}
|
||||
</TableHeader>
|
||||
)
|
||||
}, [i18n.locale, colLength])
|
||||
}
|
||||
|
||||
const SystemTableRow = memo(
|
||||
({ row, length, colLength }: { row: Row<SystemRecord>; length: number; colLength: number }) => {
|
||||
const SystemTableRow = memo(function ({
|
||||
row,
|
||||
virtualRow,
|
||||
colLength,
|
||||
}: {
|
||||
row: Row<SystemRecord>
|
||||
virtualRow: VirtualItem
|
||||
length: number
|
||||
colLength: number
|
||||
}) {
|
||||
const system = row.original
|
||||
const { t } = useLingui()
|
||||
return useMemo(() => {
|
||||
@@ -338,8 +389,9 @@ const SystemTableRow = memo(
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
height: virtualRow.size,
|
||||
}}
|
||||
className={length > 10 ? "py-2" : "py-2.5"}
|
||||
className="py-0"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
@@ -347,8 +399,7 @@ const SystemTableRow = memo(
|
||||
</TableRow>
|
||||
)
|
||||
}, [system, system.status, colLength, t])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const SystemCard = memo(
|
||||
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
|
||||
@@ -368,13 +419,11 @@ const SystemCard = memo(
|
||||
)}
|
||||
>
|
||||
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="text-base tracking-normal shrink-1 text-primary/90 flex items-center min-w-0 gap-2.5">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="flex items-center gap-2 w-full overflow-hidden">
|
||||
<CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5">
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<IndicatorDot system={system} />
|
||||
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
|
||||
{system.name}
|
||||
</CardTitle>
|
||||
<span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
{table.getColumn("actions")?.getIsVisible() && (
|
||||
|
@@ -101,7 +101,7 @@ function CommandItem({ className, ...props }: React.ComponentProps<typeof Comman
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[selected=true]:bg-accent/70 data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@@ -13,7 +13,11 @@ Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("bg-muted/30 [&_tr]:border-b", className)} {...props} />
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn("bg-table-header border-b border-border/50 [&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
@@ -1,14 +1,15 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant light (&:is(.light *));
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@custom-variant safari (@supports (hanging-punctuation: first) and (-webkit-appearance: none));
|
||||
|
||||
:root {
|
||||
--background: hsl(30 8% 98%);
|
||||
--foreground: hsl(30 0% 0%);
|
||||
--foreground: hsl(30 0% 10%);
|
||||
--card: hsl(30 0% 100%);
|
||||
--card-foreground: hsl(240 6.67% 2.94%);
|
||||
--card-foreground: hsl(240 6% 12%);
|
||||
--popover: hsl(30 0% 100%);
|
||||
--popover-foreground: hsl(240 10% 6.2%);
|
||||
--primary: hsl(240 5.88% 10%);
|
||||
@@ -30,6 +31,7 @@
|
||||
--chart-3: hsl(30 80% 55%);
|
||||
--chart-4: hsl(280 65% 60%);
|
||||
--chart-5: hsl(340 75% 55%);
|
||||
--table-header: hsl(225, 6%, 97%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -53,6 +55,7 @@
|
||||
--border: hsl(220 3% 16%);
|
||||
--input: hsl(220 4% 22%);
|
||||
--ring: hsl(220 4% 80%);
|
||||
--table-header: hsl(220, 6%, 13%);
|
||||
--radius: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -103,6 +106,7 @@
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-table-header: var(--table-header);
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
import { $alerts, $systems, $userSettings } from "./stores"
|
||||
import { $alerts, $longestSystemNameLen, $systems, $userSettings } from "./stores"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { chartTimeData } from "./utils"
|
||||
@@ -88,11 +88,29 @@ export const updateSystemList = (() => {
|
||||
}
|
||||
isFetchingSystems = true
|
||||
try {
|
||||
const records = await pb
|
||||
let records = await pb
|
||||
.collection<SystemRecord>("systems")
|
||||
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
|
||||
|
||||
if (records.length) {
|
||||
// records = [
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ...records,
|
||||
// ]
|
||||
// we need to loop once to get the longest name
|
||||
let longestName = $longestSystemNameLen.get()
|
||||
for (const { name } of records) {
|
||||
if (name.length > longestName) {
|
||||
$longestSystemNameLen.set(Math.min(20, name.length))
|
||||
}
|
||||
}
|
||||
$systems.set(records)
|
||||
} else {
|
||||
verifyAuth()
|
||||
|
@@ -53,3 +53,8 @@ export const $copyContent = atom("")
|
||||
|
||||
/** Direction for localization */
|
||||
export const $direction = atom<"ltr" | "rtl">("ltr")
|
||||
|
||||
/** Longest system name length. Used to set table column width. I know this
|
||||
* is stupid but the table is virtualized and I know this will work.
|
||||
*/
|
||||
export const $longestSystemNameLen = atom(8)
|
||||
|
@@ -102,7 +102,7 @@ const Layout = () => {
|
||||
<div className="container">
|
||||
<Navbar />
|
||||
</div>
|
||||
<div className="container mb-14 relative">
|
||||
<div className="container relative">
|
||||
<App />
|
||||
{copyContent && (
|
||||
<Suspense>
|
||||
|
@@ -1,93 +0,0 @@
|
||||
import { memo, useMemo } from "react"
|
||||
import { Row, TableType } from "@tanstack/react-table"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Link } from "@/components/ui/link"
|
||||
import { getPagePath } from "@/lib/page-path"
|
||||
import { useRouter } from "next/router"
|
||||
import { flexRender } from "@tanstack/react-table"
|
||||
import { ColumnDef } from "@tanstack/table-core"
|
||||
import { SystemRecord } from "@/lib/types"
|
||||
import { IndicatorDot } from "@/components/indicator-dot"
|
||||
import { AlertsButton } from "@/components/alerts-button"
|
||||
import { ActionsButton } from "@/components/actions-button"
|
||||
import { EyeIcon } from "@/components/icons"
|
||||
|
||||
const SystemCard = memo(
|
||||
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
|
||||
const system = row.original
|
||||
const { t } = useLingui()
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<Card
|
||||
key={system.id}
|
||||
className={cn(
|
||||
"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
|
||||
{
|
||||
"opacity-50": system.status === "paused",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="text-base tracking-normal shrink-1 text-primary/90 flex items-center min-w-0 gap-2.5">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<IndicatorDot system={system} />
|
||||
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
|
||||
{system.name}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardTitle>
|
||||
{table.getColumn("actions")?.getIsVisible() && (
|
||||
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
||||
<AlertsButton system={system} />
|
||||
<ActionsButton system={system} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2.5 text-sm px-5 pt-3.5 pb-4">
|
||||
{table.getAllColumns().map((column) => {
|
||||
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
|
||||
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
|
||||
if (!cell) return null
|
||||
// @ts-ignore
|
||||
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
|
||||
|
||||
// Special case for 'lastSeen' column: add EyeIcon before value
|
||||
if (column.id === "lastSeen") {
|
||||
return (
|
||||
<div key={column.id} className="flex items-center gap-3">
|
||||
<EyeIcon className="size-4 text-muted-foreground" />
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="text-muted-foreground min-w-16">{name()}:</span>
|
||||
<div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={column.id} className="flex items-center gap-3">
|
||||
{Icon && <Icon className="size-4 text-muted-foreground" />}
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="text-muted-foreground min-w-16">{name()}:</span>
|
||||
<div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { name: row.original.name })}
|
||||
className="inset-0 absolute w-full h-full"
|
||||
>
|
||||
<span className="sr-only">{row.original.name}</span>
|
||||
</Link>
|
||||
</Card>
|
||||
)
|
||||
}, [system, colLength, t])
|
||||
}
|
||||
)
|
@@ -4,10 +4,16 @@
|
||||
|
||||
- Add status filters to All Systems table.
|
||||
|
||||
- Virtualize All Systems table to improve performance with hundreds of systems. (#1100)
|
||||
|
||||
- Fix Safari system link CSS bug.
|
||||
|
||||
- Use older cuda image for increased compatibility (#1103)
|
||||
|
||||
- Truncate long system names in All Systems table. (#1104)
|
||||
|
||||
- Fix update mirror and add `--china-mirrors` flag. (#1035)
|
||||
|
||||
## 0.12.5
|
||||
|
||||
- Downgrade `gopsutil` to `v4.25.6` to fix panic on FreeBSD (#1083)
|
||||
|
Reference in New Issue
Block a user