feat: add server docker expend view

This commit is contained in:
moonrailgun 2024-05-16 20:11:20 +08:00
parent 1dfa24df1b
commit c6433f310b
5 changed files with 295 additions and 40 deletions

View File

@ -43,22 +43,23 @@ type ReportDataPayload struct {
} }
type DockerDataPayload struct { type DockerDataPayload struct {
ID string `json:"id"` ID string `json:"id"`
Image string `json:"image"` Image string `json:"image"`
ImageID string `json:"imageId"` ImageID string `json:"imageId"`
CreatedAt int64 `json:"createdAt"` Ports []dockerTypes.Port `json:"ports"`
State string `json:"state"` CreatedAt int64 `json:"createdAt"`
Status string `json:"status"` State string `json:"state"`
CpuPercent float64 `json:"cpuPercent"` Status string `json:"status"`
Memory float64 `json:"memory"` CpuPercent float64 `json:"cpuPercent"`
MemLimit uint64 `json:"memLimit"` Memory float64 `json:"memory"`
MemPercent float64 `json:"memPercent"` MemLimit uint64 `json:"memLimit"`
StorageWriteSize uint64 `json:"storageWriteSize"` MemPercent float64 `json:"memPercent"`
StorageReadSize uint64 `json:"storageReadSize"` StorageWriteSize uint64 `json:"storageWriteSize"`
NetworkRx float64 `json:"networkRx"` StorageReadSize uint64 `json:"storageReadSize"`
NetworkTx float64 `json:"networkTx"` NetworkRx float64 `json:"networkRx"`
IORead uint64 `json:"ioRead"` NetworkTx float64 `json:"networkTx"`
IOWrite uint64 `json:"ioWrite"` IORead uint64 `json:"ioRead"`
IOWrite uint64 `json:"ioWrite"`
} }
var checkIP int var checkIP int
@ -303,6 +304,7 @@ func GetDockerStat() ([]DockerDataPayload, error) {
ID: container.ID[:10], ID: container.ID[:10],
Image: container.Image, Image: container.Image,
ImageID: container.ImageID, ImageID: container.ImageID,
Ports: container.Ports,
CreatedAt: container.Created, CreatedAt: container.Created,
State: container.State, State: container.State,
Status: container.Status, Status: container.Status,

View File

@ -5,6 +5,7 @@ import {
useReactTable, useReactTable,
createColumnHelper, createColumnHelper,
getExpandedRowModel, getExpandedRowModel,
ExpandedState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { import {
@ -17,6 +18,9 @@ import {
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Empty } from 'antd'; import { Empty } from 'antd';
import React from 'react'; import React from 'react';
import { Button } from './ui/button';
import { LuArrowRight } from 'react-icons/lu';
import { cn } from '@/utils/style';
export type { ColumnDef }; export type { ColumnDef };
export { createColumnHelper }; export { createColumnHelper };
@ -32,10 +36,18 @@ export function DataTable<TData>({
data, data,
ExpandComponent, ExpandComponent,
}: DataTableProps<TData>) { }: DataTableProps<TData>) {
const [expanded, setExpanded] = React.useState<ExpandedState>({});
const canExpand = Boolean(ExpandComponent);
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
state: {
expanded,
},
onExpandedChange: setExpanded,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getRowCanExpand: () => canExpand,
}); });
return ( return (
@ -44,6 +56,21 @@ export function DataTable<TData>({
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{canExpand && (
<TableHead className="w-9">
<Button
variant="ghost"
size="icon"
className={cn(
'mr-1 h-5 w-5',
table.getIsAllRowsExpanded() && 'rotate-90'
)}
Icon={LuArrowRight}
onClick={table.getToggleAllRowsExpandedHandler()}
/>
</TableHead>
)}
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead key={header.id}>
@ -67,7 +94,22 @@ export function DataTable<TData>({
key={row.id} key={row.id}
data-state={row.getIsSelected() && 'selected'} data-state={row.getIsSelected() && 'selected'}
> >
{row.getVisibleCells().map((cell) => ( {row.getCanExpand() && (
<TableCell className="w-9">
<Button
variant="ghost"
size="icon"
className={cn(
'mr-1 h-5 w-5',
row.getIsExpanded() && 'rotate-90'
)}
Icon={LuArrowRight}
onClick={row.getToggleExpandedHandler()}
/>
</TableCell>
)}
{row.getVisibleCells().map((cell, i) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
@ -84,7 +126,7 @@ export function DataTable<TData>({
{renderedRow} {renderedRow}
<TableRow key={row.id + 'expand'}> <TableRow key={row.id + 'expand'}>
<TableCell colSpan={table.getAllLeafColumns().length}> <TableCell colSpan={table.getAllLeafColumns().length + 1}>
<ExpandComponent row={row.original} /> <ExpandComponent row={row.original} />
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -13,6 +13,8 @@ import { filesize } from 'filesize';
import prettyMilliseconds from 'pretty-ms'; import prettyMilliseconds from 'pretty-ms';
import { UpDownCounter } from '../UpDownCounter'; import { UpDownCounter } from '../UpDownCounter';
import { ScrollArea, ScrollBar } from '../ui/scroll-area'; import { ScrollArea, ScrollBar } from '../ui/scroll-area';
import { ServerRowExpendView } from './ServerRowExpendView';
import { FaDocker } from 'react-icons/fa';
const columnHelper = createColumnHelper<ServerStatusInfo>(); const columnHelper = createColumnHelper<ServerStatusInfo>();
@ -45,23 +47,37 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
columnHelper.display({ columnHelper.display({
header: t('Status'), header: t('Status'),
size: 90, size: 90,
cell: (props) => cell: (props) => (
isServerOnline(props.row.original) ? ( <div className="flex gap-2">
<Badge status="success" text={t('online')} /> {isServerOnline(props.row.original) ? (
) : ( <Badge status="success" text={t('online')} />
<Tooltip> ) : (
<TooltipTrigger> <Tooltip>
<Badge status="error" text="offline" /> <TooltipTrigger>
</TooltipTrigger> <Badge status="error" text="offline" />
<TooltipContent> </TooltipTrigger>
{t('Last online: {{time}}', { <TooltipContent>
time: dayjs(props.row.original.updatedAt).format( {t('Last online: {{time}}', {
'YYYY-MM-DD HH:mm:ss' time: dayjs(props.row.original.updatedAt).format(
), 'YYYY-MM-DD HH:mm:ss'
})} ),
</TooltipContent> })}
</Tooltip> </TooltipContent>
), </Tooltip>
)}
{props.row.original.payload.docker && (
<Tooltip>
<TooltipTrigger onClick={props.row.getToggleExpandedHandler()}>
<FaDocker />
</TooltipTrigger>
<TooltipContent>
{t('Docker in running in this server')}
</TooltipContent>
</Tooltip>
)}
</div>
),
}), }),
columnHelper.accessor('name', { columnHelper.accessor('name', {
header: t('Node Name'), header: t('Node Name'),
@ -111,10 +127,15 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
cell: (props) => ( cell: (props) => (
<div className="text-xs"> <div className="text-xs">
<div> <div>
{filesize(props.row.original.payload.memory_used * 1000)} /{' '} {filesize(props.row.original.payload.memory_used * 1024, {
base: 2,
})}{' '}
/{' '}
</div> </div>
<div> <div>
{filesize(props.row.original.payload.memory_total * 1000)} {filesize(props.row.original.payload.memory_total * 1024, {
base: 2,
})}
</div> </div>
</div> </div>
), ),
@ -125,10 +146,15 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
cell: (props) => ( cell: (props) => (
<div className="text-xs"> <div className="text-xs">
<div> <div>
{filesize(props.row.original.payload.hdd_used * 1000 * 1000)} /{' '} {filesize(props.row.original.payload.hdd_used * 1024 * 1024, {
base: 2,
})}{' '}
/{' '}
</div> </div>
<div> <div>
{filesize(props.row.original.payload.hdd_total * 1000 * 1000)} {filesize(props.row.original.payload.hdd_total * 1024 * 1024, {
base: 2,
})}
</div> </div>
</div> </div>
), ),
@ -160,7 +186,11 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
<ScrollArea className="flex-1 overflow-hidden"> <ScrollArea className="flex-1 overflow-hidden">
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
<DataTable columns={columns} data={dataSource} /> <DataTable
columns={columns}
data={dataSource}
ExpandComponent={ServerRowExpendView}
/>
</ScrollArea> </ScrollArea>
</div> </div>
); );

View File

@ -0,0 +1,151 @@
import React, { useMemo, useState } from 'react';
import {
ServerStatusInfo,
ServerStatusDockerContainerPayload,
} from '../../../types';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Empty } from 'antd';
import { DiDocker } from 'react-icons/di';
import { useTranslation } from '@i18next-toolkit/react';
import { DataTable, createColumnHelper } from '../DataTable';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { filesize } from 'filesize';
import { UpDownCounter } from '../UpDownCounter';
import dayjs from 'dayjs';
import { Switch } from '../ui/switch';
const columnHelper = createColumnHelper<ServerStatusDockerContainerPayload>();
export const ServerRowExpendView: React.FC<{ row: ServerStatusInfo }> =
React.memo((props) => {
const { row } = props;
const { t } = useTranslation();
const [showAll, setShowAll] = useState(false);
const columns = useMemo(() => {
return [
columnHelper.display({
header: t('Image'),
size: 90,
cell: (props) => (
<Tooltip>
<TooltipTrigger className="cursor-default">
<span>{props.row.original.image}</span>
</TooltipTrigger>
<TooltipContent>{props.row.original.imageId}</TooltipContent>
</Tooltip>
),
}),
columnHelper.display({
header: t('State'),
size: 90,
cell: (props) => (
<Tooltip>
<TooltipTrigger className="cursor-default">
<span>{props.row.original.state}</span>
</TooltipTrigger>
<TooltipContent>{props.row.original.status}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor('ports', {
header: t('ports'),
size: 130,
cell: (props) =>
props
.getValue()
.map((item, i) => (
<div
key={i}
>{`${item.IP}:${item.PublicPort} -> ${item.PrivatePort} / ${item.Type}`}</div>
)),
}),
columnHelper.accessor('cpuPercent', {
header: 'CPU(%)',
size: 90,
cell: (props) => `${props.getValue() * 100}%`,
}),
columnHelper.display({
header: t('Memory'),
size: 120,
cell: (props) => (
<div className="text-xs">
<div>{filesize(props.row.original.memory, { base: 2 })} / </div>
<div>{filesize(props.row.original.memLimit, { base: 2 })}</div>
</div>
),
}),
columnHelper.display({
header: t('Traffic'),
size: 130,
cell: (props) => (
<UpDownCounter
up={filesize(props.row.original.networkTx, { base: 2 }) + '/s'}
down={filesize(props.row.original.networkRx, { base: 2 }) + '/s'}
/>
),
}),
columnHelper.display({
header: t('Disk read/write'),
size: 120,
cell: (props) => (
<div className="text-xs">
<div>{filesize(props.row.original.ioRead)} / </div>
<div>{filesize(props.row.original.ioWrite)}</div>
</div>
),
}),
columnHelper.accessor('createdAt', {
header: t('Created At'),
size: 130,
cell: (props) => (
<Tooltip>
<TooltipTrigger className="cursor-default">
<span>{dayjs.unix(props.getValue()).fromNow()}</span>
</TooltipTrigger>
<TooltipContent>
{dayjs.unix(props.getValue()).format('YYYY-MM-DD HH:mm:ss')}
</TooltipContent>
</Tooltip>
),
}),
];
}, [t]);
const data = showAll
? row.payload.docker
: row.payload.docker?.filter((item) => item.state === 'running');
return (
<div className="p-2">
<Tabs defaultValue="docker">
<TabsList>
<TabsTrigger value="docker">Docker</TabsTrigger>
<TabsTrigger value="history" disabled={true}>
History(Comming Soon)
</TabsTrigger>
</TabsList>
<TabsContent value="docker">
{!row.payload.docker ? (
<Empty
image={<DiDocker size={100} />}
description={t(
'Docker is adrift at sea, unable to find its way. Please start Docker to get back on course.'
)}
/>
) : (
<div>
<div className="mb-2 flex items-center gap-2">
<Switch checked={showAll} onCheckedChange={setShowAll} />
<div>{t('Show All')}</div>
</div>
<DataTable columns={columns} data={data} />
</div>
)}
</TabsContent>
<TabsContent value="history">Comming Soon</TabsContent>
</Tabs>
</div>
);
});
ServerRowExpendView.displayName = 'ServerRowExpendView';

View File

@ -21,4 +21,34 @@ export interface ServerStatusInfoPayload {
network_rx: number; network_rx: number;
network_in: number; network_in: number;
network_out: number; network_out: number;
// docker info
docker?: ServerStatusDockerContainerPayload[];
}
export interface ServerStatusDockerContainerPayload {
id: string;
image: string;
imageId: string;
ports: ServerStatusDockerContainerPort[];
createdAt: number;
state: string;
status: string;
cpuPercent: number;
memory: number;
memLimit: number;
memPercent: number;
storageWriteSize: number;
storageReadSize: number;
networkRx: number;
networkTx: number;
ioRead: number;
ioWrite: number;
}
export interface ServerStatusDockerContainerPort {
IP: string;
PrivatePort: number;
PublicPort: number;
Type: 'tcp' | 'udp';
} }