增加团队管理页面
This commit is contained in:
parent
97e6b30e65
commit
68bc9757e4
195
frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx
Normal file
195
frontend/src/pages/Deploy/Server/List/components/ServerTable.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
TestTube,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Activity,
|
||||
AlertCircle,
|
||||
HelpCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { ServerResponse } from '../types';
|
||||
import { ServerStatusLabels, OsTypeLabels } from '../types';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
interface ServerTableProps {
|
||||
servers: ServerResponse[];
|
||||
loading: boolean;
|
||||
onTest: (server: ServerResponse) => void;
|
||||
onEdit: (server: ServerResponse) => void;
|
||||
onDelete: (server: ServerResponse) => void;
|
||||
isTesting?: (serverId: number) => boolean;
|
||||
getOsIcon: (osType?: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
servers,
|
||||
loading,
|
||||
onTest,
|
||||
onEdit,
|
||||
onDelete,
|
||||
isTesting,
|
||||
getOsIcon,
|
||||
}) => {
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-';
|
||||
return dayjs(time).fromNow();
|
||||
};
|
||||
|
||||
const getStatusIcon = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'ONLINE':
|
||||
return <Activity className="h-4 w-4 text-green-600 dark:text-green-400" />;
|
||||
case 'OFFLINE':
|
||||
return <AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />;
|
||||
default:
|
||||
return <HelpCircle className="h-4 w-4 text-gray-600 dark:text-gray-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table minWidth="1200px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="180px">服务器名称</TableHead>
|
||||
<TableHead width="150px">IP地址</TableHead>
|
||||
<TableHead width="120px">状态</TableHead>
|
||||
<TableHead width="100px">操作系统</TableHead>
|
||||
<TableHead width="80px">CPU核心</TableHead>
|
||||
<TableHead width="100px">内存(GB)</TableHead>
|
||||
<TableHead width="100px">磁盘(GB)</TableHead>
|
||||
<TableHead width="120px">SSH认证</TableHead>
|
||||
<TableHead width="140px">分类</TableHead>
|
||||
<TableHead width="150px">最后连接</TableHead>
|
||||
<TableHead sticky width="140px" className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{servers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="text-center py-8">
|
||||
<span className="text-muted-foreground">暂无服务器数据</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
servers.map((server) => (
|
||||
<TableRow key={server.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground">{server.serverName}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-mono text-sm">{server.hostIp}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(server.status)}
|
||||
<Badge className={
|
||||
server.status === 'ONLINE' ? 'bg-green-500/10 text-green-700 border-green-500/30 dark:text-green-400' :
|
||||
server.status === 'OFFLINE' ? 'bg-red-500/10 text-red-700 border-red-500/30 dark:text-red-400' :
|
||||
'bg-gray-500/10 text-gray-700 border-gray-500/30 dark:text-gray-400'
|
||||
}>
|
||||
{ServerStatusLabels[server.status]?.label || server.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{getOsIcon(server.osType)}
|
||||
<span className="text-sm">{OsTypeLabels[server.osType]?.label || server.osType}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">{server.cpuCores || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">{server.memorySize || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">{server.diskSize || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.authType === 'PASSWORD' ? '密码' : '密钥'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{server.categoryName || '未分类'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-muted-foreground">{formatTime(server.lastConnectTime)}</span>
|
||||
</TableCell>
|
||||
<TableCell sticky className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onTest(server)}
|
||||
disabled={isTesting?.(server.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{isTesting?.(server.id) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<TestTube className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>测试连接</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(server)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>编辑</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(server)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>删除</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@ -11,6 +11,8 @@ import {
|
||||
Filter,
|
||||
Search,
|
||||
RotateCcw,
|
||||
Grid3x3,
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@ -39,16 +41,25 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types';
|
||||
import { ServerStatusLabels, OsTypeLabels } from './types';
|
||||
import { getServers, getServerCategories, deleteServer, testServerConnection } from './service';
|
||||
import { CategoryManageDialog } from './components/CategoryManageDialog';
|
||||
import { ServerEditDialog } from './components/ServerEditDialog';
|
||||
import { ServerCard } from './components/ServerCard';
|
||||
import { ServerTable } from './components/ServerTable';
|
||||
|
||||
const ServerList: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
|
||||
// 视图模式
|
||||
type ViewMode = 'grid' | 'table';
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
|
||||
// 搜索关键字
|
||||
const [searchKeyword, setSearchKeyword] = useState<string>('');
|
||||
|
||||
// 状态管理
|
||||
const [categories, setCategories] = useState<ServerCategoryResponse[]>([]);
|
||||
const [servers, setServers] = useState<ServerResponse[]>([]);
|
||||
@ -253,9 +264,21 @@ const ServerList: React.FC = () => {
|
||||
setSelectedCategoryId(undefined);
|
||||
setSelectedStatus(undefined);
|
||||
setSelectedOsType(undefined);
|
||||
setSearchKeyword('');
|
||||
setPageIndex(0); // 重置到第一页
|
||||
};
|
||||
|
||||
// 客户端搜索过滤(在加载数据后进行)
|
||||
const filteredServers = servers.filter(server => {
|
||||
if (!searchKeyword.trim()) return true;
|
||||
const keyword = searchKeyword.toLowerCase();
|
||||
return (
|
||||
server.serverName.toLowerCase().includes(keyword) ||
|
||||
server.hostIp.includes(keyword) ||
|
||||
(server.hostname && server.hostname.toLowerCase().includes(keyword))
|
||||
);
|
||||
});
|
||||
|
||||
// 监听筛选条件和分页变化
|
||||
useEffect(() => {
|
||||
loadServers();
|
||||
@ -266,64 +289,14 @@ const ServerList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="h-screen flex flex-col overflow-hidden">
|
||||
<div className="flex-shrink-0 space-y-6 p-6">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-900">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总服务器</CardTitle>
|
||||
<Server className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">{stats.total}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">所有服务器</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-900">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">在线</CardTitle>
|
||||
<Activity className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">{stats.online}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">运行中</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-950/20 dark:to-rose-950/20 border-red-200 dark:border-red-900">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">离线</CardTitle>
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.offline}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">不可用</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-gray-50 to-slate-50 dark:from-gray-950/20 dark:to-slate-950/20 border-gray-200 dark:border-gray-800">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">未知</CardTitle>
|
||||
<HelpCircle className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-gray-600 dark:text-gray-400">{stats.unknown}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">状态未知</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 服务器管理卡片 - 可滚动区域 */}
|
||||
<div className="flex-1 overflow-hidden px-6 pb-6">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<div className="h-screen flex flex-col overflow-hidden bg-background">
|
||||
{/* 顶部区域 - 统计和标题 */}
|
||||
<div className="flex-shrink-0 space-y-4 p-6">
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-2xl">服务器管理</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
管理和监控服务器资源,支持SSH连接和分类管理
|
||||
</CardDescription>
|
||||
<h1 className="text-3xl font-bold">服务器管理</h1>
|
||||
<p className="text-muted-foreground mt-1">管理和监控服务器资源,支持SSH连接和分类管理</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
|
||||
@ -342,12 +315,94 @@ const ServerList: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Separator className="flex-shrink-0" />
|
||||
<ScrollArea className="flex-1">
|
||||
<CardContent className="pt-6">
|
||||
|
||||
{/* 统计卡片 - 更紧凑的布局 */}
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 border-blue-200 dark:border-blue-900">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground font-medium">总服务器</p>
|
||||
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">{stats.total}</p>
|
||||
</div>
|
||||
<Server className="h-8 w-8 text-blue-600 dark:text-blue-400 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-900">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground font-medium">在线</p>
|
||||
<p className="text-2xl font-bold text-green-600 dark:text-green-400 mt-1">{stats.online}</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-green-600 dark:text-green-400 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-950/20 dark:to-rose-950/20 border-red-200 dark:border-red-900">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground font-medium">离线</p>
|
||||
<p className="text-2xl font-bold text-red-600 dark:text-red-400 mt-1">{stats.offline}</p>
|
||||
</div>
|
||||
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gradient-to-br from-gray-50 to-slate-50 dark:from-gray-950/20 dark:to-slate-950/20 border-gray-200 dark:border-gray-800">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground font-medium">未知</p>
|
||||
<p className="text-2xl font-bold text-gray-600 dark:text-gray-400 mt-1">{stats.unknown}</p>
|
||||
</div>
|
||||
<HelpCircle className="h-8 w-8 text-gray-600 dark:text-gray-400 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 - 可滚动 */}
|
||||
<div className="flex-1 overflow-hidden px-6 pb-6">
|
||||
<Card className="h-full flex flex-col">
|
||||
{/* 搜索和视图切换栏 */}
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<div className="space-y-4">
|
||||
{/* 搜索框 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="搜索服务器名称、IP或主机名..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-1">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('grid')}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'table' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('table')}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className="flex items-center gap-3 mb-6 p-4 bg-muted/30 rounded-lg">
|
||||
<div className="flex items-center gap-3 p-4 bg-muted/30 rounded-lg flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">筛选:</span>
|
||||
@ -428,8 +483,14 @@ const ServerList: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* 服务器卡片网格 */}
|
||||
<Separator className="flex-shrink-0" />
|
||||
|
||||
{/* 内容区域 */}
|
||||
<ScrollArea className="flex-1">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
{/* 加载状态 - 骨架屏 */}
|
||||
{loading && servers.length === 0 && (
|
||||
@ -459,18 +520,20 @@ const ServerList: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{!loading && servers.length === 0 && (
|
||||
{!loading && filteredServers.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-muted-foreground">
|
||||
<Server className="h-16 w-16 mb-4 opacity-20" />
|
||||
<p className="text-lg font-medium">暂无服务器</p>
|
||||
<p className="text-sm mt-2">点击上方"新增服务器"按钮添加服务器</p>
|
||||
<p className="text-sm mt-2">
|
||||
{servers.length === 0 ? '点击上方"新增服务器"按钮添加服务器' : '没有匹配搜索条件的服务器'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 服务器列表 */}
|
||||
{!loading && servers.length > 0 && (
|
||||
{/* 网格视图 */}
|
||||
{!loading && filteredServers.length > 0 && viewMode === 'grid' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{servers.map((server) => (
|
||||
{filteredServers.map((server) => (
|
||||
<ServerCard
|
||||
key={server.id}
|
||||
server={server}
|
||||
@ -484,11 +547,27 @@ const ServerList: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 表格视图 */}
|
||||
{!loading && filteredServers.length > 0 && viewMode === 'table' && (
|
||||
<ServerTable
|
||||
servers={filteredServers}
|
||||
loading={loading}
|
||||
onTest={handleTestConnection}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
isTesting={(serverId) => testingServerId === serverId}
|
||||
getOsIcon={getOsIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 分页 */}
|
||||
{!loading && servers.length > 0 && (
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
共 {totalElements} 条记录,第 {pageIndex + 1} / {Math.ceil(totalElements / pageSize)} 页
|
||||
{searchKeyword && filteredServers.length > 0 && (
|
||||
<span>,搜索结果 {filteredServers.length} 条</span>
|
||||
)}
|
||||
</div>
|
||||
<DataTablePagination
|
||||
pageIndex={pageIndex}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user