feat: Ansible 模板克隆 (后端 ClonePlaybook API + 前端卡片悬浮克隆按钮 + 模板管理入口)

This commit is contained in:
attacker committed 2026-06-19 13:52:37 +00:00
1 parent 2d02d63806
commit 7be277658a
4 files changed
+78 -1

No files matched your search

+35
View File
@@ -121,6 +121,41 @@ func (h *AnsibleHandler) DeletePlaybook(c *gin.Context) {
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
}
// ClonePlaybook 克隆 Playbook 为自定义模板 // Logan.Li
// POST /api/v1/ansible/playbooks/:id/clone
func (h *AnsibleHandler) ClonePlaybook(c *gin.Context) {
id := c.Param("id")
var src models.AnsiblePlaybook
if err := h.db.First(&src, id); err.Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Playbook 不存在"})
return
}
var req struct {
Name *string `json:"name"` // 可选自定义名称
}
c.ShouldBindJSON(&req)
cloneName := src.Name + " (副本)"
if req.Name != nil && *req.Name != "" {
cloneName = *req.Name
}
clone := models.AnsiblePlaybook{
Name: cloneName,
Description: src.Description,
Content: src.Content,
Tags: src.Tags,
Category: src.Category,
CreatedBy: c.GetString("username"),
}
if err := h.db.Create(&clone); err.Error != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "克隆失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: clone})
}
// BatchDeletePlaybooks 批量删除 Playbook // Logan.Li
func (h *AnsibleHandler) BatchDeletePlaybooks(c *gin.Context) {
var req models.IDsRequest
+1
View File
@@ -443,6 +443,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
ansible.DELETE("/playbooks/:id", ansibleHandler.DeletePlaybook)
ansible.DELETE("/playbooks/batch-delete", ansibleHandler.BatchDeletePlaybooks)
ansible.POST("/playbooks/:id/execute", ansibleHandler.ExecutePlaybook)
ansible.POST("/playbooks/:id/clone", ansibleHandler.ClonePlaybook)
ansible.GET("/jobs", ansibleHandler.ListJobs)
ansible.GET("/jobs/:id", ansibleHandler.GetJob)
@@ -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'}`} />
+4
View File
@@ -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);