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 { Dashboard } from './pages/Dashboard/Dashboard'
|
||||
import { Metrics } from './pages/Metrics/Metrics'
|
||||
import { RouteDetail } from './pages/RouteDetail/RouteDetail'
|
||||
import { ExchangeDetail } from './pages/ExchangeDetail/ExchangeDetail'
|
||||
import { AgentHealth } from './pages/AgentHealth/AgentHealth'
|
||||
import { AgentInstance } from './pages/AgentInstance/AgentInstance'
|
||||
@@ -20,32 +19,31 @@ import { buildSearchData } from './mocks/searchData'
|
||||
import { exchanges } from './mocks/exchanges'
|
||||
import { routes } from './mocks/routes'
|
||||
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 */
|
||||
function computeSidebarRevealPath(result: SearchResult): string | undefined {
|
||||
if (!result.path) return undefined
|
||||
|
||||
if (result.category === 'application') {
|
||||
// /apps/:id — already a sidebar node path
|
||||
return result.path
|
||||
}
|
||||
|
||||
if (result.category === 'route') {
|
||||
// /routes/:id — already a sidebar node path
|
||||
return result.path
|
||||
}
|
||||
|
||||
if (result.category === 'agent') {
|
||||
// /agents/:appId/:agentId — already a sidebar node path
|
||||
return result.path
|
||||
}
|
||||
|
||||
if (result.category === 'exchange') {
|
||||
// /exchanges/:id — no sidebar entry; resolve to the parent route
|
||||
const exchange = exchanges.find((e) => e.id === result.id)
|
||||
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="/apps" element={<Dashboard />} />
|
||||
<Route path="/apps/:id" element={<Dashboard />} />
|
||||
<Route path="/apps/:id/:routeId" element={<Dashboard />} />
|
||||
<Route path="/metrics" element={<Metrics />} />
|
||||
<Route path="/routes/:id" element={<RouteDetail />} />
|
||||
<Route path="/exchanges/:id" element={<ExchangeDetail />} />
|
||||
<Route path="/agents/:appId/:instanceId" element={<AgentInstance />} />
|
||||
<Route path="/agents/*" element={<AgentHealth />} />
|
||||
|
||||
@@ -57,7 +57,7 @@ function buildAppTreeNodes(apps: SidebarApp[]): SidebarTreeNode[] {
|
||||
label: route.name,
|
||||
icon: <span className={styles.routeArrow}>▸</span>,
|
||||
badge: formatCount(route.exchangeCount),
|
||||
path: `/routes/${route.id}`,
|
||||
path: `/apps/${app.id}/${route.id}`,
|
||||
starrable: true,
|
||||
})),
|
||||
}))
|
||||
@@ -118,7 +118,7 @@ function collectStarredItems(apps: SidebarApp[], starredIds: Set<string>): Starr
|
||||
items.push({
|
||||
starKey: key,
|
||||
label: route.name,
|
||||
path: `/routes/${route.id}`,
|
||||
path: `/apps/${app.id}/${route.id}`,
|
||||
type: 'route',
|
||||
parentApp: app.name,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SearchResult } from '../design-system/composites/CommandPalette/ty
|
||||
import { exchanges, type Exchange } from './exchanges'
|
||||
import { routes } from './routes'
|
||||
import { agents } from './agents'
|
||||
import { SIDEBAR_APPS, type SidebarApp } from './sidebar'
|
||||
import { SIDEBAR_APPS, buildRouteToAppMap, type SidebarApp } from './sidebar'
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
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) {
|
||||
const appIdForRoute = routeToApp.get(route.id)
|
||||
results.push({
|
||||
id: route.id,
|
||||
category: 'route',
|
||||
title: route.name,
|
||||
badges: [{ label: route.group }],
|
||||
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[]
|
||||
}
|
||||
|
||||
/** 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[] = [
|
||||
{
|
||||
id: 'order-service',
|
||||
|
||||
@@ -69,14 +69,9 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.routeGroup {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Customer text */
|
||||
.customerText {
|
||||
/* Application column */
|
||||
.appName {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProv
|
||||
// Mock data
|
||||
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
function formatDuration(ms: number): string {
|
||||
@@ -38,7 +41,13 @@ function formatDuration(ms: number): 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' {
|
||||
@@ -77,25 +86,15 @@ const COLUMNS: Column<Exchange>[] = [
|
||||
header: 'Route',
|
||||
sortable: true,
|
||||
render: (_, row) => (
|
||||
<div>
|
||||
<div className={styles.routeName}>{row.route}</div>
|
||||
<div className={styles.routeGroup}>{row.routeGroup}</div>
|
||||
</div>
|
||||
<span className={styles.routeName}>{row.route}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'orderId',
|
||||
header: 'Order ID',
|
||||
key: 'routeGroup',
|
||||
header: 'Application',
|
||||
sortable: true,
|
||||
render: (_, row) => (
|
||||
<MonoText size="sm">{row.orderId}</MonoText>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'customer',
|
||||
header: 'Customer',
|
||||
render: (_, row) => (
|
||||
<MonoText size="xs" className={styles.customerText}>{row.customer}</MonoText>
|
||||
<span className={styles.appName}>{ROUTE_TO_APP.get(row.route) ?? row.routeGroup}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -145,7 +144,7 @@ const SHORTCUTS = [
|
||||
|
||||
// ─── Dashboard component ──────────────────────────────────────────────────────
|
||||
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 [panelOpen, setPanelOpen] = useState(false)
|
||||
const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null)
|
||||
@@ -160,11 +159,12 @@ export function Dashboard() {
|
||||
return new Set(app.routes.map((r) => r.id))
|
||||
}, [appId])
|
||||
|
||||
// Scope all data to the selected app
|
||||
// Scope all data to the selected app (and optionally route)
|
||||
const scopedExchanges = useMemo(() => {
|
||||
if (routeId) return exchanges.filter((e) => e.route === routeId)
|
||||
if (!appRouteIds) return exchanges
|
||||
return exchanges.filter((e) => appRouteIds.has(e.route))
|
||||
}, [appRouteIds])
|
||||
}, [appRouteIds, routeId])
|
||||
|
||||
// Filter exchanges (scoped + global filters)
|
||||
const filteredExchanges = useMemo(() => {
|
||||
@@ -313,9 +313,12 @@ export function Dashboard() {
|
||||
>
|
||||
{/* Top bar */}
|
||||
<TopBar
|
||||
breadcrumb={appId
|
||||
? [{ label: 'Applications', href: '/apps' }, { label: appId }]
|
||||
: [{ label: 'Applications' }]
|
||||
breadcrumb={
|
||||
routeId
|
||||
? [{ label: 'Applications', href: '/apps' }, { label: appId!, href: `/apps/${appId}` }, { label: routeId }]
|
||||
: appId
|
||||
? [{ label: 'Applications', href: '/apps' }, { label: appId }]
|
||||
: [{ label: 'Applications' }]
|
||||
}
|
||||
environment="PRODUCTION"
|
||||
user={{ name: 'hendrik' }}
|
||||
|
||||
@@ -21,7 +21,9 @@ import { InfoCallout } from '../../design-system/primitives/InfoCallout/InfoCall
|
||||
|
||||
// Mock data
|
||||
import { exchanges } from '../../mocks/exchanges'
|
||||
import { SIDEBAR_APPS } from '../../mocks/sidebar'
|
||||
import { SIDEBAR_APPS, buildRouteToAppMap } from '../../mocks/sidebar'
|
||||
|
||||
const ROUTE_TO_APP = buildRouteToAppMap()
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
function formatDuration(ms: number): string {
|
||||
@@ -145,7 +147,7 @@ export function ExchangeDetail() {
|
||||
<TopBar
|
||||
breadcrumb={[
|
||||
{ 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 },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
@@ -166,7 +168,7 @@ export function ExchangeDetail() {
|
||||
<Badge label={statusLabel} color={statusVariant} variant="filled" />
|
||||
</div>
|
||||
<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>
|
||||
Order: <MonoText size="xs">{exchange.orderId}</MonoText>
|
||||
<span className={styles.headerDivider}>·</span>
|
||||
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
routeMetrics,
|
||||
type RouteMetricRow,
|
||||
} 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) ─────────────────────────────────────
|
||||
const METRIC_KPIS = [
|
||||
@@ -245,7 +247,7 @@ export function Metrics() {
|
||||
columns={ROUTE_COLUMNS}
|
||||
data={routeMetricsWithId}
|
||||
sortable
|
||||
onRowClick={(row) => navigate(`/routes/${row.routeId}`)}
|
||||
onRowClick={(row) => navigate(`/apps/${ROUTE_TO_APP.get(row.routeId) ?? row.routeId}/${row.routeId}`)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user