1122 lines
44 KiB
Markdown
1122 lines
44 KiB
Markdown
|
|
# Execution Overlay & Debugger — Implementation Plan
|
||
|
|
|
||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
|
|
||
|
|
**Goal:** Overlay real execution data onto the ProcessDiagram component, turning it into an after-the-fact debugger with node status visualization, iteration stepping, and a tabbed detail panel.
|
||
|
|
|
||
|
|
**Architecture:** An `ExecutionDiagram` wrapper component composes the existing `ProcessDiagram` with execution data from `useExecutionDetail`. ProcessDiagram gains optional overlay props that tint nodes, color edges, and add iteration steppers to compound nodes. A resizable bottom panel shows per-processor details across 7 tabs. Backend prerequisites add iteration fields to the storage layer and a snapshot-by-processorId endpoint.
|
||
|
|
|
||
|
|
**Tech Stack:** Java 17 / Spring Boot (backend), React / TypeScript / CSS Modules (frontend), PostgreSQL (storage), React Query (data fetching)
|
||
|
|
|
||
|
|
**Design spec:** `docs/superpowers/specs/2026-03-27-execution-overlay-design.md`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
### Backend Changes
|
||
|
|
|
||
|
|
| Action | File | Responsibility |
|
||
|
|
|--------|------|---------------|
|
||
|
|
| Create | `cameleer3-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql` | Add iteration columns to processor_executions |
|
||
|
|
| Modify | `cameleer3-server-core/.../storage/ExecutionStore.java` | Extend ProcessorRecord with iteration fields |
|
||
|
|
| Modify | `cameleer3-server-app/.../storage/PostgresExecutionStore.java` | Update SQL queries for new columns |
|
||
|
|
| Modify | `cameleer3-server-core/.../ingestion/IngestionService.java` | Store iteration fields during ingestion |
|
||
|
|
| Modify | `cameleer3-server-core/.../detail/ProcessorNode.java` | Add iteration fields to detail model |
|
||
|
|
| Modify | `cameleer3-server-core/.../detail/DetailService.java` | Pass iteration fields through tree builder |
|
||
|
|
| Modify | `cameleer3-server-app/.../controller/DetailController.java` | Add snapshot-by-processorId endpoint |
|
||
|
|
|
||
|
|
### Frontend Changes
|
||
|
|
|
||
|
|
| Action | File | Responsibility |
|
||
|
|
|--------|------|---------------|
|
||
|
|
| Modify | `ui/src/api/openapi.json` | Regenerate with new fields/endpoints |
|
||
|
|
| Modify | `ui/src/api/schema.d.ts` | Regenerate TypeScript types |
|
||
|
|
| Modify | `ui/src/api/queries/executions.ts` | Add useProcessorSnapshotById hook |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/types.ts` | Add overlay props + NodeExecutionState |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/DiagramNode.tsx` | Execution overlay visuals (tint, badges, duration) |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/DiagramEdge.tsx` | Traversed/not-traversed edge styling |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/CompoundNode.tsx` | Iteration stepper in compound header |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/ErrorSection.tsx` | Pass overlay props to child nodes |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/ProcessDiagram.tsx` | Pass overlay props through to children |
|
||
|
|
| Modify | `ui/src/components/ProcessDiagram/ProcessDiagram.module.css` | Overlay-related styles |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx` | Root wrapper: exchange bar + diagram + detail panel |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css` | Layout styles (splitter, exchange bar, panel) |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/useExecutionOverlay.ts` | Maps execution data → node overlay state |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/useIterationState.ts` | Per-compound iteration tracking |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/DetailPanel.tsx` | Bottom panel: tabs container + processor header |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx` | Processor metadata + attributes |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/tabs/HeadersTab.tsx` | Input/output headers side-by-side |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx` | Formatted message body (shared by Input/Output) |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/tabs/ErrorTab.tsx` | Exception details + stack trace |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/tabs/ConfigTab.tsx` | Placeholder (TODO: agent data) |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/tabs/TimelineTab.tsx` | Gantt-style processor duration chart |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/types.ts` | Overlay-specific types |
|
||
|
|
| Create | `ui/src/components/ExecutionDiagram/index.ts` | Public exports |
|
||
|
|
| Modify | `ui/src/pages/ExchangeDetail/ExchangeDetail.tsx` | Replace RouteFlow with ExecutionDiagram |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Prerequisites
|
||
|
|
|
||
|
|
Before starting, verify that the `cameleer3-common` dependency (in the agent repo) exposes iteration getters on `ProcessorExecution`: `getLoopIndex()`, `getLoopSize()`, `getSplitIndex()`, `getSplitSize()`, `getMulticastIndex()`. If these methods do not exist in the current published version, they must be added to `cameleer3-common` and a new SNAPSHOT or release published before Task 1 Step 4 can work. Check `cameleer3/cameleer3-common/src/main/java/com/cameleer3/common/model/ProcessorExecution.java`.
|
||
|
|
|
||
|
|
Note on migration versioning: The next migration is V8. If other work merges before this plan executes, bump the version number accordingly.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Tasks
|
||
|
|
|
||
|
|
### Task 1: Add iteration fields to backend storage
|
||
|
|
|
||
|
|
Add `loop_index`, `loop_size`, `split_index`, `split_size`, `multicast_index` columns to the database and thread them through the storage → ingestion → detail pipeline.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `cameleer3-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql`
|
||
|
|
- Modify: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java`
|
||
|
|
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java`
|
||
|
|
- Modify: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java`
|
||
|
|
- Modify: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java`
|
||
|
|
- Modify: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create Flyway migration V8**
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- V8__processor_iteration_fields.sql
|
||
|
|
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_index INTEGER;
|
||
|
|
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_size INTEGER;
|
||
|
|
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_index INTEGER;
|
||
|
|
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_size INTEGER;
|
||
|
|
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS multicast_index INTEGER;
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Extend ProcessorRecord in ExecutionStore.java**
|
||
|
|
|
||
|
|
Add five new fields to the `ProcessorRecord` record. Current signature has 18 fields; add after `attributes`:
|
||
|
|
|
||
|
|
```java
|
||
|
|
record ProcessorRecord(
|
||
|
|
String executionId, String processorId, String processorType,
|
||
|
|
String applicationName, String routeId,
|
||
|
|
int depth, String parentProcessorId, String status,
|
||
|
|
Instant startTime, Instant endTime, Long durationMs,
|
||
|
|
String errorMessage, String errorStacktrace,
|
||
|
|
String inputBody, String outputBody, String inputHeaders, String outputHeaders,
|
||
|
|
String attributes,
|
||
|
|
// NEW iteration fields:
|
||
|
|
Integer loopIndex, Integer loopSize,
|
||
|
|
Integer splitIndex, Integer splitSize,
|
||
|
|
Integer multicastIndex
|
||
|
|
) {}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Update PostgresExecutionStore SQL and row mapper**
|
||
|
|
|
||
|
|
In `upsertProcessors()`: add the 5 new columns to the INSERT and ON CONFLICT SET clauses. In the `ProcessorRecord` RowMapper: read the new columns using `rs.getObject("loop_index") != null ? rs.getInt("loop_index") : null` pattern. In `findProcessors()`: add the new columns to the SELECT list.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Update IngestionService.flattenProcessors()**
|
||
|
|
|
||
|
|
In the `flattenProcessors` method, extract iteration fields from `ProcessorExecution` and pass them to `ProcessorRecord`:
|
||
|
|
|
||
|
|
```java
|
||
|
|
flat.add(new ProcessorRecord(
|
||
|
|
executionId, p.getProcessorId(), p.getProcessorType(),
|
||
|
|
applicationName, routeId,
|
||
|
|
depth, parentProcessorId,
|
||
|
|
p.getStatus() != null ? p.getStatus().name() : "RUNNING",
|
||
|
|
p.getStartTime() != null ? p.getStartTime() : execStartTime,
|
||
|
|
p.getEndTime(),
|
||
|
|
p.getDurationMs(),
|
||
|
|
p.getErrorMessage(), p.getErrorStackTrace(),
|
||
|
|
truncateBody(p.getInputBody()), truncateBody(p.getOutputBody()),
|
||
|
|
toJson(p.getInputHeaders()), toJson(p.getOutputHeaders()),
|
||
|
|
toJson(p.getAttributes()),
|
||
|
|
p.getLoopIndex(), p.getLoopSize(),
|
||
|
|
p.getSplitIndex(), p.getSplitSize(),
|
||
|
|
p.getMulticastIndex()
|
||
|
|
));
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Add iteration fields to ProcessorNode.java**
|
||
|
|
|
||
|
|
Add 5 private fields, update constructor, add getters:
|
||
|
|
|
||
|
|
```java
|
||
|
|
private final Integer loopIndex;
|
||
|
|
private final Integer loopSize;
|
||
|
|
private final Integer splitIndex;
|
||
|
|
private final Integer splitSize;
|
||
|
|
private final Integer multicastIndex;
|
||
|
|
```
|
||
|
|
|
||
|
|
Constructor adds these after `attributes`. Add corresponding getters.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Update DetailService.buildTree() to pass iteration fields**
|
||
|
|
|
||
|
|
In the node creation inside `buildTree()`:
|
||
|
|
|
||
|
|
```java
|
||
|
|
ProcessorNode node = new ProcessorNode(
|
||
|
|
p.processorId(), p.processorType(), p.status(),
|
||
|
|
p.startTime(), p.endTime(),
|
||
|
|
p.durationMs() != null ? p.durationMs() : 0L,
|
||
|
|
p.errorMessage(), p.errorStacktrace(),
|
||
|
|
parseAttributes(p.attributes()),
|
||
|
|
p.loopIndex(), p.loopSize(),
|
||
|
|
p.splitIndex(), p.splitSize(),
|
||
|
|
p.multicastIndex()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Verify backend compiles**
|
||
|
|
|
||
|
|
Run: `mvn clean compile -DskipTests`
|
||
|
|
Expected: BUILD SUCCESS
|
||
|
|
|
||
|
|
- [ ] **Step 8: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add cameleer3-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql \
|
||
|
|
cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java \
|
||
|
|
cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java \
|
||
|
|
cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java \
|
||
|
|
cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java \
|
||
|
|
cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java
|
||
|
|
git commit -m "feat: add iteration fields to processor storage and detail model"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 2: Add snapshot-by-processorId endpoint
|
||
|
|
|
||
|
|
Add a REST endpoint that fetches processor snapshot data by processorId instead of positional index.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java`
|
||
|
|
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java`
|
||
|
|
- Modify: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java`
|
||
|
|
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DetailController.java`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add findProcessorById to ExecutionStore interface**
|
||
|
|
|
||
|
|
```java
|
||
|
|
Optional<ProcessorRecord> findProcessorById(String executionId, String processorId);
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement in PostgresExecutionStore**
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Override
|
||
|
|
public Optional<ProcessorRecord> findProcessorById(String executionId, String processorId) {
|
||
|
|
String sql = "SELECT * FROM processor_executions WHERE execution_id = ? AND processor_id = ? LIMIT 1";
|
||
|
|
List<ProcessorRecord> results = jdbc.query(sql, processorRowMapper, executionId, processorId);
|
||
|
|
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add getProcessorSnapshot method to DetailService**
|
||
|
|
|
||
|
|
```java
|
||
|
|
public Optional<Map<String, String>> getProcessorSnapshot(String executionId, String processorId) {
|
||
|
|
return executionStore.findProcessorById(executionId, processorId)
|
||
|
|
.map(p -> {
|
||
|
|
Map<String, String> snapshot = new LinkedHashMap<>();
|
||
|
|
if (p.inputBody() != null) snapshot.put("inputBody", p.inputBody());
|
||
|
|
if (p.outputBody() != null) snapshot.put("outputBody", p.outputBody());
|
||
|
|
if (p.inputHeaders() != null) snapshot.put("inputHeaders", p.inputHeaders());
|
||
|
|
if (p.outputHeaders() != null) snapshot.put("outputHeaders", p.outputHeaders());
|
||
|
|
return snapshot;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Add endpoint to DetailController**
|
||
|
|
|
||
|
|
```java
|
||
|
|
@GetMapping("/{executionId}/processors/by-id/{processorId}/snapshot")
|
||
|
|
public ResponseEntity<Map<String, String>> processorSnapshotById(
|
||
|
|
@PathVariable String executionId,
|
||
|
|
@PathVariable String processorId) {
|
||
|
|
return detailService.getProcessorSnapshot(executionId, processorId)
|
||
|
|
.map(ResponseEntity::ok)
|
||
|
|
.orElse(ResponseEntity.notFound().build());
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Verify backend compiles**
|
||
|
|
|
||
|
|
Run: `mvn clean compile -DskipTests`
|
||
|
|
Expected: BUILD SUCCESS
|
||
|
|
|
||
|
|
- [ ] **Step 6: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java \
|
||
|
|
cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java \
|
||
|
|
cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java \
|
||
|
|
cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DetailController.java
|
||
|
|
git commit -m "feat: add snapshot-by-processorId endpoint"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 3: Regenerate OpenAPI spec and TypeScript types
|
||
|
|
|
||
|
|
Update `openapi.json` with the new fields and endpoint, then regenerate `schema.d.ts`.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ui/src/api/openapi.json`
|
||
|
|
- Modify: `ui/src/api/schema.d.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Start the server and regenerate openapi.json**
|
||
|
|
|
||
|
|
Run the server locally, then fetch the OpenAPI spec:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Build and run
|
||
|
|
cd cameleer3-server-app && mvn spring-boot:run -DskipTests &
|
||
|
|
# Wait for startup, then fetch
|
||
|
|
curl -s http://localhost:8080/v3/api-docs | python3 -m json.tool > ui/src/api/openapi.json
|
||
|
|
```
|
||
|
|
|
||
|
|
If the server cannot start (no database), manually add the following to `openapi.json`:
|
||
|
|
|
||
|
|
1. Add `loopIndex`, `loopSize`, `splitIndex`, `splitSize`, `multicastIndex` (all `integer`, nullable) to the `ProcessorNode` schema
|
||
|
|
2. Add the new endpoint path `/api/v1/executions/{executionId}/processors/by-id/{processorId}/snapshot`
|
||
|
|
|
||
|
|
- [ ] **Step 2: Regenerate schema.d.ts**
|
||
|
|
|
||
|
|
Run the project's type generation script, or manually add the new fields to the `ProcessorNode` interface in `ui/src/api/schema.d.ts`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Add to ProcessorNode in schema.d.ts
|
||
|
|
loopIndex?: number;
|
||
|
|
loopSize?: number;
|
||
|
|
splitIndex?: number;
|
||
|
|
splitSize?: number;
|
||
|
|
multicastIndex?: number;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note:** The current `schema.d.ts` has a stale `diagramNodeId` field on `ProcessorNode` — this was dropped in V6 migration and never existed in the Java model. Remove it during regeneration to avoid confusion. The `processorId` field IS the diagram node ID.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Verify frontend types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
Expected: No errors
|
||
|
|
|
||
|
|
- [ ] **Step 4: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/api/openapi.json ui/src/api/schema.d.ts
|
||
|
|
git commit -m "chore: regenerate openapi spec with iteration fields and snapshot-by-id endpoint"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 4: Add overlay types, extend ProcessDiagramProps, and add diagramLayout prop
|
||
|
|
|
||
|
|
Define the execution overlay type system and extend ProcessDiagram to accept optional overlay props. Critically, add a `diagramLayout` prop so ExecutionDiagram can load the diagram by `diagramContentHash` and pass the pre-fetched layout — ensuring the diagram version matches the execution, not the latest.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/types.ts`
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/types.ts`
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/useDiagramData.ts`
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/ProcessDiagram.tsx`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create ExecutionDiagram types**
|
||
|
|
|
||
|
|
Create `ui/src/components/ExecutionDiagram/types.ts`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import type { components } from '../../api/schema';
|
||
|
|
|
||
|
|
export type ExecutionDetail = components['schemas']['ExecutionDetail'];
|
||
|
|
export type ProcessorNode = components['schemas']['ProcessorNode'];
|
||
|
|
|
||
|
|
export interface NodeExecutionState {
|
||
|
|
status: 'COMPLETED' | 'FAILED';
|
||
|
|
durationMs: number;
|
||
|
|
/** True if this node's target sub-route failed (DIRECT/SEDA) */
|
||
|
|
subRouteFailed?: boolean;
|
||
|
|
/** True if trace data is available for this processor */
|
||
|
|
hasTraceData?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IterationInfo {
|
||
|
|
/** Current iteration index (0-based) */
|
||
|
|
current: number;
|
||
|
|
/** Total number of iterations */
|
||
|
|
total: number;
|
||
|
|
/** Type of iteration (determines label) */
|
||
|
|
type: 'loop' | 'split' | 'multicast';
|
||
|
|
}
|
||
|
|
|
||
|
|
export type DetailTab = 'info' | 'headers' | 'input' | 'output' | 'error' | 'config' | 'timeline';
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note:** The spec defines `iterationState` as `Map<string, number>`. This plan uses `Map<string, IterationInfo>` instead, which carries `total` and `type` in addition to `current` — needed by the CompoundNode stepper to render "3/5" and label the type. This is a deliberate improvement over the spec.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Extend ProcessDiagramProps with overlay and diagramLayout props**
|
||
|
|
|
||
|
|
Add to `ui/src/components/ProcessDiagram/types.ts`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import type { DiagramLayout } from '../../api/queries/diagrams';
|
||
|
|
import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types';
|
||
|
|
|
||
|
|
// Add to ProcessDiagramProps:
|
||
|
|
/** Pre-fetched diagram layout (bypasses internal fetch by application/routeId) */
|
||
|
|
diagramLayout?: DiagramLayout;
|
||
|
|
/** Execution overlay: maps diagram node ID → execution state */
|
||
|
|
executionOverlay?: Map<string, NodeExecutionState>;
|
||
|
|
/** Per-compound iteration info: maps compound node ID → iteration info */
|
||
|
|
iterationState?: Map<string, IterationInfo>;
|
||
|
|
/** Called when user changes iteration on a compound stepper */
|
||
|
|
onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void;
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Update useDiagramData to accept pre-fetched layout**
|
||
|
|
|
||
|
|
Modify `ui/src/components/ProcessDiagram/useDiagramData.ts` to accept an optional `DiagramLayout` parameter. When provided, skip the `useDiagramByRoute` fetch and process the pre-fetched layout directly:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export function useDiagramData(
|
||
|
|
application: string, routeId: string, direction: 'LR' | 'TB',
|
||
|
|
preloadedLayout?: DiagramLayout, // NEW
|
||
|
|
) {
|
||
|
|
const { data: fetchedLayout, isLoading, error } = useDiagramByRoute(
|
||
|
|
preloadedLayout ? undefined : application, // disable fetch when preloaded
|
||
|
|
preloadedLayout ? undefined : routeId,
|
||
|
|
direction,
|
||
|
|
);
|
||
|
|
const layout = preloadedLayout ?? fetchedLayout;
|
||
|
|
// ... rest of section/edge separation logic uses `layout`
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Update ProcessDiagram to pass diagramLayout through**
|
||
|
|
|
||
|
|
In `ProcessDiagram.tsx`, pass the new `diagramLayout` prop to `useDiagramData`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const { sections, totalWidth, totalHeight, isLoading, error } = useDiagramData(
|
||
|
|
application, currentRouteId, direction, diagramLayout,
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
Expected: No errors
|
||
|
|
|
||
|
|
- [ ] **Step 4: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ExecutionDiagram/types.ts \
|
||
|
|
ui/src/components/ProcessDiagram/types.ts
|
||
|
|
git commit -m "feat: add execution overlay types and ProcessDiagram overlay props"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 5: DiagramNode overlay visual states
|
||
|
|
|
||
|
|
Update DiagramNode to render execution status: green tint + checkmark for completed, red tint + ! for failed, dimmed for skipped, duration badge.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/DiagramNode.tsx`
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/ProcessDiagram.module.css`
|
||
|
|
|
||
|
|
**Reference:** Design spec Section 3 — Node Visual States
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add overlay prop to DiagramNode**
|
||
|
|
|
||
|
|
Add `executionState?: NodeExecutionState` to the `DiagramNodeProps` interface. Import `NodeExecutionState` from `ExecutionDiagram/types`.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement overlay visuals**
|
||
|
|
|
||
|
|
When `executionState` is present:
|
||
|
|
|
||
|
|
**Completed state:**
|
||
|
|
- Card background fill: `#F0F9F1` (green tint) instead of white
|
||
|
|
- Border stroke: `#3D7C47` at 1.5px + left accent bar 4px green
|
||
|
|
- Checkmark badge: green circle (16px, `#3D7C47`) with white `✓` at top-right corner (x: node.width - 8, y: -8)
|
||
|
|
- Duration text: green `#3D7C47` at bottom-right (e.g., "5ms")
|
||
|
|
|
||
|
|
**Failed state:**
|
||
|
|
- Card background fill: `#FDF2F0` (red tint)
|
||
|
|
- Border stroke: `#C0392B` at 2px
|
||
|
|
- Error badge: red circle (16px, `#C0392B`) with white `!` at top-right
|
||
|
|
- Duration text: red `#C0392B` at bottom-right
|
||
|
|
- Label text color: `#C0392B`
|
||
|
|
|
||
|
|
**Sub-route failure** (`subRouteFailed` flag):
|
||
|
|
- Same as failed, plus a drill-down arrow icon (↳) at bottom-left
|
||
|
|
|
||
|
|
**No execution state and overlay is active (skipped):**
|
||
|
|
- Set group opacity to 0.35
|
||
|
|
- This requires knowing whether overlay mode is active. Add an `overlayActive?: boolean` prop. When true and no `executionState`, dim the node.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add CSS for overlay elements**
|
||
|
|
|
||
|
|
Add to `ProcessDiagram.module.css`: styles for duration badge text positioning and any overlay-specific hover states.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ProcessDiagram/DiagramNode.tsx \
|
||
|
|
ui/src/components/ProcessDiagram/ProcessDiagram.module.css
|
||
|
|
git commit -m "feat: DiagramNode execution overlay visuals (tint, badges, duration)"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 6: DiagramEdge traversed/not-traversed states
|
||
|
|
|
||
|
|
Update DiagramEdge to show green solid edges for traversed paths and dashed gray for not-traversed.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/DiagramEdge.tsx`
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/ProcessDiagram.tsx`
|
||
|
|
|
||
|
|
**Reference:** Design spec Section 3 — Edge States
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add traversal prop to DiagramEdge**
|
||
|
|
|
||
|
|
Add `traversed?: boolean | undefined` to DiagramEdgeProps. When `undefined` (no overlay), render as current default. When `true`, render green solid. When `false`, render gray dashed.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Traversed: green solid
|
||
|
|
stroke={traversed === true ? '#3D7C47' : traversed === false ? '#9CA3AF' : '#9CA3AF'}
|
||
|
|
strokeWidth={traversed === true ? 1.5 : 1}
|
||
|
|
strokeDasharray={traversed === false ? '4,3' : undefined}
|
||
|
|
markerEnd={traversed === false ? undefined : 'url(#arrowhead)'}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Add a green arrowhead marker**
|
||
|
|
|
||
|
|
In ProcessDiagram.tsx `<defs>`, add a second marker with green fill:
|
||
|
|
```xml
|
||
|
|
<marker id="arrowhead-green" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||
|
|
<polygon points="0 0, 8 3, 0 6" fill="#3D7C47" />
|
||
|
|
</marker>
|
||
|
|
```
|
||
|
|
|
||
|
|
Use `url(#arrowhead-green)` for traversed edges.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Compute edge traversal in ProcessDiagram**
|
||
|
|
|
||
|
|
In ProcessDiagram.tsx, when `executionOverlay` is present, determine if an edge is traversed by checking if both its `sourceId` and `targetId` have entries in the overlay map. Pass this as `traversed` prop to each `DiagramEdge`.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ProcessDiagram/DiagramEdge.tsx \
|
||
|
|
ui/src/components/ProcessDiagram/ProcessDiagram.tsx
|
||
|
|
git commit -m "feat: DiagramEdge traversed/not-traversed overlay states"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 7: CompoundNode iteration stepper
|
||
|
|
|
||
|
|
Add an iteration stepper widget to compound node headers (LOOP, SPLIT, MULTICAST) when overlay iteration data is present.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/CompoundNode.tsx`
|
||
|
|
- Modify: `ui/src/components/ProcessDiagram/ProcessDiagram.module.css`
|
||
|
|
|
||
|
|
**Reference:** Design spec Section 4 — Per-Compound Iteration Stepper
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add overlay props to CompoundNode**
|
||
|
|
|
||
|
|
Add to CompoundNodeProps:
|
||
|
|
```typescript
|
||
|
|
executionOverlay?: Map<string, NodeExecutionState>;
|
||
|
|
overlayActive?: boolean;
|
||
|
|
iterationState?: Map<string, IterationInfo>;
|
||
|
|
onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void;
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Render iteration stepper in compound header**
|
||
|
|
|
||
|
|
When `iterationState` has an entry for this compound node's ID, render a stepper widget inside the colored header bar (right-aligned):
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
{iterationInfo && (
|
||
|
|
<foreignObject x={headerWidth - 80} y={1} width={75} height={20}>
|
||
|
|
<div className={styles.iterationStepper}>
|
||
|
|
<button onClick={() => onIterationChange?.(node.id!, iterationInfo.current - 1)}
|
||
|
|
disabled={iterationInfo.current <= 0}>‹</button>
|
||
|
|
<span>{iterationInfo.current + 1} / {iterationInfo.total}</span>
|
||
|
|
<button onClick={() => onIterationChange?.(node.id!, iterationInfo.current + 1)}
|
||
|
|
disabled={iterationInfo.current >= iterationInfo.total - 1}>›</button>
|
||
|
|
</div>
|
||
|
|
</foreignObject>
|
||
|
|
)}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add CSS for iteration stepper**
|
||
|
|
|
||
|
|
```css
|
||
|
|
.iterationStepper {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 2px;
|
||
|
|
background: rgba(255, 255, 255, 0.15);
|
||
|
|
border-radius: 3px;
|
||
|
|
padding: 1px 3px;
|
||
|
|
font-size: 10px;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
.iterationStepper button {
|
||
|
|
width: 16px;
|
||
|
|
height: 16px;
|
||
|
|
border: none;
|
||
|
|
background: rgba(255, 255, 255, 0.2);
|
||
|
|
color: white;
|
||
|
|
border-radius: 2px;
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 10px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 0;
|
||
|
|
}
|
||
|
|
.iterationStepper button:disabled {
|
||
|
|
opacity: 0.3;
|
||
|
|
cursor: default;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Pass overlay props through from ProcessDiagram**
|
||
|
|
|
||
|
|
In ProcessDiagram.tsx, pass `executionOverlay`, `overlayActive` (= `!!executionOverlay`), `iterationState`, and `onIterationChange` down to CompoundNode instances and ErrorSection instances. Both components render child nodes that need overlay state.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Update ErrorSection to accept and forward overlay props**
|
||
|
|
|
||
|
|
`ErrorSection.tsx` internally renders `DiagramNode` and `CompoundNode`. Add `executionOverlay`, `overlayActive`, `iterationState`, and `onIterationChange` to its props interface, and pass them through to its child node renderers. Without this, error/completion handler section nodes will render without overlay styling.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Apply overlay dimming to compound children**
|
||
|
|
|
||
|
|
When overlay is active: compound children without execution state should be dimmed (opacity 0.35). Pass `overlayActive` and `executionOverlay` down to child DiagramNode and recursive CompoundNode calls.
|
||
|
|
|
||
|
|
**Compound node overlay behavior:** Compound nodes (LOOP, SPLIT, CHOICE) appear as processors in the execution tree but should NOT get status badges. The `useExecutionOverlay` hook (Task 9) should either skip compound-type processor IDs or mark them with a flag. Compound nodes derive their visual from their children — if all children are green, the compound looks normal; if a child failed, the child shows red.
|
||
|
|
|
||
|
|
- [ ] **Step 7: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 8: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ProcessDiagram/CompoundNode.tsx \
|
||
|
|
ui/src/components/ProcessDiagram/ErrorSection.tsx \
|
||
|
|
ui/src/components/ProcessDiagram/ProcessDiagram.tsx \
|
||
|
|
ui/src/components/ProcessDiagram/ProcessDiagram.module.css
|
||
|
|
git commit -m "feat: CompoundNode iteration stepper and overlay prop threading"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 8: useProcessorSnapshotById hook
|
||
|
|
|
||
|
|
Add a frontend hook for the new snapshot-by-processorId endpoint.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ui/src/api/queries/executions.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add the hook**
|
||
|
|
|
||
|
|
**Important:** This project uses `openapi-fetch` with typed `api.GET()` calls (uppercase, path template parameters), not axios-style `api.get()`. Follow the existing pattern in `executions.ts`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export function useProcessorSnapshotById(
|
||
|
|
executionId: string | null,
|
||
|
|
processorId: string | null,
|
||
|
|
) {
|
||
|
|
return useQuery({
|
||
|
|
queryKey: ['processor-snapshot-by-id', executionId, processorId],
|
||
|
|
queryFn: async () => {
|
||
|
|
const { data, error } = await api.GET(
|
||
|
|
'/executions/{executionId}/processors/by-id/{processorId}/snapshot',
|
||
|
|
{ params: { path: { executionId: executionId!, processorId: processorId! } } },
|
||
|
|
);
|
||
|
|
if (error) throw error;
|
||
|
|
return data as Record<string, string>;
|
||
|
|
},
|
||
|
|
enabled: !!executionId && !!processorId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note:** This endpoint path must exist in `openapi.json` (added in Task 3) for the typed path to work. If the openapi spec was manually updated rather than regenerated, ensure the path template matches exactly.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 3: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/api/queries/executions.ts
|
||
|
|
git commit -m "feat: add useProcessorSnapshotById hook"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 9: useExecutionOverlay and useIterationState hooks
|
||
|
|
|
||
|
|
Create the hooks that map execution data to diagram overlay state and manage per-compound iteration.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/useExecutionOverlay.ts`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/useIterationState.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create useExecutionOverlay**
|
||
|
|
|
||
|
|
This hook takes an `ExecutionDetail` and the current iteration state, and produces a `Map<string, NodeExecutionState>` mapping processor IDs to overlay states.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { useMemo } from 'react';
|
||
|
|
import type { NodeExecutionState, IterationInfo } from './types';
|
||
|
|
import type { components } from '../../api/schema';
|
||
|
|
|
||
|
|
type ProcessorNode = components['schemas']['ProcessorNode'];
|
||
|
|
|
||
|
|
export function useExecutionOverlay(
|
||
|
|
processors: ProcessorNode[] | undefined,
|
||
|
|
iterationState: Map<string, IterationInfo>,
|
||
|
|
): Map<string, NodeExecutionState> {
|
||
|
|
return useMemo(() => {
|
||
|
|
if (!processors) return new Map();
|
||
|
|
const overlay = new Map<string, NodeExecutionState>();
|
||
|
|
buildOverlay(processors, overlay, iterationState);
|
||
|
|
return overlay;
|
||
|
|
}, [processors, iterationState]);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The `buildOverlay` function recursively walks the `ProcessorNode` tree:
|
||
|
|
- For each processor with a `processorId`, create a `NodeExecutionState` with `status` and `durationMs`
|
||
|
|
- If the processor is a DIRECT/SEDA type and has status FAILED, set `subRouteFailed: true` (the target sub-route failed)
|
||
|
|
- For processors with `loopIndex`/`splitIndex`: only include the processor if it matches the current iteration selected in `iterationState` for its parent compound
|
||
|
|
- Set `hasTraceData: true` if the processor has non-null fields (as a heuristic, or always true since snapshot can be fetched lazily)
|
||
|
|
|
||
|
|
- [ ] **Step 2: Create useIterationState**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { useCallback, useState } from 'react';
|
||
|
|
import type { IterationInfo } from './types';
|
||
|
|
import type { components } from '../../api/schema';
|
||
|
|
|
||
|
|
type ProcessorNode = components['schemas']['ProcessorNode'];
|
||
|
|
|
||
|
|
export function useIterationState(processors: ProcessorNode[] | undefined) {
|
||
|
|
const [state, setState] = useState<Map<string, IterationInfo>>(new Map());
|
||
|
|
|
||
|
|
// Initialize iteration info from processor tree when processors change
|
||
|
|
// Walk the tree, find compounds with children that have loopSize/splitSize
|
||
|
|
// Set default iteration to 0 for each compound
|
||
|
|
|
||
|
|
const setIteration = useCallback((compoundId: string, index: number) => {
|
||
|
|
setState(prev => {
|
||
|
|
const next = new Map(prev);
|
||
|
|
const info = next.get(compoundId);
|
||
|
|
if (info && index >= 0 && index < info.total) {
|
||
|
|
next.set(compoundId, { ...info, current: index });
|
||
|
|
}
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return { iterationState: state, setIteration };
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Key logic: Walk the `ProcessorNode` tree looking for processors that are children of the same parent and have different `loopIndex` or `splitIndex` values. Group by parent, compute total from `loopSize`/`splitSize`, initialize `current: 0`.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 4: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ExecutionDiagram/useExecutionOverlay.ts \
|
||
|
|
ui/src/components/ExecutionDiagram/useIterationState.ts
|
||
|
|
git commit -m "feat: useExecutionOverlay and useIterationState hooks"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 10: DetailPanel with tab infrastructure
|
||
|
|
|
||
|
|
Create the bottom panel component with tab bar, processor header, and tab content area.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/DetailPanel.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/tabs/HeadersTab.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/tabs/ErrorTab.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/tabs/ConfigTab.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/tabs/TimelineTab.tsx`
|
||
|
|
- Modify: `ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css`
|
||
|
|
|
||
|
|
**Reference:** Design spec Section 6 — Detail Panel
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create DetailPanel shell**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface DetailPanelProps {
|
||
|
|
/** Currently selected processor (or null for exchange-level view) */
|
||
|
|
selectedProcessor: ProcessorNode | null;
|
||
|
|
/** The full execution detail (for exchange-level tabs and timeline) */
|
||
|
|
executionDetail: ExecutionDetail;
|
||
|
|
/** Execution ID for fetching snapshots */
|
||
|
|
executionId: string;
|
||
|
|
/** Callback to select a processor (from timeline clicks) */
|
||
|
|
onSelectProcessor: (processorId: string) => void;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The panel renders:
|
||
|
|
1. A processor header bar showing selected processor name, status badge, processor ID, duration — or exchange-level info when nothing selected
|
||
|
|
2. A tab bar with 7 tabs: Info, Headers, Input, Output, Error, Config, Timeline
|
||
|
|
3. Active tab content area (scrollable)
|
||
|
|
|
||
|
|
Track `activeTab` state internally. When `selectedProcessor` changes, keep the current tab unless it was Error (auto-switch to Info for non-failed processors).
|
||
|
|
|
||
|
|
- [ ] **Step 2: Create InfoTab**
|
||
|
|
|
||
|
|
Grid layout showing:
|
||
|
|
- Processor: ID, Type, Status, Start/End time, Duration, Endpoint URI
|
||
|
|
- Attributes: tap-extracted attributes rendered as pill badges
|
||
|
|
- When no processor selected: show exchange-level metadata (executionId, correlationId, route, app, total duration, engine level, route attributes)
|
||
|
|
|
||
|
|
Data source: `ProcessorNode` fields directly (no API call needed).
|
||
|
|
|
||
|
|
- [ ] **Step 3: Create HeadersTab**
|
||
|
|
|
||
|
|
Side-by-side layout:
|
||
|
|
- Left: Input headers parsed from JSON string → key/value table
|
||
|
|
- Right: Output headers parsed from JSON string → key/value table
|
||
|
|
- Compare and highlight new/changed headers in green
|
||
|
|
|
||
|
|
Data source: `useProcessorSnapshotById(executionId, processorId)` → `inputHeaders`, `outputHeaders`. When no processor selected, use `executionDetail.inputHeaders` / `executionDetail.outputHeaders`.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Create BodyTab (shared by Input and Output)**
|
||
|
|
|
||
|
|
Formatted body display:
|
||
|
|
- Auto-detect format (try JSON parse → formatted, otherwise plain text)
|
||
|
|
- Dark-themed code block with monospace font
|
||
|
|
- Copy button
|
||
|
|
- Byte size indicator
|
||
|
|
|
||
|
|
Props: `{ body: string | undefined; label: string }`. The Input tab passes `snapshot.inputBody`, the Output tab passes `snapshot.outputBody`.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Create ErrorTab**
|
||
|
|
|
||
|
|
When processor has error:
|
||
|
|
- Exception type (errorType from attributes if available, otherwise derive from errorMessage)
|
||
|
|
- Error message
|
||
|
|
- Root cause (if available in attributes)
|
||
|
|
- Stack trace in monospace pre block
|
||
|
|
|
||
|
|
When no error: show grayed-out "No error on this processor" message.
|
||
|
|
|
||
|
|
Data source: `ProcessorNode.errorMessage`, `ProcessorNode.errorStackTrace`.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Create ConfigTab (placeholder)**
|
||
|
|
|
||
|
|
Show a centered message: "Processor configuration data is not yet available." Styled with muted text.
|
||
|
|
|
||
|
|
- [ ] **Step 7: Create TimelineTab**
|
||
|
|
|
||
|
|
Gantt-style horizontal bar chart:
|
||
|
|
- Flatten `ExecutionDetail.processors` tree into execution-order list
|
||
|
|
- One row per executed processor
|
||
|
|
- Bar width: `(processor.durationMs / execution.durationMs) * 100%`
|
||
|
|
- Bar offset: `((processor.startTime - execution.startTime) / execution.durationMs) * 100%`
|
||
|
|
- Green bars for COMPLETED, red for FAILED
|
||
|
|
- Click handler: `onSelectProcessor(processorId)`
|
||
|
|
- Highlight the currently selected processor
|
||
|
|
|
||
|
|
- [ ] **Step 8: Add CSS for detail panel**
|
||
|
|
|
||
|
|
Add to `ExecutionDiagram.module.css`:
|
||
|
|
- `.detailPanel` — flex column layout
|
||
|
|
- `.processorHeader` — flex row with processor info
|
||
|
|
- `.tabBar` — flex row of tab buttons
|
||
|
|
- `.tabButton`, `.tabButtonActive` — styled tabs
|
||
|
|
- `.tabContent` — scrollable content area
|
||
|
|
- Tab-specific styles (`.infoGrid`, `.headersTable`, `.codeBlock`, `.ganttRow`, etc.)
|
||
|
|
|
||
|
|
- [ ] **Step 9: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 10: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ExecutionDiagram/DetailPanel.tsx \
|
||
|
|
ui/src/components/ExecutionDiagram/tabs/ \
|
||
|
|
ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
|
||
|
|
git commit -m "feat: DetailPanel with 7 tabs (Info, Headers, Input, Output, Error, Config, Timeline)"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 11: ExecutionDiagram wrapper component
|
||
|
|
|
||
|
|
Create the root wrapper that composes ProcessDiagram with execution overlay data, the exchange summary bar, resizable splitter, and the detail panel.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx`
|
||
|
|
- Create: `ui/src/components/ExecutionDiagram/index.ts`
|
||
|
|
- Modify: `ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css`
|
||
|
|
|
||
|
|
**Reference:** Design spec Sections 1, 5, 7
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create ExecutionDiagram component**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ExecutionDiagramProps {
|
||
|
|
executionId: string;
|
||
|
|
executionDetail?: ExecutionDetail;
|
||
|
|
direction?: 'LR' | 'TB';
|
||
|
|
knownRouteIds?: Set<string>;
|
||
|
|
onNodeAction?: (nodeId: string, action: NodeAction) => void;
|
||
|
|
nodeConfigs?: Map<string, NodeConfig>;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The component:
|
||
|
|
1. Fetches `ExecutionDetail` via `useExecutionDetail(executionId)` if not pre-fetched
|
||
|
|
2. **Loads diagram by content hash** via `useDiagramLayout(detail.diagramContentHash, direction)` — this ensures the diagram version matches the execution, not the latest version
|
||
|
|
3. Initializes `useIterationState(detail.processors)`
|
||
|
|
4. Computes `useExecutionOverlay(detail.processors, iterationState)`
|
||
|
|
5. Manages `selectedProcessorId` state
|
||
|
|
6. Finds the selected `ProcessorNode` in the tree by walking `detail.processors`
|
||
|
|
|
||
|
|
**RUNNING execution handling:** If `detail.status === 'RUNNING'`, the overlay shows completed processors with green/red and unreached processors as dimmed. No special RUNNING visual is needed — the overlay hook only creates entries for processors that have executed.
|
||
|
|
|
||
|
|
Layout:
|
||
|
|
```tsx
|
||
|
|
<div className={styles.executionDiagram}>
|
||
|
|
{/* Exchange summary bar */}
|
||
|
|
<div className={styles.exchangeBar}>
|
||
|
|
<ExchangeId /> <StatusBadge /> <RouteInfo /> <Duration />
|
||
|
|
{detail.status === 'FAILED' && <JumpToErrorButton />}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Diagram area */}
|
||
|
|
<div className={styles.diagramArea} style={{ flex: `1 1 ${splitPercent}%` }}>
|
||
|
|
<ProcessDiagram
|
||
|
|
application={detail.applicationName}
|
||
|
|
routeId={detail.routeId}
|
||
|
|
direction={direction}
|
||
|
|
diagramLayout={diagramLayout}
|
||
|
|
selectedNodeId={selectedProcessorId}
|
||
|
|
onNodeSelect={setSelectedProcessorId}
|
||
|
|
onNodeAction={onNodeAction}
|
||
|
|
nodeConfigs={nodeConfigs}
|
||
|
|
knownRouteIds={knownRouteIds}
|
||
|
|
executionOverlay={overlay}
|
||
|
|
iterationState={iterationState}
|
||
|
|
onIterationChange={setIteration}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Resizable splitter */}
|
||
|
|
<div className={styles.splitter} onPointerDown={startResize} />
|
||
|
|
|
||
|
|
{/* Detail panel */}
|
||
|
|
<div className={styles.detailArea} style={{ flex: `0 0 ${100 - splitPercent}%` }}>
|
||
|
|
<DetailPanel
|
||
|
|
selectedProcessor={findProcessorInTree(detail.processors, selectedProcessorId)}
|
||
|
|
executionDetail={detail}
|
||
|
|
executionId={executionId}
|
||
|
|
onSelectProcessor={setSelectedProcessorId}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement resizable splitter**
|
||
|
|
|
||
|
|
Track `splitPercent` state (default 60). On pointer down on splitter, capture pointer, update percentage on pointer move based on container height. Minimum panel height ~120px.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement "Jump to Error"**
|
||
|
|
|
||
|
|
Find first FAILED `ProcessorNode` in tree. Set it as selected. If it's a DIRECT/SEDA node, trigger drill-down into the sub-route via `ProcessDiagram`'s double-click mechanism. Auto-switch detail panel to Error tab.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Add exchange summary bar CSS**
|
||
|
|
|
||
|
|
```css
|
||
|
|
.exchangeBar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
padding: 8px 14px;
|
||
|
|
background: var(--bg-surface, #FFFFFF);
|
||
|
|
border-bottom: 1px solid var(--border, #E4DFD8);
|
||
|
|
font-size: 12px;
|
||
|
|
color: var(--text-secondary, #5C5347);
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Add layout CSS**
|
||
|
|
|
||
|
|
```css
|
||
|
|
.executionDiagram {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
min-height: 400px;
|
||
|
|
}
|
||
|
|
.diagramArea {
|
||
|
|
overflow: hidden;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
.splitter {
|
||
|
|
height: 4px;
|
||
|
|
background: var(--border, #E4DFD8);
|
||
|
|
cursor: row-resize;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.detailArea {
|
||
|
|
overflow: hidden;
|
||
|
|
min-height: 120px;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 6: Create index.ts**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export { ExecutionDiagram } from './ExecutionDiagram';
|
||
|
|
export type { ExecutionDiagramProps } from './ExecutionDiagram';
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 8: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/components/ExecutionDiagram/
|
||
|
|
git commit -m "feat: ExecutionDiagram wrapper with exchange bar, splitter, and detail panel"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 12: Integrate into ExchangeDetail page
|
||
|
|
|
||
|
|
Replace the existing RouteFlow-based "Flow" view in ExchangeDetail with the new ExecutionDiagram component.
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ui/src/pages/ExchangeDetail/ExchangeDetail.tsx`
|
||
|
|
- Modify: `ui/src/pages/ExchangeDetail/ExchangeDetail.module.css`
|
||
|
|
|
||
|
|
**Reference:** Design spec Section 9; existing Flow view at lines ~486-507 of ExchangeDetail.tsx
|
||
|
|
|
||
|
|
- [ ] **Step 1: Import ExecutionDiagram**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { ExecutionDiagram } from '../../components/ExecutionDiagram';
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Replace the Flow view section**
|
||
|
|
|
||
|
|
Replace the `RouteFlow` rendering in the "flow" view branch with:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
{timelineView === 'flow' && (
|
||
|
|
<div className={styles.executionDiagramContainer}>
|
||
|
|
<ExecutionDiagram
|
||
|
|
executionId={id!}
|
||
|
|
executionDetail={detail}
|
||
|
|
knownRouteIds={knownRouteIds}
|
||
|
|
onNodeAction={handleNodeAction}
|
||
|
|
nodeConfigs={nodeConfigs}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
```
|
||
|
|
|
||
|
|
Build `knownRouteIds` from the route catalog (same pattern as DevDiagram page). Build `nodeConfigs` from existing `tracedMap`.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Remove or simplify redundant sections**
|
||
|
|
|
||
|
|
The ExchangeDetail page currently has its own processor snapshot display (Message IN/OUT panels). With the detail panel inside ExecutionDiagram, these become redundant when in Flow view. Keep them for the Gantt view, but hide when Flow view is active.
|
||
|
|
|
||
|
|
The exchange header card at the top of the page stays — it provides context. The ExecutionDiagram's exchange bar provides a compact summary inside the diagram area.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Add CSS for container**
|
||
|
|
|
||
|
|
```css
|
||
|
|
.executionDiagramContainer {
|
||
|
|
height: 600px;
|
||
|
|
border: 1px solid var(--border, #E4DFD8);
|
||
|
|
border-radius: var(--radius-md, 8px);
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Verify types compile**
|
||
|
|
|
||
|
|
Run: `cd ui && npx tsc -p tsconfig.app.json --noEmit`
|
||
|
|
|
||
|
|
- [ ] **Step 6: Manual test**
|
||
|
|
|
||
|
|
Navigate to an exchange detail page for a known execution. Toggle to Flow view. Verify:
|
||
|
|
- Diagram renders with execution overlay
|
||
|
|
- Completed nodes are green with checkmarks
|
||
|
|
- Failed nodes are red with ! badge
|
||
|
|
- Skipped nodes are dimmed
|
||
|
|
- Clicking a node updates the detail panel
|
||
|
|
- Tabs work (Info, Headers, Input, Output, Error, Timeline)
|
||
|
|
- "Jump to Error" navigates to the failed processor
|
||
|
|
|
||
|
|
- [ ] **Step 7: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add ui/src/pages/ExchangeDetail/ExchangeDetail.tsx \
|
||
|
|
ui/src/pages/ExchangeDetail/ExchangeDetail.module.css
|
||
|
|
git commit -m "feat: integrate ExecutionDiagram into ExchangeDetail flow view"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Verification
|
||
|
|
|
||
|
|
1. `mvn clean compile -DskipTests` passes (backend changes)
|
||
|
|
2. `cd ui && npx tsc -p tsconfig.app.json --noEmit` passes (frontend types)
|
||
|
|
3. ExecutionDiagram renders on ExchangeDetail page for a known failed exchange
|
||
|
|
4. Completed nodes show green tint + checkmark + duration badge
|
||
|
|
5. Failed nodes show red tint + ! badge + red duration
|
||
|
|
6. Skipped nodes are dimmed to 35% opacity
|
||
|
|
7. Edges between executed nodes turn green; edges to skipped nodes are dashed gray
|
||
|
|
8. Loop/split compounds show iteration stepper; stepping updates child overlay
|
||
|
|
9. Nested loops step independently (outer loop at iteration 2, inner split at branch 1)
|
||
|
|
10. CHOICE compounds highlight taken branch, dim untaken branches
|
||
|
|
11. Clicking a node shows its data in the detail panel
|
||
|
|
11. Detail panel tabs: Info shows metadata + attributes, Headers shows side-by-side, Input/Output show formatted body, Error shows exception + stack trace, Timeline shows Gantt chart
|
||
|
|
12. "Jump to Error" navigates to and selects the failed processor
|
||
|
|
13. Error tab grayed out for non-failed processors
|
||
|
|
14. Config tab shows placeholder
|
||
|
|
15. Resizable splitter between diagram and detail panel works
|