diff --git a/ui/src/api/queries/admin/admin-api.ts b/ui/src/api/queries/admin/admin-api.ts new file mode 100644 index 00000000..20c71eb7 --- /dev/null +++ b/ui/src/api/queries/admin/admin-api.ts @@ -0,0 +1,22 @@ +import { config } from '../../../config'; +import { useAuthStore } from '../../../auth/auth-store'; + +export async function adminFetch(path: string, options?: RequestInit): Promise { + const token = useAuthStore.getState().accessToken; + const res = await fetch(`${config.apiBaseUrl}/admin${path}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + 'X-Cameleer-Protocol-Version': '1', + ...options?.headers, + }, + }); + if (res.status === 401 || res.status === 403) { + useAuthStore.getState().logout(); + throw new Error('Unauthorized'); + } + if (!res.ok) throw new Error(`API error: ${res.status}`); + if (res.status === 204) return undefined as T; + return res.json(); +} diff --git a/ui/src/api/queries/admin/audit.ts b/ui/src/api/queries/admin/audit.ts new file mode 100644 index 00000000..2ea3399a --- /dev/null +++ b/ui/src/api/queries/admin/audit.ts @@ -0,0 +1,45 @@ +import { useQuery } from '@tanstack/react-query'; +import { adminFetch } from './admin-api'; + +export interface AuditEvent { + id: string; + timestamp: string; + username: string; + category: string; + action: string; + target: string; + result: string; + detail: Record; +} + +export interface AuditLogParams { + from?: string; + to?: string; + username?: string; + category?: string; + search?: string; + page?: number; + size?: number; +} + +export interface AuditLogResponse { + events: AuditEvent[]; + total: number; +} + +export function useAuditLog(params: AuditLogParams) { + const query = new URLSearchParams(); + if (params.from) query.set('from', params.from); + if (params.to) query.set('to', params.to); + if (params.username) query.set('username', params.username); + if (params.category) query.set('category', params.category); + if (params.search) query.set('search', params.search); + if (params.page !== undefined) query.set('page', String(params.page)); + if (params.size !== undefined) query.set('size', String(params.size)); + const qs = query.toString(); + + return useQuery({ + queryKey: ['admin', 'audit', params], + queryFn: () => adminFetch(`/audit${qs ? `?${qs}` : ''}`), + }); +} diff --git a/ui/src/api/queries/admin/database.ts b/ui/src/api/queries/admin/database.ts new file mode 100644 index 00000000..b83eb7bf --- /dev/null +++ b/ui/src/api/queries/admin/database.ts @@ -0,0 +1,71 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { adminFetch } from './admin-api'; + +export interface DatabaseStatus { + connected: boolean; + version: string; + host: string; + schema: string; +} + +export interface PoolStats { + activeConnections: number; + idleConnections: number; + pendingConnections: number; + maxConnections: number; + maxWaitMillis: number; +} + +export interface TableInfo { + tableName: string; + rowEstimate: number; + dataSize: string; + indexSize: string; +} + +export interface ActiveQuery { + pid: number; + durationMs: number; + state: string; + query: string; +} + +export function useDatabaseStatus() { + return useQuery({ + queryKey: ['admin', 'database', 'status'], + queryFn: () => adminFetch('/database/status'), + }); +} + +export function useDatabasePool() { + return useQuery({ + queryKey: ['admin', 'database', 'pool'], + queryFn: () => adminFetch('/database/pool'), + refetchInterval: 15000, + }); +} + +export function useDatabaseTables() { + return useQuery({ + queryKey: ['admin', 'database', 'tables'], + queryFn: () => adminFetch('/database/tables'), + }); +} + +export function useDatabaseQueries() { + return useQuery({ + queryKey: ['admin', 'database', 'queries'], + queryFn: () => adminFetch('/database/queries'), + refetchInterval: 15000, + }); +} + +export function useKillQuery() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (pid: number) => { + await adminFetch(`/database/queries/${pid}`, { method: 'DELETE' }); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'database', 'queries'] }), + }); +} diff --git a/ui/src/api/queries/admin/opensearch.ts b/ui/src/api/queries/admin/opensearch.ts new file mode 100644 index 00000000..15b5133b --- /dev/null +++ b/ui/src/api/queries/admin/opensearch.ts @@ -0,0 +1,102 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { adminFetch } from './admin-api'; + +export interface OpenSearchStatus { + connected: boolean; + clusterName: string; + clusterHealth: string; + version: string; + numberOfNodes: number; + host: string; +} + +export interface PipelineStats { + queueDepth: number; + maxQueueSize: number; + totalIndexed: number; + totalFailed: number; + avgLatencyMs: number; +} + +export interface IndexInfo { + name: string; + health: string; + status: string; + docsCount: number; + storeSize: string; + primaryShards: number; + replicas: number; +} + +export interface PerformanceStats { + queryCacheHitRate: number; + requestCacheHitRate: number; + avgQueryLatencyMs: number; + avgIndexLatencyMs: number; + jvmHeapUsedPercent: number; + jvmHeapUsedBytes: number; + jvmHeapMaxBytes: number; +} + +export interface IndicesParams { + search?: string; + health?: string; + sortBy?: string; + sortDir?: 'asc' | 'desc'; + page?: number; + size?: number; +} + +export function useOpenSearchStatus() { + return useQuery({ + queryKey: ['admin', 'opensearch', 'status'], + queryFn: () => adminFetch('/opensearch/status'), + }); +} + +export function usePipelineStats() { + return useQuery({ + queryKey: ['admin', 'opensearch', 'pipeline'], + queryFn: () => adminFetch('/opensearch/pipeline'), + refetchInterval: 15000, + }); +} + +export function useIndices(params: IndicesParams) { + const query = new URLSearchParams(); + if (params.search) query.set('search', params.search); + if (params.health) query.set('health', params.health); + if (params.sortBy) query.set('sortBy', params.sortBy); + if (params.sortDir) query.set('sortDir', params.sortDir); + if (params.page !== undefined) query.set('page', String(params.page)); + if (params.size !== undefined) query.set('size', String(params.size)); + const qs = query.toString(); + + return useQuery({ + queryKey: ['admin', 'opensearch', 'indices', params], + queryFn: () => + adminFetch<{ indices: IndexInfo[]; total: number }>( + `/opensearch/indices${qs ? `?${qs}` : ''}`, + ), + }); +} + +export function usePerformanceStats() { + return useQuery({ + queryKey: ['admin', 'opensearch', 'performance'], + queryFn: () => adminFetch('/opensearch/performance'), + refetchInterval: 15000, + }); +} + +export function useDeleteIndex() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (indexName: string) => { + await adminFetch(`/opensearch/indices/${encodeURIComponent(indexName)}`, { + method: 'DELETE', + }); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'opensearch', 'indices'] }), + }); +} diff --git a/ui/src/api/queries/admin/thresholds.ts b/ui/src/api/queries/admin/thresholds.ts new file mode 100644 index 00000000..ffcc09eb --- /dev/null +++ b/ui/src/api/queries/admin/thresholds.ts @@ -0,0 +1,33 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { adminFetch } from './admin-api'; + +export interface Thresholds { + poolWarningPercent: number; + poolCriticalPercent: number; + queryDurationWarningSeconds: number; + queryDurationCriticalSeconds: number; + osQueueWarningPercent: number; + osQueueCriticalPercent: number; + osHeapWarningPercent: number; + osHeapCriticalPercent: number; +} + +export function useThresholds() { + return useQuery({ + queryKey: ['admin', 'thresholds'], + queryFn: () => adminFetch('/thresholds'), + }); +} + +export function useSaveThresholds() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (body: Thresholds) => { + await adminFetch('/thresholds', { + method: 'PUT', + body: JSON.stringify(body), + }); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'thresholds'] }), + }); +}