refactor: unify /apps routing with application and route filtering
All checks were successful
Build & Publish / publish (push) Successful in 44s
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:
14
src/App.tsx
14
src/App.tsx
@@ -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 />} />
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function buildAppTreeNodes(apps: SidebarApp[]): SidebarTreeNode[] {
|
|||||||
label: route.name,
|
label: route.name,
|
||||||
icon: <span className={styles.routeArrow}>▸</span>,
|
icon: <span className={styles.routeArrow}>▸</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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user