2026-03-28 15:48:38 +01:00
|
|
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
2026-04-09 18:51:49 +02:00
|
|
|
import { X } from 'lucide-react';
|
2026-03-28 15:58:38 +01:00
|
|
|
import { useNavigate, useLocation, useParams } from 'react-router';
|
2026-03-29 13:08:58 +02:00
|
|
|
import { useGlobalFilters, useToast } from '@cameleer/design-system';
|
2026-03-28 14:22:34 +01:00
|
|
|
import { useExecutionDetail } from '../../api/queries/executions';
|
2026-03-28 13:57:13 +01:00
|
|
|
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
2026-04-08 23:43:14 +02:00
|
|
|
import { useCatalog } from '../../api/queries/catalog';
|
2026-04-04 13:49:28 +02:00
|
|
|
import { useAgents } from '../../api/queries/agents';
|
2026-03-29 13:08:58 +02:00
|
|
|
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
2026-04-02 19:08:00 +02:00
|
|
|
import type { TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
|
2026-04-09 16:28:09 +02:00
|
|
|
import { useEnvironmentStore } from '../../api/environment-store';
|
2026-04-06 15:51:15 +02:00
|
|
|
import { useCanControl } from '../../auth/auth-store';
|
2026-03-28 16:04:53 +01:00
|
|
|
import { useTracingStore } from '../../stores/tracing-store';
|
|
|
|
|
import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types';
|
2026-03-29 13:08:58 +02:00
|
|
|
import { TapConfigModal } from '../../components/TapConfigModal';
|
2026-03-28 13:57:13 +01:00
|
|
|
import { ExchangeHeader } from './ExchangeHeader';
|
2026-04-04 13:49:28 +02:00
|
|
|
import { RouteControlBar } from './RouteControlBar';
|
2026-03-28 13:57:13 +01:00
|
|
|
import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram';
|
|
|
|
|
import { ProcessDiagram } from '../../components/ProcessDiagram';
|
|
|
|
|
import styles from './ExchangesPage.module.css';
|
|
|
|
|
|
|
|
|
|
import Dashboard from '../Dashboard/Dashboard';
|
2026-03-28 15:20:17 +01:00
|
|
|
import type { SelectedExchange } from '../Dashboard/Dashboard';
|
2026-03-28 13:57:13 +01:00
|
|
|
|
|
|
|
|
export default function ExchangesPage() {
|
2026-03-28 15:48:38 +01:00
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const location = useLocation();
|
2026-04-09 16:28:09 +02:00
|
|
|
const selectedEnv = useEnvironmentStore((s) => s.environment);
|
2026-03-31 15:26:36 +02:00
|
|
|
const { appId: scopedAppId, routeId: scopedRouteId, exchangeId: scopedExchangeId } =
|
|
|
|
|
useParams<{ appId?: string; routeId?: string; exchangeId?: string }>();
|
2026-03-28 15:48:38 +01:00
|
|
|
|
|
|
|
|
// Restore selection from browser history state (enables Back/Forward)
|
|
|
|
|
const stateSelected = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
|
|
|
|
|
|
2026-03-31 15:26:36 +02:00
|
|
|
// Derive selection from URL params when no state-based selection exists (Cmd-K, bookmarks)
|
|
|
|
|
const urlDerivedExchange: SelectedExchange | null =
|
|
|
|
|
(scopedExchangeId && scopedAppId && scopedRouteId)
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
? { executionId: scopedExchangeId, applicationId: scopedAppId, routeId: scopedRouteId }
|
2026-03-31 15:26:36 +02:00
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
const [selected, setSelectedInternal] = useState<SelectedExchange | null>(stateSelected ?? urlDerivedExchange);
|
|
|
|
|
|
|
|
|
|
// Sync selection from history state or URL params on navigation changes
|
2026-03-28 15:48:38 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const restored = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
|
2026-03-31 15:26:36 +02:00
|
|
|
if (restored) {
|
|
|
|
|
setSelectedInternal(restored);
|
|
|
|
|
} else if (scopedExchangeId && scopedAppId && scopedRouteId) {
|
|
|
|
|
setSelectedInternal({
|
|
|
|
|
executionId: scopedExchangeId,
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
applicationId: scopedAppId,
|
2026-03-31 15:26:36 +02:00
|
|
|
routeId: scopedRouteId,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedInternal(null);
|
|
|
|
|
}
|
|
|
|
|
}, [location.state, scopedExchangeId, scopedAppId, scopedRouteId]);
|
2026-03-28 15:48:38 +01:00
|
|
|
|
2026-03-28 14:29:19 +01:00
|
|
|
const [splitPercent, setSplitPercent] = useState(50);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
2026-03-28 15:48:38 +01:00
|
|
|
// Select an exchange: push a history entry so Back restores the previous state
|
2026-03-28 15:20:17 +01:00
|
|
|
const handleExchangeSelect = useCallback((exchange: SelectedExchange) => {
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(exchange);
|
|
|
|
|
navigate(location.pathname + location.search, {
|
|
|
|
|
state: { ...location.state, selectedExchange: exchange },
|
|
|
|
|
});
|
|
|
|
|
}, [navigate, location.pathname, location.search, location.state]);
|
2026-03-28 15:20:17 +01:00
|
|
|
|
2026-03-28 15:48:38 +01:00
|
|
|
// Select a correlated exchange: push another history entry
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
const handleCorrelatedSelect = useCallback((executionId: string, applicationId: string, routeId: string) => {
|
|
|
|
|
const exchange = { executionId, applicationId, routeId };
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(exchange);
|
|
|
|
|
navigate(location.pathname + location.search, {
|
|
|
|
|
state: { ...location.state, selectedExchange: exchange },
|
|
|
|
|
});
|
|
|
|
|
}, [navigate, location.pathname, location.search, location.state]);
|
2026-03-28 15:26:01 +01:00
|
|
|
|
2026-03-31 15:26:36 +02:00
|
|
|
// Clear selection: navigate up to route level when URL has exchangeId
|
2026-03-28 15:42:45 +01:00
|
|
|
const handleClearSelection = useCallback(() => {
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(null);
|
2026-03-31 15:26:36 +02:00
|
|
|
if (scopedExchangeId && scopedAppId && scopedRouteId) {
|
|
|
|
|
navigate(`/exchanges/${scopedAppId}/${scopedRouteId}`, {
|
|
|
|
|
state: { ...location.state, selectedExchange: undefined },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [scopedExchangeId, scopedAppId, scopedRouteId, navigate, location.state]);
|
2026-03-28 15:42:45 +01:00
|
|
|
|
2026-03-28 14:29:19 +01:00
|
|
|
const handleSplitterDown = useCallback((e: React.PointerEvent) => {
|
|
|
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
|
|
|
const container = containerRef.current;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const onMove = (me: PointerEvent) => {
|
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
|
|
|
const x = me.clientX - rect.left;
|
|
|
|
|
const pct = Math.min(80, Math.max(20, (x / rect.width) * 100));
|
|
|
|
|
setSplitPercent(pct);
|
|
|
|
|
};
|
|
|
|
|
const onUp = () => {
|
|
|
|
|
document.removeEventListener('pointermove', onMove);
|
|
|
|
|
document.removeEventListener('pointerup', onUp);
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('pointermove', onMove);
|
|
|
|
|
document.addEventListener('pointerup', onUp);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// Show split view when a route is scoped (sidebar) or an exchange is selected
|
|
|
|
|
const showSplit = !!selected || !!scopedRouteId;
|
|
|
|
|
|
|
|
|
|
if (!showSplit) {
|
2026-04-03 11:28:26 +02:00
|
|
|
return <Dashboard onExchangeSelect={handleExchangeSelect} activeExchangeId={selected?.executionId} />;
|
2026-03-28 15:20:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// Determine what the right panel shows
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
const panelAppId = selected?.applicationId ?? scopedAppId!;
|
2026-03-28 15:58:38 +01:00
|
|
|
const panelRouteId = selected?.routeId ?? scopedRouteId!;
|
|
|
|
|
const panelExchangeId = selected?.executionId ?? undefined;
|
|
|
|
|
|
2026-03-28 13:57:13 +01:00
|
|
|
return (
|
2026-03-28 14:29:19 +01:00
|
|
|
<div ref={containerRef} className={styles.splitView}>
|
|
|
|
|
<div className={styles.leftPanel} style={{ width: `${splitPercent}%` }}>
|
2026-04-03 11:28:26 +02:00
|
|
|
<Dashboard onExchangeSelect={handleExchangeSelect} activeExchangeId={selected?.executionId} />
|
2026-03-28 14:22:34 +01:00
|
|
|
</div>
|
2026-03-28 14:29:19 +01:00
|
|
|
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
|
|
|
|
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
2026-04-09 18:51:49 +02:00
|
|
|
<button className={styles.closeBtn} onClick={handleClearSelection} title="Close panel" aria-label="Close panel">
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
2026-03-28 15:20:17 +01:00
|
|
|
<DiagramPanel
|
2026-03-28 15:58:38 +01:00
|
|
|
appId={panelAppId}
|
|
|
|
|
routeId={panelRouteId}
|
|
|
|
|
exchangeId={panelExchangeId}
|
2026-03-28 15:26:01 +01:00
|
|
|
onCorrelatedSelect={handleCorrelatedSelect}
|
2026-03-28 15:42:45 +01:00
|
|
|
onClearSelection={handleClearSelection}
|
2026-03-28 15:20:17 +01:00
|
|
|
/>
|
2026-03-28 14:22:34 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-28 13:57:13 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:20:17 +01:00
|
|
|
// ─── Right panel: diagram + execution overlay ───────────────────────────────
|
2026-03-28 13:57:13 +01:00
|
|
|
|
2026-03-28 14:22:34 +01:00
|
|
|
interface DiagramPanelProps {
|
2026-03-28 13:57:13 +01:00
|
|
|
appId: string;
|
|
|
|
|
routeId: string;
|
2026-03-28 15:58:38 +01:00
|
|
|
exchangeId?: string;
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
onCorrelatedSelect: (executionId: string, applicationId: string, routeId: string) => void;
|
2026-03-28 15:42:45 +01:00
|
|
|
onClearSelection: () => void;
|
2026-03-28 13:57:13 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:42:45 +01:00
|
|
|
function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearSelection }: DiagramPanelProps) {
|
2026-04-09 16:28:09 +02:00
|
|
|
const selectedEnv = useEnvironmentStore((s) => s.environment);
|
2026-03-28 13:57:13 +01:00
|
|
|
const { timeRange } = useGlobalFilters();
|
|
|
|
|
const timeFrom = timeRange.start.toISOString();
|
|
|
|
|
const timeTo = timeRange.end.toISOString();
|
|
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
const { data: detail } = useExecutionDetail(exchangeId ?? null);
|
|
|
|
|
const diagramQuery = useDiagramByRoute(appId, routeId);
|
2026-03-28 13:57:13 +01:00
|
|
|
|
2026-04-08 23:43:14 +02:00
|
|
|
const { data: catalog } = useCatalog();
|
2026-04-04 13:49:28 +02:00
|
|
|
|
|
|
|
|
// Route state + capabilities for topology-only control bar
|
|
|
|
|
const { data: agents } = useAgents(undefined, appId);
|
2026-04-06 15:51:15 +02:00
|
|
|
const canControl = useCanControl();
|
2026-04-04 13:49:28 +02:00
|
|
|
const { hasRouteControl, hasReplay } = useMemo(() => {
|
|
|
|
|
if (!agents) return { hasRouteControl: false, hasReplay: false };
|
|
|
|
|
const agentList = agents as any[];
|
|
|
|
|
return {
|
|
|
|
|
hasRouteControl: agentList.some((a: any) => a.capabilities?.routeControl === true),
|
|
|
|
|
hasReplay: agentList.some((a: any) => a.capabilities?.replay === true),
|
|
|
|
|
};
|
|
|
|
|
}, [agents]);
|
|
|
|
|
const routeState = useMemo(() => {
|
|
|
|
|
if (!catalog) return undefined;
|
|
|
|
|
for (const app of catalog as any[]) {
|
2026-04-08 23:43:14 +02:00
|
|
|
if (app.slug !== appId) continue;
|
2026-04-04 13:49:28 +02:00
|
|
|
for (const r of app.routes || []) {
|
|
|
|
|
if (r.routeId === routeId) return (r.routeState ?? 'started') as 'started' | 'stopped' | 'suspended';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}, [catalog, appId, routeId]);
|
|
|
|
|
|
2026-03-28 13:57:13 +01:00
|
|
|
const knownRouteIds = useMemo(() => {
|
|
|
|
|
const ids = new Set<string>();
|
|
|
|
|
if (catalog) {
|
|
|
|
|
for (const app of catalog as any[]) {
|
|
|
|
|
for (const r of app.routes || []) {
|
|
|
|
|
ids.add(r.routeId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ids;
|
|
|
|
|
}, [catalog]);
|
|
|
|
|
|
2026-03-28 18:31:08 +01:00
|
|
|
// Build endpoint URI → routeId map for cross-route drill-down
|
|
|
|
|
const endpointRouteMap = useMemo(() => {
|
|
|
|
|
const map = new Map<string, string>();
|
|
|
|
|
if (catalog) {
|
|
|
|
|
for (const app of catalog as any[]) {
|
|
|
|
|
for (const r of app.routes || []) {
|
|
|
|
|
if (r.fromEndpointUri) {
|
|
|
|
|
map.set(r.fromEndpointUri, r.routeId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}, [catalog]);
|
|
|
|
|
|
2026-03-30 18:08:40 +02:00
|
|
|
// Build nodeConfigs from app config (for TRACE/TAP badges)
|
feat!: scope per-app config and settings by environment
BREAKING: wipe dev PostgreSQL before deploying — V1 checksum changes.
Agents must now send environmentId on registration (400 if missing).
Two tables previously keyed on app name alone caused cross-environment
data bleed: writing config for (app=X, env=dev) would overwrite the row
used by (app=X, env=prod) agents, and agent startup fetches ignored env
entirely.
- V1 schema: application_config and app_settings are now PK (app, env).
- Repositories: env-keyed finders/saves; env is the authoritative column,
stamped on the stored JSON so the row agrees with itself.
- ApplicationConfigController.getConfig is dual-mode — AGENT role uses
JWT env claim (agents cannot spoof env); non-agent callers provide env
via ?environment= query param.
- AppSettingsController endpoints now require ?environment=.
- SensitiveKeysAdminController fan-out iterates (app, env) slices so each
env gets its own merged keys.
- DiagramController ingestion stamps env on TaggedDiagram; ClickHouse
route_diagrams INSERT + findProcessorRouteMapping are env-scoped.
- AgentRegistrationController: environmentId is required on register;
removed all "default" fallbacks from register/refresh/heartbeat auto-heal.
- UI hooks (useApplicationConfig, useProcessorRouteMapping, useAppSettings,
useAllAppSettings, useUpdateAppSettings) take env, wired to
useEnvironmentStore at all call sites.
- New ConfigEnvIsolationIT covers env-isolation for both repositories.
Plan in docs/superpowers/plans/2026-04-16-environment-scoping.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 22:25:21 +02:00
|
|
|
const { data: appConfig } = useApplicationConfig(appId, selectedEnv);
|
2026-03-28 16:04:53 +01:00
|
|
|
const nodeConfigs = useMemo(() => {
|
|
|
|
|
const map = new Map<string, NodeConfig>();
|
2026-03-30 18:08:40 +02:00
|
|
|
if (appConfig?.tracedProcessors) {
|
|
|
|
|
for (const pid of Object.keys(appConfig.tracedProcessors)) {
|
2026-03-28 16:04:53 +01:00
|
|
|
map.set(pid, { traceEnabled: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (appConfig?.taps) {
|
|
|
|
|
for (const tap of appConfig.taps) {
|
|
|
|
|
if (tap.enabled) {
|
|
|
|
|
const existing = map.get(tap.processorId);
|
|
|
|
|
map.set(tap.processorId, { ...existing, tapExpression: tap.expression });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
2026-03-30 18:08:40 +02:00
|
|
|
}, [appConfig]);
|
2026-03-28 16:04:53 +01:00
|
|
|
|
2026-03-29 13:08:58 +02:00
|
|
|
// Processor options for tap modal dropdown
|
|
|
|
|
const processorOptions = useMemo(() => {
|
|
|
|
|
const nodes = diagramQuery.data?.nodes;
|
|
|
|
|
if (!nodes) return [];
|
|
|
|
|
return (nodes as Array<{ id?: string; label?: string }>)
|
|
|
|
|
.filter(n => n.id)
|
|
|
|
|
.map(n => ({ value: n.id!, label: n.label || n.id! }));
|
|
|
|
|
}, [diagramQuery.data]);
|
|
|
|
|
|
|
|
|
|
// Tap modal state
|
|
|
|
|
const [tapModalOpen, setTapModalOpen] = useState(false);
|
|
|
|
|
const [tapModalTarget, setTapModalTarget] = useState<string | undefined>();
|
|
|
|
|
const [editingTap, setEditingTap] = useState<TapDefinition | null>(null);
|
|
|
|
|
|
|
|
|
|
const updateConfig = useUpdateApplicationConfig();
|
|
|
|
|
const { toast } = useToast();
|
|
|
|
|
|
|
|
|
|
const handleTapSave = useCallback((updatedConfig: typeof appConfig) => {
|
|
|
|
|
if (!updatedConfig) return;
|
2026-04-09 16:28:09 +02:00
|
|
|
updateConfig.mutate({ config: updatedConfig, environment: selectedEnv }, {
|
2026-04-02 19:08:00 +02:00
|
|
|
onSuccess: (saved: ConfigUpdateResponse) => {
|
|
|
|
|
if (saved.pushResult.success) {
|
|
|
|
|
toast({ title: 'Tap configuration saved', description: `Pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents (v${saved.config.version})`, variant: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut];
|
|
|
|
|
toast({ title: 'Tap configuration saved — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 });
|
|
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
onError: () => {
|
2026-04-02 19:08:00 +02:00
|
|
|
toast({ title: 'Tap update failed', description: 'Could not save configuration', variant: 'error', duration: 86_400_000 });
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}, [updateConfig, toast]);
|
|
|
|
|
|
|
|
|
|
const handleTapDelete = useCallback((tap: TapDefinition) => {
|
|
|
|
|
if (!appConfig) return;
|
|
|
|
|
const taps = appConfig.taps.filter(t => t.tapId !== tap.tapId);
|
2026-04-09 16:28:09 +02:00
|
|
|
updateConfig.mutate({ config: { ...appConfig, taps }, environment: selectedEnv }, {
|
2026-04-02 19:08:00 +02:00
|
|
|
onSuccess: (saved: ConfigUpdateResponse) => {
|
|
|
|
|
if (saved.pushResult.success) {
|
|
|
|
|
toast({ title: 'Tap deleted', description: `${tap.attributeName} removed — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents (v${saved.config.version})`, variant: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut];
|
|
|
|
|
toast({ title: 'Tap deleted — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 });
|
|
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
onError: () => {
|
2026-04-02 19:08:00 +02:00
|
|
|
toast({ title: 'Tap delete failed', description: 'Could not save configuration', variant: 'error', duration: 86_400_000 });
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}, [appConfig, updateConfig, toast]);
|
|
|
|
|
|
2026-03-28 14:37:58 +01:00
|
|
|
const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => {
|
|
|
|
|
if (action === 'configure-tap') {
|
2026-03-29 13:08:58 +02:00
|
|
|
if (!appConfig) return;
|
|
|
|
|
// Check if there's an existing tap for this processor
|
|
|
|
|
const existing = appConfig.taps?.find(t => t.processorId === nodeId) ?? null;
|
|
|
|
|
setEditingTap(existing);
|
|
|
|
|
setTapModalTarget(nodeId);
|
|
|
|
|
setTapModalOpen(true);
|
|
|
|
|
} else if (action === 'toggle-trace') {
|
|
|
|
|
if (!appConfig) return;
|
|
|
|
|
const newMap = useTracingStore.getState().toggleProcessor(appId, nodeId);
|
|
|
|
|
const enabled = nodeId in newMap;
|
|
|
|
|
const tracedProcessors: Record<string, string> = {};
|
|
|
|
|
for (const [k, v] of Object.entries(newMap)) tracedProcessors[k] = v;
|
2026-04-09 16:28:09 +02:00
|
|
|
updateConfig.mutate({ config: {
|
2026-03-29 13:08:58 +02:00
|
|
|
...appConfig,
|
|
|
|
|
tracedProcessors,
|
2026-04-09 16:28:09 +02:00
|
|
|
}, environment: selectedEnv }, {
|
2026-04-02 19:08:00 +02:00
|
|
|
onSuccess: (saved: ConfigUpdateResponse) => {
|
|
|
|
|
if (saved.pushResult.success) {
|
|
|
|
|
toast({ title: `Tracing ${enabled ? 'enabled' : 'disabled'}`, description: `${nodeId} — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents (v${saved.config.version})`, variant: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut];
|
|
|
|
|
toast({ title: `Tracing update — partial push failure`, description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 });
|
|
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
onError: () => {
|
|
|
|
|
useTracingStore.getState().toggleProcessor(appId, nodeId);
|
2026-04-02 19:08:00 +02:00
|
|
|
toast({ title: 'Tracing update failed', description: 'Could not save configuration', variant: 'error', duration: 86_400_000 });
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
});
|
2026-03-28 14:37:58 +01:00
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
}, [appId, appConfig, updateConfig, toast]);
|
|
|
|
|
|
|
|
|
|
const tapModal = appConfig && (
|
|
|
|
|
<TapConfigModal
|
|
|
|
|
open={tapModalOpen}
|
|
|
|
|
onClose={() => setTapModalOpen(false)}
|
|
|
|
|
tap={editingTap}
|
|
|
|
|
processorOptions={processorOptions}
|
|
|
|
|
defaultProcessorId={tapModalTarget}
|
|
|
|
|
application={appId}
|
|
|
|
|
config={appConfig}
|
|
|
|
|
onSave={handleTapSave}
|
|
|
|
|
onDelete={handleTapDelete}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-03-28 14:37:58 +01:00
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// Exchange selected: show header + execution diagram
|
|
|
|
|
if (exchangeId && detail) {
|
2026-03-28 14:22:34 +01:00
|
|
|
return (
|
|
|
|
|
<>
|
2026-03-28 15:42:45 +01:00
|
|
|
<ExchangeHeader detail={detail} onCorrelatedSelect={onCorrelatedSelect} onClearSelection={onClearSelection} />
|
2026-03-28 14:22:34 +01:00
|
|
|
<ExecutionDiagram
|
|
|
|
|
executionId={exchangeId}
|
|
|
|
|
executionDetail={detail}
|
|
|
|
|
knownRouteIds={knownRouteIds}
|
2026-03-28 18:31:08 +01:00
|
|
|
endpointRouteMap={endpointRouteMap}
|
2026-04-06 15:51:15 +02:00
|
|
|
onNodeAction={canControl ? handleNodeAction : undefined}
|
2026-03-28 16:04:53 +01:00
|
|
|
nodeConfigs={nodeConfigs}
|
2026-03-28 14:22:34 +01:00
|
|
|
/>
|
2026-03-29 13:08:58 +02:00
|
|
|
{tapModal}
|
2026-03-28 14:22:34 +01:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-28 13:57:13 +01:00
|
|
|
|
2026-04-04 13:49:28 +02:00
|
|
|
// No exchange selected: show topology-only diagram with route control bar
|
2026-03-28 15:58:38 +01:00
|
|
|
if (diagramQuery.data) {
|
|
|
|
|
return (
|
2026-03-29 13:08:58 +02:00
|
|
|
<>
|
2026-04-04 13:49:28 +02:00
|
|
|
{canControl && (hasRouteControl || hasReplay) && (
|
|
|
|
|
<RouteControlBar
|
|
|
|
|
application={appId}
|
|
|
|
|
routeId={routeId}
|
|
|
|
|
routeState={routeState}
|
|
|
|
|
hasRouteControl={hasRouteControl}
|
|
|
|
|
hasReplay={hasReplay}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-03-29 13:08:58 +02:00
|
|
|
<ProcessDiagram
|
|
|
|
|
application={appId}
|
|
|
|
|
routeId={routeId}
|
|
|
|
|
diagramLayout={diagramQuery.data}
|
|
|
|
|
knownRouteIds={knownRouteIds}
|
|
|
|
|
endpointRouteMap={endpointRouteMap}
|
2026-04-06 15:51:15 +02:00
|
|
|
onNodeAction={canControl ? handleNodeAction : undefined}
|
2026-03-29 13:08:58 +02:00
|
|
|
nodeConfigs={nodeConfigs}
|
|
|
|
|
/>
|
|
|
|
|
{tapModal}
|
|
|
|
|
</>
|
2026-03-28 15:58:38 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 14:22:34 +01:00
|
|
|
return (
|
|
|
|
|
<div className={styles.emptyRight}>
|
2026-03-28 15:58:38 +01:00
|
|
|
Loading diagram...
|
2026-03-28 13:57:13 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|