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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<IndicesPageResponse> 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<IndexInfoResponse> 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<Void> 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();
|
||||
|
||||
@@ -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<IndicesPage>(`/opensearch/indices?${params}`);
|
||||
},
|
||||
|
||||
63
ui/src/pages/Admin/OpenSearchAdminPage.module.css
Normal file
63
ui/src/pages/Admin/OpenSearchAdminPage.module.css
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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<any>[] = [
|
||||
@@ -20,37 +21,55 @@ export default function OpenSearchAdminPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: '1rem' }}>OpenSearch Administration</h2>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
||||
<div className={styles.statStrip}>
|
||||
<StatCard label="Status" value={status?.reachable ? 'Connected' : 'Disconnected'} accent={status?.reachable ? 'success' : 'error'} />
|
||||
<StatCard label="Health" value={status?.clusterHealth ?? '—'} accent={status?.clusterHealth === 'green' ? 'success' : 'warning'} />
|
||||
<StatCard label="Version" value={status?.version ?? '—'} />
|
||||
<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 && (
|
||||
<Card>
|
||||
<div style={{ padding: '1rem' }}>
|
||||
<h3 style={{ marginBottom: '0.5rem' }}>Indexing Pipeline</h3>
|
||||
<ProgressBar value={(pipeline.queueDepth / pipeline.maxQueueSize) * 100} />
|
||||
<div style={{ display: 'flex', gap: '2rem', marginTop: '0.5rem', fontSize: '0.875rem' }}>
|
||||
<span>Queue: {pipeline.queueDepth}/{pipeline.maxQueueSize}</span>
|
||||
<span>Indexed: {pipeline.indexedCount}</span>
|
||||
<span>Failed: {pipeline.failedCount}</span>
|
||||
<span>Rate: {pipeline.indexingRate}/s</span>
|
||||
</div>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<h3 style={{ marginBottom: '0.75rem' }}>Indices</h3>
|
||||
<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={(indicesData?.indices || []).map((i: any) => ({ ...i, id: i.name }))}
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user