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:
hsiegeln
2026-03-17 16:36:11 +01:00
parent 329e4b0b16
commit 038b663b8c
7 changed files with 153 additions and 168 deletions

View File

@@ -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"

View File

@@ -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`;

View File

@@ -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>