refactor: AnsibleManager split into 5 files (740 to 535 lines)
- ansible-helpers.tsx (114): types, mapPlaybook, categories, samplePlaybook, getStatusBadge, formatDate - AnsibleFormDialog.tsx (111): 新建/编辑 Playbook 弹窗 - AnsibleDetailDialog.tsx (47): 查看 Playbook 内容弹窗 - AnsibleLogsDialog.tsx (38): 执行日志弹窗 - AnsibleManager.tsx (740->535): 主组件保留统计卡片+Tab布局+列表+表格+状态管理
This commit is contained in:
1 parent
e6d9f364e5
commit
30cc899268
5 files changed
+344
-239
No files matched your search
@@ -0,0 +1,47 @@
|
||||
// Ansible 查看 Playbook 内容弹窗 // Logan.Li
|
||||
import { FileCode, Edit } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { Playbook } from './ansible-helpers';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
playbook: Playbook | null;
|
||||
onEdit: (pb: Playbook) => void;
|
||||
}
|
||||
|
||||
const AnsibleDetailDialog: React.FC<Props> = ({ open, onOpenChange, playbook, onEdit }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileCode className="w-5 h-5 text-blue-500" />
|
||||
{playbook?.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{playbook?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[500px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{playbook?.content || '(空)'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
|
||||
{playbook && (
|
||||
<Button onClick={() => onEdit(playbook)}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnsibleDetailDialog;
|
||||
@@ -0,0 +1,111 @@
|
||||
// Ansible 新建/编辑 Playbook 弹窗 // Logan.Li
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { categories } from './ansible-helpers';
|
||||
|
||||
export interface PlaybookFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
category: string;
|
||||
tags: string;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingId: number | null;
|
||||
formData: PlaybookFormData;
|
||||
setFormData: React.Dispatch<React.SetStateAction<PlaybookFormData>>;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
const AnsibleFormDialog: React.FC<Props> = ({ open, onOpenChange, editingId, formData, setFormData, onSave }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId !== null ? '编辑 Playbook' : '新建 Playbook'}</DialogTitle>
|
||||
<DialogDescription>编写 Ansible Playbook 实现自动化运维</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 overflow-y-auto max-h-[60vh] pr-1">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Playbook 名称"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>分类</Label>
|
||||
<Select value={formData.category} onValueChange={(v) => setFormData({ ...formData, category: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(c => (
|
||||
<SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Input
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Playbook 描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>标签(逗号分隔)</Label>
|
||||
<Input
|
||||
value={formData.tags}
|
||||
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||
placeholder="如: nginx, web, 部署"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>超时(秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.timeout}
|
||||
onChange={(e) => setFormData({ ...formData, timeout: Number(e.target.value) })}
|
||||
placeholder="600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Playbook 内容 (YAML)</Label>
|
||||
<Textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
className="min-h-[380px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
||||
<Button onClick={onSave}>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{editingId !== null ? '保存' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnsibleFormDialog;
|
||||
@@ -0,0 +1,38 @@
|
||||
// Ansible 执行日志弹窗 // Logan.Li
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { getStatusBadge, type ExecutionRecord } from './ansible-helpers';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
exec: ExecutionRecord | null;
|
||||
}
|
||||
|
||||
const AnsibleLogsDialog: React.FC<Props> = ({ open, onOpenChange, exec }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>执行日志</DialogTitle>
|
||||
<DialogDescription>
|
||||
{exec?.task_name} · {getStatusBadge(exec?.status || '')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[480px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{exec?.output || '暂无日志输出...'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnsibleLogsDialog;
|
||||
@@ -1,112 +1,25 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Play, FileCode, Clock, CheckCircle, XCircle, RotateCcw, Eye,
|
||||
Plus, Search, Trash2, Edit, Copy, Terminal, Loader2, Zap,
|
||||
FileCode, CheckCircle, XCircle, RotateCcw, Eye,
|
||||
Plus, Search, Trash2, Edit, Copy, Terminal, Loader2, Play,
|
||||
} 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 {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
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 { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { ansibleApi, taskExecutionApi } from '@/services/api';
|
||||
|
||||
// ---- 类型定义 ----
|
||||
|
||||
interface Playbook {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
timeout: number;
|
||||
run_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ExecutionRecord {
|
||||
id: number;
|
||||
task_type: string;
|
||||
task_id: number;
|
||||
task_name: string;
|
||||
status: string;
|
||||
output: string;
|
||||
duration: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 将后端返回的 playbook 原始对象映射为前端结构(解析 tags JSON)
|
||||
function mapPlaybook(raw: any): Playbook {
|
||||
let tags: string[] = [];
|
||||
try {
|
||||
if (raw.tags) tags = JSON.parse(raw.tags);
|
||||
} catch {
|
||||
tags = [];
|
||||
}
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
description: raw.description || '',
|
||||
content: raw.content || '',
|
||||
category: raw.category || 'deployment',
|
||||
tags,
|
||||
timeout: raw.timeout || 600,
|
||||
run_count: raw.run_count || 0,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ value: 'deployment', label: '应用部署' },
|
||||
{ value: 'configuration', label: '配置管理' },
|
||||
{ value: 'maintenance', label: '系统维护' },
|
||||
{ value: 'security', label: '安全加固' },
|
||||
{ value: 'backup', label: '备份恢复' },
|
||||
{ value: 'monitoring', label: '监控配置' },
|
||||
];
|
||||
|
||||
const samplePlaybook = `---
|
||||
- name: Deploy Application
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
app_name: myapp
|
||||
deploy_path: /opt/{{ app_name }}
|
||||
|
||||
tasks:
|
||||
- name: Install dependencies
|
||||
package:
|
||||
name:
|
||||
- nginx
|
||||
state: present
|
||||
|
||||
- name: Create deploy directory
|
||||
file:
|
||||
path: "{{ deploy_path }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
handlers:
|
||||
- name: restart app
|
||||
service:
|
||||
name: nginx
|
||||
state: restarted`;
|
||||
import {
|
||||
mapPlaybook, categories, samplePlaybook, getStatusBadge, formatDate,
|
||||
type Playbook, type ExecutionRecord,
|
||||
} from './ansible-helpers';
|
||||
import AnsibleFormDialog, { type PlaybookFormData } from './AnsibleFormDialog';
|
||||
import AnsibleDetailDialog from './AnsibleDetailDialog';
|
||||
import AnsibleLogsDialog from './AnsibleLogsDialog';
|
||||
|
||||
const AnsibleManager: React.FC = () => {
|
||||
const _confirm = useConfirm();
|
||||
@@ -120,15 +33,15 @@ const AnsibleManager: React.FC = () => {
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
|
||||
// 弹窗状态
|
||||
const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑
|
||||
const [detailOpen, setDetailOpen] = useState(false); // 查看
|
||||
const [logsOpen, setLogsOpen] = useState(false); // 执行日志
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [selectedPlaybook, setSelectedPlaybook] = useState<Playbook | null>(null);
|
||||
const [selectedExec, setSelectedExec] = useState<ExecutionRecord | null>(null);
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
const [formData, setFormData] = useState<PlaybookFormData>({
|
||||
name: '', description: '', content: '', category: 'deployment', tags: '', timeout: 600,
|
||||
});
|
||||
|
||||
@@ -181,7 +94,7 @@ const AnsibleManager: React.FC = () => {
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
// 将逗号分隔的标签字符串转为 JSON 数组字符串
|
||||
// 将逗号分隔的标签字符串转为 JSON 数组字符串 // Logan.Li
|
||||
const tagsToJson = (raw: string): string => {
|
||||
const arr = raw.split(',').map(t => t.trim()).filter(Boolean);
|
||||
return JSON.stringify(arr);
|
||||
@@ -317,27 +230,6 @@ const AnsibleManager: React.FC = () => {
|
||||
|
||||
// ---- 辅助 ----
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge variant="outline" className="gap-1"><Clock className="w-3 h-3" />等待中</Badge>;
|
||||
case 'running':
|
||||
return <Badge variant="default" className="bg-blue-500 gap-1"><Loader2 className="w-3 h-3 animate-spin" />执行中</Badge>;
|
||||
case 'success':
|
||||
return <Badge variant="default" className="bg-green-600 gap-1"><CheckCircle className="w-3 h-3" />成功</Badge>;
|
||||
case 'failed':
|
||||
return <Badge variant="destructive" className="gap-1"><XCircle className="w-3 h-3" />失败</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">{status || '未知'}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (s: string | null) => {
|
||||
if (!s) return '-';
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? '-' : d.toLocaleString('zh-CN', { hour12: false });
|
||||
};
|
||||
|
||||
const filteredPlaybooks = playbooks.filter(pb => {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchSearch = pb.name.toLowerCase().includes(q) ||
|
||||
@@ -613,126 +505,29 @@ const AnsibleManager: React.FC = () => {
|
||||
</Tabs>
|
||||
|
||||
{/* ===== 新建/编辑 Playbook 对话框 ===== */}
|
||||
<Dialog open={editorOpen} onOpenChange={setEditorOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId !== null ? '编辑 Playbook' : '新建 Playbook'}</DialogTitle>
|
||||
<DialogDescription>编写 Ansible Playbook 实现自动化运维</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 overflow-y-auto max-h-[60vh] pr-1">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Playbook 名称"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>分类</Label>
|
||||
<Select value={formData.category} onValueChange={(v) => setFormData({ ...formData, category: v })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(c => (
|
||||
<SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Input
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Playbook 描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>标签(逗号分隔)</Label>
|
||||
<Input
|
||||
value={formData.tags}
|
||||
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||
placeholder="如: nginx, web, 部署"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>超时(秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.timeout}
|
||||
onChange={(e) => setFormData({ ...formData, timeout: Number(e.target.value) })}
|
||||
placeholder="600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Playbook 内容 (YAML)</Label>
|
||||
<Textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
className="min-h-[380px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditorOpen(false)}>取消</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{editingId !== null ? '保存' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AnsibleFormDialog
|
||||
open={editorOpen}
|
||||
onOpenChange={setEditorOpen}
|
||||
editingId={editingId}
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
{/* ===== 查看 Playbook 内容 ===== */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileCode className="w-5 h-5 text-blue-500" />
|
||||
{selectedPlaybook?.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{selectedPlaybook?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[500px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{selectedPlaybook?.content || '(空)'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>关闭</Button>
|
||||
{selectedPlaybook && (
|
||||
<Button onClick={() => { setDetailOpen(false); openEdit(selectedPlaybook); }}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AnsibleDetailDialog
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
playbook={selectedPlaybook}
|
||||
onEdit={(pb) => { setDetailOpen(false); openEdit(pb); }}
|
||||
/>
|
||||
|
||||
{/* ===== 执行日志 ===== */}
|
||||
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>执行日志</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedExec?.task_name} · {getStatusBadge(selectedExec?.status || '')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[480px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{selectedExec?.output || '暂无日志输出...'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setLogsOpen(false)}>关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AnsibleLogsDialog
|
||||
open={logsOpen}
|
||||
onOpenChange={setLogsOpen}
|
||||
exec={selectedExec}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Ansible 模块共享类型与辅助函数 // Logan.Li
|
||||
import { Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
// ---- 类型定义 ----
|
||||
|
||||
export interface Playbook {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
timeout: number;
|
||||
run_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ExecutionRecord {
|
||||
id: number;
|
||||
task_type: string;
|
||||
task_id: number;
|
||||
task_name: string;
|
||||
status: string;
|
||||
output: string;
|
||||
duration: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 将后端返回的 playbook 原始对象映射为前端结构(解析 tags JSON) // Logan.Li
|
||||
export function mapPlaybook(raw: any): Playbook {
|
||||
let tags: string[] = [];
|
||||
try {
|
||||
if (raw.tags) tags = JSON.parse(raw.tags);
|
||||
} catch {
|
||||
tags = [];
|
||||
}
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
description: raw.description || '',
|
||||
content: raw.content || '',
|
||||
category: raw.category || 'deployment',
|
||||
tags,
|
||||
timeout: raw.timeout || 600,
|
||||
run_count: raw.run_count || 0,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
// Playbook 分类选项 // Logan.Li
|
||||
export const categories = [
|
||||
{ value: 'deployment', label: '应用部署' },
|
||||
{ value: 'configuration', label: '配置管理' },
|
||||
{ value: 'maintenance', label: '系统维护' },
|
||||
{ value: 'security', label: '安全加固' },
|
||||
{ value: 'backup', label: '备份恢复' },
|
||||
{ value: 'monitoring', label: '监控配置' },
|
||||
];
|
||||
|
||||
// 默认 Playbook 模板 // Logan.Li
|
||||
export const samplePlaybook = `---
|
||||
- name: Deploy Application
|
||||
hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
app_name: myapp
|
||||
deploy_path: /opt/{{ app_name }}
|
||||
|
||||
tasks:
|
||||
- name: Install dependencies
|
||||
package:
|
||||
name:
|
||||
- nginx
|
||||
state: present
|
||||
|
||||
- name: Create deploy directory
|
||||
file:
|
||||
path: "{{ deploy_path }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
handlers:
|
||||
- name: restart app
|
||||
service:
|
||||
name: nginx
|
||||
state: restarted`;
|
||||
|
||||
// 执行状态 Badge 组件 // Logan.Li
|
||||
export function getStatusBadge(status: string) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge variant="outline" className="gap-1"><Clock className="w-3 h-3" />等待中</Badge>;
|
||||
case 'running':
|
||||
return <Badge variant="default" className="bg-blue-500 gap-1"><Loader2 className="w-3 h-3 animate-spin" />执行中</Badge>;
|
||||
case 'success':
|
||||
return <Badge variant="default" className="bg-green-600 gap-1"><CheckCircle className="w-3 h-3" />成功</Badge>;
|
||||
case 'failed':
|
||||
return <Badge variant="destructive" className="gap-1"><XCircle className="w-3 h-3" />失败</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">{status || '未知'}</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期格式化 // Logan.Li
|
||||
export function formatDate(s: string | null): string {
|
||||
if (!s) return '-';
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? '-' : d.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
Reference in new issue
Block a user