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

@ -46,6 +46,7 @@ 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"`
Ports []dockerTypes.Port `json:"ports"`
CreatedAt int64 `json:"createdAt"` CreatedAt int64 `json:"createdAt"`
State string `json:"state"` State string `json:"state"`
Status string `json:"status"` Status string `json:"status"`
@ -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,8 +47,9 @@ 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">
{isServerOnline(props.row.original) ? (
<Badge status="success" text={t('online')} /> <Badge status="success" text={t('online')} />
) : ( ) : (
<Tooltip> <Tooltip>
@ -61,6 +64,19 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
})} })}
</TooltipContent> </TooltipContent>
</Tooltip> </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', {
@ -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';
} }