refactor: redesign servers table in new design
This commit is contained in:
parent
fc259f7d8e
commit
ccf7b8d4aa
80
src/client/components/DataTable.tsx
Normal file
80
src/client/components/DataTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
159
src/client/components/server/ServerList.tsx
Normal file
159
src/client/components/server/ServerList.tsx
Normal 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';
|
11
src/client/components/server/useServerMap.ts
Normal file
11
src/client/components/server/useServerMap.ts
Normal 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;
|
||||
}
|
@ -43,7 +43,7 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@ -102,7 +102,7 @@ const TableCaption = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
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}
|
||||
/>
|
||||
));
|
||||
|
@ -134,6 +134,9 @@ function useServerMap(): Record<string, ServerStatusInfo> {
|
||||
return serverMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const ServerList: React.FC<{
|
||||
hideOfflineServer: boolean;
|
||||
}> = React.memo((props) => {
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonList } from '@/components/CommonList';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { ServerList } from '@/components/server/ServerList';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
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 { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||
import { AddServerStep, InstallScript, ServerList } from '@/pages/Servers';
|
||||
import { AddServerStep, InstallScript } from '@/pages/Servers';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
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} />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</CommonWrapper>
|
||||
);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user