Fix command palette: agent ID propagation, result selection, and scope tabs
- Propagate authenticated agent identity through write buffers via TaggedExecution/TaggedDiagram wrappers so ClickHouse rows get real agent IDs instead of empty strings - Add execution_id to text search LIKE clause so selecting an execution by ID in the palette actually finds it - Clear status filter to all three statuses on palette selection so the chosen execution/agent isn't filtered out - Add disabled Routes and Exchanges scope tabs with "coming soon" state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
package com.cameleer3.server.app.config;
|
package com.cameleer3.server.app.config;
|
||||||
|
|
||||||
import com.cameleer3.common.graph.RouteGraph;
|
|
||||||
import com.cameleer3.common.model.RouteExecution;
|
|
||||||
import com.cameleer3.server.core.ingestion.IngestionService;
|
import com.cameleer3.server.core.ingestion.IngestionService;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedDiagram;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedExecution;
|
||||||
import com.cameleer3.server.core.ingestion.WriteBuffer;
|
import com.cameleer3.server.core.ingestion.WriteBuffer;
|
||||||
import com.cameleer3.server.core.storage.model.MetricsSnapshot;
|
import com.cameleer3.server.core.storage.model.MetricsSnapshot;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -18,12 +18,12 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
public class IngestionBeanConfig {
|
public class IngestionBeanConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WriteBuffer<RouteExecution> executionBuffer(IngestionConfig config) {
|
public WriteBuffer<TaggedExecution> executionBuffer(IngestionConfig config) {
|
||||||
return new WriteBuffer<>(config.getBufferCapacity());
|
return new WriteBuffer<>(config.getBufferCapacity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WriteBuffer<RouteGraph> diagramBuffer(IngestionConfig config) {
|
public WriteBuffer<TaggedDiagram> diagramBuffer(IngestionConfig config) {
|
||||||
return new WriteBuffer<>(config.getBufferCapacity());
|
return new WriteBuffer<>(config.getBufferCapacity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ public class IngestionBeanConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public IngestionService ingestionService(WriteBuffer<RouteExecution> executionBuffer,
|
public IngestionService ingestionService(WriteBuffer<TaggedExecution> executionBuffer,
|
||||||
WriteBuffer<RouteGraph> diagramBuffer,
|
WriteBuffer<TaggedDiagram> diagramBuffer,
|
||||||
WriteBuffer<MetricsSnapshot> metricsBuffer) {
|
WriteBuffer<MetricsSnapshot> metricsBuffer) {
|
||||||
return new IngestionService(executionBuffer, diagramBuffer, metricsBuffer);
|
return new IngestionService(executionBuffer, diagramBuffer, metricsBuffer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cameleer3.server.app.controller;
|
|||||||
|
|
||||||
import com.cameleer3.common.graph.RouteGraph;
|
import com.cameleer3.common.graph.RouteGraph;
|
||||||
import com.cameleer3.server.core.ingestion.IngestionService;
|
import com.cameleer3.server.core.ingestion.IngestionService;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedDiagram;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -12,6 +13,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -46,13 +49,17 @@ public class DiagramController {
|
|||||||
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
||||||
@ApiResponse(responseCode = "503", description = "Buffer full, retry later")
|
@ApiResponse(responseCode = "503", description = "Buffer full, retry later")
|
||||||
public ResponseEntity<Void> ingestDiagrams(@RequestBody String body) throws JsonProcessingException {
|
public ResponseEntity<Void> ingestDiagrams(@RequestBody String body) throws JsonProcessingException {
|
||||||
|
String agentId = extractAgentId();
|
||||||
List<RouteGraph> graphs = parsePayload(body);
|
List<RouteGraph> graphs = parsePayload(body);
|
||||||
boolean accepted;
|
List<TaggedDiagram> tagged = graphs.stream()
|
||||||
|
.map(graph -> new TaggedDiagram(agentId, graph))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (graphs.size() == 1) {
|
boolean accepted;
|
||||||
accepted = ingestionService.acceptDiagram(graphs.get(0));
|
if (tagged.size() == 1) {
|
||||||
|
accepted = ingestionService.acceptDiagram(tagged.get(0));
|
||||||
} else {
|
} else {
|
||||||
accepted = ingestionService.acceptDiagrams(graphs);
|
accepted = ingestionService.acceptDiagrams(tagged);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
@@ -65,6 +72,11 @@ public class DiagramController {
|
|||||||
return ResponseEntity.accepted().build();
|
return ResponseEntity.accepted().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractAgentId() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
return auth != null ? auth.getName() : "";
|
||||||
|
}
|
||||||
|
|
||||||
private List<RouteGraph> parsePayload(String body) throws JsonProcessingException {
|
private List<RouteGraph> parsePayload(String body) throws JsonProcessingException {
|
||||||
String trimmed = body.strip();
|
String trimmed = body.strip();
|
||||||
if (trimmed.startsWith("[")) {
|
if (trimmed.startsWith("[")) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cameleer3.server.app.controller;
|
|||||||
|
|
||||||
import com.cameleer3.common.model.RouteExecution;
|
import com.cameleer3.common.model.RouteExecution;
|
||||||
import com.cameleer3.server.core.ingestion.IngestionService;
|
import com.cameleer3.server.core.ingestion.IngestionService;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedExecution;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -12,6 +13,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -47,13 +50,17 @@ public class ExecutionController {
|
|||||||
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
||||||
@ApiResponse(responseCode = "503", description = "Buffer full, retry later")
|
@ApiResponse(responseCode = "503", description = "Buffer full, retry later")
|
||||||
public ResponseEntity<Void> ingestExecutions(@RequestBody String body) throws JsonProcessingException {
|
public ResponseEntity<Void> ingestExecutions(@RequestBody String body) throws JsonProcessingException {
|
||||||
|
String agentId = extractAgentId();
|
||||||
List<RouteExecution> executions = parsePayload(body);
|
List<RouteExecution> executions = parsePayload(body);
|
||||||
boolean accepted;
|
List<TaggedExecution> tagged = executions.stream()
|
||||||
|
.map(exec -> new TaggedExecution(agentId, exec))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (executions.size() == 1) {
|
boolean accepted;
|
||||||
accepted = ingestionService.acceptExecution(executions.get(0));
|
if (tagged.size() == 1) {
|
||||||
|
accepted = ingestionService.acceptExecution(tagged.get(0));
|
||||||
} else {
|
} else {
|
||||||
accepted = ingestionService.acceptExecutions(executions);
|
accepted = ingestionService.acceptExecutions(tagged);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
@@ -66,6 +73,11 @@ public class ExecutionController {
|
|||||||
return ResponseEntity.accepted().build();
|
return ResponseEntity.accepted().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractAgentId() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
return auth != null ? auth.getName() : "";
|
||||||
|
}
|
||||||
|
|
||||||
private List<RouteExecution> parsePayload(String body) throws JsonProcessingException {
|
private List<RouteExecution> parsePayload(String body) throws JsonProcessingException {
|
||||||
String trimmed = body.strip();
|
String trimmed = body.strip();
|
||||||
if (trimmed.startsWith("[")) {
|
if (trimmed.startsWith("[")) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.cameleer3.server.app.ingestion;
|
package com.cameleer3.server.app.ingestion;
|
||||||
|
|
||||||
import com.cameleer3.common.graph.RouteGraph;
|
|
||||||
import com.cameleer3.common.model.RouteExecution;
|
|
||||||
import com.cameleer3.server.app.config.IngestionConfig;
|
import com.cameleer3.server.app.config.IngestionConfig;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedDiagram;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedExecution;
|
||||||
import com.cameleer3.server.core.ingestion.WriteBuffer;
|
import com.cameleer3.server.core.ingestion.WriteBuffer;
|
||||||
import com.cameleer3.server.core.storage.DiagramRepository;
|
import com.cameleer3.server.core.storage.DiagramRepository;
|
||||||
import com.cameleer3.server.core.storage.ExecutionRepository;
|
import com.cameleer3.server.core.storage.ExecutionRepository;
|
||||||
@@ -27,8 +27,8 @@ public class ClickHouseFlushScheduler implements SmartLifecycle {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ClickHouseFlushScheduler.class);
|
private static final Logger log = LoggerFactory.getLogger(ClickHouseFlushScheduler.class);
|
||||||
|
|
||||||
private final WriteBuffer<RouteExecution> executionBuffer;
|
private final WriteBuffer<TaggedExecution> executionBuffer;
|
||||||
private final WriteBuffer<RouteGraph> diagramBuffer;
|
private final WriteBuffer<TaggedDiagram> diagramBuffer;
|
||||||
private final WriteBuffer<MetricsSnapshot> metricsBuffer;
|
private final WriteBuffer<MetricsSnapshot> metricsBuffer;
|
||||||
private final ExecutionRepository executionRepository;
|
private final ExecutionRepository executionRepository;
|
||||||
private final DiagramRepository diagramRepository;
|
private final DiagramRepository diagramRepository;
|
||||||
@@ -37,8 +37,8 @@ public class ClickHouseFlushScheduler implements SmartLifecycle {
|
|||||||
|
|
||||||
private volatile boolean running = false;
|
private volatile boolean running = false;
|
||||||
|
|
||||||
public ClickHouseFlushScheduler(WriteBuffer<RouteExecution> executionBuffer,
|
public ClickHouseFlushScheduler(WriteBuffer<TaggedExecution> executionBuffer,
|
||||||
WriteBuffer<RouteGraph> diagramBuffer,
|
WriteBuffer<TaggedDiagram> diagramBuffer,
|
||||||
WriteBuffer<MetricsSnapshot> metricsBuffer,
|
WriteBuffer<MetricsSnapshot> metricsBuffer,
|
||||||
ExecutionRepository executionRepository,
|
ExecutionRepository executionRepository,
|
||||||
DiagramRepository diagramRepository,
|
DiagramRepository diagramRepository,
|
||||||
@@ -62,7 +62,7 @@ public class ClickHouseFlushScheduler implements SmartLifecycle {
|
|||||||
|
|
||||||
private void flushExecutions() {
|
private void flushExecutions() {
|
||||||
try {
|
try {
|
||||||
List<RouteExecution> batch = executionBuffer.drain(batchSize);
|
List<TaggedExecution> batch = executionBuffer.drain(batchSize);
|
||||||
if (!batch.isEmpty()) {
|
if (!batch.isEmpty()) {
|
||||||
executionRepository.insertBatch(batch);
|
executionRepository.insertBatch(batch);
|
||||||
log.debug("Flushed {} executions to ClickHouse", batch.size());
|
log.debug("Flushed {} executions to ClickHouse", batch.size());
|
||||||
@@ -74,9 +74,9 @@ public class ClickHouseFlushScheduler implements SmartLifecycle {
|
|||||||
|
|
||||||
private void flushDiagrams() {
|
private void flushDiagrams() {
|
||||||
try {
|
try {
|
||||||
List<RouteGraph> batch = diagramBuffer.drain(batchSize);
|
List<TaggedDiagram> batch = diagramBuffer.drain(batchSize);
|
||||||
for (RouteGraph graph : batch) {
|
for (TaggedDiagram diagram : batch) {
|
||||||
diagramRepository.store(graph);
|
diagramRepository.store(diagram);
|
||||||
}
|
}
|
||||||
if (!batch.isEmpty()) {
|
if (!batch.isEmpty()) {
|
||||||
log.debug("Flushed {} diagrams to ClickHouse", batch.size());
|
log.debug("Flushed {} diagrams to ClickHouse", batch.size());
|
||||||
@@ -130,8 +130,8 @@ public class ClickHouseFlushScheduler implements SmartLifecycle {
|
|||||||
private void drainAll() {
|
private void drainAll() {
|
||||||
drainBufferCompletely("executions", executionBuffer, batch -> executionRepository.insertBatch(batch));
|
drainBufferCompletely("executions", executionBuffer, batch -> executionRepository.insertBatch(batch));
|
||||||
drainBufferCompletely("diagrams", diagramBuffer, batch -> {
|
drainBufferCompletely("diagrams", diagramBuffer, batch -> {
|
||||||
for (RouteGraph g : batch) {
|
for (TaggedDiagram d : batch) {
|
||||||
diagramRepository.store(g);
|
diagramRepository.store(d);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
drainBufferCompletely("metrics", metricsBuffer, batch -> metricsRepository.insertBatch(batch));
|
drainBufferCompletely("metrics", metricsBuffer, batch -> metricsRepository.insertBatch(batch));
|
||||||
|
|||||||
@@ -123,14 +123,19 @@ public class ClickHouseSearchEngine implements SearchEngine {
|
|||||||
}
|
}
|
||||||
if (req.text() != null && !req.text().isBlank()) {
|
if (req.text() != null && !req.text().isBlank()) {
|
||||||
String pattern = "%" + escapeLike(req.text()) + "%";
|
String pattern = "%" + escapeLike(req.text()) + "%";
|
||||||
conditions.add("(route_id LIKE ? OR agent_id LIKE ? OR error_message LIKE ? OR error_stacktrace LIKE ? OR exchange_bodies LIKE ? OR exchange_headers LIKE ?)");
|
String[] textColumns = {
|
||||||
params.add(pattern);
|
"execution_id", "route_id", "agent_id",
|
||||||
params.add(pattern);
|
"error_message", "error_stacktrace",
|
||||||
params.add(pattern);
|
"exchange_bodies", "exchange_headers"
|
||||||
params.add(pattern);
|
};
|
||||||
params.add(pattern);
|
var likeClauses = java.util.Arrays.stream(textColumns)
|
||||||
|
.map(col -> col + " LIKE ?")
|
||||||
|
.toList();
|
||||||
|
conditions.add("(" + String.join(" OR ", likeClauses) + ")");
|
||||||
|
for (int i = 0; i < textColumns.length; i++) {
|
||||||
params.add(pattern);
|
params.add(pattern);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (req.textInBody() != null && !req.textInBody().isBlank()) {
|
if (req.textInBody() != null && !req.textInBody().isBlank()) {
|
||||||
conditions.add("exchange_bodies LIKE ?");
|
conditions.add("exchange_bodies LIKE ?");
|
||||||
params.add("%" + escapeLike(req.textInBody()) + "%");
|
params.add("%" + escapeLike(req.textInBody()) + "%");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.cameleer3.server.app.storage;
|
package com.cameleer3.server.app.storage;
|
||||||
|
|
||||||
import com.cameleer3.common.graph.RouteGraph;
|
import com.cameleer3.common.graph.RouteGraph;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedDiagram;
|
||||||
import com.cameleer3.server.core.storage.DiagramRepository;
|
import com.cameleer3.server.core.storage.DiagramRepository;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -54,16 +55,16 @@ public class ClickHouseDiagramRepository implements DiagramRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void store(RouteGraph graph) {
|
public void store(TaggedDiagram diagram) {
|
||||||
try {
|
try {
|
||||||
|
RouteGraph graph = diagram.graph();
|
||||||
|
String agentId = diagram.agentId() != null ? diagram.agentId() : "";
|
||||||
String json = objectMapper.writeValueAsString(graph);
|
String json = objectMapper.writeValueAsString(graph);
|
||||||
String contentHash = sha256Hex(json);
|
String contentHash = sha256Hex(json);
|
||||||
String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";
|
String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";
|
||||||
// agent_id is not part of RouteGraph -- set empty, controllers can enrich
|
|
||||||
String agentId = "";
|
|
||||||
|
|
||||||
jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, json);
|
jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, json);
|
||||||
log.debug("Stored diagram for route={} with hash={}", routeId, contentHash);
|
log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new RuntimeException("Failed to serialize RouteGraph to JSON", e);
|
throw new RuntimeException("Failed to serialize RouteGraph to JSON", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.cameleer3.common.model.ExchangeSnapshot;
|
|||||||
import com.cameleer3.common.model.ProcessorExecution;
|
import com.cameleer3.common.model.ProcessorExecution;
|
||||||
import com.cameleer3.common.model.RouteExecution;
|
import com.cameleer3.common.model.RouteExecution;
|
||||||
import com.cameleer3.server.core.detail.RawExecutionRow;
|
import com.cameleer3.server.core.detail.RawExecutionRow;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedExecution;
|
||||||
import com.cameleer3.server.core.storage.DiagramRepository;
|
import com.cameleer3.server.core.storage.DiagramRepository;
|
||||||
import com.cameleer3.server.core.storage.ExecutionRepository;
|
import com.cameleer3.server.core.storage.ExecutionRepository;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
@@ -62,7 +63,7 @@ public class ClickHouseExecutionRepository implements ExecutionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void insertBatch(List<RouteExecution> executions) {
|
public void insertBatch(List<TaggedExecution> executions) {
|
||||||
if (executions.isEmpty()) {
|
if (executions.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,13 +71,15 @@ public class ClickHouseExecutionRepository implements ExecutionRepository {
|
|||||||
jdbcTemplate.batchUpdate(INSERT_SQL, new BatchPreparedStatementSetter() {
|
jdbcTemplate.batchUpdate(INSERT_SQL, new BatchPreparedStatementSetter() {
|
||||||
@Override
|
@Override
|
||||||
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
public void setValues(PreparedStatement ps, int i) throws SQLException {
|
||||||
RouteExecution exec = executions.get(i);
|
TaggedExecution tagged = executions.get(i);
|
||||||
|
RouteExecution exec = tagged.execution();
|
||||||
|
String agentId = tagged.agentId() != null ? tagged.agentId() : "";
|
||||||
List<FlatProcessor> flatProcessors = flattenWithMetadata(exec.getProcessors());
|
List<FlatProcessor> flatProcessors = flattenWithMetadata(exec.getProcessors());
|
||||||
|
|
||||||
int col = 1;
|
int col = 1;
|
||||||
ps.setString(col++, UUID.randomUUID().toString());
|
ps.setString(col++, UUID.randomUUID().toString());
|
||||||
ps.setString(col++, nullSafe(exec.getRouteId()));
|
ps.setString(col++, nullSafe(exec.getRouteId()));
|
||||||
ps.setString(col++, ""); // agent_id set by controller header or empty
|
ps.setString(col++, agentId);
|
||||||
ps.setString(col++, exec.getStatus() != null ? exec.getStatus().name() : "RUNNING");
|
ps.setString(col++, exec.getStatus() != null ? exec.getStatus().name() : "RUNNING");
|
||||||
ps.setObject(col++, toTimestamp(exec.getStartTime()));
|
ps.setObject(col++, toTimestamp(exec.getStartTime()));
|
||||||
ps.setObject(col++, toTimestamp(exec.getEndTime()));
|
ps.setObject(col++, toTimestamp(exec.getEndTime()));
|
||||||
@@ -142,7 +145,7 @@ public class ClickHouseExecutionRepository implements ExecutionRepository {
|
|||||||
ps.setObject(col++, outputHeaders); // processor_output_headers
|
ps.setObject(col++, outputHeaders); // processor_output_headers
|
||||||
ps.setObject(col++, diagramNodeIds); // processor_diagram_node_ids
|
ps.setObject(col++, diagramNodeIds); // processor_diagram_node_ids
|
||||||
String diagramHash = diagramRepository
|
String diagramHash = diagramRepository
|
||||||
.findContentHashForRoute(exec.getRouteId(), "")
|
.findContentHashForRoute(exec.getRouteId(), agentId)
|
||||||
.orElse("");
|
.orElse("");
|
||||||
ps.setString(col++, diagramHash); // diagram_content_hash
|
ps.setString(col++, diagramHash); // diagram_content_hash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.cameleer3.server.core.ingestion;
|
package com.cameleer3.server.core.ingestion;
|
||||||
|
|
||||||
import com.cameleer3.common.graph.RouteGraph;
|
|
||||||
import com.cameleer3.common.model.RouteExecution;
|
|
||||||
import com.cameleer3.server.core.storage.model.MetricsSnapshot;
|
import com.cameleer3.server.core.storage.model.MetricsSnapshot;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -14,12 +12,12 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class IngestionService {
|
public class IngestionService {
|
||||||
|
|
||||||
private final WriteBuffer<RouteExecution> executionBuffer;
|
private final WriteBuffer<TaggedExecution> executionBuffer;
|
||||||
private final WriteBuffer<RouteGraph> diagramBuffer;
|
private final WriteBuffer<TaggedDiagram> diagramBuffer;
|
||||||
private final WriteBuffer<MetricsSnapshot> metricsBuffer;
|
private final WriteBuffer<MetricsSnapshot> metricsBuffer;
|
||||||
|
|
||||||
public IngestionService(WriteBuffer<RouteExecution> executionBuffer,
|
public IngestionService(WriteBuffer<TaggedExecution> executionBuffer,
|
||||||
WriteBuffer<RouteGraph> diagramBuffer,
|
WriteBuffer<TaggedDiagram> diagramBuffer,
|
||||||
WriteBuffer<MetricsSnapshot> metricsBuffer) {
|
WriteBuffer<MetricsSnapshot> metricsBuffer) {
|
||||||
this.executionBuffer = executionBuffer;
|
this.executionBuffer = executionBuffer;
|
||||||
this.diagramBuffer = diagramBuffer;
|
this.diagramBuffer = diagramBuffer;
|
||||||
@@ -27,39 +25,39 @@ public class IngestionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a batch of route executions into the buffer.
|
* Accept a batch of tagged route executions into the buffer.
|
||||||
*
|
*
|
||||||
* @return true if all items were buffered, false if buffer is full (backpressure)
|
* @return true if all items were buffered, false if buffer is full (backpressure)
|
||||||
*/
|
*/
|
||||||
public boolean acceptExecutions(List<RouteExecution> executions) {
|
public boolean acceptExecutions(List<TaggedExecution> executions) {
|
||||||
return executionBuffer.offerBatch(executions);
|
return executionBuffer.offerBatch(executions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a single route execution into the buffer.
|
* Accept a single tagged route execution into the buffer.
|
||||||
*
|
*
|
||||||
* @return true if the item was buffered, false if buffer is full (backpressure)
|
* @return true if the item was buffered, false if buffer is full (backpressure)
|
||||||
*/
|
*/
|
||||||
public boolean acceptExecution(RouteExecution execution) {
|
public boolean acceptExecution(TaggedExecution execution) {
|
||||||
return executionBuffer.offer(execution);
|
return executionBuffer.offer(execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a single route diagram into the buffer.
|
* Accept a single tagged route diagram into the buffer.
|
||||||
*
|
*
|
||||||
* @return true if the item was buffered, false if buffer is full (backpressure)
|
* @return true if the item was buffered, false if buffer is full (backpressure)
|
||||||
*/
|
*/
|
||||||
public boolean acceptDiagram(RouteGraph graph) {
|
public boolean acceptDiagram(TaggedDiagram diagram) {
|
||||||
return diagramBuffer.offer(graph);
|
return diagramBuffer.offer(diagram);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a batch of route diagrams into the buffer.
|
* Accept a batch of tagged route diagrams into the buffer.
|
||||||
*
|
*
|
||||||
* @return true if all items were buffered, false if buffer is full (backpressure)
|
* @return true if all items were buffered, false if buffer is full (backpressure)
|
||||||
*/
|
*/
|
||||||
public boolean acceptDiagrams(List<RouteGraph> graphs) {
|
public boolean acceptDiagrams(List<TaggedDiagram> diagrams) {
|
||||||
return diagramBuffer.offerBatch(graphs);
|
return diagramBuffer.offerBatch(diagrams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,14 +93,14 @@ public class IngestionService {
|
|||||||
/**
|
/**
|
||||||
* @return the execution write buffer (for use by flush scheduler)
|
* @return the execution write buffer (for use by flush scheduler)
|
||||||
*/
|
*/
|
||||||
public WriteBuffer<RouteExecution> getExecutionBuffer() {
|
public WriteBuffer<TaggedExecution> getExecutionBuffer() {
|
||||||
return executionBuffer;
|
return executionBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the diagram write buffer (for use by flush scheduler)
|
* @return the diagram write buffer (for use by flush scheduler)
|
||||||
*/
|
*/
|
||||||
public WriteBuffer<RouteGraph> getDiagramBuffer() {
|
public WriteBuffer<TaggedDiagram> getDiagramBuffer() {
|
||||||
return diagramBuffer;
|
return diagramBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.cameleer3.server.core.ingestion;
|
||||||
|
|
||||||
|
import com.cameleer3.common.graph.RouteGraph;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pairs a {@link RouteGraph} with the authenticated agent identity.
|
||||||
|
* <p>
|
||||||
|
* The agent ID is extracted from the SecurityContext in the controller layer
|
||||||
|
* and carried through the write buffer so the flush scheduler can persist it.
|
||||||
|
*/
|
||||||
|
public record TaggedDiagram(String agentId, RouteGraph graph) {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.cameleer3.server.core.ingestion;
|
||||||
|
|
||||||
|
import com.cameleer3.common.model.RouteExecution;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pairs a {@link RouteExecution} with the authenticated agent identity.
|
||||||
|
* <p>
|
||||||
|
* The agent ID is extracted from the SecurityContext in the controller layer
|
||||||
|
* and carried through the write buffer so the flush scheduler can persist it.
|
||||||
|
*/
|
||||||
|
public record TaggedExecution(String agentId, RouteExecution execution) {}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.cameleer3.server.core.storage;
|
package com.cameleer3.server.core.storage;
|
||||||
|
|
||||||
import com.cameleer3.common.graph.RouteGraph;
|
import com.cameleer3.common.graph.RouteGraph;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedDiagram;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -10,9 +11,9 @@ import java.util.Optional;
|
|||||||
public interface DiagramRepository {
|
public interface DiagramRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a route graph. Uses content-hash deduplication via ReplacingMergeTree.
|
* Store a tagged route graph. Uses content-hash deduplication via ReplacingMergeTree.
|
||||||
*/
|
*/
|
||||||
void store(RouteGraph graph);
|
void store(TaggedDiagram diagram);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a route graph by its content hash.
|
* Find a route graph by its content hash.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.cameleer3.server.core.storage;
|
package com.cameleer3.server.core.storage;
|
||||||
|
|
||||||
import com.cameleer3.common.model.RouteExecution;
|
|
||||||
import com.cameleer3.server.core.detail.RawExecutionRow;
|
import com.cameleer3.server.core.detail.RawExecutionRow;
|
||||||
|
import com.cameleer3.server.core.ingestion.TaggedExecution;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -12,10 +12,10 @@ import java.util.Optional;
|
|||||||
public interface ExecutionRepository {
|
public interface ExecutionRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a batch of route executions.
|
* Insert a batch of tagged route executions.
|
||||||
* Implementations must perform a single batch insert for efficiency.
|
* Implementations must perform a single batch insert for efficiency.
|
||||||
*/
|
*/
|
||||||
void insertBatch(List<RouteExecution> executions);
|
void insertBatch(List<TaggedExecution> executions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a raw execution row by execution ID, including all parallel arrays
|
* Find a raw execution row by execution ID, including all parallel arrays
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ export function CommandPalette() {
|
|||||||
(result: PaletteResult) => {
|
(result: PaletteResult) => {
|
||||||
if (result.type === 'execution') {
|
if (result.type === 'execution') {
|
||||||
const exec = result.data as ExecutionSummary;
|
const exec = result.data as ExecutionSummary;
|
||||||
|
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
|
||||||
execSearch.setText(exec.executionId);
|
execSearch.setText(exec.executionId);
|
||||||
execSearch.setRouteId('');
|
execSearch.setRouteId('');
|
||||||
execSearch.setAgentId('');
|
execSearch.setAgentId('');
|
||||||
execSearch.setProcessorType('');
|
execSearch.setProcessorType('');
|
||||||
} else if (result.type === 'agent') {
|
} else if (result.type === 'agent') {
|
||||||
const agent = result.data as AgentInstance;
|
const agent = result.data as AgentInstance;
|
||||||
|
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
|
||||||
execSearch.setAgentId(agent.agentId);
|
execSearch.setAgentId(agent.agentId);
|
||||||
execSearch.setText('');
|
execSearch.setText('');
|
||||||
execSearch.setRouteId('');
|
execSearch.setRouteId('');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ResultsListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ResultsList({ results, isLoading, onSelect }: ResultsListProps) {
|
export function ResultsList({ results, isLoading, onSelect }: ResultsListProps) {
|
||||||
const { selectedIndex, query } = useCommandPalette();
|
const { selectedIndex, query, scope } = useCommandPalette();
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -21,6 +21,24 @@ export function ResultsList({ results, isLoading, onSelect }: ResultsListProps)
|
|||||||
items?.[selectedIndex]?.scrollIntoView({ block: 'nearest' });
|
items?.[selectedIndex]?.scrollIntoView({ block: 'nearest' });
|
||||||
}, [selectedIndex]);
|
}, [selectedIndex]);
|
||||||
|
|
||||||
|
if (scope === 'routes' || scope === 'exchanges') {
|
||||||
|
const label = scope === 'routes' ? 'Route' : 'Exchange';
|
||||||
|
return (
|
||||||
|
<div className={styles.results}>
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<svg className={styles.emptyIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||||
|
<path d="M12 6v6l4 2" />
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
</svg>
|
||||||
|
<span className={styles.emptyText}>{label} search coming soon</span>
|
||||||
|
<span className={styles.emptyHint}>
|
||||||
|
This feature is planned for a future release
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading && results.length === 0) {
|
if (isLoading && results.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.results}>
|
<div className={styles.results}>
|
||||||
|
|||||||
@@ -10,16 +10,18 @@ const SCOPES: { key: PaletteScope; label: string; disabled?: boolean }[] = [
|
|||||||
{ key: 'all', label: 'All' },
|
{ key: 'all', label: 'All' },
|
||||||
{ key: 'executions', label: 'Executions' },
|
{ key: 'executions', label: 'Executions' },
|
||||||
{ key: 'agents', label: 'Agents' },
|
{ key: 'agents', label: 'Agents' },
|
||||||
|
{ key: 'routes', label: 'Routes', disabled: true },
|
||||||
|
{ key: 'exchanges', label: 'Exchanges', disabled: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ScopeTabs({ executionCount, agentCount }: ScopeTabsProps) {
|
export function ScopeTabs({ executionCount, agentCount }: ScopeTabsProps) {
|
||||||
const { scope, setScope } = useCommandPalette();
|
const { scope, setScope } = useCommandPalette();
|
||||||
|
|
||||||
function getCount(key: PaletteScope): number {
|
function getCount(key: PaletteScope): string | number {
|
||||||
if (key === 'all') return executionCount + agentCount;
|
if (key === 'all') return executionCount + agentCount;
|
||||||
if (key === 'executions') return executionCount;
|
if (key === 'executions') return executionCount;
|
||||||
if (key === 'agents') return agentCount;
|
if (key === 'agents') return agentCount;
|
||||||
return 0;
|
return '\u2014';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +29,13 @@ export function ScopeTabs({ executionCount, agentCount }: ScopeTabsProps) {
|
|||||||
{SCOPES.map((s) => (
|
{SCOPES.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.key}
|
key={s.key}
|
||||||
className={scope === s.key ? styles.scopeTabActive : styles.scopeTab}
|
className={
|
||||||
|
s.disabled
|
||||||
|
? styles.scopeTabDisabled
|
||||||
|
: scope === s.key
|
||||||
|
? styles.scopeTabActive
|
||||||
|
: styles.scopeTab
|
||||||
|
}
|
||||||
onClick={() => !s.disabled && setScope(s.key)}
|
onClick={() => !s.disabled && setScope(s.key)}
|
||||||
>
|
>
|
||||||
{s.label}
|
{s.label}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
export type PaletteScope = 'all' | 'executions' | 'agents';
|
export type PaletteScope = 'all' | 'executions' | 'agents' | 'routes' | 'exchanges';
|
||||||
|
|
||||||
export interface PaletteFilter {
|
export interface PaletteFilter {
|
||||||
key: 'status' | 'route' | 'agent' | 'processor';
|
key: 'status' | 'route' | 'agent' | 'processor';
|
||||||
|
|||||||
Reference in New Issue
Block a user