feat: remove OpenSearch, add ClickHouse admin page
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 33s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

Remove all OpenSearch code, dependencies, configuration, deployment
manifests, and CI/CD references. Replace the OpenSearch admin page
with a ClickHouse admin page showing cluster status, table sizes,
performance metrics, and indexer pipeline stats.

- Delete 11 OpenSearch Java files (config, search impl, admin controller, DTOs, tests)
- Delete 3 OpenSearch frontend files (admin page, CSS, query hooks)
- Delete deploy/opensearch.yaml K8s manifest
- Remove opensearch Maven dependencies from pom.xml
- Remove opensearch config from application.yml, Dockerfile, docker-compose
- Remove opensearch from CI workflow (secrets, deploy, cleanup steps)
- Simplify ThresholdConfig (remove OpenSearch thresholds, database-only)
- Change default search backend from opensearch to clickhouse
- Add ClickHouseAdminController with /status, /tables, /performance, /pipeline
- Add ClickHouseAdminPage with StatCards, pipeline ProgressBar, tables DataTable
- Update CLAUDE.md, HOWTO.md, and source comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-01 18:56:06 +02:00
parent 5ed7d38bf7
commit 283e38a20d
49 changed files with 356 additions and 1753 deletions

View File

@@ -7,7 +7,7 @@ const ADMIN_TABS = [
{ label: 'OIDC', value: '/admin/oidc' },
{ label: 'App Config', value: '/admin/appconfig' },
{ label: 'Database', value: '/admin/database' },
{ label: 'OpenSearch', value: '/admin/opensearch' },
{ label: 'ClickHouse', value: '/admin/clickhouse' },
];
export default function AdminLayout() {

View File

@@ -33,7 +33,7 @@
font-family: var(--font-mono);
}
.indexSection {
.tableSection {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
@@ -42,7 +42,7 @@
overflow: hidden;
}
.indexHeader {
.tableHeader {
display: flex;
align-items: center;
justify-content: space-between;
@@ -50,13 +50,13 @@
border-bottom: 1px solid var(--border-subtle);
}
.indexTitle {
.tableTitle {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.indexMeta {
.tableMeta {
font-size: 11px;
color: var(--text-muted);
font-family: var(--font-mono);

View File

@@ -0,0 +1,65 @@
import { StatCard, DataTable, ProgressBar } from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useClickHouseStatus, useClickHouseTables, useClickHousePerformance, useIndexerPipeline } from '../../api/queries/admin/clickhouse';
import styles from './ClickHouseAdminPage.module.css';
export default function ClickHouseAdminPage() {
const { data: status, isError: statusError } = useClickHouseStatus();
const { data: tables } = useClickHouseTables();
const { data: perf } = useClickHousePerformance();
const { data: pipeline } = useIndexerPipeline();
const unreachable = statusError || (status && !status.reachable);
const tableColumns: Column<any>[] = [
{ key: 'name', header: 'Table', sortable: true },
{ key: 'engine', header: 'Engine' },
{ key: 'rowCount', header: 'Rows', sortable: true, render: (v) => Number(v).toLocaleString() },
{ key: 'dataSize', header: 'Size', sortable: true },
{ key: 'partitionCount', header: 'Partitions', sortable: true },
];
return (
<div>
<div className={styles.statStrip}>
<StatCard label="Status" value={unreachable ? 'Disconnected' : status ? 'Connected' : '\u2014'} accent={unreachable ? 'error' : status ? 'success' : undefined} />
<StatCard label="Version" value={status?.version ?? '\u2014'} />
<StatCard label="Uptime" value={status?.uptime ?? '\u2014'} />
</div>
{pipeline && (
<div className={styles.pipelineCard}>
<div className={styles.pipelineTitle}>Indexer Pipeline</div>
<ProgressBar value={pipeline.maxQueueSize > 0 ? (pipeline.queueDepth / pipeline.maxQueueSize) * 100 : 0} />
<div className={styles.pipelineMetrics}>
<span>Queue: {pipeline.queueDepth}/{pipeline.maxQueueSize}</span>
<span>Indexed: {pipeline.indexedCount.toLocaleString()}</span>
<span>Failed: {pipeline.failedCount}</span>
<span>Rate: {pipeline.indexingRate.toFixed(1)}/s</span>
</div>
</div>
)}
{perf && (
<div className={styles.statStrip}>
<StatCard label="Select Queries" value={perf.queryCount.toLocaleString()} />
<StatCard label="Insert Queries" value={perf.insertQueryCount.toLocaleString()} />
<StatCard label="Memory" value={perf.memoryUsage} />
<StatCard label="Rows Read" value={perf.readRows.toLocaleString()} />
</div>
)}
<div className={styles.tableSection}>
<div className={styles.tableHeader}>
<span className={styles.tableTitle}>Tables ({(tables || []).length})</span>
</div>
<DataTable
columns={tableColumns}
data={(tables || []).map((t: any) => ({ ...t, id: t.name }))}
sortable
pageSize={20}
flush
/>
</div>
</div>
);
}

View File

@@ -1,78 +0,0 @@
import { StatCard, DataTable, Badge, ProgressBar } from '@cameleer/design-system';
import type { Column } from '@cameleer/design-system';
import { useOpenSearchStatus, usePipelineStats, useOpenSearchIndices, useOpenSearchPerformance, useDeleteIndex } from '../../api/queries/admin/opensearch';
import styles from './OpenSearchAdminPage.module.css';
export default function OpenSearchAdminPage() {
const { data: status, isError: statusError } = useOpenSearchStatus();
const { data: pipeline } = usePipelineStats();
const { data: perf } = useOpenSearchPerformance();
const { data: execIndices } = useOpenSearchIndices(0, 50, '', 'executions');
const { data: logIndices } = useOpenSearchIndices(0, 50, '', 'logs');
const unreachable = statusError || (status && !status.reachable);
const deleteIndex = useDeleteIndex();
const indexColumns: Column<any>[] = [
{ key: 'name', header: 'Index' },
{ key: 'health', header: 'Health', render: (v) => <Badge label={String(v)} color={v === 'green' ? 'success' : v === 'yellow' ? 'warning' : 'error'} /> },
{ key: 'docCount', header: 'Documents', sortable: true, render: (v) => Number(v).toLocaleString() },
{ key: 'size', header: 'Size' },
{ key: 'primaryShards', header: 'Shards' },
];
return (
<div>
<div className={styles.statStrip}>
<StatCard label="Status" value={unreachable ? 'Disconnected' : status ? 'Connected' : '\u2014'} accent={unreachable ? 'error' : status ? 'success' : undefined} />
<StatCard label="Health" value={status?.clusterHealth ?? '\u2014'} accent={status?.clusterHealth === 'green' ? 'success' : 'warning'} />
<StatCard label="Version" value={status?.version ?? '\u2014'} />
<StatCard label="Nodes" value={status?.nodeCount ?? 0} />
</div>
{pipeline && (
<div className={styles.pipelineCard}>
<div className={styles.pipelineTitle}>Indexing Pipeline</div>
<ProgressBar value={(pipeline.queueDepth / pipeline.maxQueueSize) * 100} />
<div className={styles.pipelineMetrics}>
<span>Queue: {pipeline.queueDepth}/{pipeline.maxQueueSize}</span>
<span>Indexed: {pipeline.indexedCount.toLocaleString()}</span>
<span>Failed: {pipeline.failedCount}</span>
<span>Rate: {pipeline.indexingRate}/s</span>
</div>
</div>
)}
<div className={styles.indexSection}>
<div className={styles.indexHeader}>
<span className={styles.indexTitle}>Execution Indices ({execIndices?.totalIndices ?? 0})</span>
<span className={styles.indexMeta}>
{execIndices ? `${execIndices.totalDocs.toLocaleString()} docs \u00b7 ${execIndices.totalSize}` : ''}
</span>
</div>
<DataTable
columns={indexColumns}
data={(execIndices?.indices || []).map((i: any) => ({ ...i, id: i.name }))}
sortable
pageSize={20}
flush
/>
</div>
<div className={styles.indexSection}>
<div className={styles.indexHeader}>
<span className={styles.indexTitle}>Log Indices ({logIndices?.totalIndices ?? 0})</span>
<span className={styles.indexMeta}>
{logIndices ? `${logIndices.totalDocs.toLocaleString()} docs \u00b7 ${logIndices.totalSize}` : ''}
</span>
</div>
<DataTable
columns={indexColumns}
data={(logIndices?.indices || []).map((i: any) => ({ ...i, id: i.name }))}
sortable
pageSize={20}
flush
/>
</div>
</div>
);
}