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:
@@ -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') {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 },
|
||||
]}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -206,7 +206,7 @@ export function Metrics() {
|
||||
{/* Top bar */}
|
||||
<TopBar
|
||||
breadcrumb={[
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Applications', href: '/apps' },
|
||||
{ label: 'Metrics' },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user