refactor: unify /apps routing with application and route filtering
All checks were successful
Build & Publish / publish (push) Successful in 44s

- Table columns: Status, Route, Application, Started (yyyy-mm-dd hh:mm:ss),
  Duration, Agent (removed Order ID and Customer)
- /apps shows all exchanges, /apps/:id filters by application,
  /apps/:id/:routeId filters by application and route
- Route paths changed from /routes/:id to /apps/:appId/:routeId across
  sidebar, search, breadcrumbs, metrics, and exchange detail
- Added buildRouteToAppMap utility for route→application lookup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-19 12:39:45 +01:00
parent 4f3e9c0f35
commit 9c9063dc1b
8 changed files with 60 additions and 47 deletions

View File

@@ -2,7 +2,6 @@ import { useMemo, useCallback } from 'react'
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom' import { Routes, Route, Navigate, useNavigate } from 'react-router-dom'
import { Dashboard } from './pages/Dashboard/Dashboard' import { Dashboard } from './pages/Dashboard/Dashboard'
import { Metrics } from './pages/Metrics/Metrics' import { Metrics } from './pages/Metrics/Metrics'
import { RouteDetail } from './pages/RouteDetail/RouteDetail'
import { ExchangeDetail } from './pages/ExchangeDetail/ExchangeDetail' import { ExchangeDetail } from './pages/ExchangeDetail/ExchangeDetail'
import { AgentHealth } from './pages/AgentHealth/AgentHealth' import { AgentHealth } from './pages/AgentHealth/AgentHealth'
import { AgentInstance } from './pages/AgentInstance/AgentInstance' import { AgentInstance } from './pages/AgentInstance/AgentInstance'
@@ -20,32 +19,31 @@ import { buildSearchData } from './mocks/searchData'
import { exchanges } from './mocks/exchanges' import { exchanges } from './mocks/exchanges'
import { routes } from './mocks/routes' import { routes } from './mocks/routes'
import { agents } from './mocks/agents' import { agents } from './mocks/agents'
import { SIDEBAR_APPS } from './mocks/sidebar' import { SIDEBAR_APPS, buildRouteToAppMap } from './mocks/sidebar'
const routeToApp = buildRouteToAppMap()
/** Compute which sidebar path to reveal for a given search result */ /** Compute which sidebar path to reveal for a given search result */
function computeSidebarRevealPath(result: SearchResult): string | undefined { function computeSidebarRevealPath(result: SearchResult): string | undefined {
if (!result.path) return undefined if (!result.path) return undefined
if (result.category === 'application') { if (result.category === 'application') {
// /apps/:id — already a sidebar node path
return result.path return result.path
} }
if (result.category === 'route') { if (result.category === 'route') {
// /routes/:id — already a sidebar node path
return result.path return result.path
} }
if (result.category === 'agent') { if (result.category === 'agent') {
// /agents/:appId/:agentId — already a sidebar node path
return result.path return result.path
} }
if (result.category === 'exchange') { if (result.category === 'exchange') {
// /exchanges/:id — no sidebar entry; resolve to the parent route
const exchange = exchanges.find((e) => e.id === result.id) const exchange = exchanges.find((e) => e.id === result.id)
if (exchange) { if (exchange) {
return `/routes/${exchange.route}` const appId = routeToApp.get(exchange.route)
if (appId) return `/apps/${appId}/${exchange.route}`
} }
} }
@@ -83,8 +81,8 @@ export default function App() {
<Route path="/" element={<Navigate to="/apps" replace />} /> <Route path="/" element={<Navigate to="/apps" replace />} />
<Route path="/apps" element={<Dashboard />} /> <Route path="/apps" element={<Dashboard />} />
<Route path="/apps/:id" element={<Dashboard />} /> <Route path="/apps/:id" element={<Dashboard />} />
<Route path="/apps/:id/:routeId" element={<Dashboard />} />
<Route path="/metrics" element={<Metrics />} /> <Route path="/metrics" element={<Metrics />} />
<Route path="/routes/:id" element={<RouteDetail />} />
<Route path="/exchanges/:id" element={<ExchangeDetail />} /> <Route path="/exchanges/:id" element={<ExchangeDetail />} />
<Route path="/agents/:appId/:instanceId" element={<AgentInstance />} /> <Route path="/agents/:appId/:instanceId" element={<AgentInstance />} />
<Route path="/agents/*" element={<AgentHealth />} /> <Route path="/agents/*" element={<AgentHealth />} />

View File

@@ -57,7 +57,7 @@ function buildAppTreeNodes(apps: SidebarApp[]): SidebarTreeNode[] {
label: route.name, label: route.name,
icon: <span className={styles.routeArrow}>&#9656;</span>, icon: <span className={styles.routeArrow}>&#9656;</span>,
badge: formatCount(route.exchangeCount), badge: formatCount(route.exchangeCount),
path: `/routes/${route.id}`, path: `/apps/${app.id}/${route.id}`,
starrable: true, starrable: true,
})), })),
})) }))
@@ -118,7 +118,7 @@ function collectStarredItems(apps: SidebarApp[], starredIds: Set<string>): Starr
items.push({ items.push({
starKey: key, starKey: key,
label: route.name, label: route.name,
path: `/routes/${route.id}`, path: `/apps/${app.id}/${route.id}`,
type: 'route', type: 'route',
parentApp: app.name, parentApp: app.name,
}) })

