refactor(frontend): P1-P2 架构统一 — utils/hooks/api 模块化

P1:
- utils/storage.ts、theme.ts、validate.ts + index 统一出口
- request/store/useTheme 接入 storage;清零 confirm/alert
- PipelineManager、TerminalAuditTab 拆分子模块(<700行红线)

P2:
- api/monitor、api/deploy 子目录化
- hooks/api 扩展:servers/pipelines/alerts/audit/sessionCommands
- PipelineManager、AuditOperationsTab 接入 TanStack Query
This commit is contained in:
Logan.Li committed 2026-06-26 00:18:14 +08:00
1 parent d19a03d427
commit f45e203e21
196 files changed
+3228 -2018

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
// hooks/api 统一出口 // Logan.Li
export { queryKeys } from './queryKeys';
export { unwrapList, unwrapTotal } from './unwrap';
export { useTerminalSessions, useTerminalSessionMutations, type TerminalSession } from './useTerminalSessions';
export { useHostList, useHostHistory, useHealthProbes, usePortPolicy } from './useHostMonitor';
export { useServers, useServerMutations } from './useServers';
export { usePipelines, usePipelineRuns, usePipelineMutations } from './usePipelines';
export { useAlertRules, useAlerts, useAlertMutations } from './useAlerts';
export { useAuditLogs } from './useAuditLogs';
export { useSessionCommands, type SessionCommand } from './useSessionCommands';
+9 -1
View File
@@ -1,4 +1,4 @@
// TanStack Query key 工厂 — 统一缓存键 // Logan.Li
// TanStack Query key 工厂 // Logan.Li
export const queryKeys = {
terminalSessions: (params?: Record<string, unknown>) => ['terminal-sessions', params] as const,
@@ -8,4 +8,12 @@ export const queryKeys = {
probes: (hostname: string) => ['host-monitor', 'probes', hostname] as const,
portPolicy: (hostname: string) => ['host-monitor', 'port-policy', hostname] as const,
},
servers: (params?: Record<string, unknown>) => ['servers', params] as const,
pipelines: (params?: Record<string, unknown>) => ['pipelines', params] as const,
pipelineRuns: (code: string) => ['pipeline-runs', code] as const,
alerts: (params?: Record<string, unknown>) => ['alerts', params] as const,
alertRules: () => ['alert-rules'] as const,
auditLogs: (params?: Record<string, unknown>) => ['audit-logs', params] as const,
sessionCommands: (params?: Record<string, unknown>) => ['session-commands', params] as const,
k8sClusters: () => ['k8s-clusters'] as const,
};
+18
View File
@@ -0,0 +1,18 @@
// 列表响应解包 — http.ts 已取 data,兼容数组/list 形态 // Logan.Li
export function unwrapList<T>(data: unknown): T[] {
if (Array.isArray(data)) return data;
if (data && typeof data === 'object') {
const obj = data as Record<string, unknown>;
if (Array.isArray(obj.list)) return obj.list as T[];
if (Array.isArray(obj.data)) return obj.data as T[];
}
return [];
}
export function unwrapTotal(data: unknown): number {
if (data && typeof data === 'object' && 'total' in data) {
return Number((data as { total: number }).total) || 0;
}
return unwrapList(data).length;
}
+51
View File
@@ -0,0 +1,51 @@
// 告警规则 / 告警事件 — TanStack Query // Logan.Li
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { alertApi, alertCenterApi } from '@/api';
import { queryKeys } from './queryKeys';
import { unwrapList } from './unwrap';
export function useAlertRules() {
return useQuery({
queryKey: queryKeys.alertRules(),
queryFn: async () => {
const resp = await alertApi.getRules();
return unwrapList<Record<string, unknown>>(resp);
},
staleTime: 30_000,
});
}
export function useAlerts(params: { page?: number; page_size?: number; status?: string; severity?: string } = {}) {
return useQuery({
queryKey: queryKeys.alerts(params),
queryFn: async () => {
const resp = await alertCenterApi.getAlerts(params);
return unwrapList<Record<string, unknown>>(resp);
},
staleTime: 15_000,
});
}
export function useAlertMutations() {
const qc = useQueryClient();
const invalidateRules = () => qc.invalidateQueries({ queryKey: queryKeys.alertRules() });
const invalidateAlerts = () => qc.invalidateQueries({ queryKey: ['alerts'] });
const toggleRuleMutation = useMutation({
mutationFn: (id: number | string) => alertApi.toggleRule(id),
onSuccess: invalidateRules,
});
const deleteRuleMutation = useMutation({
mutationFn: (id: number | string) => alertApi.deleteRule(id),
onSuccess: invalidateRules,
});
const ackMutation = useMutation({
mutationFn: (id: number | string) => alertCenterApi.acknowledge(id),
onSuccess: invalidateAlerts,
});
return { toggleRuleMutation, deleteRuleMutation, ackMutation, invalidateRules, invalidateAlerts };
}
+30
View File
@@ -0,0 +1,30 @@
// 审计日志 — TanStack Query // Logan.Li
import { useQuery } from '@tanstack/react-query';
import { auditApi } from '@/api';
import { queryKeys } from './queryKeys';
import { unwrapList, unwrapTotal } from './unwrap';
type AuditParams = {
page?: number;
page_size?: number;
module?: string;
action?: string;
username?: string;
start_time?: string;
end_time?: string;
};
export function useAuditLogs(params: AuditParams = { page: 1, page_size: 50 }) {
return useQuery({
queryKey: queryKeys.auditLogs(params),
queryFn: async () => {
const resp = await auditApi.getLogs(params);
return {
list: unwrapList<Record<string, unknown>>(resp),
total: unwrapTotal(resp),
};
},
staleTime: 20_000,
});
}
+1 -10
View File
@@ -3,16 +3,7 @@
import { useQuery } from '@tanstack/react-query';
import { agentApi } from '@/api';
import { queryKeys } from './queryKeys';
function unwrapList<T>(data: unknown): T[] {
if (Array.isArray(data)) return data;
if (data && typeof data === 'object') {
const obj = data as Record<string, unknown>;
if (Array.isArray(obj.list)) return obj.list as T[];
if (Array.isArray(obj.data)) return obj.data as T[];
}
return [];
}
import { unwrapList } from './unwrap';
export function useHostList() {
return useQuery({
+53
View File
@@ -0,0 +1,53 @@
// 流水线 — TanStack Query // Logan.Li
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { pipelineApi } from '@/api';
import { queryKeys } from './queryKeys';
import { unwrapList } from './unwrap';
type ListParams = { page?: number; page_size?: number };
export function usePipelines(params: ListParams = { page: 1, page_size: 100 }) {
return useQuery({
queryKey: queryKeys.pipelines(params),
queryFn: async () => {
const resp = await pipelineApi.getPipelines(params);
return unwrapList<Record<string, unknown>>(resp);
},
staleTime: 20_000,
});
}
export function usePipelineRuns(code: string, enabled = true) {
return useQuery({
queryKey: queryKeys.pipelineRuns(code),
queryFn: async () => {
const resp = await pipelineApi.getRunsByCode(code, 50);
return unwrapList<Record<string, unknown>>(resp);
},
enabled: !!code && enabled,
staleTime: 10_000,
});
}
export function usePipelineMutations() {
const qc = useQueryClient();
const invalidate = () => qc.invalidateQueries({ queryKey: ['pipelines'] });
const triggerMutation = useMutation({
mutationFn: (code: string) => pipelineApi.triggerByCode(code),
onSuccess: invalidate,
});
const deleteMutation = useMutation({
mutationFn: (id: number | string) => pipelineApi.deletePipeline(id),
onSuccess: invalidate,
});
const stopRunMutation = useMutation({
mutationFn: ({ code, runId }: { code: string; runId: number | string }) =>
pipelineApi.stopRunByCode(code, runId),
});
return { triggerMutation, deleteMutation, stopRunMutation, invalidate };
}
+41
View File
@@ -0,0 +1,41 @@
// 服务器列表 — TanStack Query // Logan.Li
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { serverApi } from '@/api';
import { queryKeys } from './queryKeys';
import { unwrapList } from './unwrap';
type ListParams = {
page?: number;
page_size?: number;
keyword?: string;
status?: string;
};
export function useServers(params: ListParams = { page: 1, page_size: 100 }) {
return useQuery({
queryKey: queryKeys.servers(params),
queryFn: async () => {
const resp = await serverApi.getServers(params);
return unwrapList<Record<string, unknown>>(resp);
},
staleTime: 30_000,
});
}
export function useServerMutations() {
const qc = useQueryClient();
const invalidate = () => qc.invalidateQueries({ queryKey: ['servers'] });
const deleteMutation = useMutation({
mutationFn: (id: number | string) => serverApi.deleteServer(id),
onSuccess: invalidate,
});
const batchDeleteMutation = useMutation({
mutationFn: (ids: number[]) => serverApi.batchDelete(ids),
onSuccess: invalidate,
});
return { deleteMutation, batchDeleteMutation, invalidate };
}
@@ -0,0 +1,33 @@
// 会话命令列表 — TanStack Query // Logan.Li
import { useQuery } from '@tanstack/react-query';
import { sessionCommandApi } from '@/api';
import { queryKeys } from './queryKeys';
import { unwrapList } from './unwrap';
export interface SessionCommand {
id: number;
session_id: number;
username: string;
command: string;
exit_code: number;
output_size: number;
created_at: string;
}
type ListParams = {
page?: number;
page_size?: number;
session_id?: number;
username?: string;
};
export function useSessionCommands(params: ListParams = { page: 1, page_size: 200 }) {
return useQuery({
queryKey: queryKeys.sessionCommands(params),
queryFn: async () => {
const resp = await sessionCommandApi.list(params);
return unwrapList<SessionCommand>(resp);
},
});
}
@@ -3,6 +3,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { terminalSessionApi } from '@/api';
import { queryKeys } from './queryKeys';
import { unwrapList } from './unwrap';
export interface TerminalSession {
id: number;
@@ -28,14 +29,6 @@ type ListParams = {
status?: string;
};
function unwrapList<T>(data: unknown): T[] {
if (Array.isArray(data)) return data;
if (data && typeof data === 'object' && 'list' in data) {
return (data as { list: T[] }).list ?? [];
}
return [];
}
export function useTerminalSessions(params: ListParams = { page: 1, page_size: 100 }) {
return useQuery({
queryKey: queryKeys.terminalSessions(params),
+18 -64
View File
@@ -1,38 +1,16 @@
import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
import {
type Theme,
type ThemeOption,
THEME_OPTIONS,
getInitialTheme,
persistTheme,
applyThemeToDocument,
getNextTheme,
} from '@/utils/theme';
export type Theme =
| 'ocean-deep'
| 'light-professional'
| 'midnight-purple'
| 'pine-green'
| 'orange-vitality';
export interface ThemeOption {
id: Theme;
name: string;
description: string;
isDark: boolean;
primaryColor: string;
}
export const THEME_OPTIONS: ThemeOption[] = [
{ id: 'ocean-deep', name: '深海蓝', description: '专业稳重的深蓝色调', isDark: true, primaryColor: '#3b82f6' },
{ id: 'light-professional', name: '明亮白', description: '清爽干净的浅色主题', isDark: false, primaryColor: '#2563eb' },
{ id: 'midnight-purple', name: '午夜紫', description: '神秘优雅的紫色调', isDark: true, primaryColor: '#a855f7' },
{ id: 'pine-green', name: '松林绿', description: '自然清新的绿色主题', isDark: true, primaryColor: '#22c55e' },
{ id: 'orange-vitality', name: '活力橙', description: '温暖活力的橙色主题', isDark: true, primaryColor: '#f97316' },
];
const THEME_ORDER: Theme[] = THEME_OPTIONS.map((o) => o.id);
const DEFAULT_THEME: Theme = 'ocean-deep';
const STORAGE_KEY = 'aiops-theme';
// Backward compatibility: map old theme names to new ones
function migrateLegacyTheme(value: string | null): Theme | null {
if (value === 'dark') return 'ocean-deep';
if (value === 'light') return 'light-professional';
return null;
}
export type { Theme, ThemeOption };
export { THEME_OPTIONS };
interface ThemeContextType {
theme: Theme;
@@ -42,53 +20,29 @@ interface ThemeContextType {
const ThemeContext = createContext<ThemeContextType | null>(null);
function getInitialTheme(): Theme {
if (typeof window === 'undefined') return DEFAULT_THEME;
const stored = localStorage.getItem(STORAGE_KEY);
const migrated = migrateLegacyTheme(stored);
if (migrated) {
// Persist the migrated value so we don't migrate every time
localStorage.setItem(STORAGE_KEY, migrated);
return migrated;
}
if (stored && THEME_ORDER.includes(stored as Theme)) {
return stored as Theme;
}
return DEFAULT_THEME;
}
function applyTheme(theme: Theme) {
document.documentElement.setAttribute('data-theme', theme);
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
useEffect(() => {
applyTheme(theme);
applyThemeToDocument(theme);
}, [theme]);
const toggleTheme = useCallback(() => {
setThemeState((prev) => {
const idx = THEME_ORDER.indexOf(prev);
const next = THEME_ORDER[(idx + 1) % THEME_ORDER.length];
localStorage.setItem(STORAGE_KEY, next);
const next = getNextTheme(prev);
persistTheme(next);
return next;
});
}, []);
const setTheme = useCallback((t: Theme) => {
localStorage.setItem(STORAGE_KEY, t);
persistTheme(t);
setThemeState(t);
}, []);
const value = useMemo(
() => ({
theme,
toggleTheme,
setTheme,
}),
[theme, toggleTheme, setTheme]
() => ({ theme, toggleTheme, setTheme }),
[theme, toggleTheme, setTheme],
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
@@ -98,4 +52,4 @@ export function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
}