重构消息通知弹窗
This commit is contained in:
parent
0a10f1def1
commit
69fc7fe163
@ -197,104 +197,96 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 最近部署记录 */}
|
{/* 最近部署记录 - 固定显示2条槽位 */}
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground mb-1">
|
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground mb-1">
|
||||||
<History className="h-3 w-3" />
|
<History className="h-3 w-3" />
|
||||||
<span>最近记录</span>
|
<span>最近记录</span>
|
||||||
</div>
|
</div>
|
||||||
{app.recentDeployRecords && app.recentDeployRecords.length > 0 ? (
|
{/* 始终显示2条记录槽位,确保卡片高度一致 */}
|
||||||
<>
|
{Array.from({ length: 2 }).map((_, index) => {
|
||||||
{/* 显示实际记录(最多2条) */}
|
const record = app.recentDeployRecords?.[index];
|
||||||
{app.recentDeployRecords.slice(0, 2).map((record) => {
|
|
||||||
const { icon: StatusIcon, color } = getStatusIcon(record.status);
|
if (record) {
|
||||||
return (
|
// 显示实际记录
|
||||||
<div key={record.id} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1">
|
const { icon: StatusIcon, color } = getStatusIcon(record.status);
|
||||||
{/* 第一行:状态、编号、部署人 */}
|
return (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div key={record.id} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1 h-[60px]">
|
||||||
|
{/* 第一行:状态、编号、部署人 */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<StatusIcon className={cn("h-3.5 w-3.5 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
|
||||||
|
<span className={cn("text-[10px] font-semibold", color)}>{getStatusText(record.status)}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeployRecordClick(record)}
|
||||||
|
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-background/50 hover:bg-background/80 transition-colors cursor-pointer"
|
||||||
|
title="点击查看工作流详情"
|
||||||
|
>
|
||||||
|
<Hash className="h-2.5 w-2.5 text-muted-foreground" />
|
||||||
|
<span className="text-[10px] text-muted-foreground font-mono hover:text-primary transition-colors">
|
||||||
|
{record.id}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{record.deployBy && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<StatusIcon className={cn("h-3.5 w-3.5 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
|
<User className="h-2.5 w-2.5 text-muted-foreground" />
|
||||||
<span className={cn("text-[10px] font-semibold", color)}>{getStatusText(record.status)}</span>
|
<span className="text-[10px] text-muted-foreground">{record.deployBy}</span>
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeployRecordClick(record)}
|
|
||||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-background/50 hover:bg-background/80 transition-colors cursor-pointer"
|
|
||||||
title="点击查看工作流详情"
|
|
||||||
>
|
|
||||||
<Hash className="h-2.5 w-2.5 text-muted-foreground" />
|
|
||||||
<span className="text-[10px] text-muted-foreground font-mono hover:text-primary transition-colors">
|
|
||||||
{record.id}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{record.deployBy && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<User className="h-2.5 w-2.5 text-muted-foreground" />
|
|
||||||
<span className="text-[10px] text-muted-foreground">{record.deployBy}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 第二行:时间信息(一行显示) */}
|
|
||||||
{(record.startTime || record.endTime || record.duration) && (
|
|
||||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground flex-nowrap overflow-hidden">
|
|
||||||
<Clock className="h-2.5 w-2.5 shrink-0" />
|
|
||||||
<div className="flex items-center gap-1 flex-nowrap min-w-0">
|
|
||||||
{record.startTime && (
|
|
||||||
<span className="whitespace-nowrap">{formatTime(record.startTime)}</span>
|
|
||||||
)}
|
|
||||||
{record.endTime && (
|
|
||||||
<>
|
|
||||||
<span className="px-0.5 shrink-0">→</span>
|
|
||||||
<span className="whitespace-nowrap">{formatTime(record.endTime)}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{record.duration && (
|
|
||||||
<>
|
|
||||||
<span className="px-0.5 text-muted-foreground/50 shrink-0">•</span>
|
|
||||||
<span className="font-medium whitespace-nowrap">{formatDuration(record.duration)}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 第三行:备注 */}
|
|
||||||
{record.deployRemark && (
|
|
||||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground pt-0.5 border-t border-muted/30">
|
|
||||||
<FileText className="h-2.5 w-2.5 shrink-0" />
|
|
||||||
<span className="truncate">{record.deployRemark}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
{/* 第二行:时间信息(一行显示) */}
|
||||||
{/* 如果记录少于2条,用骨架屏补充 */}
|
{(record.startTime || record.endTime || record.duration) && (
|
||||||
{app.recentDeployRecords.length < 2 && (
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground flex-nowrap overflow-hidden">
|
||||||
Array.from({ length: 2 - app.recentDeployRecords.length }).map((_, index) => (
|
<Clock className="h-2.5 w-2.5 shrink-0" />
|
||||||
<div key={`skeleton-${index}`} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1">
|
<div className="flex items-center gap-1 flex-nowrap min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
{record.startTime && (
|
||||||
<Skeleton className="h-3.5 w-16" />
|
<span className="whitespace-nowrap">{formatTime(record.startTime)}</span>
|
||||||
<Skeleton className="h-4 w-12" />
|
)}
|
||||||
<Skeleton className="h-3.5 w-12 ml-auto" />
|
{record.endTime && (
|
||||||
|
<>
|
||||||
|
<span className="px-0.5 shrink-0">→</span>
|
||||||
|
<span className="whitespace-nowrap">{formatTime(record.endTime)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{record.duration && (
|
||||||
|
<>
|
||||||
|
<span className="px-0.5 text-muted-foreground/50 shrink-0">•</span>
|
||||||
|
<span className="font-medium whitespace-nowrap">{formatDuration(record.duration)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-3 w-32" />
|
)}
|
||||||
</div>
|
|
||||||
))
|
{/* 第三行:备注 */}
|
||||||
)}
|
{record.deployRemark && (
|
||||||
</>
|
<div className="flex items-center gap-1 text-[10px] text-muted-foreground pt-0.5 border-t border-muted/30">
|
||||||
) : (
|
<FileText className="h-2.5 w-2.5 shrink-0" />
|
||||||
/* 如果没有记录,显示2条骨架记录 */
|
<span className="truncate">{record.deployRemark}</span>
|
||||||
Array.from({ length: 2 }).map((_, index) => (
|
</div>
|
||||||
<div key={`skeleton-${index}`} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1">
|
)}
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skeleton className="h-3.5 w-16" />
|
|
||||||
<Skeleton className="h-4 w-12" />
|
|
||||||
<Skeleton className="h-3.5 w-12 ml-auto" />
|
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-3 w-32" />
|
);
|
||||||
</div>
|
} else {
|
||||||
))
|
// 显示骨架屏占位 - 3条线模拟实际记录的3行
|
||||||
)}
|
return (
|
||||||
|
<div key={`skeleton-${index}`} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1 h-[60px]">
|
||||||
|
{/* 第一行:状态、编号、部署人 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Skeleton className="h-3.5 w-16" />
|
||||||
|
<Skeleton className="h-4 w-12" />
|
||||||
|
<Skeleton className="h-3.5 w-12 ml-auto" />
|
||||||
|
</div>
|
||||||
|
{/* 第二行:时间信息 */}
|
||||||
|
<Skeleton className="h-3 w-32" />
|
||||||
|
{/* 第三行:备注 */}
|
||||||
|
<Skeleton className="h-3 w-24" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
372
frontend/src/pages/Resource/External/List/index.tsx
vendored
372
frontend/src/pages/Resource/External/List/index.tsx
vendored
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useMemo, useRef } from 'react';
|
||||||
import { PageContainer } from '@/components/ui/page-container';
|
import { PageContainer } from '@/components/ui/page-container';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -6,21 +6,11 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/table';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
|
||||||
import { Plus, Edit, Trash2, Link, Server, Activity, Database } from 'lucide-react';
|
import { Plus, Edit, Trash2, Link, Server, Activity, Database } from 'lucide-react';
|
||||||
import EditDialog from './components/EditDialog';
|
import EditDialog from './components/EditDialog';
|
||||||
import DeleteDialog from './components/DeleteDialog';
|
import DeleteDialog from './components/DeleteDialog';
|
||||||
@ -31,13 +21,7 @@ const DEFAULT_PAGE_SIZE = 10;
|
|||||||
|
|
||||||
const ExternalPage: React.FC = () => {
|
const ExternalPage: React.FC = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [list, setList] = useState<ExternalSystemResponse[]>([]);
|
const tableRef = useRef<PaginatedTableRef<ExternalSystemResponse>>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [query, setQuery] = useState<ExternalSystemQuery>({
|
|
||||||
pageNum: 0,
|
|
||||||
pageSize: DEFAULT_PAGE_SIZE,
|
|
||||||
});
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
|
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
|
||||||
|
|
||||||
// 对话框状态
|
// 对话框状态
|
||||||
@ -45,43 +29,21 @@ const ExternalPage: React.FC = () => {
|
|||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [currentRecord, setCurrentRecord] = useState<ExternalSystemResponse>();
|
const [currentRecord, setCurrentRecord] = useState<ExternalSystemResponse>();
|
||||||
|
|
||||||
// 加载数据
|
// 包装 fetchFn
|
||||||
const loadData = async () => {
|
const fetchData = async (query: ExternalSystemQuery) => {
|
||||||
setLoading(true);
|
const response = await service.getExternalSystemsPage(query);
|
||||||
try {
|
// 计算统计数据
|
||||||
const response = await service.getExternalSystemsPage(query);
|
const all = response.content || [];
|
||||||
if (response) {
|
setStats({
|
||||||
setList(response.content || []);
|
total: response.totalElements || 0,
|
||||||
setTotal(response.totalElements || 0);
|
enabled: all.filter((item) => item.enabled).length,
|
||||||
// 计算统计数据
|
disabled: all.filter((item) => !item.enabled).length,
|
||||||
const all = response.content || [];
|
});
|
||||||
setStats({
|
return response;
|
||||||
total: all.length,
|
|
||||||
enabled: all.filter((item) => item.enabled).length,
|
|
||||||
disabled: all.filter((item) => !item.enabled).length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({ title: '加载失败', description: '无法加载外部系统列表', variant: 'destructive' });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSuccess = () => {
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
}, [query.pageNum, query.pageSize]);
|
|
||||||
|
|
||||||
// 搜索
|
|
||||||
const handleSearch = () => {
|
|
||||||
setQuery({ ...query, pageNum: 0 });
|
|
||||||
loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置
|
|
||||||
const handleReset = () => {
|
|
||||||
setQuery({ pageNum: 0, pageSize: DEFAULT_PAGE_SIZE });
|
|
||||||
loadData();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 测试连接
|
// 测试连接
|
||||||
@ -94,7 +56,7 @@ const ExternalPage: React.FC = () => {
|
|||||||
variant: success ? 'default' : 'destructive',
|
variant: success ? 'default' : 'destructive',
|
||||||
});
|
});
|
||||||
// 刷新列表以更新最后连接时间等信息
|
// 刷新列表以更新最后连接时间等信息
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({ title: '测试失败', description: '无法测试连接', variant: 'destructive' });
|
toast({ title: '测试失败', description: '无法测试连接', variant: 'destructive' });
|
||||||
}
|
}
|
||||||
@ -105,7 +67,7 @@ const ExternalPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await service.updateStatus(id, enabled);
|
await service.updateStatus(id, enabled);
|
||||||
toast({ title: '更新成功', description: `系统状态已${enabled ? '启用' : '禁用'}` });
|
toast({ title: '更新成功', description: `系统状态已${enabled ? '启用' : '禁用'}` });
|
||||||
loadData();
|
tableRef.current?.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({ title: '更新失败', description: '无法更新系统状态', variant: 'destructive' });
|
toast({ title: '更新失败', description: '无法更新系统状态', variant: 'destructive' });
|
||||||
}
|
}
|
||||||
@ -131,6 +93,143 @@ const ExternalPage: React.FC = () => {
|
|||||||
return authTypeMap[authType];
|
return authTypeMap[authType];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 搜索字段定义
|
||||||
|
const searchFields: SearchFieldDef[] = useMemo(() => [
|
||||||
|
{ key: 'name', type: 'input', placeholder: '系统名称', width: 'w-[200px]' },
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '系统类型',
|
||||||
|
width: 'w-[200px]',
|
||||||
|
options: [
|
||||||
|
{ label: 'Jenkins', value: 'JENKINS' },
|
||||||
|
{ label: 'Git', value: 'GIT' },
|
||||||
|
{ label: '禅道', value: 'ZENTAO' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '状态',
|
||||||
|
width: 'w-[200px]',
|
||||||
|
options: [
|
||||||
|
{ label: '已启用', value: 'true' },
|
||||||
|
{ label: '已禁用', value: 'false' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
|
// 列定义
|
||||||
|
const columns: ColumnDef<ExternalSystemResponse>[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: '80px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: '系统名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: '150px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
title: '系统类型',
|
||||||
|
width: '100px',
|
||||||
|
render: (_, item) => {
|
||||||
|
const typeInfo = getSystemTypeLabel(item.type);
|
||||||
|
return <Badge variant={typeInfo.variant}>{typeInfo.label}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'url',
|
||||||
|
title: '系统地址',
|
||||||
|
width: '250px',
|
||||||
|
render: (_, item) => (
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:underline flex items-center gap-1"
|
||||||
|
onClick={() => {
|
||||||
|
setTimeout(() => tableRef.current?.refresh(), 100);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link className="h-3 w-3" />
|
||||||
|
{item.url}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authType',
|
||||||
|
title: '认证方式',
|
||||||
|
width: '120px',
|
||||||
|
render: (_, item) => getAuthTypeLabel(item.authType),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lastConnectTime',
|
||||||
|
title: '最后连接时间',
|
||||||
|
dataIndex: 'lastConnectTime',
|
||||||
|
width: '150px',
|
||||||
|
render: (value) => value || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
title: '状态',
|
||||||
|
width: '100px',
|
||||||
|
render: (_, item) => (
|
||||||
|
<Switch
|
||||||
|
checked={item.enabled}
|
||||||
|
onCheckedChange={(checked) => handleStatusChange(item.id, checked)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
title: '操作',
|
||||||
|
width: '250px',
|
||||||
|
sticky: true,
|
||||||
|
render: (_, item) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentRecord(item);
|
||||||
|
setEditDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-3.5 w-3.5 mr-1" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2"
|
||||||
|
onClick={() => handleTestConnection(item.id)}
|
||||||
|
>
|
||||||
|
<Link className="h-3.5 w-3.5 mr-1" />
|
||||||
|
测试连接
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2 text-destructive hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentRecord(item);
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
@ -179,160 +278,21 @@ const ExternalPage: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索过滤 */}
|
|
||||||
<Card>
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Input
|
|
||||||
placeholder="系统名称"
|
|
||||||
value={query.name || ''}
|
|
||||||
onChange={(e) => setQuery({ ...query, name: e.target.value })}
|
|
||||||
className="max-w-[200px]"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={query.type}
|
|
||||||
onValueChange={(value) => setQuery({ ...query, type: value as SystemType })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="max-w-[200px]">
|
|
||||||
<SelectValue placeholder="系统类型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="JENKINS">Jenkins</SelectItem>
|
|
||||||
<SelectItem value="GIT">Git</SelectItem>
|
|
||||||
<SelectItem value="ZENTAO">禅道</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
value={query.enabled?.toString()}
|
|
||||||
onValueChange={(value) => setQuery({ ...query, enabled: value === 'true' })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="max-w-[200px]">
|
|
||||||
<SelectValue placeholder="状态" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="true">已启用</SelectItem>
|
|
||||||
<SelectItem value="false">已禁用</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button variant="outline" onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSearch}>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 数据表格 */}
|
{/* 数据表格 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>系统列表</CardTitle>
|
<CardTitle>系统列表</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="rounded-md border">
|
<PaginatedTable<ExternalSystemResponse, ExternalSystemQuery>
|
||||||
<Table minWidth="1120px">
|
ref={tableRef}
|
||||||
<TableHeader>
|
fetchFn={fetchData}
|
||||||
<TableRow>
|
columns={columns}
|
||||||
<TableHead width="150px">系统名称</TableHead>
|
searchFields={searchFields}
|
||||||
<TableHead width="100px">系统类型</TableHead>
|
rowKey="id"
|
||||||
<TableHead width="250px">系统地址</TableHead>
|
minWidth="1120px"
|
||||||
<TableHead width="120px">认证方式</TableHead>
|
pageSize={DEFAULT_PAGE_SIZE}
|
||||||
<TableHead width="150px">最后连接时间</TableHead>
|
/>
|
||||||
<TableHead width="100px">状态</TableHead>
|
|
||||||
<TableHead width="250px" sticky>操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{list.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="h-24 text-center">
|
|
||||||
暂无数据
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
list.map((item) => (
|
|
||||||
<TableRow key={item.id}>
|
|
||||||
<TableCell width="150px" className="font-medium">{item.name}</TableCell>
|
|
||||||
<TableCell width="100px">
|
|
||||||
<Badge variant={getSystemTypeLabel(item.type).variant}>
|
|
||||||
{getSystemTypeLabel(item.type).label}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell width="250px">
|
|
||||||
<a
|
|
||||||
href={item.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:underline flex items-center gap-1"
|
|
||||||
onClick={() => {
|
|
||||||
// 点击链接后刷新列表
|
|
||||||
setTimeout(() => loadData(), 100);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link className="h-3 w-3" />
|
|
||||||
{item.url}
|
|
||||||
</a>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell width="120px">{getAuthTypeLabel(item.authType)}</TableCell>
|
|
||||||
<TableCell width="150px" className="text-muted-foreground">
|
|
||||||
{item.lastConnectTime || '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell width="100px">
|
|
||||||
<Switch
|
|
||||||
checked={item.enabled}
|
|
||||||
onCheckedChange={(checked) => handleStatusChange(item.id, checked)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell width="250px" sticky>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentRecord(item);
|
|
||||||
setEditDialogOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4 mr-1" />
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleTestConnection(item.id)}
|
|
||||||
>
|
|
||||||
<Link className="h-4 w-4 mr-1" />
|
|
||||||
测试连接
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentRecord(item);
|
|
||||||
setDeleteDialogOpen(true);
|
|
||||||
}}
|
|
||||||
className="text-destructive hover:text-destructive"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<div className="flex justify-end border-t border-border bg-muted/40">
|
|
||||||
<DataTablePagination
|
|
||||||
pageIndex={query.pageNum || 0}
|
|
||||||
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
|
||||||
pageCount={Math.ceil(total / (query.pageSize || DEFAULT_PAGE_SIZE))}
|
|
||||||
onPageChange={(page) => setQuery({ ...query, pageNum: page })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -341,13 +301,13 @@ const ExternalPage: React.FC = () => {
|
|||||||
open={editDialogOpen}
|
open={editDialogOpen}
|
||||||
record={currentRecord}
|
record={currentRecord}
|
||||||
onOpenChange={setEditDialogOpen}
|
onOpenChange={setEditDialogOpen}
|
||||||
onSuccess={loadData}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
open={deleteDialogOpen}
|
open={deleteDialogOpen}
|
||||||
record={currentRecord}
|
record={currentRecord}
|
||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
onSuccess={loadData}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user