View File

@@ -2,7 +2,7 @@ import type { SearchResult } from '../design-system/composites/CommandPalette/ty
import { exchanges, type Exchange } from './exchanges' import { exchanges, type Exchange } from './exchanges'
import { routes } from './routes' import { routes } from './routes'
import { agents } from './agents' import { agents } from './agents'
import { SIDEBAR_APPS, type SidebarApp } from './sidebar' import { SIDEBAR_APPS, buildRouteToAppMap, type SidebarApp } from './sidebar'
function formatDuration(ms: number): string { function formatDuration(ms: number): string {
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s` if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
@@ -72,14 +72,16 @@ export function buildSearchData(
}) })
} }
const routeToApp = buildRouteToAppMap(apps)
for (const route of rts) { for (const route of rts) {
const appIdForRoute = routeToApp.get(route.id)
results.push({ results.push({
id: route.id, id: route.id,
category: 'route', category: 'route',
title: route.name, title: route.name,
badges: [{ label: route.group }], badges: [{ label: route.group }],
meta: `${route.exchangeCount.toLocaleString()} exchanges · ${route.successRate}% success`, meta: `${route.exchangeCount.toLocaleString()} exchanges · ${route.successRate}% success`,
path: `/routes/${route.id}`, path: appIdForRoute ? `/apps/${appIdForRoute}/${route.id}` : `/apps/${route.id}`,
}) })
} }

View File

@@ -20,6 +20,17 @@ export interface SidebarApp {
agents: SidebarAgent[] agents: SidebarAgent[]
} }
/** Build a routeId → appId lookup from the sidebar tree */
export function buildRouteToAppMap(apps: SidebarApp[] = SIDEBAR_APPS): Map<string, string> {
const map = new Map<string, string>()
for (const app of apps) {
for (const route of app.routes) {
map.set(route.id, app.id)
}
}
return map
}
export const SIDEBAR_APPS: SidebarApp[] = [ export const SIDEBAR_APPS: SidebarApp[] = [
{ {
id: 'order-service', id: 'order-service',

View File

@@ -69,14 +69,9 @@
color: var(--text-primary); color: var(--text-primary);
} }
.routeGroup { /* Application column */
font-size: 10px; .appName {
color: var(--text-muted); font-size: 12px;
font-family: var(--font-mono);
}
/* Customer text */
.customerText {
color: var(--text-secondary); color: var(--text-secondary);
} }

View File

@@ -28,7 +28,10 @@ import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProv
// Mock data // Mock data
import { exchanges, type Exchange } from '../../mocks/exchanges' import { exchanges, type Exchange } from '../../mocks/exchanges'
import { kpiMetrics } from '../../mocks/metrics' import { kpiMetrics } from '../../mocks/metrics'
import { SIDEBAR_APPS } from '../../mocks/sidebar' import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar'
// Route → Application lookup
const ROUTE_TO_APP = buildRouteToAppMap()
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
function formatDuration(ms: number): string { function formatDuration(ms: number): string {
@@ -38,7 +41,13 @@ function formatDuration(ms: number): string {
} }
function formatTimestamp(date: Date): string { function formatTimestamp(date: Date): string {
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) const y = date.getFullYear()
const mo = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const mi = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `${y}-${mo}-${d} ${h}:${mi}:${s}`
} }
function statusToVariant(status: Exchange['status']): 'success' | 'error' | 'running' | 'warning' { function statusToVariant(status: Exchange['status']): 'success' | 'error' | 'running' | 'warning' {
@@ -77,25 +86,15 @@ const COLUMNS: Column<Exchange>[] = [
header: 'Route', header: 'Route',
sortable: true, sortable: true,
render: (_, row) => ( render: (_, row) => (
<div> <span className={styles.routeName}>{row.route}</span>
<div className={styles.routeName}>{row.route}</div>
<div className={styles.routeGroup}>{row.routeGroup}</div>
</div>
), ),
}, },
{ {
key: 'orderId', key: 'routeGroup',
header: 'Order ID', header: 'Application',
sortable: true, sortable: true,
render: (_, row) => ( render: (_, row) => (
<MonoText size="sm">{row.orderId}</MonoText> <span className={styles.appName}>{ROUTE_TO_APP.get(row.route) ?? row.routeGroup}</span>
),
},
{
key: 'customer',
header: 'Customer',
render: (_, row) => (
<MonoText size="xs" className={styles.customerText}>{row.customer}</MonoText>
), ),
}, },
{ {
@@ -145,7 +144,7 @@ const SHORTCUTS = [
// ─── Dashboard component ────────────────────────────────────────────────────── // ─── Dashboard component ──────────────────────────────────────────────────────
export function Dashboard() { export function Dashboard() {
const { id: appId } = useParams<{ id: string }>() const { id: appId, routeId } = useParams<{ id: string; routeId: string }>()
const [selectedId, setSelectedId] = useState<string | undefined>() const [selectedId, setSelectedId] = useState<string | undefined>()
const [panelOpen, setPanelOpen] = useState(false) const [panelOpen, setPanelOpen] = useState(false)
const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null) const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null)
@@ -160,11 +159,12 @@ export function Dashboard() {
return new Set(app.routes.map((r) => r.id)) return new Set(app.routes.map((r) => r.id))
}, [appId]) }, [appId])
// Scope all data to the selected app // Scope all data to the selected app (and optionally route)
const scopedExchanges = useMemo(() => { const scopedExchanges = useMemo(() => {
if (routeId) return exchanges.filter((e) => e.route === routeId)
if (!appRouteIds) return exchanges if (!appRouteIds) return exchanges
return exchanges.filter((e) => appRouteIds.has(e.route)) return exchanges.filter((e) => appRouteIds.has(e.route))
}, [appRouteIds]) }, [appRouteIds, routeId])
// Filter exchanges (scoped + global filters) // Filter exchanges (scoped + global filters)
const filteredExchanges = useMemo(() => { const filteredExchanges = useMemo(() => {
@@ -313,9 +313,12 @@ export function Dashboard() {
> >
{/* Top bar */} {/* Top bar */}
<TopBar <TopBar
breadcrumb={appId breadcrumb={
? [{ label: 'Applications', href: '/apps' }, { label: appId }] routeId
: [{ label: 'Applications' }] ? [{ label: 'Applications', href: '/apps' }, { label: appId!, href: `/apps/${appId}` }, { label: routeId }]
: appId
? [{ label: 'Applications', href: '/apps' }, { label: appId }]
: [{ label: 'Applications' }]
} }
environment="PRODUCTION" environment="PRODUCTION"
user={{ name: 'hendrik' }} user={{ name: 'hendrik' }}

View File

@@ -21,7 +21,9 @@ import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCall
// Mock data // Mock data
import { exchanges } from '../../mocks/exchanges' import { exchanges } from '../../mocks/exchanges'
import { SIDEBAR_APPS } from '../../mocks/sidebar' import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar'
const ROUTE_TO_APP = buildRouteToAppMap()
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
function formatDuration(ms: number): string { function formatDuration(ms: number): string {
@@ -145,7 +147,7 @@ export function ExchangeDetail() {
<TopBar <TopBar
breadcrumb={[ breadcrumb={[
{ label: 'Applications', href: '/apps' }, { label: 'Applications', href: '/apps' },
{ label: exchange.route, href: `/routes/${exchange.route}` }, { label: exchange.route, href: `/apps/${ROUTE_TO_APP.get(exchange.route) ?? exchange.route}/${exchange.route}` },
{ label: exchange.id }, { label: exchange.id },
]} ]}
environment="PRODUCTION" environment="PRODUCTION"
@@ -166,7 +168,7 @@ export function ExchangeDetail() {
<Badge label={statusLabel} color={statusVariant} variant="filled" /> <Badge label={statusLabel} color={statusVariant} variant="filled" />
</div> </div>
<div className={styles.exchangeRoute}> <div className={styles.exchangeRoute}>
Route: <span className={styles.routeLink} onClick={() => navigate(`/routes/${exchange.route}`)}>{exchange.route}</span> Route: <span className={styles.routeLink} onClick={() => navigate(`/apps/${ROUTE_TO_APP.get(exchange.route) ?? exchange.route}/${exchange.route}`)}>{exchange.route}</span>
<span className={styles.headerDivider}>·</span> <span className={styles.headerDivider}>·</span>
Order: <MonoText size="xs">{exchange.orderId}</MonoText> Order: <MonoText size="xs">{exchange.orderId}</MonoText>
<span className={styles.headerDivider}>·</span> <span className={styles.headerDivider}>·</span>

View File

@@ -27,7 +27,9 @@ import {
routeMetrics, routeMetrics,
type RouteMetricRow, type RouteMetricRow,
} from '../../mocks/metrics' } from '../../mocks/metrics'
import { SIDEBAR_APPS } from '../../mocks/sidebar' import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar'
const ROUTE_TO_APP = buildRouteToAppMap()
// ─── Metrics KPI cards (5 cards per spec) ───────────────────────────────────── // ─── Metrics KPI cards (5 cards per spec) ─────────────────────────────────────
const METRIC_KPIS = [ const METRIC_KPIS = [
@@ -245,7 +247,7 @@ export function Metrics() {
columns={ROUTE_COLUMNS} columns={ROUTE_COLUMNS}
data={routeMetricsWithId} data={routeMetricsWithId}
sortable sortable
onRowClick={(row) => navigate(`/routes/${row.routeId}`)} onRowClick={(row) => navigate(`/apps/${ROUTE_TO_APP.get(row.routeId) ?? row.routeId}/${row.routeId}`)}
/> />
</div> </div>