refactor(frontend): P0 API 层收口与 http 统一
- 统一 http.ts baseURL:get/post/download/upload 均走 resolveApiUrl - agentUtils 下沉至 lib/agent-utils,API 层移除对 components 的依赖 - 扩展 metricsApi/snmpApi/prometheusProxyApi/zabbixProxyApi - 扩展 pipelineApi/credentialApi 覆盖流水线与凭证场景 - 14 个组件/页面禁止直连 http,统一走 @/services/api
This commit is contained in:
1 parent
bfd6431b9d
commit
7fb2236b4d
27 files changed
+194
-150
No files matched your search
@@ -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';
|
||||
@@ -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<string, any> = await http.get('/credentials?page_size=100');
|
||||
const res: Record<string, any> = await credentialApi.list({ page_size: 100 });
|
||||
setCredentials(res?.list || []);
|
||||
} catch { /* silent */ }
|
||||
};
|
||||
|
||||
@@ -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<string, any> = await http.get(`/credentials?${params.toString()}`);
|
||||
const res: Record<string, any> = 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('操作失败'); }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string, any>)?.list) ? (rR as Record<string, any>).list : []);
|
||||
setRegistries(Array.isArray((rReg as Record<string, any>)?.list) ? (rReg as Record<string, any>).list : []);
|
||||
@@ -891,7 +890,7 @@ function PipelineQueuePanel({ pipeline, clusterId: _cid }: { pipeline: any; clus
|
||||
if (!code) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: Record<string, any> = await http.get(`/pipelines/${code}/queue`);
|
||||
const res: Record<string, any> = 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('取消失败'); }
|
||||
|
||||
@@ -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<string, any> = await http.get(`/pipelines/${pipelineCode}`);
|
||||
const data: Record<string, any> = 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<string, any> = await http.get(`/pipelines/${pipelineCode}/runs?page_size=${pageSize}`);
|
||||
const data: Record<string, any> = 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<string, any> = await http.get(`/pipelines/runs/${run.id}/log-stream`);
|
||||
const data: Record<string, any> = 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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<Props> = ({ dsUrl }) => {
|
||||
const doConnect = async (u: string) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const data: Record<string, any> = await http.post('/prometheus/connect', { url: u });
|
||||
const data: Record<string, any> = await prometheusProxyApi.connect(u);
|
||||
setConnected(true); setTargets(data.targets||0);
|
||||
loadAll(u);
|
||||
} catch (e: any) { setConnected(false); setError((e as Record<string, any>).message||'连接失败'); }
|
||||
@@ -149,7 +149,7 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
const loadAll = async (u: string) => {
|
||||
for (const t of ['kubernetes','node','pod','jvm']) {
|
||||
try {
|
||||
const data: Record<string, any> = await http.post('/prometheus/dashboard', { url: u, type: t });
|
||||
const data: Record<string, any> = await prometheusProxyApi.dashboard(u, t);
|
||||
const items: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(data||{}))
|
||||
items[k] = Array.isArray(v) ? v : ((v as Record<string, any>)?.data?.result ? parsePrune((v as Record<string, any>).data.result) : v);
|
||||
|
||||
@@ -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<Props> = ({ dsUrl, dsUsername, dsPassword
|
||||
const doConnect = async (u: string, un: string, pw: string) => {
|
||||
setLoading(true); setError('');
|
||||
try {
|
||||
const data: Record<string, any> = await http.post('/zabbix/connect', { url: u, username: un, password: pw });
|
||||
const data: Record<string, any> = await zabbixProxyApi.connect(u, un, pw);
|
||||
setHosts(data.hosts || []);
|
||||
setVersion(data.version || '');
|
||||
setConnected(true);
|
||||
|
||||
@@ -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([]));
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -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('操作失败'); }
|
||||
|
||||
@@ -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<string, any> = await http.get('/metrics/targets?page_size=200');
|
||||
const res: Record<string, any> = 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(() => [])
|
||||
)
|
||||
|
||||
@@ -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<TargetsResponse>('/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<string, any>).message || '加载失败');
|
||||
|
||||
@@ -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<SnmpStats>('/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<DeviceListResponse>('/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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<ListResponse>(`/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<string, unknown>) => http.post('/credentials', data),
|
||||
update: (id: number | string, data: Record<string, unknown>) => 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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<string, string | number | boolean | undefined>) =>
|
||||
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<string, unknown>) => 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 }),
|
||||
};
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useUserStore } from '@/store';
|
||||
|
||||
const BASE_URL = '/api/v1';
|
||||
const API_PREFIX = '/api/v1';
|
||||
|
||||
// 后端统一响应格式
|
||||
export interface ApiResponse<T = unknown> {
|
||||
@@ -17,14 +17,33 @@ export interface ListResponse<T = unknown> {
|
||||
|
||||
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<T = any>(
|
||||
method: string,
|
||||
url: string,
|
||||
body?: Json,
|
||||
params?: Record<string, string | number | boolean | undefined>
|
||||
params?: Record<string, string | number | boolean | undefined>,
|
||||
): Promise<T> {
|
||||
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<T = any>(
|
||||
if (qs) fullUrl += '?' + qs;
|
||||
}
|
||||
|
||||
const token = useUserStore.getState().token || localStorage.getItem('token');
|
||||
|
||||
const token = getAuthToken();
|
||||
const headers: Record<string, string> = {
|
||||
'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: <T = any>(url: string, params?: Record<string, string | number | boolean | undefined>) =>
|
||||
request<T>('GET', url, undefined, params),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
post: <T = any>(url: string, body?: Json) =>
|
||||
request<T>('POST', url, body),
|
||||
post: <T = any>(url: string, body?: Json) => request<T>('POST', url, body),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
put: <T = any>(url: string, body?: Json) =>
|
||||
request<T>('PUT', url, body),
|
||||
put: <T = any>(url: string, body?: Json) => request<T>('PUT', url, body),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete: <T = any>(url: string, body?: Json) =>
|
||||
request<T>('DELETE', url, body),
|
||||
delete: <T = any>(url: string, body?: Json) => request<T>('DELETE', url, body),
|
||||
|
||||
getRaw: async (url: string): Promise<string> => {
|
||||
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<unknown> => {
|
||||
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<void> => {
|
||||
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 <T = any>(url: string, formData: FormData): Promise<T> => {
|
||||
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;
|
||||
Reference in new issue
Block a user