refactor: 拆分AnsibleManager.tsx(535→399行)提取AnsibleStatsCards+AnsiblePlaybookCard+AnsibleExecutionsTable子组件

This commit is contained in:
attacker committed 2026-06-17 06:05:03 +08:00
1 parent 7432431fc3
commit c6e4b519e8
4 files changed
+255 -163

No files matched your search

@@ -0,0 +1,73 @@
// Ansible Playbook 执行记录表格(6 列:#/Playbook/状态/开始时间/耗时/操作) // Logan.Li
import React from 'react';
import { RotateCcw, Terminal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { getStatusBadge, formatDate, type ExecutionRecord } from './ansible-helpers';
interface AnsibleExecutionsTableProps {
executions: ExecutionRecord[];
onRefresh: () => void;
onViewLogs: (exec: ExecutionRecord) => void;
}
const AnsibleExecutionsTable: React.FC<AnsibleExecutionsTableProps> = ({
executions, onRefresh, onViewLogs,
}) => {
return (
<div className="space-y-4">
<div className="flex justify-end">
<Button variant="outline" onClick={onRefresh}>
<RotateCcw className="w-4 h-4 mr-2" />
刷新
</Button>
</div>
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-muted/50">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">#</th>
<th className="px-4 py-3 text-left text-sm font-medium">Playbook</th>
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
<th className="px-4 py-3 text-left text-sm font-medium">开始时间</th>
<th className="px-4 py-3 text-left text-sm font-medium">耗时</th>
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
</tr>
</thead>
<tbody className="divide-y">
{executions.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">
暂无执行记录
</td>
</tr>
) : (
executions.map((job) => (
<tr key={job.id} className="hover:bg-muted/50">
<td className="px-4 py-3 text-sm text-muted-foreground">{job.id}</td>
<td className="px-4 py-3 text-sm">{job.task_name}</td>
<td className="px-4 py-3">{getStatusBadge(job.status)}</td>
<td className="px-4 py-3 text-sm text-muted-foreground">{formatDate(job.started_at)}</td>
<td className="px-4 py-3 text-sm">
{job.duration ? `${(job.duration / 1000).toFixed(1)}s` : '-'}
</td>
<td className="px-4 py-3">
<Button variant="ghost" size="sm" title="查看日志" onClick={() => onViewLogs(job)}>
<Terminal className="w-4 h-4" />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
};
export default AnsibleExecutionsTable;
@@ -1,12 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
FileCode, CheckCircle, XCircle, RotateCcw, Eye, RotateCcw, Plus, Search, Trash2, Loader2,
Plus, Search, Trash2, Edit, Copy, Terminal, Loader2, Play,
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
@@ -14,12 +11,15 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog'; import { useConfirm } from '@/components/ui/confirm-dialog';
import { ansibleApi, taskExecutionApi } from '@/services/api'; import { ansibleApi, taskExecutionApi } from '@/services/api';
import { import {
mapPlaybook, categories, samplePlaybook, getStatusBadge, formatDate, mapPlaybook, categories, samplePlaybook,
type Playbook, type ExecutionRecord, type Playbook, type ExecutionRecord,
} from './ansible-helpers'; } from './ansible-helpers';
import AnsibleFormDialog, { type PlaybookFormData } from './AnsibleFormDialog'; import AnsibleFormDialog, { type PlaybookFormData } from './AnsibleFormDialog';
import AnsibleDetailDialog from './AnsibleDetailDialog'; import AnsibleDetailDialog from './AnsibleDetailDialog';
import AnsibleLogsDialog from './AnsibleLogsDialog'; import AnsibleLogsDialog from './AnsibleLogsDialog';
import AnsibleStatsCards from './AnsibleStatsCards';
import AnsiblePlaybookCard from './AnsiblePlaybookCard';
import AnsibleExecutionsTable from './AnsibleExecutionsTable';
const AnsibleManager: React.FC = () => { const AnsibleManager: React.FC = () => {
const _confirm = useConfirm(); const _confirm = useConfirm();
@@ -250,60 +250,12 @@ const AnsibleManager: React.FC = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* 统计卡片 */} {/* 统计卡片 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <AnsibleStatsCards
<Card> total={playbooks.length}
<CardContent className="p-4"> running={runningCount}
<div className="flex items-center justify-between"> todaySuccess={todaySuccess}
<div> todayFailed={todayFailed}
<p className="text-sm text-muted-foreground">Playbook 总数</p> />
<p className="text-2xl font-bold">{playbooks.length}</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-lg">
<FileCode className="w-5 h-5 text-blue-500" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">执行中</p>
<p className="text-2xl font-bold text-blue-600">{runningCount}</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-lg">
<Loader2 className="w-5 h-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">今日成功</p>
<p className="text-2xl font-bold text-green-600">{todaySuccess}</p>
</div>
<div className="p-3 bg-green-500/10 rounded-lg">
<CheckCircle className="w-5 h-5 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">今日失败</p>
<p className="text-2xl font-bold text-red-600">{todayFailed}</p>
</div>
<div className="p-3 bg-red-500/10 rounded-lg">
<XCircle className="w-5 h-5 text-red-600" />
</div>
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}> <Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}>
<TabsList className="grid w-full grid-cols-2 lg:w-[360px]"> <TabsList className="grid w-full grid-cols-2 lg:w-[360px]">
@@ -389,60 +341,18 @@ const AnsibleManager: React.FC = () => {
<div className="col-span-full text-center py-8 text-muted-foreground">暂无 Playbook</div> <div className="col-span-full text-center py-8 text-muted-foreground">暂无 Playbook</div>
) : ( ) : (
filteredPlaybooks.map((playbook) => ( filteredPlaybooks.map((playbook) => (
<Card key={playbook.id} className="hover:shadow-md transition-shadow"> <AnsiblePlaybookCard
<CardHeader className="pb-3"> key={playbook.id}
<div className="flex items-start justify-between gap-2"> playbook={playbook}
<div className="flex items-center gap-2 min-w-0"> selected={selectedIds.includes(playbook.id)}
<Checkbox executing={executing}
checked={selectedIds.includes(playbook.id)} onToggleSelect={toggleSelect}
onCheckedChange={() => toggleSelect(playbook.id)} onView={(pb) => { setSelectedPlaybook(pb); setDetailOpen(true); }}
className="shrink-0" onExecute={handleExecute}
onEdit={openEdit}
onClone={handleClone}
onDelete={handleDelete}
/> />
<FileCode className="w-5 h-5 text-blue-500 shrink-0" />
<CardTitle className="text-base truncate">{playbook.name}</CardTitle>
</div>
<Badge variant="outline" className="shrink-0">
{categories.find(c => c.value === playbook.category)?.label || playbook.category}
</Badge>
</div>
<CardDescription className="line-clamp-2">{playbook.description || '暂无描述'}</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{playbook.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{playbook.tags.map((tag, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">{tag}</Badge>
))}
</div>
)}
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3">
<span>执行 {playbook.run_count} 次</span>
<span>超时 {playbook.timeout}s</span>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" title="查看内容"
onClick={() => { setSelectedPlaybook(playbook); setDetailOpen(true); }}>
<Eye className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" title="执行"
disabled={executing}
onClick={() => handleExecute(playbook)}>
{executing
? <Loader2 className="w-4 h-4 animate-spin text-blue-600" />
: <Play className="w-4 h-4 text-green-600" />}
</Button>
<Button variant="ghost" size="sm" title="编辑" onClick={() => openEdit(playbook)}>
<Edit className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" title="复制" onClick={() => handleClone(playbook)}>
<Copy className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" title="删除" onClick={() => handleDelete(playbook)}>
<Trash2 className="w-4 h-4 text-red-600" />
</Button>
</div>
</CardContent>
</Card>
)) ))
)} )}
</div> </div>
@@ -450,57 +360,11 @@ const AnsibleManager: React.FC = () => {
{/* ============ 执行记录 ============ */} {/* ============ 执行记录 ============ */}
<TabsContent value="jobs" className="space-y-4"> <TabsContent value="jobs" className="space-y-4">
<div className="flex justify-end"> <AnsibleExecutionsTable
<Button variant="outline" onClick={fetchExecutions}> executions={executions}
<RotateCcw className="w-4 h-4 mr-2" /> onRefresh={fetchExecutions}
刷新 onViewLogs={(exec) => { setSelectedExec(exec); setLogsOpen(true); }}
</Button> />
</div>
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-muted/50">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">#</th>
<th className="px-4 py-3 text-left text-sm font-medium">Playbook</th>
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
<th className="px-4 py-3 text-left text-sm font-medium">开始时间</th>
<th className="px-4 py-3 text-left text-sm font-medium">耗时</th>
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
</tr>
</thead>
<tbody className="divide-y">
{executions.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">
暂无执行记录
</td>
</tr>
) : (
executions.map((job) => (
<tr key={job.id} className="hover:bg-muted/50">
<td className="px-4 py-3 text-sm text-muted-foreground">{job.id}</td>
<td className="px-4 py-3 text-sm">{job.task_name}</td>
<td className="px-4 py-3">{getStatusBadge(job.status)}</td>
<td className="px-4 py-3 text-sm text-muted-foreground">{formatDate(job.started_at)}</td>
<td className="px-4 py-3 text-sm">
{job.duration ? `${(job.duration / 1000).toFixed(1)}s` : '-'}
</td>
<td className="px-4 py-3">
<Button variant="ghost" size="sm" title="查看日志"
onClick={() => { setSelectedExec(job); setLogsOpen(true); }}>
<Terminal className="w-4 h-4" />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
@@ -0,0 +1,81 @@
// Ansible Playbook 卡片(复选框 + 名称 + 分类徽章 + 描述 + 标签 + 元信息 + 5 行操作按钮) // Logan.Li
import React from 'react';
import { FileCode, Eye, Edit, Copy, Trash2, Play, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { categories, type Playbook } from './ansible-helpers';
interface AnsiblePlaybookCardProps {
playbook: Playbook;
selected: boolean;
executing: boolean;
onToggleSelect: (id: number) => void;
onView: (pb: Playbook) => void;
onExecute: (pb: Playbook) => void;
onEdit: (pb: Playbook) => void;
onClone: (pb: Playbook) => void;
onDelete: (pb: Playbook) => void;
}
const AnsiblePlaybookCard: React.FC<AnsiblePlaybookCardProps> = ({
playbook, selected, executing,
onToggleSelect, onView, onExecute, onEdit, onClone, onDelete,
}) => {
return (
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<Checkbox
checked={selected}
onCheckedChange={() => onToggleSelect(playbook.id)}
className="shrink-0"
/>
<FileCode className="w-5 h-5 text-blue-500 shrink-0" />
<CardTitle className="text-base truncate">{playbook.name}</CardTitle>
</div>
<Badge variant="outline" className="shrink-0">
{categories.find(c => c.value === playbook.category)?.label || playbook.category}
</Badge>
</div>
<CardDescription className="line-clamp-2">{playbook.description || '暂无描述'}</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{playbook.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{playbook.tags.map((tag, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">{tag}</Badge>
))}
</div>
)}
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3">
<span>执行 {playbook.run_count} 次</span>
<span>超时 {playbook.timeout}s</span>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" title="查看内容" onClick={() => onView(playbook)}>
<Eye className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" title="执行" disabled={executing} onClick={() => onExecute(playbook)}>
{executing
? <Loader2 className="w-4 h-4 animate-spin text-blue-600" />
: <Play className="w-4 h-4 text-green-600" />}
</Button>
<Button variant="ghost" size="sm" title="编辑" onClick={() => onEdit(playbook)}>
<Edit className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" title="复制" onClick={() => onClone(playbook)}>
<Copy className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" title="删除" onClick={() => onDelete(playbook)}>
<Trash2 className="w-4 h-4 text-red-600" />
</Button>
</div>
</CardContent>
</Card>
);
};
export default AnsiblePlaybookCard;
@@ -0,0 +1,74 @@
// Ansible Playbook 统计卡片(4 张:总数/执行中/今日成功/今日失败) // Logan.Li
import React from 'react';
import { FileCode, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
interface AnsibleStatsCardsProps {
total: number;
running: number;
todaySuccess: number;
todayFailed: number;
}
const AnsibleStatsCards: React.FC<AnsibleStatsCardsProps> = ({
total, running, todaySuccess, todayFailed,
}) => {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Playbook 总数</p>
<p className="text-2xl font-bold">{total}</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-lg">
<FileCode className="w-5 h-5 text-blue-500" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">执行中</p>
<p className="text-2xl font-bold text-blue-600">{running}</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-lg">
<Loader2 className="w-5 h-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">今日成功</p>
<p className="text-2xl font-bold text-green-600">{todaySuccess}</p>
</div>
<div className="p-3 bg-green-500/10 rounded-lg">
<CheckCircle className="w-5 h-5 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">今日失败</p>
<p className="text-2xl font-bold text-red-600">{todayFailed}</p>
</div>
<div className="p-3 bg-red-500/10 rounded-lg">
<XCircle className="w-5 h-5 text-red-600" />
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export default AnsibleStatsCards;