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:
1 parent
d19a03d427
commit
f45e203e21
196 files changed
+3228
-2018
No files matched your search
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user