feat: add server docker expend view
This commit is contained in:
parent
1dfa24df1b
commit
c6433f310b
@ -43,22 +43,23 @@ type ReportDataPayload struct {
|
||||
}
|
||||
|
||||
type DockerDataPayload struct {
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"imageId"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
CpuPercent float64 `json:"cpuPercent"`
|
||||
Memory float64 `json:"memory"`
|
||||
MemLimit uint64 `json:"memLimit"`
|
||||
MemPercent float64 `json:"memPercent"`
|
||||
StorageWriteSize uint64 `json:"storageWriteSize"`
|
||||
StorageReadSize uint64 `json:"storageReadSize"`
|
||||
NetworkRx float64 `json:"networkRx"`
|
||||
NetworkTx float64 `json:"networkTx"`
|
||||
IORead uint64 `json:"ioRead"`
|
||||
IOWrite uint64 `json:"ioWrite"`
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"imageId"`
|
||||
Ports []dockerTypes.Port `json:"ports"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
CpuPercent float64 `json:"cpuPercent"`
|
||||
Memory float64 `json:"memory"`
|
||||
MemLimit uint64 `json:"memLimit"`
|
||||
MemPercent float64 `json:"memPercent"`
|
||||
StorageWriteSize uint64 `json:"storageWriteSize"`
|
||||
StorageReadSize uint64 `json:"storageReadSize"`
|
||||
NetworkRx float64 `json:"networkRx"`
|
||||
NetworkTx float64 `json:"networkTx"`
|
||||
IORead uint64 `json:"ioRead"`
|
||||
IOWrite uint64 `json:"ioWrite"`
|
||||
}
|
||||
|
||||
var checkIP int
|
||||
@ -303,6 +304,7 @@ func GetDockerStat() ([]DockerDataPayload, error) {
|
||||
ID: container.ID[:10],
|
||||
Image: container.Image,
|
||||
ImageID: container.ImageID,
|
||||
Ports: container.Ports,
|
||||
CreatedAt: container.Created,
|
||||
State: container.State,
|
||||
Status: container.Status,
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
useReactTable,
|
||||
createColumnHelper,
|
||||
getExpandedRowModel,
|
||||
ExpandedState,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import {
|
||||
@ -17,6 +18,9 @@ import {
|
||||
} from '@/components/ui/table';
|
||||
import { Empty } from 'antd';
|
||||
import React from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { LuArrowRight } from 'react-icons/lu';
|
||||
import { cn } from '@/utils/style';
|
||||
|
||||
export type { ColumnDef };
|
||||
export { createColumnHelper };
|
||||
@ -32,10 +36,18 @@ export function DataTable<TData>({
|
||||
data,
|
||||
ExpandComponent,
|
||||
}: DataTableProps<TData>) {
|
||||
const [expanded, setExpanded] = React.useState<ExpandedState>({});
|
||||
const canExpand = Boolean(ExpandComponent);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
expanded,
|
||||
},
|
||||
onExpandedChange: setExpanded,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowCanExpand: () => canExpand,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -44,6 +56,21 @@ export function DataTable<TData>({
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<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) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
@ -67,7 +94,22 @@ export function DataTable<TData>({
|
||||
key={row.id}
|
||||
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}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
@ -84,7 +126,7 @@ export function DataTable<TData>({
|
||||
{renderedRow}
|
||||
|
||||
<TableRow key={row.id + 'expand'}>
|
||||
<TableCell colSpan={table.getAllLeafColumns().length}>
|
||||
<TableCell colSpan={table.getAllLeafColumns().length + 1}>
|
||||
<ExpandComponent row={row.original} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@ -13,6 +13,8 @@ import { filesize } from 'filesize';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
import { UpDownCounter } from '../UpDownCounter';
|
||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||
import { ServerRowExpendView } from './ServerRowExpendView';
|
||||
import { FaDocker } from 'react-icons/fa';
|
||||
|
||||
const columnHelper = createColumnHelper<ServerStatusInfo>();
|
||||
|
||||
@ -45,23 +47,37 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
|
||||
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>
|
||||
),
|
||||
cell: (props) => (
|
||||
<div className="flex gap-2">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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', {
|
||||
header: t('Node Name'),
|
||||
@ -111,10 +127,15 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
|
||||
cell: (props) => (
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
{filesize(props.row.original.payload.memory_used * 1000)} /{' '}
|
||||
{filesize(props.row.original.payload.memory_used * 1024, {
|
||||
base: 2,
|
||||
})}{' '}
|
||||
/{' '}
|
||||
</div>
|
||||
<div>
|
||||
{filesize(props.row.original.payload.memory_total * 1000)}
|
||||
{filesize(props.row.original.payload.memory_total * 1024, {
|
||||
base: 2,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@ -125,10 +146,15 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
|
||||
cell: (props) => (
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
{filesize(props.row.original.payload.hdd_used * 1000 * 1000)} /{' '}
|
||||
{filesize(props.row.original.payload.hdd_used * 1024 * 1024, {
|
||||
base: 2,
|
||||
})}{' '}
|
||||
/{' '}
|
||||
</div>
|
||||
<div>
|
||||
{filesize(props.row.original.payload.hdd_total * 1000 * 1000)}
|
||||
{filesize(props.row.original.payload.hdd_total * 1024 * 1024, {
|
||||
base: 2,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@ -160,7 +186,11 @@ export const ServerList: React.FC<ServerListProps> = React.memo((props) => {
|
||||
|
||||
<ScrollArea className="flex-1 overflow-hidden">
|
||||
<ScrollBar orientation="horizontal" />
|
||||
<DataTable columns={columns} data={dataSource} />
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={dataSource}
|
||||
ExpandComponent={ServerRowExpendView}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
|
151
src/client/components/server/ServerRowExpendView.tsx
Normal file
151
src/client/components/server/ServerRowExpendView.tsx
Normal 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';
|
@ -21,4 +21,34 @@ export interface ServerStatusInfoPayload {
|
||||
network_rx: number;
|
||||
network_in: 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';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user