feat: add server docker expend view
This commit is contained in:
parent
1dfa24df1b
commit
c6433f310b
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
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_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';
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user