diff --git a/frontend/src/components/agent/agentUtils.ts b/frontend/src/components/agent/agentUtils.ts index ea1d910..49d5687 100644 --- a/frontend/src/components/agent/agentUtils.ts +++ b/frontend/src/components/agent/agentUtils.ts @@ -1,32 +1,2 @@ -// Agent 监控相关工具函数(从 AgentMonitor.tsx 提取,供多组件共享) // Logan.Li - -/** 在线判定:3 分钟内有上报视为在线 */ -export function isOnline( lastReport: any): boolean { - if (!lastReport) return false; - const t = new Date(lastReport).getTime(); - if (Number.isNaN(t)) return false; - return Date.now() - t < 3 * 60 * 1000; -} - -/** 相对时间格式化("3分钟前" / "2小时前") */ -export function timeAgo( ts: any): string { - if (!ts) return '--'; - const t = new Date(ts).getTime(); - if (Number.isNaN(t)) return '--'; - const diff = Math.floor((Date.now() - t) / 1000); - if (diff < 60) return `${diff}秒前`; - if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`; - if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`; - return new Date(ts).toLocaleString(); -} - -/** 兼容多种列表响应包装:数组 / {list} / {items} / {data} */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function asList( data: any): any[] { - if (!data) return []; - if (Array.isArray(data)) return data; - if (Array.isArray(data.list)) return data.list; - if (Array.isArray(data.items)) return data.items; - if (Array.isArray(data.data)) return data.data; - return []; -} +// 兼容旧路径,统一从 lib 导出 // Logan.Li +export { isOnline, timeAgo, asList } from '@/lib/agent-utils'; \ No newline at end of file diff --git a/frontend/src/components/deploy/CredentialFormDialog.tsx b/frontend/src/components/deploy/CredentialFormDialog.tsx index 8d8e01a..251d5f0 100644 --- a/frontend/src/components/deploy/CredentialFormDialog.tsx +++ b/frontend/src/components/deploy/CredentialFormDialog.tsx @@ -8,8 +8,7 @@ import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Plus, Key, Globe, Container } from 'lucide-react'; import { toast } from 'sonner'; -import { deployApi } from '@/services/api'; -import http from '@/services/http'; +import { credentialApi, deployApi } from '@/services/api'; interface Repo { id: number; name: string; url: string; credential_id: number | null; @@ -48,7 +47,7 @@ export default function CredentialFormDialog({ open, onOpenChange, type, editId, const loadCredentials = async () => { try { - const res: Record = await http.get('/credentials?page_size=100'); + const res: Record = await credentialApi.list({ page_size: 100 }); setCredentials(res?.list || []); } catch { /* silent */ } }; diff --git a/frontend/src/components/deploy/CredentialManager.tsx b/frontend/src/components/deploy/CredentialManager.tsx index 60be383..044e3c7 100644 --- a/frontend/src/components/deploy/CredentialManager.tsx +++ b/frontend/src/components/deploy/CredentialManager.tsx @@ -34,7 +34,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; -import http from '@/services/http'; +import { credentialApi } from '@/services/api'; // ────── Types ────── @@ -85,7 +85,7 @@ function CredDetailDialog({ open, onClose, credId }: { open: boolean; onClose: ( useEffect(() => { if (!credId || !open) return; setLoading(true); - http.get(`/credentials/${credId}`).then(( res: any) => { + credentialApi.get(credId).then(( res: any) => { setDetail(res); }).catch(() => toast.error('加载详情失败')).finally(() => setLoading(false)); }, [credId, open]); @@ -372,10 +372,10 @@ function CredFormDialog({ open, onClose, onSaved, editData }: { } if (isEdit) { - await http.put(`/credentials/${editData!.id}`, payload); + await credentialApi.update(editData!.id, payload); toast.success('已更新'); } else { - await http.post('/credentials', payload); + await credentialApi.create(payload); toast.success('已创建'); } onSaved(); @@ -643,11 +643,11 @@ export default function CredentialManager() { const fetchAll = useCallback(async () => { setLoading(true); try { - const params = new URLSearchParams(); - if (filterType) params.set('secret_type', filterType); - if (filterSearch) params.set('search', filterSearch); - params.set('page_size', '100'); - const res: Record = await http.get(`/credentials?${params.toString()}`); + const res: Record = await credentialApi.list({ + page_size: 100, + ...(filterType ? { secret_type: filterType } : {}), + ...(filterSearch ? { search: filterSearch } : {}), + }); setCredentials(res?.list || []); } catch { toast.error('获取凭证列表失败'); } finally { setLoading(false); } @@ -664,7 +664,7 @@ export default function CredentialManager() { }); if (!ok) return; try { - await http.delete(`/credentials/${cred.id}`); + await credentialApi.delete(cred.id); toast.success('已删除'); fetchAll(); } catch { toast.error('删除失败'); } @@ -672,7 +672,7 @@ export default function CredentialManager() { const handleToggleActive = async (cred: Credential) => { try { - await http.put(`/credentials/${cred.id}`, { is_active: !cred.is_active }); + await credentialApi.toggleActive(cred.id, !cred.is_active); toast.success(cred.is_active ? '已禁用' : '已启用'); fetchAll(); } catch { toast.error('操作失败'); } diff --git a/frontend/src/components/deploy/FileBrowserDialog.tsx b/frontend/src/components/deploy/FileBrowserDialog.tsx index 950f4b4..006ca8f 100644 --- a/frontend/src/components/deploy/FileBrowserDialog.tsx +++ b/frontend/src/components/deploy/FileBrowserDialog.tsx @@ -17,7 +17,7 @@ import { Minimize2, } from 'lucide-react'; import { cn } from '@/lib/utils'; -import http from '@/services/http'; +import { pipelineApi } from '@/services/api'; import { useUserStore } from '@/store'; interface FileItem { @@ -69,8 +69,7 @@ export function FileBrowserDialog({ setLoading(true); setError(''); try { - const params = path ? `?path=${encodeURIComponent(path)}` : ''; - const data = await http.get(`/pipelines/runs/${runId}/files${params}`); + const data = await pipelineApi.listRunFiles(runId, path || undefined); setListing(data); setCurrentPath(data?.current || ''); } catch (err: any) { diff --git a/frontend/src/components/deploy/PipelineManager.tsx b/frontend/src/components/deploy/PipelineManager.tsx index fdc8278..32b75c5 100644 --- a/frontend/src/components/deploy/PipelineManager.tsx +++ b/frontend/src/components/deploy/PipelineManager.tsx @@ -40,8 +40,7 @@ import { Package, } from 'lucide-react'; -import { pipelineApi, deployApi } from '@/services/api'; -import http from '@/services/http'; +import { credentialApi, deployApi, pipelineApi } from '@/services/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import { nameToCode, getStatusBadge } from './pipeline-helpers'; @@ -140,7 +139,7 @@ export function PipelineManager() { deployApi.getRepos().catch(() => ({ list: [] })), deployApi.getRegistries().catch(() => ({ list: [] })), deployApi.getEnvironments().catch(() => ({ list: [] })), - http.get('/credentials', { page_size: 100 }).catch(() => ({ list: [] })), + credentialApi.list({ page_size: 100 }).catch(() => ({ list: [] })), ] as const); setRepos(Array.isArray((rR as Record)?.list) ? (rR as Record).list : []); setRegistries(Array.isArray((rReg as Record)?.list) ? (rReg as Record).list : []); @@ -891,7 +890,7 @@ function PipelineQueuePanel({ pipeline, clusterId: _cid }: { pipeline: any; clus if (!code) return; setLoading(true); try { - const res: Record = await http.get(`/pipelines/${code}/queue`); + const res: Record = await pipelineApi.getQueue(code); setQueue(res?.data || res); } catch { setQueue(null); } finally { setLoading(false); } @@ -901,7 +900,7 @@ function PipelineQueuePanel({ pipeline, clusterId: _cid }: { pipeline: any; clus const handleCancel = async (runId: number) => { try { - await http.delete(`/pipelines/${code}/queue/${runId}`); + await pipelineApi.removeFromQueue(code, runId); toast.success('已取消'); loadQueue(); } catch { toast.error('取消失败'); } diff --git a/frontend/src/components/deploy/PipelineRunDetail.tsx b/frontend/src/components/deploy/PipelineRunDetail.tsx index 4570d3d..dcefcff 100644 --- a/frontend/src/components/deploy/PipelineRunDetail.tsx +++ b/frontend/src/components/deploy/PipelineRunDetail.tsx @@ -10,7 +10,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { toast } from 'sonner'; -import http from '@/services/http'; +import { pipelineApi } from '@/services/api'; import { FileBrowserDialog } from './FileBrowserDialog'; // ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════ @@ -147,7 +147,7 @@ export default function PipelineRunDetail() { // 加载流水线详情 const fetchPipeline = useCallback(async () => { try { - const data: Record = await http.get(`/pipelines/${pipelineCode}`); + const data: Record = await pipelineApi.getByCode(pipelineCode); setPipeline(data?.data || data); } catch { /* silent, pipeline info optional */ } }, [pipelineCode]); @@ -156,7 +156,7 @@ export default function PipelineRunDetail() { const fetchRuns = useCallback(async () => { setLoading(true); try { - const data: Record = await http.get(`/pipelines/${pipelineCode}/runs?page_size=${pageSize}`); + const data: Record = await pipelineApi.getRunsByCode(pipelineCode, pageSize); const list = data?.list || []; setRuns(list); if (!selectedRun && list.length > 0) { @@ -180,7 +180,7 @@ export default function PipelineRunDetail() { // 加载日志 const fetchLogs = useCallback(async (run: PipelineRun) => { try { - const data: Record = await http.get(`/pipelines/runs/${run.id}/log-stream`); + const data: Record = await pipelineApi.getRunLogStream(run.id); if (data?.logs) { const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs; setLogs(Array.isArray(parsed) ? parsed : []); @@ -232,7 +232,7 @@ export default function PipelineRunDetail() { // 触发 const triggerRun = async () => { try { - await http.post(`/pipelines/${pipelineCode}/trigger`, {}); + await pipelineApi.triggerByCode(pipelineCode); toast.success('已触发'); fetchRuns(); } catch { toast.error('触发失败'); } @@ -241,7 +241,7 @@ export default function PipelineRunDetail() { // 停止 const stopRun = async (runId: number) => { try { - await http.post(`/pipelines/${pipelineCode}/runs/${runId}/stop`, {}); + await pipelineApi.stopRunByCode(pipelineCode, runId); toast.success('已停止'); fetchRuns(); } catch { toast.error('停止失败'); } @@ -252,7 +252,7 @@ export default function PipelineRunDetail() { if (selectedIds.size === 0) return; setDeleting(true); try { - await http.post(`/pipelines/${pipelineCode}/runs/batch-delete`, { ids: Array.from(selectedIds) }); + await pipelineApi.batchDeleteRuns(pipelineCode, Array.from(selectedIds)); toast.success(`已删除 ${selectedIds.size} 条记录`); setSelectedIds(new Set()); fetchRuns(); diff --git a/frontend/src/components/deploy/WorkspaceTermDialog.tsx b/frontend/src/components/deploy/WorkspaceTermDialog.tsx index d251a2d..7f52475 100644 --- a/frontend/src/components/deploy/WorkspaceTermDialog.tsx +++ b/frontend/src/components/deploy/WorkspaceTermDialog.tsx @@ -11,7 +11,7 @@ import 'xterm/css/xterm.css'; import { Maximize2, Minimize2, X, FolderOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useUserStore } from '@/store'; -import http from '@/services/http'; +import { pipelineApi } from '@/services/api'; interface WorkspaceTermDialogProps { open: boolean; @@ -40,7 +40,7 @@ export function WorkspaceTermDialog({ // 获取 workspace 路径 useEffect(() => { if (!open || !runId) return; - http.get(`/pipelines/runs/${runId}/workspace`) + pipelineApi.getWorkspace(runId) .then(( data: any) => setWorkspace(data?.workspace_path || '')) .catch(() => {}); }, [open, runId]); diff --git a/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx b/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx index 2238d7c..b779764 100644 --- a/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx +++ b/frontend/src/components/monitor/dashboard/GrafanaDashboard.tsx @@ -39,8 +39,7 @@ import { dashboardApi, promDatasourceApi, zabbixDatasourceApi, agentApi, serverA import { GrafanaPanelContent } from './GrafanaPanelContent'; import { ZabbixDashboard } from './ZabbixDashboard'; import { PrometheusDashboard } from './PrometheusDashboard'; -import { http } from '@/services/http'; -import { asList } from '@/components/agent/agentUtils'; +import { asList } from '@/lib/agent-utils'; import { AgentWizard } from '@/components/agent/AgentWizard'; import { Checkbox } from '@/components/ui/checkbox'; import { useConfirm } from '@/components/ui/confirm-dialog'; @@ -94,7 +93,7 @@ const AgentTab: React.FC = () => { finally { setBatchUninstalling(false); } }; const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize)); - const fetch = async () => { try { setHosts(asList(await http.get('/agent/latest'))); } catch {} finally { setLoading(false); } }; + const fetch = async () => { try { setHosts(asList(await agentApi.getLatest())); } catch {} finally { setLoading(false); } }; useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []); const toggle = (f: SortField) => { if (sf === f) setSd(d => d === 'asc' ? 'desc' : 'asc'); else { setSf(f); setSd('desc'); } }; const sorted = [...hosts].sort((a, b) => { diff --git a/frontend/src/components/monitor/dashboard/PrometheusDashboard.tsx b/frontend/src/components/monitor/dashboard/PrometheusDashboard.tsx index 29c75c4..dc412b3 100644 --- a/frontend/src/components/monitor/dashboard/PrometheusDashboard.tsx +++ b/frontend/src/components/monitor/dashboard/PrometheusDashboard.tsx @@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { StatStrip } from '@/components/ui/stat-strip'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { http } from '@/services/http'; +import { prometheusProxyApi } from '@/services/api'; interface Props { dsUrl?: string; } @@ -139,7 +139,7 @@ export const PrometheusDashboard: React.FC = ({ dsUrl }) => { const doConnect = async (u: string) => { setLoading(true); setError(''); try { - const data: Record = await http.post('/prometheus/connect', { url: u }); + const data: Record = await prometheusProxyApi.connect(u); setConnected(true); setTargets(data.targets||0); loadAll(u); } catch (e: any) { setConnected(false); setError((e as Record).message||'连接失败'); } @@ -149,7 +149,7 @@ export const PrometheusDashboard: React.FC = ({ dsUrl }) => { const loadAll = async (u: string) => { for (const t of ['kubernetes','node','pod','jvm']) { try { - const data: Record = await http.post('/prometheus/dashboard', { url: u, type: t }); + const data: Record = await prometheusProxyApi.dashboard(u, t); const items: Record = {}; for (const [k, v] of Object.entries(data||{})) items[k] = Array.isArray(v) ? v : ((v as Record)?.data?.result ? parsePrune((v as Record).data.result) : v); diff --git a/frontend/src/components/monitor/dashboard/ZabbixDashboard.tsx b/frontend/src/components/monitor/dashboard/ZabbixDashboard.tsx index 808f9af..13bfa7a 100644 --- a/frontend/src/components/monitor/dashboard/ZabbixDashboard.tsx +++ b/frontend/src/components/monitor/dashboard/ZabbixDashboard.tsx @@ -16,7 +16,7 @@ import { import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { http } from '@/services/http'; +import { zabbixProxyApi } from '@/services/api'; interface ZabbixHost { hostid: string; hostname: string; name: string; ip: string; available: string; @@ -45,7 +45,7 @@ export const ZabbixDashboard: React.FC = ({ dsUrl, dsUsername, dsPassword const doConnect = async (u: string, un: string, pw: string) => { setLoading(true); setError(''); try { - const data: Record = await http.post('/zabbix/connect', { url: u, username: un, password: pw }); + const data: Record = await zabbixProxyApi.connect(u, un, pw); setHosts(data.hosts || []); setVersion(data.version || ''); setConnected(true); diff --git a/frontend/src/components/server/ServerForm.tsx b/frontend/src/components/server/ServerForm.tsx index 4c23f58..e4c8959 100644 --- a/frontend/src/components/server/ServerForm.tsx +++ b/frontend/src/components/server/ServerForm.tsx @@ -25,7 +25,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Server, ChevronDown, Loader2, Save, Key, Shield } from 'lucide-react'; -import http from '@/services/http'; +import { credentialApi } from '@/services/api'; // 表单数据结构 export interface ServerFormData { @@ -101,9 +101,9 @@ export function ServerForm({ // 加载凭证列表 useEffect(() => { if (open) { - http.get('/credentials') + credentialApi.list() .then(( res: any) => { - const list = Array.isArray(res) ? res : (res?.data || []); + const list = Array.isArray(res) ? res : (res?.list || res?.data || []); setCredentials(list); }) .catch(() => setCredentials([])); diff --git a/frontend/src/lib/agent-utils.ts b/frontend/src/lib/agent-utils.ts new file mode 100644 index 0000000..6817221 --- /dev/null +++ b/frontend/src/lib/agent-utils.ts @@ -0,0 +1,32 @@ +// Agent 监控相关工具函数(供组件与 API 层共享) // Logan.Li + +/** 在线判定:3 分钟内有上报视为在线 */ +export function isOnline(lastReport: unknown): boolean { + if (!lastReport) return false; + const t = new Date(lastReport as string | number | Date).getTime(); + if (Number.isNaN(t)) return false; + return Date.now() - t < 3 * 60 * 1000; +} + +/** 相对时间格式化("3分钟前" / "2小时前") */ +export function timeAgo(ts: unknown): string { + if (!ts) return '--'; + const t = new Date(ts as string | number | Date).getTime(); + if (Number.isNaN(t)) return '--'; + const diff = Math.floor((Date.now() - t) / 1000); + if (diff < 60) return `${diff}秒前`; + if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`; + if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`; + return new Date(ts as string | number | Date).toLocaleString(); +} + +/** 兼容多种列表响应包装:数组 / {list} / {items} / {data} */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function asList(data: any): any[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (Array.isArray(data.list)) return data.list; + if (Array.isArray(data.items)) return data.items; + if (Array.isArray(data.data)) return data.data; + return []; +} \ No newline at end of file diff --git a/frontend/src/pages/ApprovalWorkflowPage.tsx b/frontend/src/pages/ApprovalWorkflowPage.tsx index 4c13a16..6ee0df1 100644 --- a/frontend/src/pages/ApprovalWorkflowPage.tsx +++ b/frontend/src/pages/ApprovalWorkflowPage.tsx @@ -7,7 +7,7 @@ import { import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; -import { http } from '@/services/http'; +import { approvalWorkflowApi, userApi } from '@/services/api'; /* ------------------------------------------------------------------ */ /* Types */ @@ -80,16 +80,7 @@ const STEP_TYPES = [ { value: 'execute' as const, label: '执行阶段', desc: '执行人员操作' }, ]; -// API helpers -const workflowApi = { - list: (params?: { category?: string }) => http.get('/approval-workflows', params), - get: (id: number) => http.get(`/approval-workflows/${id}`), - create: (data: any) => http.post('/approval-workflows', data), - update: (id: number, data: any) => http.put(`/approval-workflows/${id}`, data), - delete: (id: number) => http.delete(`/approval-workflows/${id}`), - setDefault: (id: number) => http.put(`/approval-workflows/${id}/default`), -}; -const userListApi = () => http.get('/admin/users'); + /* ------------------------------------------------------------------ */ /* UserSelectInline */ @@ -299,9 +290,9 @@ function WorkflowEditorModal({ workflow, users, onClose, onSaved }: EditorProps) })), }; if (isEdit && workflow) { - await workflowApi.update(workflow.id, payload); + await approvalWorkflowApi.update(workflow.id, payload); } else { - await workflowApi.create(payload); + await approvalWorkflowApi.create(payload); } onSaved(); } catch (e: unknown) { @@ -459,7 +450,7 @@ export default function ApprovalWorkflowPage() { try { setLoading(true); setError(null); - const res: any = await workflowApi.list(); + const res: any = await approvalWorkflowApi.list(); const data = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : []); const normalized = data.map((w: any) => ({ ...w, steps: w.steps || [] })); setItems(normalized); @@ -472,7 +463,7 @@ export default function ApprovalWorkflowPage() { const fetchUsers = useCallback(async () => { try { - const res: any = await userListApi(); + const res: any = await userApi.getUsers(); const data = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : []); // Normalize: extract role code from roles array const normalized = data.map((u: any) => ({ @@ -490,7 +481,7 @@ export default function ApprovalWorkflowPage() { const toggleActive = useCallback(async (wf: WorkflowItem) => { try { - await workflowApi.update(wf.id, { is_active: !wf.is_active }); + await approvalWorkflowApi.update(wf.id, { is_active: !wf.is_active }); setItems((prev) => prev.map((w) => (w.id === wf.id ? { ...w, is_active: !w.is_active } : w))); toast.success(wf.is_active ? '已停用' : '已启用'); } catch { toast.error('操作失败'); } @@ -499,7 +490,7 @@ export default function ApprovalWorkflowPage() { const handleDelete = useCallback(async (id: number, name: string) => { if (!window.confirm(`确定删除审批流程「${name}」?删除后不可恢复。`)) return; try { - await workflowApi.delete(id); + await approvalWorkflowApi.delete(id); setItems((prev) => prev.filter((w) => w.id !== id)); toast.success('已删除'); } catch { toast.error('删除失败'); } @@ -507,7 +498,7 @@ export default function ApprovalWorkflowPage() { const setDefault = useCallback(async (id: number) => { try { - await workflowApi.setDefault(id); + await approvalWorkflowApi.setDefault(id); setItems((prev) => prev.map((w) => ({ ...w, is_default: w.id === id }))); toast.success('已设为默认'); } catch { toast.error('操作失败'); } diff --git a/frontend/src/pages/MetricsExplorer.tsx b/frontend/src/pages/MetricsExplorer.tsx index fa74808..4b97386 100644 --- a/frontend/src/pages/MetricsExplorer.tsx +++ b/frontend/src/pages/MetricsExplorer.tsx @@ -5,7 +5,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Activity, Search, Filter, BarChart3, Cpu, HardDrive, Network, Database, Plus } from 'lucide-react'; -import { http } from '@/services/http'; +import { metricsApi } from '@/services/api'; interface _MetricName { name: string; @@ -51,7 +51,7 @@ export function MetricsExplorer() { // Load targets const loadTargets = useCallback(async () => { try { - const res: Record = await http.get('/metrics/targets?page_size=200'); + const res: Record = await metricsApi.getTargets({ page_size: 200 }); setTargets(res?.list || []); } catch { /* empty */ } finally { setLoading(false); } @@ -60,7 +60,7 @@ export function MetricsExplorer() { // Load metric names const loadMetricNames = useCallback(async () => { try { - const names: any = await http.get('/metrics/names'); + const names: any = await metricsApi.getNames(); setMetricNames(names || []); } catch { /* empty */ } }, []); @@ -78,7 +78,7 @@ export function MetricsExplorer() { Promise.all( selectedMetrics.map(mn => - http.get('/metrics/query', { target_id: selectedTarget, metric_name: mn, from: String(from), to: String(to), limit: 200 }) + metricsApi.query({ target_id: selectedTarget, metric_name: mn, from: String(from), to: String(to), limit: 200 }) .then(( data: any) => (Array.isArray(data) ? data : [])) .catch(() => []) ) diff --git a/frontend/src/pages/MiddlewareMonitor.tsx b/frontend/src/pages/MiddlewareMonitor.tsx index 958531c..bdc2c94 100644 --- a/frontend/src/pages/MiddlewareMonitor.tsx +++ b/frontend/src/pages/MiddlewareMonitor.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Database, Server, Activity, Loader2, Circle } from 'lucide-react'; -import { http } from '@/services/http'; +import { metricsApi } from '@/services/api'; interface TargetInfo { id: number; @@ -68,7 +68,7 @@ export function MiddlewareMonitor() { try { setLoading(true); setError(''); - const data = await http.get('/metrics/targets', { type: 'middleware', page_size: 200 }); + const data = await metricsApi.getTargets({ type: 'middleware', page_size: 200 }) as TargetsResponse; setTargets(data?.list || []); } catch (err: any) { setError((err as Record).message || '加载失败'); diff --git a/frontend/src/pages/SNMPMonitor.tsx b/frontend/src/pages/SNMPMonitor.tsx index afb85bb..577227a 100644 --- a/frontend/src/pages/SNMPMonitor.tsx +++ b/frontend/src/pages/SNMPMonitor.tsx @@ -30,7 +30,7 @@ import { TableRow, } from '@/components/ui/table'; import { Radio, Router, HardDrive, Plus, Trash2 } from 'lucide-react'; -import http from '@/services/http'; +import { snmpApi } from '@/services/api'; // ---------- types ---------- interface SnmpStats { @@ -81,7 +81,7 @@ export function SNMPMonitor() { // ---------- data fetching ---------- const loadStats = useCallback(async () => { try { - const data = await http.get('/snmp/stats'); + const data = await snmpApi.getStats() as SnmpStats; setStats(data); } catch { // backend unavailable — leave defaults @@ -90,7 +90,7 @@ export function SNMPMonitor() { const loadDevices = useCallback(async () => { try { - const data = await http.get('/snmp/devices'); + const data = await snmpApi.getDevices() as DeviceListResponse; setDevices(data.list ?? []); } catch { setDevices([]); @@ -113,7 +113,7 @@ export function SNMPMonitor() { if (!form.name.trim() || !form.ip.trim()) return; setSubmitting(true); try { - await http.post('/snmp/devices', { + await snmpApi.createDevice({ name: form.name.trim(), ip: form.ip.trim(), port: Number(form.port), @@ -133,7 +133,7 @@ export function SNMPMonitor() { const handleDelete = async (id: number) => { try { - await http.delete(`/snmp/devices/${id}`); + await snmpApi.deleteDevice(id); refresh(); } catch { // silently fail diff --git a/frontend/src/services/api/agent.ts b/frontend/src/services/api/agent.ts index 7527bfb..d9382bb 100644 --- a/frontend/src/services/api/agent.ts +++ b/frontend/src/services/api/agent.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http, type ListResponse } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const agentApi = { getLatest: () => http.get('/agent/latest'), diff --git a/frontend/src/services/api/ai.ts b/frontend/src/services/api/ai.ts index de3b8e5..2896332 100644 --- a/frontend/src/services/api/ai.ts +++ b/frontend/src/services/api/ai.ts @@ -3,7 +3,6 @@ import { http } from '../http'; import { useUserStore } from '@/store'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export interface LLMConfigData { configured?: boolean; diff --git a/frontend/src/services/api/auth.ts b/frontend/src/services/api/auth.ts index f421998..68f472b 100644 --- a/frontend/src/services/api/auth.ts +++ b/frontend/src/services/api/auth.ts @@ -1,9 +1,8 @@ // 用户认证 & LDAP // 从 services/api.ts 拆分 — Q3 api.ts 模块化 -import { http, type ListResponse } from '../http'; +import { http, getApiBase, type ListResponse } from '../http'; import { useUserStore } from '@/store'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const userApi = { login: async (username: string, password: string) => { @@ -43,9 +42,8 @@ export const userApi = { return http.post('/admin/users/batch-import', data); }, downloadTemplate: () => { - const token = useUserStore.getState().token; - const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1'; - window.open(`${baseUrl}/admin/users/import-template?token=${token}`, '_blank'); + const token = useUserStore.getState().token || localStorage.getItem('token') || ''; + window.open(`${getApiBase()}/admin/users/import-template?token=${token}`, '_blank'); }, // 角色列表 // Logan.Li getRoles: async () => { diff --git a/frontend/src/services/api/automation.ts b/frontend/src/services/api/automation.ts index 1419e38..1dfab21 100644 --- a/frontend/src/services/api/automation.ts +++ b/frontend/src/services/api/automation.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http, type ListResponse } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const cronJobApi = { getJobs: async (params?: { page?: number; page_size?: number }) => { diff --git a/frontend/src/services/api/cloud.ts b/frontend/src/services/api/cloud.ts index 1a99e7d..f93e711 100644 --- a/frontend/src/services/api/cloud.ts +++ b/frontend/src/services/api/cloud.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http, type ListResponse } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const cloudApi = { getAccounts: async () => { diff --git a/frontend/src/services/api/dashboard.ts b/frontend/src/services/api/dashboard.ts index b11ccf3..ee8efa7 100644 --- a/frontend/src/services/api/dashboard.ts +++ b/frontend/src/services/api/dashboard.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const dashboardStatsApi = { getStats: async () => { diff --git a/frontend/src/services/api/deploy.ts b/frontend/src/services/api/deploy.ts index f1d83ca..df6631c 100644 --- a/frontend/src/services/api/deploy.ts +++ b/frontend/src/services/api/deploy.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http, type ListResponse } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const pipelineApi = { getPipelines: async (params?: { page?: number; page_size?: number }) => { @@ -47,6 +46,22 @@ export const pipelineApi = { assignGroup: async (pipelineId: number | string, data: { group_id: number; role: string }) => { return http.post(`/pipelines/${pipelineId}/groups`, data); }, + // 按 code 操作(列表页/详情页常用) // Logan.Li + getByCode: (code: string) => http.get(`/pipelines/${code}`), + getRunsByCode: (code: string, pageSize = 50) => + http.get(`/pipelines/${code}/runs`, { page_size: pageSize }), + triggerByCode: (code: string) => http.post(`/pipelines/${code}/trigger`, {}), + stopRunByCode: (code: string, runId: number | string) => + http.post(`/pipelines/${code}/runs/${runId}/stop`, {}), + batchDeleteRuns: (code: string, ids: number[]) => + http.post(`/pipelines/${code}/runs/batch-delete`, { ids }), + getRunLogStream: (runId: number | string) => http.get(`/pipelines/runs/${runId}/log-stream`), + getQueue: (code: string) => http.get(`/pipelines/${code}/queue`), + removeFromQueue: (code: string, runId: number | string) => + http.delete(`/pipelines/${code}/queue/${runId}`), + getWorkspace: (runId: number | string) => http.get(`/pipelines/runs/${runId}/workspace`), + listRunFiles: (runId: number | string, path?: string) => + http.get(`/pipelines/runs/${runId}/files`, path ? { path } : undefined), }; // ==================== 应用发布 API ==================== @@ -121,12 +136,14 @@ export const deployApi = { // ==================== Ansible 自动化 API ==================== export const credentialApi = { - list: () => http.get<{ list: any[]; total: number }>('/credentials'), - create: (data: { name: string; type: string; content: string; description?: string }) => - http.post('/credentials', data), - update: (id: number | string, data: { name: string; type: string; content?: string; description?: string }) => - http.put(`/credentials/${id}`, data), + list: (params?: { page?: number; page_size?: number; secret_type?: string; search?: string }) => + http.get<{ list: any[]; total: number }>('/credentials', params), + get: (id: number | string) => http.get(`/credentials/${id}`), + create: (data: Record) => http.post('/credentials', data), + update: (id: number | string, data: Record) => http.put(`/credentials/${id}`, data), delete: (id: number | string) => http.delete(`/credentials/${id}`), + toggleActive: (id: number | string, isActive: boolean) => + http.put(`/credentials/${id}`, { is_active: isActive }), }; // 服务器凭据关联 API diff --git a/frontend/src/services/api/kubernetes.ts b/frontend/src/services/api/kubernetes.ts index a2b507f..56c5376 100644 --- a/frontend/src/services/api/kubernetes.ts +++ b/frontend/src/services/api/kubernetes.ts @@ -3,7 +3,6 @@ import { http, type ListResponse } from '../http'; import { useUserStore } from '@/store'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const kubernetesApi = { getClusters: async () => { diff --git a/frontend/src/services/api/monitor.ts b/frontend/src/services/api/monitor.ts index 021f781..ec0ee28 100644 --- a/frontend/src/services/api/monitor.ts +++ b/frontend/src/services/api/monitor.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http, type ListResponse } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const alertApi = { getAlerts: async (params?: { page?: number; page_size?: number; status?: string; severity?: string }) => { @@ -185,3 +184,39 @@ export const zabbixDatasourceApi = { testConnection: ( data: any) => http.post('/zabbix-datasources/test', data), }; +// ==================== 指标探索 / 中间件监控 API ==================== // Logan.Li + +export const metricsApi = { + getTargets: (params?: Record) => + http.get('/metrics/targets', params), + getNames: () => http.get('/metrics/names'), + query: (params: { + target_id: string | number; + metric_name: string; + from: string; + to: string; + limit?: number; + }) => http.get('/metrics/query', params), +}; + +// ==================== SNMP 监控 API ==================== // Logan.Li + +export const snmpApi = { + getStats: () => http.get('/snmp/stats'), + getDevices: () => http.get('/snmp/devices'), + createDevice: (data: Record) => http.post('/snmp/devices', data), + deleteDevice: (id: number | string) => http.delete(`/snmp/devices/${id}`), +}; + +// ==================== Prometheus / Zabbix 大屏代理 API ==================== // Logan.Li + +export const prometheusProxyApi = { + connect: (url: string) => http.post('/prometheus/connect', { url }), + dashboard: (url: string, type: string) => http.post('/prometheus/dashboard', { url, type }), +}; + +export const zabbixProxyApi = { + connect: (url: string, username: string, password: string) => + http.post('/zabbix/connect', { url, username, password }), +}; + diff --git a/frontend/src/services/api/server.ts b/frontend/src/services/api/server.ts index 7c9840c..7664da5 100644 --- a/frontend/src/services/api/server.ts +++ b/frontend/src/services/api/server.ts @@ -2,7 +2,6 @@ // 从 services/api.ts 拆分 — Q3 api.ts 模块化 import { http, type ListResponse } from '../http'; -import { isOnline, timeAgo, asList } from '../../components/agent/agentUtils'; export const serverApi = { getServers: async (params?: { page?: number; page_size?: number; keyword?: string; status?: string }) => { diff --git a/frontend/src/services/http.ts b/frontend/src/services/http.ts index 7e69917..860135b 100644 --- a/frontend/src/services/http.ts +++ b/frontend/src/services/http.ts @@ -1,6 +1,6 @@ import { useUserStore } from '@/store'; -const BASE_URL = '/api/v1'; +const API_PREFIX = '/api/v1'; // 后端统一响应格式 export interface ApiResponse { @@ -17,14 +17,33 @@ export interface ListResponse { type Json = unknown; +/** 统一 API 根路径,避免 get/post 与 download/upload 行为不一致 // Logan.Li */ +export function getApiBase(): string { + const envBase = import.meta.env.VITE_API_BASE_URL; + if (envBase) { + const normalized = String(envBase).replace(/\/$/, ''); + return normalized.endsWith(API_PREFIX) ? normalized : `${normalized}${API_PREFIX}`; + } + return API_PREFIX; +} + +function resolveApiUrl(path: string): string { + const p = path.startsWith('/') ? path : `/${path}`; + return `${getApiBase()}${p}`; +} + +function getAuthToken(): string { + return useUserStore.getState().token || localStorage.getItem('token') || ''; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any async function request( method: string, url: string, body?: Json, - params?: Record + params?: Record, ): Promise { - let fullUrl = `${BASE_URL}${url}`; + let fullUrl = resolveApiUrl(url); if (params) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { @@ -36,13 +55,12 @@ async function request( if (qs) fullUrl += '?' + qs; } - const token = useUserStore.getState().token || localStorage.getItem('token'); - + const token = getAuthToken(); const headers: Record = { 'Content-Type': 'application/json', }; if (token) { - headers['Authorization'] = 'Bearer ' + token; + headers.Authorization = 'Bearer ' + token; } const response = await fetch(fullUrl, { @@ -71,29 +89,27 @@ export const http = { get: (url: string, params?: Record) => request('GET', url, undefined, params), // eslint-disable-next-line @typescript-eslint/no-explicit-any - post: (url: string, body?: Json) => - request('POST', url, body), + post: (url: string, body?: Json) => request('POST', url, body), // eslint-disable-next-line @typescript-eslint/no-explicit-any - put: (url: string, body?: Json) => - request('PUT', url, body), + put: (url: string, body?: Json) => request('PUT', url, body), // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete: (url: string, body?: Json) => - request('DELETE', url, body), + delete: (url: string, body?: Json) => request('DELETE', url, body), getRaw: async (url: string): Promise => { - const tk = useUserStore.getState().token; - const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; - const resp = await fetch(baseUrl + '/api/v1' + url, { headers: { Authorization: 'Bearer ' + tk } }); + const resp = await fetch(resolveApiUrl(url), { + headers: { Authorization: 'Bearer ' + getAuthToken() }, + }); if (!resp.ok) throw new Error('HTTP ' + resp.status); return resp.text(); }, putRaw: async (url: string, body: string, contentType: string): Promise => { - const tk = useUserStore.getState().token; - const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; - const resp = await fetch(baseUrl + '/api/v1' + url, { + const resp = await fetch(resolveApiUrl(url), { method: 'PUT', - headers: { Authorization: 'Bearer ' + tk, 'Content-Type': contentType }, + headers: { + Authorization: 'Bearer ' + getAuthToken(), + 'Content-Type': contentType, + }, body, }); if (!resp.ok) throw new Error('HTTP ' + resp.status); @@ -101,10 +117,8 @@ export const http = { }, download: async (url: string, filename?: string): Promise => { - const tk = useUserStore.getState().token; - const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; - const resp = await fetch(baseUrl + '/api/v1' + url, { - headers: { Authorization: 'Bearer ' + tk }, + const resp = await fetch(resolveApiUrl(url), { + headers: { Authorization: 'Bearer ' + getAuthToken() }, }); if (!resp.ok) throw new Error('HTTP ' + resp.status); const blob = await resp.blob(); @@ -121,11 +135,9 @@ export const http = { // eslint-disable-next-line @typescript-eslint/no-explicit-any upload: async (url: string, formData: FormData): Promise => { - const tk = useUserStore.getState().token; - const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; - const resp = await fetch(baseUrl + '/api/v1' + url, { + const resp = await fetch(resolveApiUrl(url), { method: 'POST', - headers: { Authorization: 'Bearer ' + tk }, + headers: { Authorization: 'Bearer ' + getAuthToken() }, body: formData, }); if (!resp.ok) throw new Error('HTTP ' + resp.status); @@ -133,4 +145,4 @@ export const http = { }, }; -export default http; +export default http; \ No newline at end of file