diff --git a/ui/src/api/queries/admin/audit.ts b/ui/src/api/queries/admin/audit.ts index 2ea3399a..46edd70f 100644 --- a/ui/src/api/queries/admin/audit.ts +++ b/ui/src/api/queries/admin/audit.ts @@ -2,14 +2,16 @@ import { useQuery } from '@tanstack/react-query'; import { adminFetch } from './admin-api'; export interface AuditEvent { - id: string; + id: number; timestamp: string; username: string; - category: string; action: string; + category: string; target: string; - result: string; detail: Record; + result: string; + ipAddress: string; + userAgent: string; } export interface AuditLogParams { @@ -18,13 +20,18 @@ export interface AuditLogParams { username?: string; category?: string; search?: string; + sort?: string; + order?: string; page?: number; size?: number; } export interface AuditLogResponse { - events: AuditEvent[]; - total: number; + items: AuditEvent[]; + totalCount: number; + page: number; + pageSize: number; + totalPages: number; } export function useAuditLog(params: AuditLogParams) { @@ -34,6 +41,8 @@ export function useAuditLog(params: AuditLogParams) { 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.sort) query.set('sort', params.sort); + if (params.order) query.set('order', params.order); if (params.page !== undefined) query.set('page', String(params.page)); if (params.size !== undefined) query.set('size', String(params.size)); const qs = query.toString(); diff --git a/ui/src/api/queries/admin/database.ts b/ui/src/api/queries/admin/database.ts index b83eb7bf..662e888b 100644 --- a/ui/src/api/queries/admin/database.ts +++ b/ui/src/api/queries/admin/database.ts @@ -6,26 +6,29 @@ export interface DatabaseStatus { version: string; host: string; schema: string; + timescaleDb: boolean; } export interface PoolStats { activeConnections: number; idleConnections: number; - pendingConnections: number; - maxConnections: number; - maxWaitMillis: number; + pendingThreads: number; + maxPoolSize: number; + maxWaitMs: number; } export interface TableInfo { tableName: string; - rowEstimate: number; + rowCount: number; dataSize: string; indexSize: string; + dataSizeBytes: number; + indexSizeBytes: number; } export interface ActiveQuery { pid: number; - durationMs: number; + durationSeconds: number; state: string; query: string; } @@ -64,7 +67,7 @@ export function useKillQuery() { const qc = useQueryClient(); return useMutation({ mutationFn: async (pid: number) => { - await adminFetch(`/database/queries/${pid}`, { method: 'DELETE' }); + await adminFetch(`/database/queries/${pid}/kill`, { method: 'POST' }); }, 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 index 15b5133b..0f2ecaa3 100644 --- a/ui/src/api/queries/admin/opensearch.ts +++ b/ui/src/api/queries/admin/opensearch.ts @@ -2,47 +2,54 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { adminFetch } from './admin-api'; export interface OpenSearchStatus { - connected: boolean; - clusterName: string; + reachable: boolean; clusterHealth: string; version: string; - numberOfNodes: number; + nodeCount: number; host: string; } export interface PipelineStats { queueDepth: number; maxQueueSize: number; - totalIndexed: number; - totalFailed: number; - avgLatencyMs: number; + indexedCount: number; + failedCount: number; + debounceMs: number; + indexingRate: number; + lastIndexedAt: string | null; } export interface IndexInfo { name: string; health: string; - status: string; - docsCount: number; - storeSize: string; + docCount: number; + size: string; + sizeBytes: number; primaryShards: number; - replicas: number; + replicaShards: number; +} + +export interface IndicesPageResponse { + indices: IndexInfo[]; + totalIndices: number; + totalDocs: number; + totalSize: string; + page: number; + pageSize: number; + totalPages: number; } export interface PerformanceStats { queryCacheHitRate: number; requestCacheHitRate: number; - avgQueryLatencyMs: number; - avgIndexLatencyMs: number; - jvmHeapUsedPercent: number; + searchLatencyMs: number; + indexingLatencyMs: number; jvmHeapUsedBytes: number; jvmHeapMaxBytes: number; } export interface IndicesParams { search?: string; - health?: string; - sortBy?: string; - sortDir?: 'asc' | 'desc'; page?: number; size?: number; } @@ -65,9 +72,6 @@ export function usePipelineStats() { 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(); @@ -75,7 +79,7 @@ export function useIndices(params: IndicesParams) { return useQuery({ queryKey: ['admin', 'opensearch', 'indices', params], queryFn: () => - adminFetch<{ indices: IndexInfo[]; total: number }>( + adminFetch( `/opensearch/indices${qs ? `?${qs}` : ''}`, ), }); diff --git a/ui/src/api/queries/admin/thresholds.ts b/ui/src/api/queries/admin/thresholds.ts index ffcc09eb..3a7aaa1d 100644 --- a/ui/src/api/queries/admin/thresholds.ts +++ b/ui/src/api/queries/admin/thresholds.ts @@ -1,29 +1,41 @@ 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 interface DatabaseThresholds { + connectionPoolWarning: number; + connectionPoolCritical: number; + queryDurationWarning: number; + queryDurationCritical: number; +} + +export interface OpenSearchThresholds { + clusterHealthWarning: string; + clusterHealthCritical: string; + queueDepthWarning: number; + queueDepthCritical: number; + jvmHeapWarning: number; + jvmHeapCritical: number; + failedDocsWarning: number; + failedDocsCritical: number; +} + +export interface ThresholdConfig { + database: DatabaseThresholds; + opensearch: OpenSearchThresholds; } export function useThresholds() { return useQuery({ queryKey: ['admin', 'thresholds'], - queryFn: () => adminFetch('/thresholds'), + queryFn: () => adminFetch('/thresholds'), }); } export function useSaveThresholds() { const qc = useQueryClient(); return useMutation({ - mutationFn: async (body: Thresholds) => { - await adminFetch('/thresholds', { + mutationFn: async (body: ThresholdConfig) => { + await adminFetch('/thresholds', { method: 'PUT', body: JSON.stringify(body), }); diff --git a/ui/src/pages/admin/AuditLogPage.tsx b/ui/src/pages/admin/AuditLogPage.tsx index cf39e5fa..1aa1f3ad 100644 --- a/ui/src/pages/admin/AuditLogPage.tsx +++ b/ui/src/pages/admin/AuditLogPage.tsx @@ -36,7 +36,7 @@ function AuditLogContent() { const [category, setCategory] = useState(''); const [search, setSearch] = useState(''); const [page, setPage] = useState(0); - const [expandedRow, setExpandedRow] = useState(null); + const [expandedRow, setExpandedRow] = useState(null); const pageSize = 25; const params: AuditLogParams = { @@ -51,16 +51,16 @@ function AuditLogContent() { const audit = useAuditLog(params); const data = audit.data; - const totalPages = data ? Math.ceil(data.total / pageSize) : 0; - const showingFrom = data && data.total > 0 ? page * pageSize + 1 : 0; - const showingTo = data ? Math.min((page + 1) * pageSize, data.total) : 0; + const totalPages = data?.totalPages ?? 0; + const showingFrom = data && data.totalCount > 0 ? page * pageSize + 1 : 0; + const showingTo = data ? Math.min((page + 1) * pageSize, data.totalCount) : 0; return (

Audit Log

{data && ( - {data.total.toLocaleString()} events + {data.totalCount.toLocaleString()} events )}
@@ -121,7 +121,7 @@ function AuditLogContent() { {audit.isLoading ? (
Loading...
- ) : !data || data.events.length === 0 ? ( + ) : !data || data.items.length === 0 ? (
No audit events found for the selected filters.
) : ( <> @@ -138,7 +138,7 @@ function AuditLogContent() { - {data.events.map((event) => ( + {data.items.map((event) => ( <> - Showing {showingFrom}-{showingTo} of {data.total.toLocaleString()} + Showing {showingFrom}-{showingTo} of {data.totalCount.toLocaleString()}
@@ -328,8 +331,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { update('poolCriticalPercent', Number(e.target.value))} + value={current.database.connectionPoolCritical} + onChange={(e) => updateDb('connectionPoolCritical', Number(e.target.value))} />
@@ -337,8 +340,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { update('queryDurationWarningSeconds', Number(e.target.value))} + value={current.database.queryDurationWarning} + onChange={(e) => updateDb('queryDurationWarning', Number(e.target.value))} />
@@ -346,8 +349,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { update('queryDurationCriticalSeconds', Number(e.target.value))} + value={current.database.queryDurationCritical} + onChange={(e) => updateDb('queryDurationCritical', Number(e.target.value))} />
@@ -370,9 +373,9 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { ); } -function formatDuration(ms: number): string { - if (ms < 1000) return `${ms}ms`; - const s = Math.floor(ms / 1000); +function formatDuration(seconds: number): string { + if (seconds < 1) return `${Math.round(seconds * 1000)}ms`; + const s = Math.floor(seconds); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); return `${m}m ${s % 60}s`; diff --git a/ui/src/pages/admin/OpenSearchAdminPage.tsx b/ui/src/pages/admin/OpenSearchAdminPage.tsx index 45653e05..f069acc8 100644 --- a/ui/src/pages/admin/OpenSearchAdminPage.tsx +++ b/ui/src/pages/admin/OpenSearchAdminPage.tsx @@ -11,7 +11,7 @@ import { useDeleteIndex, type IndicesParams, } from '../../api/queries/admin/opensearch'; -import { useThresholds, useSaveThresholds, type Thresholds } from '../../api/queries/admin/thresholds'; +import { useThresholds, useSaveThresholds, type ThresholdConfig } from '../../api/queries/admin/thresholds'; import styles from './OpenSearchAdminPage.module.css'; function clusterHealthToStatus(health: string | undefined): Status { @@ -67,8 +67,8 @@ function OpenSearchAdminContent() { label={os?.clusterHealth ?? 'Unknown'} /> {os?.version && v{os.version}} - {os?.numberOfNodes !== undefined && ( - {os.numberOfNodes} node(s) + {os?.nodeCount !== undefined && ( + {os.nodeCount} node(s) )} {os?.host && {os.host}} @@ -100,7 +100,7 @@ function PipelineSection({ thresholds, }: { pipeline: ReturnType; - thresholds?: Thresholds; + thresholds?: ThresholdConfig; }) { const data = pipeline.data; if (!data) return null; @@ -109,8 +109,8 @@ function PipelineSection({ ? Math.round((data.queueDepth / data.maxQueueSize) * 100) : 0; const barColor = - thresholds?.osQueueCriticalPercent && queuePct >= thresholds.osQueueCriticalPercent ? '#ef4444' - : thresholds?.osQueueWarningPercent && queuePct >= thresholds.osQueueWarningPercent ? '#eab308' + thresholds?.opensearch?.queueDepthCritical && data.queueDepth >= thresholds.opensearch.queueDepthCritical ? '#ef4444' + : thresholds?.opensearch?.queueDepthWarning && data.queueDepth >= thresholds.opensearch.queueDepthWarning ? '#eab308' : '#22c55e'; return ( @@ -134,16 +134,16 @@ function PipelineSection({
- {data.totalIndexed.toLocaleString()} + {data.indexedCount.toLocaleString()} Total Indexed
- {data.totalFailed.toLocaleString()} + {data.failedCount.toLocaleString()} Total Failed
- {data.avgLatencyMs}ms - Avg Latency + {data.indexingRate.toFixed(1)}/s + Indexing Rate
@@ -152,18 +152,12 @@ function PipelineSection({ function IndicesSection() { const [search, setSearch] = useState(''); - const [healthFilter, setHealthFilter] = useState(''); - const [sortBy, setSortBy] = useState('name'); - const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); const [page, setPage] = useState(0); const pageSize = 10; const [deleteTarget, setDeleteTarget] = useState(null); const params: IndicesParams = { search: search || undefined, - health: healthFilter || undefined, - sortBy, - sortDir, page, size: pageSize, }; @@ -171,18 +165,8 @@ function IndicesSection() { const indices = useIndices(params); const deleteMutation = useDeleteIndex(); - function toggleSort(col: string) { - if (sortBy === col) { - setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); - } else { - setSortBy(col); - setSortDir('asc'); - } - setPage(0); - } - const data = indices.data; - const totalPages = data ? Math.ceil(data.total / pageSize) : 0; + const totalPages = data?.totalPages ?? 0; return ( { setSearch(e.target.value); setPage(0); }} /> - {!data ? ( @@ -218,10 +192,10 @@ function IndicesSection() { - - - - + + + + @@ -235,9 +209,9 @@ function IndicesSection() { {idx.health} - - - + + + - ); -} - function PerformanceSection({ performance, thresholds, }: { performance: ReturnType; - thresholds?: Thresholds; + thresholds?: ThresholdConfig; }) { const data = performance.data; if (!data) return null; - const heapPct = data.jvmHeapUsedPercent; + const heapPct = data.jvmHeapMaxBytes > 0 + ? Math.round((data.jvmHeapUsedBytes / data.jvmHeapMaxBytes) * 100) + : 0; const heapColor = - thresholds?.osHeapCriticalPercent && heapPct >= thresholds.osHeapCriticalPercent ? '#ef4444' - : thresholds?.osHeapWarningPercent && heapPct >= thresholds.osHeapWarningPercent ? '#eab308' + thresholds?.opensearch?.jvmHeapCritical && heapPct >= thresholds.opensearch.jvmHeapCritical ? '#ef4444' + : thresholds?.opensearch?.jvmHeapWarning && heapPct >= thresholds.opensearch.jvmHeapWarning ? '#eab308' : '#22c55e'; return ( @@ -358,11 +309,11 @@ function PerformanceSection({ Request Cache Hit
- {data.avgQueryLatencyMs}ms + {data.searchLatencyMs.toFixed(1)}ms Query Latency
- {data.avgIndexLatencyMs}ms + {data.indexingLatencyMs.toFixed(1)}ms Index Latency
@@ -400,16 +351,19 @@ function OperationsSection() { ); } -function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { - const [form, setForm] = useState(null); +function OsThresholdsSection({ thresholds }: { thresholds?: ThresholdConfig }) { + const [form, setForm] = useState(null); const saveMutation = useSaveThresholds(); const [status, setStatus] = useState<{ type: 'success' | 'error'; msg: string } | null>(null); const current = form ?? thresholds; if (!current) return null; - function update(key: keyof Thresholds, value: number) { - setForm((prev) => ({ ...(prev ?? thresholds!), [key]: value })); + function updateOs(key: keyof ThresholdConfig['opensearch'], value: number | string) { + setForm((prev) => { + const base = prev ?? thresholds!; + return { ...base, opensearch: { ...base.opensearch, [key]: value } }; + }); } async function handleSave() { @@ -427,21 +381,21 @@ function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
- + update('osQueueWarningPercent', Number(e.target.value))} + value={current.opensearch.queueDepthWarning} + onChange={(e) => updateOs('queueDepthWarning', Number(e.target.value))} />
- + update('osQueueCriticalPercent', Number(e.target.value))} + value={current.opensearch.queueDepthCritical} + onChange={(e) => updateOs('queueDepthCritical', Number(e.target.value))} />
@@ -449,8 +403,8 @@ function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { update('osHeapWarningPercent', Number(e.target.value))} + value={current.opensearch.jvmHeapWarning} + onChange={(e) => updateOs('jvmHeapWarning', Number(e.target.value))} />
@@ -458,8 +412,8 @@ function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) { update('osHeapCriticalPercent', Number(e.target.value))} + value={current.opensearch.jvmHeapCritical} + onChange={(e) => updateOs('jvmHeapCritical', Number(e.target.value))} />
NameHealthDocsSize Shards
{idx.docsCount.toLocaleString()}{idx.storeSize}{idx.primaryShards}p / {idx.replicas}r{idx.docCount.toLocaleString()}{idx.size}{idx.primaryShards}p / {idx.replicaShards}r onSort(col)} - > - {label} - {isActive && {dir === 'asc' ? ' \u25B2' : ' \u25BC'}} -