refactor: 拆分AnsibleManager.tsx(535→399行)提取AnsibleStatsCards+AnsiblePlaybookCard+AnsibleExecutionsTable子组件
This commit is contained in:
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 {
|
||||
FileCode, CheckCircle, XCircle, RotateCcw, Eye,
|
||||
Plus, Search, Trash2, Edit, Copy, Terminal, Loader2, Play,
|
||||
RotateCcw, Plus, Search, Trash2, Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -14,12 +11,15 @@ import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { ansibleApi, taskExecutionApi } from '@/services/api';
|
||||
import {
|
||||
mapPlaybook, categories, samplePlaybook, getStatusBadge, formatDate,
|
||||
mapPlaybook, categories, samplePlaybook,
|
||||
type Playbook, type ExecutionRecord,
|
||||
} from './ansible-helpers';
|
||||
import AnsibleFormDialog, { type PlaybookFormData } from './AnsibleFormDialog';
|
||||
import AnsibleDetailDialog from './AnsibleDetailDialog';
|
||||
import AnsibleLogsDialog from './AnsibleLogsDialog';
|
||||
import AnsibleStatsCards from './AnsibleStatsCards';
|
||||
import AnsiblePlaybookCard from './AnsiblePlaybookCard';
|
||||
import AnsibleExecutionsTable from './AnsibleExecutionsTable';
|
||||
|
||||
const AnsibleManager: React.FC = () => {
|
||||
const _confirm = useConfirm();
|
||||
@@ -250,60 +250,12 @@ const AnsibleManager: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 统计卡片 */}
|
||||
<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">{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>
|
||||
<AnsibleStatsCards
|
||||
total={playbooks.length}
|
||||
running={runningCount}
|
||||
todaySuccess={todaySuccess}
|
||||
todayFailed={todayFailed}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}>
|
||||
<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>
|
||||
) : (
|
||||
filteredPlaybooks.map((playbook) => (
|
||||
<Card key={playbook.id} 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={selectedIds.includes(playbook.id)}
|
||||
onCheckedChange={() => toggleSelect(playbook.id)}
|
||||
className="shrink-0"
|
||||
<AnsiblePlaybookCard
|
||||
key={playbook.id}
|
||||
playbook={playbook}
|
||||
selected={selectedIds.includes(playbook.id)}
|
||||
executing={executing}
|
||||
onToggleSelect={toggleSelect}
|
||||
onView={(pb) => { setSelectedPlaybook(pb); setDetailOpen(true); }}
|
||||
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>
|
||||
@@ -450,57 +360,11 @@ const AnsibleManager: React.FC = () => {
|
||||
|
||||
{/* ============ 执行记录 ============ */}
|
||||
<TabsContent value="jobs" className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={fetchExecutions}>
|
||||
<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={() => { setSelectedExec(job); setLogsOpen(true); }}>
|
||||
<Terminal className="w-4 h-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnsibleExecutionsTable
|
||||
executions={executions}
|
||||
onRefresh={fetchExecutions}
|
||||
onViewLogs={(exec) => { setSelectedExec(exec); setLogsOpen(true); }}
|
||||
/>
|
||||
</TabsContent>
|
||||
</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;
|
||||
Reference in new issue
Block a user