fix: align frontend interfaces with backend DTO field names
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown>;
|
||||
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();
|
||||
|
||||
@@ -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<void>(`/database/queries/${pid}`, { method: 'DELETE' });
|
||||
await adminFetch<void>(`/database/queries/${pid}/kill`, { method: 'POST' });
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'database', 'queries'] }),
|
||||
});
|
||||
|
||||
@@ -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<IndicesPageResponse>(
|
||||
`/opensearch/indices${qs ? `?${qs}` : ''}`,
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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>('/thresholds'),
|
||||
queryFn: () => adminFetch<ThresholdConfig>('/thresholds'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveThresholds() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (body: Thresholds) => {
|
||||
await adminFetch<Thresholds>('/thresholds', {
|
||||
mutationFn: async (body: ThresholdConfig) => {
|
||||
await adminFetch<ThresholdConfig>('/thresholds', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ function AuditLogContent() {
|
||||
const [category, setCategory] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<number | null>(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 (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.pageTitle}>Audit Log</h1>
|
||||
{data && (
|
||||
<span className={styles.totalCount}>{data.total.toLocaleString()} events</span>
|
||||
<span className={styles.totalCount}>{data.totalCount.toLocaleString()} events</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,7 @@ function AuditLogContent() {
|
||||
|
||||
{audit.isLoading ? (
|
||||
<div className={styles.loading}>Loading...</div>
|
||||
) : !data || data.events.length === 0 ? (
|
||||
) : !data || data.items.length === 0 ? (
|
||||
<div className={styles.emptyState}>No audit events found for the selected filters.</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -138,7 +138,7 @@ function AuditLogContent() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.events.map((event) => (
|
||||
{data.items.map((event) => (
|
||||
<>
|
||||
<tr
|
||||
key={event.id}
|
||||
@@ -191,7 +191,7 @@ function AuditLogContent() {
|
||||
Previous
|
||||
</button>
|
||||
<span className={styles.pageInfo}>
|
||||
Showing {showingFrom}-{showingTo} of {data.total.toLocaleString()}
|
||||
Showing {showingFrom}-{showingTo} of {data.totalCount.toLocaleString()}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useDatabaseQueries,
|
||||
useKillQuery,
|
||||
} from '../../api/queries/admin/database';
|
||||
import { useThresholds, useSaveThresholds, type Thresholds } from '../../api/queries/admin/thresholds';
|
||||
import { useThresholds, useSaveThresholds, type ThresholdConfig } from '../../api/queries/admin/thresholds';
|
||||
import styles from './DatabaseAdminPage.module.css';
|
||||
|
||||
export function DatabaseAdminPage() {
|
||||
@@ -78,13 +78,13 @@ function DatabaseAdminContent() {
|
||||
|
||||
<PoolSection
|
||||
pool={pool}
|
||||
warningPct={thresholds.data?.poolWarningPercent}
|
||||
criticalPct={thresholds.data?.poolCriticalPercent}
|
||||
warningPct={thresholds.data?.database?.connectionPoolWarning}
|
||||
criticalPct={thresholds.data?.database?.connectionPoolCritical}
|
||||
/>
|
||||
<TablesSection tables={tables} />
|
||||
<QueriesSection
|
||||
queries={queries}
|
||||
warningSeconds={thresholds.data?.queryDurationWarningSeconds}
|
||||
warningSeconds={thresholds.data?.database?.queryDurationWarning}
|
||||
/>
|
||||
<MaintenanceSection />
|
||||
<ThresholdsSection thresholds={thresholds.data} />
|
||||
@@ -104,8 +104,8 @@ function PoolSection({
|
||||
const data = pool.data;
|
||||
if (!data) return null;
|
||||
|
||||
const usagePct = data.maxConnections > 0
|
||||
? Math.round((data.activeConnections / data.maxConnections) * 100)
|
||||
const usagePct = data.maxPoolSize > 0
|
||||
? Math.round((data.activeConnections / data.maxPoolSize) * 100)
|
||||
: 0;
|
||||
const barColor =
|
||||
criticalPct && usagePct >= criticalPct ? '#ef4444'
|
||||
@@ -121,7 +121,7 @@ function PoolSection({
|
||||
>
|
||||
<div className={styles.progressContainer}>
|
||||
<div className={styles.progressLabel}>
|
||||
{data.activeConnections} / {data.maxConnections} connections
|
||||
{data.activeConnections} / {data.maxPoolSize} connections
|
||||
<span className={styles.progressPct}>{usagePct}%</span>
|
||||
</div>
|
||||
<div className={styles.progressBar}>
|
||||
@@ -141,11 +141,11 @@ function PoolSection({
|
||||
<span className={styles.metricLabel}>Idle</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.pendingConnections}</span>
|
||||
<span className={styles.metricValue}>{data.pendingThreads}</span>
|
||||
<span className={styles.metricLabel}>Pending</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.maxWaitMillis}ms</span>
|
||||
<span className={styles.metricValue}>{data.maxWaitMs}ms</span>
|
||||
<span className={styles.metricLabel}>Max Wait</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +179,7 @@ function TablesSection({ tables }: { tables: ReturnType<typeof useDatabaseTables
|
||||
{data.map((t) => (
|
||||
<tr key={t.tableName}>
|
||||
<td className={styles.mono}>{t.tableName}</td>
|
||||
<td>{t.rowEstimate.toLocaleString()}</td>
|
||||
<td>{t.rowCount.toLocaleString()}</td>
|
||||
<td>{t.dataSize}</td>
|
||||
<td>{t.indexSize}</td>
|
||||
</tr>
|
||||
@@ -203,7 +203,7 @@ function QueriesSection({
|
||||
const killMutation = useKillQuery();
|
||||
const data = queries.data;
|
||||
|
||||
const warningMs = (warningSeconds ?? 30) * 1000;
|
||||
const warningSec = warningSeconds ?? 30;
|
||||
|
||||
return (
|
||||
<RefreshableCard
|
||||
@@ -230,10 +230,10 @@ function QueriesSection({
|
||||
{data.map((q) => (
|
||||
<tr
|
||||
key={q.pid}
|
||||
className={q.durationMs > warningMs ? styles.rowWarning : undefined}
|
||||
className={q.durationSeconds > warningSec ? styles.rowWarning : undefined}
|
||||
>
|
||||
<td className={styles.mono}>{q.pid}</td>
|
||||
<td>{formatDuration(q.durationMs)}</td>
|
||||
<td>{formatDuration(q.durationSeconds)}</td>
|
||||
<td>{q.state}</td>
|
||||
<td className={styles.queryCell} title={q.query}>
|
||||
{q.query.length > 100 ? `${q.query.slice(0, 100)}...` : q.query}
|
||||
@@ -287,16 +287,19 @@ function MaintenanceSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
const [form, setForm] = useState<Thresholds | null>(null);
|
||||
function ThresholdsSection({ thresholds }: { thresholds?: ThresholdConfig }) {
|
||||
const [form, setForm] = useState<ThresholdConfig | null>(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 updateDb(key: keyof ThresholdConfig['database'], value: number) {
|
||||
setForm((prev) => {
|
||||
const base = prev ?? thresholds!;
|
||||
return { ...base, database: { ...base.database, [key]: value } };
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
@@ -319,8 +322,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.poolWarningPercent}
|
||||
onChange={(e) => update('poolWarningPercent', Number(e.target.value))}
|
||||
value={current.database.connectionPoolWarning}
|
||||
onChange={(e) => updateDb('connectionPoolWarning', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.thresholdField}>
|
||||
@@ -328,8 +331,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.poolCriticalPercent}
|
||||
onChange={(e) => update('poolCriticalPercent', Number(e.target.value))}
|
||||
value={current.database.connectionPoolCritical}
|
||||
onChange={(e) => updateDb('connectionPoolCritical', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.thresholdField}>
|
||||
@@ -337,8 +340,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.queryDurationWarningSeconds}
|
||||
onChange={(e) => update('queryDurationWarningSeconds', Number(e.target.value))}
|
||||
value={current.database.queryDurationWarning}
|
||||
onChange={(e) => updateDb('queryDurationWarning', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.thresholdField}>
|
||||
@@ -346,8 +349,8 @@ function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.queryDurationCriticalSeconds}
|
||||
onChange={(e) => update('queryDurationCriticalSeconds', Number(e.target.value))}
|
||||
value={current.database.queryDurationCritical}
|
||||
onChange={(e) => updateDb('queryDurationCritical', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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`;
|
||||
|
||||
@@ -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 && <span className={styles.metaItem}>v{os.version}</span>}
|
||||
{os?.numberOfNodes !== undefined && (
|
||||
<span className={styles.metaItem}>{os.numberOfNodes} node(s)</span>
|
||||
{os?.nodeCount !== undefined && (
|
||||
<span className={styles.metaItem}>{os.nodeCount} node(s)</span>
|
||||
)}
|
||||
{os?.host && <span className={styles.metaItem}>{os.host}</span>}
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@ function PipelineSection({
|
||||
thresholds,
|
||||
}: {
|
||||
pipeline: ReturnType<typeof usePipelineStats>;
|
||||
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({
|
||||
</div>
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.totalIndexed.toLocaleString()}</span>
|
||||
<span className={styles.metricValue}>{data.indexedCount.toLocaleString()}</span>
|
||||
<span className={styles.metricLabel}>Total Indexed</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.totalFailed.toLocaleString()}</span>
|
||||
<span className={styles.metricValue}>{data.failedCount.toLocaleString()}</span>
|
||||
<span className={styles.metricLabel}>Total Failed</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.avgLatencyMs}ms</span>
|
||||
<span className={styles.metricLabel}>Avg Latency</span>
|
||||
<span className={styles.metricValue}>{data.indexingRate.toFixed(1)}/s</span>
|
||||
<span className={styles.metricLabel}>Indexing Rate</span>
|
||||
</div>
|
||||
</div>
|
||||
</RefreshableCard>
|
||||
@@ -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<string | null>(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 (
|
||||
<RefreshableCard
|
||||
@@ -198,16 +182,6 @@ function IndicesSection() {
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||
/>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={healthFilter}
|
||||
onChange={(e) => { setHealthFilter(e.target.value); setPage(0); }}
|
||||
>
|
||||
<option value="">All Health</option>
|
||||
<option value="green">Green</option>
|
||||
<option value="yellow">Yellow</option>
|
||||
<option value="red">Red</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{!data ? (
|
||||
@@ -218,10 +192,10 @@ function IndicesSection() {
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<SortHeader label="Name" col="name" current={sortBy} dir={sortDir} onSort={toggleSort} />
|
||||
<SortHeader label="Health" col="health" current={sortBy} dir={sortDir} onSort={toggleSort} />
|
||||
<SortHeader label="Docs" col="docsCount" current={sortBy} dir={sortDir} onSort={toggleSort} />
|
||||
<SortHeader label="Size" col="storeSize" current={sortBy} dir={sortDir} onSort={toggleSort} />
|
||||
<th>Name</th>
|
||||
<th>Health</th>
|
||||
<th>Docs</th>
|
||||
<th>Size</th>
|
||||
<th>Shards</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -235,9 +209,9 @@ function IndicesSection() {
|
||||
{idx.health}
|
||||
</span>
|
||||
</td>
|
||||
<td>{idx.docsCount.toLocaleString()}</td>
|
||||
<td>{idx.storeSize}</td>
|
||||
<td>{idx.primaryShards}p / {idx.replicas}r</td>
|
||||
<td>{idx.docCount.toLocaleString()}</td>
|
||||
<td>{idx.size}</td>
|
||||
<td>{idx.primaryShards}p / {idx.replicaShards}r</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
@@ -300,45 +274,22 @@ function IndicesSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function SortHeader({
|
||||
label,
|
||||
col,
|
||||
current,
|
||||
dir,
|
||||
onSort,
|
||||
}: {
|
||||
label: string;
|
||||
col: string;
|
||||
current: string;
|
||||
dir: 'asc' | 'desc';
|
||||
onSort: (col: string) => void;
|
||||
}) {
|
||||
const isActive = current === col;
|
||||
return (
|
||||
<th
|
||||
className={styles.sortableHeader}
|
||||
onClick={() => onSort(col)}
|
||||
>
|
||||
{label}
|
||||
{isActive && <span className={styles.sortArrow}>{dir === 'asc' ? ' \u25B2' : ' \u25BC'}</span>}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
function PerformanceSection({
|
||||
performance,
|
||||
thresholds,
|
||||
}: {
|
||||
performance: ReturnType<typeof usePerformanceStats>;
|
||||
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({
|
||||
<span className={styles.metricLabel}>Request Cache Hit</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.avgQueryLatencyMs}ms</span>
|
||||
<span className={styles.metricValue}>{data.searchLatencyMs.toFixed(1)}ms</span>
|
||||
<span className={styles.metricLabel}>Query Latency</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricValue}>{data.avgIndexLatencyMs}ms</span>
|
||||
<span className={styles.metricValue}>{data.indexingLatencyMs.toFixed(1)}ms</span>
|
||||
<span className={styles.metricLabel}>Index Latency</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,16 +351,19 @@ function OperationsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
const [form, setForm] = useState<Thresholds | null>(null);
|
||||
function OsThresholdsSection({ thresholds }: { thresholds?: ThresholdConfig }) {
|
||||
const [form, setForm] = useState<ThresholdConfig | null>(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 }) {
|
||||
<RefreshableCard title="Thresholds" collapsible defaultCollapsed>
|
||||
<div className={styles.thresholdGrid}>
|
||||
<div className={styles.thresholdField}>
|
||||
<label className={styles.thresholdLabel}>Queue Warning %</label>
|
||||
<label className={styles.thresholdLabel}>Queue Warning</label>
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.osQueueWarningPercent}
|
||||
onChange={(e) => update('osQueueWarningPercent', Number(e.target.value))}
|
||||
value={current.opensearch.queueDepthWarning}
|
||||
onChange={(e) => updateOs('queueDepthWarning', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.thresholdField}>
|
||||
<label className={styles.thresholdLabel}>Queue Critical %</label>
|
||||
<label className={styles.thresholdLabel}>Queue Critical</label>
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.osQueueCriticalPercent}
|
||||
onChange={(e) => update('osQueueCriticalPercent', Number(e.target.value))}
|
||||
value={current.opensearch.queueDepthCritical}
|
||||
onChange={(e) => updateOs('queueDepthCritical', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.thresholdField}>
|
||||
@@ -449,8 +403,8 @@ function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.osHeapWarningPercent}
|
||||
onChange={(e) => update('osHeapWarningPercent', Number(e.target.value))}
|
||||
value={current.opensearch.jvmHeapWarning}
|
||||
onChange={(e) => updateOs('jvmHeapWarning', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.thresholdField}>
|
||||
@@ -458,8 +412,8 @@ function OsThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
|
||||
<input
|
||||
type="number"
|
||||
className={styles.thresholdInput}
|
||||
value={current.osHeapCriticalPercent}
|
||||
onChange={(e) => update('osHeapCriticalPercent', Number(e.target.value))}
|
||||
value={current.opensearch.jvmHeapCritical}
|
||||
onChange={(e) => updateOs('jvmHeapCritical', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user