refactor: move Dashboard to /apps, add Metrics sidebar entry, scope by app

- Route / redirects to /apps, Dashboard serves both /apps and /apps/:id
- When appId is present, exchanges/routes/agents/search are scoped to that app
- Remove Dashboards sidebar link, add Metrics link
- Sidebar section labels (Applications, Agents) are now clickable nav links
  with separate chevron for collapse toggle
- Update all breadcrumbs from Dashboard/href:'/' to Applications/href:'/apps'
- Remove AppDetail page (replaced by scoped Dashboard)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-18 19:26:27 +01:00
parent 674444682e
commit 7cd8864f2c
11 changed files with 141 additions and 103 deletions

View File

@@ -100,7 +100,7 @@ function buildTrendData(agent: AgentHealthData) {
function buildBreadcrumb(scope: Scope) {
const crumbs: { label: string; href?: string }[] = [
{ label: 'System', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: 'Agents', href: '/agents' },
]
if (scope.level === 'app' || scope.level === 'instance') {

View File

@@ -12,7 +12,7 @@ export function AppDetail() {
<AppShell sidebar={<Sidebar apps={SIDEBAR_APPS} />}>
<TopBar
breadcrumb={[
{ label: 'Applications', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: id ?? '' },
]}
environment="PRODUCTION"

View File

@@ -1,4 +1,5 @@
import { useState, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import styles from './Dashboard.module.css'
// Layout
@@ -137,10 +138,14 @@ function durationClass(ms: number, status: Exchange['status']): string {
}
// ─── Build CommandPalette search data ────────────────────────────────────────
function buildSearchData(): SearchResult[] {
function buildSearchData(
exs: Exchange[],
rts: typeof routes,
ags: typeof agents,
): SearchResult[] {
const results: SearchResult[] = []
for (const exec of exchanges) {
for (const exec of exs) {
results.push({
id: exec.id,
category: 'exchange',
@@ -151,7 +156,7 @@ function buildSearchData(): SearchResult[] {
})
}
for (const route of routes) {
for (const route of rts) {
results.push({
id: route.id,
category: 'route',
@@ -161,7 +166,7 @@ function buildSearchData(): SearchResult[] {
})
}
for (const agent of agents) {
for (const agent of ags) {
results.push({
id: agent.id,
category: 'agent',
@@ -174,16 +179,15 @@ function buildSearchData(): SearchResult[] {
return results
}
const SEARCH_DATA = buildSearchData()
// ─── Filter options ───────────────────────────────────────────────────────────
const STATUS_FILTERS = [
{ label: 'All', value: 'all', count: exchanges.length },
{ label: 'OK', value: 'completed', count: exchanges.filter((e) => e.status === 'completed').length, color: 'success' as const },
{ label: 'Warn', value: 'warning', count: exchanges.filter((e) => e.status === 'warning').length },
{ label: 'Error', value: 'failed', count: exchanges.filter((e) => e.status === 'failed').length, color: 'error' as const },
{ label: 'Running', value: 'running', count: exchanges.filter((e) => e.status === 'running').length, color: 'running' as const },
]
function buildStatusFilters(exs: Exchange[]) {
return [
{ label: 'All', value: 'all', count: exs.length },
{ label: 'OK', value: 'completed', count: exs.filter((e) => e.status === 'completed').length, color: 'success' as const },
{ label: 'Warn', value: 'warning', count: exs.filter((e) => e.status === 'warning').length },
{ label: 'Error', value: 'failed', count: exs.filter((e) => e.status === 'failed').length, color: 'error' as const },
{ label: 'Running', value: 'running', count: exs.filter((e) => e.status === 'running').length, color: 'running' as const },
]
}
const SHORTCUTS = [
{ keys: 'Ctrl+K', label: 'Search' },
@@ -194,6 +198,7 @@ const SHORTCUTS = [
// ─── Dashboard component ──────────────────────────────────────────────────────
export function Dashboard() {
const { id: appId } = useParams<{ id: string }>()
const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([])
const [search, setSearch] = useState('')
const [selectedId, setSelectedId] = useState<string | undefined>()
@@ -201,9 +206,36 @@ export function Dashboard() {
const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null)
const [paletteOpen, setPaletteOpen] = useState(false)
// Filter exchanges
// Build set of route IDs belonging to the selected app (if any)
const appRouteIds = useMemo(() => {
if (!appId) return null
const app = SIDEBAR_APPS.find((a) => a.id === appId)
if (!app) return null
return new Set(app.routes.map((r) => r.id))
}, [appId])
const selectedApp = appId ? SIDEBAR_APPS.find((a) => a.id === appId) : null
// Scope all data to the selected app
const scopedExchanges = useMemo(() => {
if (!appRouteIds) return exchanges
return exchanges.filter((e) => appRouteIds.has(e.route))
}, [appRouteIds])
const scopedRoutes = useMemo(() => {
if (!appRouteIds) return routes
return routes.filter((r) => appRouteIds.has(r.id))
}, [appRouteIds])
const scopedAgents = useMemo(() => {
if (!selectedApp) return agents
const agentIds = new Set(selectedApp.agents.map((a) => a.id))
return agents.filter((a) => agentIds.has(a.id))
}, [selectedApp])
// Filter exchanges (scoped + user filters)
const filteredExchanges = useMemo(() => {
let data = exchanges
let data = scopedExchanges
const statusFilter = activeFilters.find((f) =>
['completed', 'failed', 'running', 'warning', 'all'].includes(f.value),
@@ -225,7 +257,12 @@ export function Dashboard() {
}
return data
}, [activeFilters, search])
}, [activeFilters, search, scopedExchanges])
const searchData = useMemo(
() => buildSearchData(scopedExchanges, scopedRoutes, scopedAgents),
[scopedExchanges, scopedRoutes, scopedAgents],
)
function handleRowClick(row: Exchange) {
setSelectedId(row.id)
@@ -349,12 +386,11 @@ export function Dashboard() {
>
{/* Top bar */}
<TopBar
breadcrumb={[
{ label: 'Applications', href: '/' },
{ label: 'order-service' },
]}
breadcrumb={appId
? [{ label: 'Applications', href: '/apps' }, { label: appId }]
: [{ label: 'Applications' }]
}
environment="PRODUCTION"
shift="Day (06:00-18:00)"
user={{ name: 'hendrik' }}
onSearchClick={() => setPaletteOpen(true)}
/>
@@ -380,7 +416,7 @@ export function Dashboard() {
{/* Filter bar */}
<FilterBar
filters={STATUS_FILTERS}
filters={buildStatusFilters(scopedExchanges)}
activeFilters={activeFilters}
onFilterChange={setActiveFilters}
searchPlaceholder="Search by Order ID, correlation ID, error message..."
@@ -395,7 +431,7 @@ export function Dashboard() {
<span className={styles.tableTitle}>Recent Exchanges</span>
<div className={styles.tableRight}>
<span className={styles.tableMeta}>
{filteredExchanges.length.toLocaleString()} of {exchanges.length.toLocaleString()} exchanges
{filteredExchanges.length.toLocaleString()} of {scopedExchanges.length.toLocaleString()} exchanges
</span>
<Badge label="LIVE" color="success" />
</div>
@@ -428,7 +464,7 @@ export function Dashboard() {
open={paletteOpen}
onClose={() => setPaletteOpen(false)}
onSelect={() => setPaletteOpen(false)}
data={SEARCH_DATA}
data={searchData}
onOpen={() => setPaletteOpen(true)}
/>

View File

@@ -117,7 +117,7 @@ export function ExchangeDetail() {
>
<TopBar
breadcrumb={[
{ label: 'Dashboard', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: 'Exchanges' },
{ label: id ?? 'Unknown' },
]}
@@ -144,7 +144,7 @@ export function ExchangeDetail() {
{/* Top bar */}
<TopBar
breadcrumb={[
{ label: 'Dashboard', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: exchange.route, href: `/routes/${exchange.route}` },
{ label: exchange.id },
]}

View File

@@ -15,7 +15,7 @@ export function Inventory() {
<div className={styles.page}>
<header className={styles.header}>
<h1 className={styles.headerTitle}>Component Inventory</h1>
<Link to="/" className={styles.backLink}> Back to app</Link>
<Link to="/apps" className={styles.backLink}> Back to app</Link>
</header>
<div className={styles.body}>

View File

@@ -206,7 +206,7 @@ export function Metrics() {
{/* Top bar */}
<TopBar
breadcrumb={[
{ label: 'Dashboard', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: 'Metrics' },
]}
environment="PRODUCTION"

View File

@@ -205,7 +205,7 @@ export function RouteDetail() {
>
<TopBar
breadcrumb={[
{ label: 'Dashboard', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: 'Routes' },
{ label: id ?? 'Unknown' },
]}
@@ -231,8 +231,8 @@ export function RouteDetail() {
{/* Top bar */}
<TopBar
breadcrumb={[
{ label: 'Dashboard', href: '/' },
{ label: 'Routes', href: '/' },
{ label: 'Applications', href: '/apps' },
{ label: 'Routes', href: '/apps' },
{ label: route.name },
]}
environment="PRODUCTION"