feat: Ansible 模板克隆 (后端 ClonePlaybook API + 前端卡片悬浮克隆按钮 + 模板管理入口)
This commit is contained in:
1 parent
2d02d63806
commit
7be277658a
4 files changed
+78
-1
No files matched your search
@@ -20,6 +20,7 @@ import {
|
||||
Play, Server, ChevronRight, ChevronLeft, CheckCircle, Loader2,
|
||||
Search, RefreshCw, Terminal, Clock, XCircle, FileText, Zap,
|
||||
Boxes, Shield, Database, Activity, Wrench, Network,
|
||||
Copy, Plus, ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
mapPlaybook, categories, getStatusBadge, formatDate,
|
||||
@@ -100,6 +101,18 @@ export default function AnsibleWorkflow() {
|
||||
finally { setLoadingPB(false); }
|
||||
}, []);
|
||||
|
||||
// 克隆 Playbook 为自定义模板 // Logan.Li
|
||||
const clonePlaybook = async (pb: Playbook, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const res = await ansibleApi.clonePlaybook(pb.id);
|
||||
const cloned = mapPlaybook((res as any)?.data ?? res);
|
||||
toast.success(`已克隆「${cloned.name}」,可在模板管理中编辑`);
|
||||
await fetchPlaybooks();
|
||||
setSelectedPB(cloned);
|
||||
} catch { toast.error('克隆失败'); }
|
||||
};
|
||||
|
||||
const fetchExecutions = useCallback(async () => {
|
||||
setLoadingExecs(true);
|
||||
try {
|
||||
@@ -301,6 +314,22 @@ export default function AnsibleWorkflow() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 快捷操作栏 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open('/ansible/manage', '_blank')}
|
||||
className="border-slate-700 text-slate-400 hover:text-slate-200 text-xs h-8"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
模板管理
|
||||
</Button>
|
||||
<span className="text-xs text-slate-600">
|
||||
点击卡片选择模板执行 · 悬停卡片可克隆为自定义模板
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loadingPB ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<Loader2 className="h-6 w-6 animate-spin mx-auto mb-2" />加载中…
|
||||
@@ -314,12 +343,20 @@ export default function AnsibleWorkflow() {
|
||||
<div
|
||||
key={pb.id}
|
||||
onClick={() => setSelectedPB(selected ? null : pb)}
|
||||
className={`p-4 rounded-lg border cursor-pointer transition-all ${
|
||||
className={`group relative p-4 rounded-lg border cursor-pointer transition-all ${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-500/10 ring-1 ring-blue-500/30'
|
||||
: 'border-slate-700 bg-slate-800/50 hover:border-slate-600'
|
||||
}`}
|
||||
>
|
||||
{/* 悬浮克隆按钮 */}
|
||||
<button
|
||||
onClick={(e) => clonePlaybook(pb, e)}
|
||||
title="克隆为自定义模板"
|
||||
className="absolute top-2 right-2 p-1.5 rounded-md opacity-0 group-hover:opacity-100 transition-opacity bg-slate-700 hover:bg-blue-600 text-slate-400 hover:text-white"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg ${selected ? 'bg-blue-500/20' : 'bg-slate-700/50'}`}>
|
||||
<Icon className={`h-5 w-5 ${selected ? 'text-blue-400' : 'text-slate-400'}`} />
|
||||
|
||||
@@ -747,6 +747,10 @@ export const ansibleApi = {
|
||||
executePlaybook: async (id: number | string, targetHosts?: string[]) => {
|
||||
return http.post(`/ansible/playbooks/${id}/execute`, targetHosts ? { target_hosts: targetHosts } : undefined);
|
||||
},
|
||||
// 克隆 Playbook 为自定义模板 // Logan.Li
|
||||
clonePlaybook: async (id: number | string, name?: string) => {
|
||||
return http.post(`/ansible/playbooks/${id}/clone`, name ? { name } : undefined);
|
||||
},
|
||||
// 主机匹配预览 // Logan.Li
|
||||
matchHosts: async (params: { pattern?: string; match_type?: string; page?: number; page_size?: number }) => {
|
||||
return http.get<ListResponse>('/ansible/match-hosts', params);
|
||||
|
||||
Reference in new issue
Block a user