diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 34694b5..03e36ce 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,7 +18,7 @@ import { AlertManager } from '@/components/monitor/AlertManager'; import { AppReleaseManager } from '@/components/deploy/AppRelease'; import { ApprovalFlowManager } from '@/components/approval/ApprovalFlow'; import { NavigationPage } from '@/components/navigation/NavigationPage'; -import { AutomationManager } from '@/components/automation/Automation'; +import { ScriptManager } from '@/components/automation/ScriptManager'; import { SystemManager } from '@/components/system/SystemManager'; import { PipelineManager } from '@/components/deploy/PipelineManager'; import { ReleaseManager } from '@/components/deploy/ReleaseManager'; @@ -85,9 +85,8 @@ function App() { } /> {/* 自动化运维 */} } /> - } /> + } /> } /> - } /> {/* AIOps智能 */} } /> } /> diff --git a/frontend/src/components/automation/Automation.tsx b/frontend/src/components/automation/Automation.tsx deleted file mode 100644 index c0b7ae4..0000000 --- a/frontend/src/components/automation/Automation.tsx +++ /dev/null @@ -1,553 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - PlayCircle, - Plus, - Search, - Clock, - Play, - Pause, - RotateCw, - FileCode, - History, - Eye, - Edit, - Copy, - Download, - Loader2, -} from 'lucide-react'; -import { cronJobApi, scriptApi } from '@/services/api'; -import { toast } from 'sonner'; - -function parseParams(params: any) { - if (Array.isArray(params)) return params; - if (typeof params === 'string') { - try { - const parsed = JSON.parse(params); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - return []; -} - -export function AutomationManager() { - const [activeTab, setActiveTab] = useState('cronjobs'); - const [searchQuery, setSearchQuery] = useState(''); - const [cronJobs, setCronJobs] = useState([]); - const [scripts, setScripts] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedScript, setSelectedScript] = useState(null); - const [isExecuteDialogOpen, setIsExecuteDialogOpen] = useState(false); - - const loadData = async () => { - try { - setLoading(true); - const [jobsData, scriptsData] = await Promise.all([ - cronJobApi.getJobs(), - scriptApi.getScripts(), - ]); - setCronJobs(jobsData.list || []); - setScripts(scriptsData.list || []); - } catch (error) { - console.error('Failed to load automation data:', error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadData(); - }, []); - - const filteredJobs = cronJobs.filter( - (job) => - (job.name || '').toLowerCase().includes(searchQuery.toLowerCase()) || - (job.description || '').toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const filteredScripts = scripts.filter( - (script) => - (script.name || '').toLowerCase().includes(searchQuery.toLowerCase()) || - (script.description || '').toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const getJobStatusBadge = (enabled: any) => { - return enabled ? ( - - - 已启用 - - ) : ( - - - 已暂停 - - ); - }; - - const getLanguageBadge = (language: any) => { - const colors: Record = { - shell: 'border-green-500/30 text-green-400 bg-green-500/10', - bash: 'border-green-500/30 text-green-400 bg-green-500/10', - python: 'border-blue-500/30 text-blue-400 bg-blue-500/10', - powershell: 'border-cyan-500/30 text-cyan-400 bg-cyan-500/10', - yaml: 'border-purple-500/30 text-purple-400 bg-purple-500/10', - }; - return ( - - {(language || 'shell').toUpperCase()} - - ); - }; - - const handleExecuteScript = async (id: any) => { - try { - const result = await scriptApi.executeScript(id); - const status = result?.status || 'success'; - if (status === 'success') { - toast.success('执行成功', { description: `耗时 ${result?.duration || 0}ms` }); - } else { - toast.error('执行失败', { description: (result?.output || '').slice(0, 100) }); - } - setIsExecuteDialogOpen(false); - } catch (error) { - console.error('Failed to execute script:', error); - toast.error('执行失败'); - } - }; - - return ( -
- {/* Page Header */} -
-
-

自动化运维

-

定时任务与脚本管理

-
-
- - -
-
- - {/* Stats Cards */} -
- - -
-
-

定时任务

-

{cronJobs.length}

-
-
- -
-
-
-
- - -
-
-

运行中

-

- {cronJobs.filter((j) => j.enabled).length} -

-
-
- -
-
-
-
- - -
-
-

脚本数量

-

{scripts.length}

-
-
- -
-
-
-
- - -
-
-

累计执行

-

- {cronJobs.reduce((sum, j) => sum + (j.success_count || 0) + (j.fail_count || 0), 0)} -

-
-
- -
-
-
-
-
- - {/* Filters */} -
-
- - setSearchQuery(e.target.value)} - className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" - /> -
-
- - {loading ? ( -
- -
- ) : null} - - {/* Tabs */} - - - - - 定时任务 - - - - 脚本库 - - - - 任务编排 - - - - - - -
- - - - 任务名称 - 状态 - Cron表达式 - 上次执行 - 下次执行 - 执行统计 - 操作 - - - - {filteredJobs.map((job) => ( - - -
-
- - {job.name} -
-

{job.description}

-
-
- {getJobStatusBadge(job.enabled)} - - - {job.cron_expr} - - - - - {job.last_run_at ? new Date(job.last_run_at).toLocaleString() : '--'} - - - - -- - - -
- {job.success_count || 0} - / - {job.fail_count || 0} -
-
- -
- - -
-
-
- ))} -
-
-
-
-
-
- - -
- {filteredScripts.map((script) => ( - - -
-
-
- -
-
- {script.name} -

{script.description}

-
-
-
- {getLanguageBadge(script.type)} -
-
-
- -
-
{(script.content || '').slice(0, 200)}...
-
-
-
- {parseParams(script.params).map((param: any) => ( - - {param.name} - {param.required && *} - - ))} -
-
- - - -
-
-
-
- ))} -
-
- - - - -
- -

任务编排

-

可视化编排复杂运维任务流程

- -
-
-
-
-
- - {/* Script Detail Dialog */} - setSelectedScript(null)}> - - - - - {selectedScript?.name} - - - {selectedScript?.description} - - - -
-
- {selectedScript && getLanguageBadge(selectedScript.type)} - - 更新于 {selectedScript && selectedScript.updated_at ? new Date(selectedScript.updated_at).toLocaleDateString() : '--'} - -
- -
-
{selectedScript?.content}
-
- - {selectedScript && parseParams(selectedScript.params).length > 0 && ( -
-

参数

-
- {parseParams(selectedScript.params).map((param: any) => ( -
- {param.name} - - {param.type} - - {param.required && ( - - 必填 - - )} - {param.description} -
- ))} -
-
- )} -
- - - - - - -
-
- - {/* Execute Script Dialog */} - - - - 执行脚本 - - {selectedScript?.name} - - - -
-
- - -
- - {selectedScript && parseParams(selectedScript.params).map((param: any) => ( -
- - -
- ))} -
- - - - - -
-
-
- ); -} diff --git a/frontend/src/components/automation/ScriptManager.tsx b/frontend/src/components/automation/ScriptManager.tsx new file mode 100644 index 0000000..0099f92 --- /dev/null +++ b/frontend/src/components/automation/ScriptManager.tsx @@ -0,0 +1,658 @@ +import React, { useState, useEffect } from 'react'; +import { + Play, FileCode, Clock, CheckCircle, XCircle, RotateCcw, Eye, + Plus, Search, Trash2, Edit, Copy, Terminal, 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 { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, +} from '@/components/ui/dialog'; +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 { toast } from 'sonner'; +import { useConfirm } from '@/components/ui/confirm-dialog'; +import { scriptApi, taskExecutionApi } from '@/services/api'; + +// ---- 类型定义 ---- + +interface Script { + id: number; + name: string; + code: string; + description: string; + type: string; // shell | python | powershell | yaml + content: string; + timeout: number; + version: 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; +} + +// 将后端返回的脚本原始对象映射为前端结构 +function mapScript(raw: any): Script { + return { + id: raw.id, + name: raw.name, + code: raw.code || '', + description: raw.description || '', + type: raw.type || 'shell', + content: raw.content || '', + timeout: raw.timeout || 300, + version: raw.version || 1, + run_count: raw.run_count || 0, + created_at: raw.created_at, + updated_at: raw.updated_at, + }; +} + +const scriptTypes = [ + { value: 'shell', label: 'Shell', sample: '#!/bin/bash\n# 系统巡检脚本\necho "===== 系统信息 ====="\nuname -a\necho ""\necho "===== 磁盘使用 ====="\ndf -h\necho ""\necho "===== 内存使用 ====="\nfree -h\n' }, + { value: 'python', label: 'Python', sample: '#!/usr/bin/env python3\n# 主机信息采集\nimport platform, socket\nprint("主机名:", socket.gethostname())\nprint("系统:", platform.platform())\nprint("Python:", platform.python_version())\n' }, + { value: 'powershell', label: 'PowerShell', sample: '# Windows 系统检查\nWrite-Host "===== 系统信息 ====="\nGet-ComputerInfo | Select-Object CsName, WindowsVersion\nWrite-Host "===== 磁盘使用 ====="\nGet-PSDrive -PSProvider FileSystem | Format-Table\n' }, + { value: 'yaml', label: 'YAML', sample: '# YAML 配置/任务定义\nname: example-task\nsteps:\n - name: step1\n action: echo\n message: hello\n' }, +]; + +const typeColors: Record = { + shell: 'border-green-500/30 text-green-500 bg-green-500/10', + bash: 'border-green-500/30 text-green-500 bg-green-500/10', + python: 'border-blue-500/30 text-blue-500 bg-blue-500/10', + powershell: 'border-cyan-500/30 text-cyan-500 bg-cyan-500/10', + yaml: 'border-purple-500/30 text-purple-500 bg-purple-500/10', +}; + +const getTypeBadge = (type: string) => ( + + {(type || 'shell').toUpperCase()} + +); + +// 生成唯一脚本编码 +const genCode = () => `sc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`; + +export function ScriptManager() { + const _confirm = useConfirm(); + const [scripts, setScripts] = useState([]); + const [executions, setExecutions] = useState([]); + const [loading, setLoading] = useState(false); + const [executing, setExecuting] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [typeFilter, setTypeFilter] = useState('all'); + const [activeTab, setActiveTab] = useState('scripts'); + + // 弹窗状态 + const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑 + const [detailOpen, setDetailOpen] = useState(false); // 查看 + const [logsOpen, setLogsOpen] = useState(false); // 执行日志 + const [editingId, setEditingId] = useState(null); + const [selectedScript, setSelectedScript] = useState