refactor: redesign servers table in new design

This commit is contained in:
moonrailgun 2024-04-21 17:54:12 +08:00
parent fc259f7d8e
commit ccf7b8d4aa
6 changed files with 261 additions and 11 deletions

View File

@ -0,0 +1,80 @@
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
createColumnHelper,
} from '@tanstack/react-table';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Empty } from 'antd';
export type { ColumnDef };
export { createColumnHelper };
interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[];
data: TData[];
}
export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody className="overflow-auto">
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
<Empty />
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}

View File

@ -0,0 +1,159 @@
import React, { useMemo } from 'react';
import { DataTable, createColumnHelper } from '../DataTable';
import { useTranslation } from '@i18next-toolkit/react';
import { useIntervalUpdate } from '@/hooks/useIntervalUpdate';
import { useServerMap } from './useServerMap';
import { isServerOnline } from '@tianji/shared';
import { max } from 'lodash-es';
import { ServerStatusInfo } from '../../../types';
import { Badge } from 'antd';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import dayjs from 'dayjs';
import { filesize } from 'filesize';
import prettyMilliseconds from 'pretty-ms';
import { UpDownCounter } from '../UpDownCounter';
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
const columnHelper = createColumnHelper<ServerStatusInfo>();
interface ServerListProps {
hideOfflineServer: boolean;
}
export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
const { t } = useTranslation();
const serverMap = useServerMap();
const inc = useIntervalUpdate(2 * 1000);
const { hideOfflineServer } = props;
const dataSource = useMemo(
() =>
Object.values(serverMap)
.sort((info) => (isServerOnline(info) ? -1 : 1))
.filter((info) => {
if (hideOfflineServer) {
return isServerOnline(info);
}
return true;
}), // make online server is up and offline is down
[serverMap, inc, hideOfflineServer]
);
const lastUpdatedAt = max(dataSource.map((d) => d.updatedAt));
const columns = useMemo(() => {
return [
columnHelper.display({
header: t('Status'),
size: 90,
cell: (props) =>
isServerOnline(props.row.original) ? (
<Badge status="success" text={t('online')} />
) : (
<Tooltip>
<TooltipTrigger>
<Badge status="error" text="offline" />
</TooltipTrigger>
<TooltipContent>
{t('Last online: {{time}}', {
time: dayjs(props.row.original.updatedAt).format(
'YYYY-MM-DD HH:mm:ss'
),
})}
</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor('name', {
header: t('Node Name'),
size: 150,
}),
columnHelper.accessor('hostname', {
header: t('Host Name'),
size: 150,
}),
columnHelper.accessor('payload.uptime', {
header: t('Uptime'),
size: 150,
cell: (props) => prettyMilliseconds(Number(props.getValue()) * 1000),
}),
columnHelper.accessor('payload.load', {
header: t('Load'),
size: 70,
}),
columnHelper.display({
header: t('Network'),
size: 110,
cell: (props) => (
<UpDownCounter
up={filesize(props.row.original.payload.network_out)}
down={filesize(props.row.original.payload.network_in)}
/>
),
}),
columnHelper.display({
header: t('Traffic'),
size: 130,
cell: (props) => (
<UpDownCounter
up={filesize(props.row.original.payload.network_tx) + '/s'}
down={filesize(props.row.original.payload.network_rx) + '/s'}
/>
),
}),
columnHelper.accessor('payload.cpu', {
header: 'CPU',
size: 80,
cell: (props) => `${props.getValue()}%`,
}),
columnHelper.display({
header: 'RAM',
size: 120,
cell: (props) => (
<div className="text-xs">
<div>
{filesize(props.row.original.payload.memory_used * 1000)} /{' '}
</div>
<div>
{filesize(props.row.original.payload.memory_total * 1000)}
</div>
</div>
),
}),
columnHelper.display({
header: 'HDD',
size: 120,
cell: (props) => (
<div className="text-xs">
<div>
{filesize(props.row.original.payload.hdd_used * 1000 * 1000)} /{' '}
</div>
<div>
{filesize(props.row.original.payload.hdd_total * 1000 * 1000)}
</div>
</div>
),
}),
columnHelper.accessor('updatedAt', {
header: t('updatedAt'),
size: 130,
cell: (props) => dayjs(props.getValue()).format('MMM D HH:mm:ss'),
}),
];
}, []);
return (
<div className="flex h-full flex-col">
<div className="mb-2 text-right text-sm opacity-80">
{t('Last updated at: {{date}}', {
date: dayjs(lastUpdatedAt).format('YYYY-MM-DD HH:mm:ss'),
})}
</div>
<ScrollArea className="flex-1 overflow-hidden">
<ScrollBar orientation="horizontal" />
<DataTable columns={columns} data={dataSource} />
</ScrollArea>
</div>
);
});
ServerList.displayName = 'ServerList';

View File

@ -0,0 +1,11 @@
import { useSocketSubscribe } from '@/api/socketio';
import { ServerStatusInfo } from '../../../types';
export function useServerMap(): Record<string, ServerStatusInfo> {
const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>(
'onServerStatusUpdate',
{}
);
return serverMap;
}

View File

@ -43,7 +43,7 @@ const TableFooter = React.forwardRef<
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn(
'border-t bg-zinc-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-zinc-800/50', 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className className
)} )}
{...props} {...props}
@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
'border-b transition-colors hover:bg-zinc-100/50 data-[state=selected]:bg-zinc-100 dark:hover:bg-zinc-800/50 dark:data-[state=selected]:bg-zinc-800', 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className className
)} )}
{...props} {...props}
@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
'h-10 px-2 text-left align-middle font-medium text-zinc-500 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] dark:text-zinc-400', 'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className className
)} )}
{...props} {...props}
@ -102,7 +102,7 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption
ref={ref} ref={ref}
className={cn('mt-4 text-sm text-zinc-500 dark:text-zinc-400', className)} className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props} {...props}
/> />
)); ));

View File

@ -134,6 +134,9 @@ function useServerMap(): Record<string, ServerStatusInfo> {
return serverMap; return serverMap;
} }
/**
* @deprecated
*/
export const ServerList: React.FC<{ export const ServerList: React.FC<{
hideOfflineServer: boolean; hideOfflineServer: boolean;
}> = React.memo((props) => { }> = React.memo((props) => {

View File

@ -1,15 +1,12 @@
import { defaultErrorHandler, trpc } from '@/api/trpc'; import { defaultErrorHandler, trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { CommonList } from '@/components/CommonList';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { ServerList } from '@/components/server/ServerList';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -19,7 +16,7 @@ import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useEventWithLoading } from '@/hooks/useEvent'; import { useEventWithLoading } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2'; import { LayoutV2 } from '@/pages/LayoutV2';
import { AddServerStep, InstallScript, ServerList } from '@/pages/Servers'; import { AddServerStep, InstallScript } from '@/pages/Servers';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -115,9 +112,9 @@ export const ServerContent: React.FC = React.memo(() => {
/> />
} }
> >
<ScrollArea className="h-full overflow-hidden p-4"> <div className="h-full overflow-hidden p-4">
<ServerList hideOfflineServer={hideOfflineServer} /> <ServerList hideOfflineServer={hideOfflineServer} />
</ScrollArea> </div>
</CommonWrapper> </CommonWrapper>
); );
}); });