From 25ca8d513232e379b3b38641343ed1408d06576c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:47:44 +0100 Subject: [PATCH] feat: show log indices on OpenSearch admin page Add prefix query parameter to /admin/opensearch/indices endpoint so the UI can fetch execution and log indices separately. OpenSearch admin page now shows two card sections: Execution Indices and Log Indices, each with doc count and size summary. Page restyled with CSS module replacing inline styles. Delete endpoint also allows log index deletion. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controller/OpenSearchAdminController.java | 14 +++-- ui/src/api/queries/admin/opensearch.ts | 5 +- .../Admin/OpenSearchAdminPage.module.css | 63 +++++++++++++++++++ ui/src/pages/Admin/OpenSearchAdminPage.tsx | 63 ++++++++++++------- 4 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 ui/src/pages/Admin/OpenSearchAdminPage.module.css diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OpenSearchAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OpenSearchAdminController.java index a7ff7fc2..11b7f135 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OpenSearchAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OpenSearchAdminController.java @@ -49,12 +49,14 @@ public class OpenSearchAdminController { private final ObjectMapper objectMapper; private final String opensearchUrl; private final String indexPrefix; + private final String logIndexPrefix; public OpenSearchAdminController(OpenSearchClient client, RestClient restClient, SearchIndexerStats indexerStats, AuditService auditService, ObjectMapper objectMapper, @Value("${opensearch.url:http://localhost:9200}") String opensearchUrl, - @Value("${opensearch.index-prefix:executions-}") String indexPrefix) { + @Value("${opensearch.index-prefix:executions-}") String indexPrefix, + @Value("${opensearch.log-index-prefix:logs-}") String logIndexPrefix) { this.client = client; this.restClient = restClient; this.indexerStats = indexerStats; @@ -62,6 +64,7 @@ public class OpenSearchAdminController { this.objectMapper = objectMapper; this.opensearchUrl = opensearchUrl; this.indexPrefix = indexPrefix; + this.logIndexPrefix = logIndexPrefix; } @GetMapping("/status") @@ -100,7 +103,8 @@ public class OpenSearchAdminController { public ResponseEntity getIndices( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, - @RequestParam(defaultValue = "") String search) { + @RequestParam(defaultValue = "") String search, + @RequestParam(defaultValue = "executions") String prefix) { try { Response response = restClient.performRequest( new Request("GET", "/_cat/indices?format=json&h=index,health,docs.count,store.size,pri,rep&bytes=b")); @@ -109,10 +113,12 @@ public class OpenSearchAdminController { indices = objectMapper.readTree(is); } + String filterPrefix = "logs".equals(prefix) ? logIndexPrefix : indexPrefix; + List allIndices = new ArrayList<>(); for (JsonNode idx : indices) { String name = idx.path("index").asText(""); - if (!name.startsWith(indexPrefix)) { + if (!name.startsWith(filterPrefix)) { continue; } if (!search.isEmpty() && !name.contains(search)) { @@ -152,7 +158,7 @@ public class OpenSearchAdminController { @Operation(summary = "Delete an OpenSearch index") public ResponseEntity deleteIndex(@PathVariable String name, HttpServletRequest request) { try { - if (!name.startsWith(indexPrefix)) { + if (!name.startsWith(indexPrefix) && !name.startsWith(logIndexPrefix)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete index outside application scope"); } boolean exists = client.indices().exists(r -> r.index(name)).value(); diff --git a/ui/src/api/queries/admin/opensearch.ts b/ui/src/api/queries/admin/opensearch.ts index 38859c2b..19f91b10 100644 --- a/ui/src/api/queries/admin/opensearch.ts +++ b/ui/src/api/queries/admin/opensearch.ts @@ -71,13 +71,14 @@ export function usePipelineStats() { }); } -export function useOpenSearchIndices(page = 0, size = 20, search = '') { +export function useOpenSearchIndices(page = 0, size = 20, search = '', prefix = 'executions') { return useQuery({ - queryKey: ['admin', 'opensearch', 'indices', page, size, search], + queryKey: ['admin', 'opensearch', 'indices', prefix, page, size, search], queryFn: () => { const params = new URLSearchParams(); params.set('page', String(page)); params.set('size', String(size)); + params.set('prefix', prefix); if (search) params.set('search', search); return adminFetch(`/opensearch/indices?${params}`); }, diff --git a/ui/src/pages/Admin/OpenSearchAdminPage.module.css b/ui/src/pages/Admin/OpenSearchAdminPage.module.css new file mode 100644 index 00000000..93403ced --- /dev/null +++ b/ui/src/pages/Admin/OpenSearchAdminPage.module.css @@ -0,0 +1,63 @@ +.statStrip { + display: flex; + gap: 10px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.pipelineCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: 16px 20px; + margin-bottom: 16px; +} + +.pipelineTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +.pipelineMetrics { + display: flex; + gap: 24px; + margin-top: 8px; + font-size: 12px; + color: var(--text-muted); +} + +.pipelineMetrics span { + font-family: var(--font-mono); +} + +.indexSection { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + margin-bottom: 16px; + overflow: hidden; +} + +.indexHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.indexTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.indexMeta { + font-size: 11px; + color: var(--text-muted); + font-family: var(--font-mono); +} diff --git a/ui/src/pages/Admin/OpenSearchAdminPage.tsx b/ui/src/pages/Admin/OpenSearchAdminPage.tsx index 664e10cd..3447517f 100644 --- a/ui/src/pages/Admin/OpenSearchAdminPage.tsx +++ b/ui/src/pages/Admin/OpenSearchAdminPage.tsx @@ -1,13 +1,14 @@ -import { StatCard, Card, DataTable, Badge, ProgressBar, Spinner } from '@cameleer/design-system'; +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 { useState } from 'react'; +import styles from './OpenSearchAdminPage.module.css'; export default function OpenSearchAdminPage() { const { data: status } = useOpenSearchStatus(); const { data: pipeline } = usePipelineStats(); const { data: perf } = useOpenSearchPerformance(); - const { data: indicesData } = useOpenSearchIndices(); + const { data: execIndices } = useOpenSearchIndices(0, 50, '', 'executions'); + const { data: logIndices } = useOpenSearchIndices(0, 50, '', 'logs'); const deleteIndex = useDeleteIndex(); const indexColumns: Column[] = [ @@ -20,37 +21,55 @@ export default function OpenSearchAdminPage() { return (
-

OpenSearch Administration

- -
+
- - + +
{pipeline && ( - -
-

Indexing Pipeline

- -
- Queue: {pipeline.queueDepth}/{pipeline.maxQueueSize} - Indexed: {pipeline.indexedCount} - Failed: {pipeline.failedCount} - Rate: {pipeline.indexingRate}/s -
+
+
Indexing Pipeline
+ +
+ Queue: {pipeline.queueDepth}/{pipeline.maxQueueSize} + Indexed: {pipeline.indexedCount.toLocaleString()} + Failed: {pipeline.failedCount} + Rate: {pipeline.indexingRate}/s
- +
)} -
-

Indices

+
+
+ Execution Indices ({execIndices?.totalIndices ?? 0}) + + {execIndices ? `${execIndices.totalDocs.toLocaleString()} docs \u00b7 ${execIndices.totalSize}` : ''} + +
({ ...i, id: i.name }))} + data={(execIndices?.indices || []).map((i: any) => ({ ...i, id: i.name }))} sortable pageSize={20} + flush + /> +
+ +
+
+ Log Indices ({logIndices?.totalIndices ?? 0}) + + {logIndices ? `${logIndices.totalDocs.toLocaleString()} docs \u00b7 ${logIndices.totalSize}` : ''} + +
+ ({ ...i, id: i.name }))} + sortable + pageSize={20} + flush />