feat: unified global search & filter system with Cmd-K navigation
Replace per-page filtering with a single GlobalFilterProvider (time range + status) consumed by a redesigned TopBar across all pages. Lift CommandPalette to App level so Cmd-K works globally with filtered results that navigate to exchanges, routes, agents, and applications. Sidebar auto-reveals and selects the target entry on Cmd-K navigation via location state. - Extract shared time preset utilities (computePresetRange, DEFAULT_PRESETS) - Add GlobalFilterProvider (time range + status) and CommandPaletteProvider - Add TimeRangeDropdown primitive with Popover preset list - Redesign TopBar: breadcrumb | time dropdown | status pills | search | env - Add application category to Cmd-K search - Remove FilterBar and local DateRangePicker from Dashboard/Metrics pages - Filter AgentHealth EventFeed by global time range - Remove shift/onSearchClick props from TopBar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ export function Admin() {
|
||||
<TopBar
|
||||
breadcrumb={[{ label: 'Admin' }]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
<EmptyState
|
||||
|
||||
@@ -18,6 +18,9 @@ import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
||||
import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
|
||||
|
||||
// Global filters
|
||||
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
||||
|
||||
// Mock data
|
||||
import { agents, type AgentHealth as AgentHealthData } from '../../mocks/agents'
|
||||
import { SIDEBAR_APPS } from '../../mocks/sidebar'
|
||||
@@ -117,6 +120,7 @@ function buildBreadcrumb(scope: Scope) {
|
||||
export function AgentHealth() {
|
||||
const scope = useScope()
|
||||
const navigate = useNavigate()
|
||||
const { isInTimeRange } = useGlobalFilters()
|
||||
|
||||
// Filter agents by scope
|
||||
const filteredAgents = useMemo(() => {
|
||||
@@ -135,8 +139,8 @@ export function AgentHealth() {
|
||||
const totalTps = filteredAgents.reduce((s, a) => s + a.tps, 0)
|
||||
const totalActiveRoutes = filteredAgents.reduce((s, a) => s + a.activeRoutes, 0)
|
||||
|
||||
// Events are a global timeline feed — show all regardless of scope
|
||||
const filteredEvents = agentEvents
|
||||
// Filter events by global time range
|
||||
const filteredEvents = agentEvents.filter((e) => isInTimeRange(e.timestamp))
|
||||
|
||||
// Single instance for expanded charts
|
||||
const singleInstance = scope.level === 'instance' ? filteredAgents[0] : null
|
||||
|
||||
@@ -10,7 +10,7 @@ export function ApiDocs() {
|
||||
<TopBar
|
||||
breadcrumb={[{ label: 'API Documentation' }]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
<EmptyState
|
||||
|
||||
@@ -16,7 +16,7 @@ export function AppDetail() {
|
||||
{ label: id ?? '' },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
<EmptyState
|
||||
|
||||
@@ -8,13 +8,9 @@ import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar'
|
||||
import { TopBar } from '../../design-system/layout/TopBar/TopBar'
|
||||
|
||||
// Composites
|
||||
import { FilterBar } from '../../design-system/composites/FilterBar/FilterBar'
|
||||
import type { ActiveFilter } from '../../design-system/composites/FilterBar/FilterBar'
|
||||
import { DataTable } from '../../design-system/composites/DataTable/DataTable'
|
||||
import type { Column } from '../../design-system/composites/DataTable/types'
|
||||
import { DetailPanel } from '../../design-system/composites/DetailPanel/DetailPanel'
|
||||
import { CommandPalette } from '../../design-system/composites/CommandPalette/CommandPalette'
|
||||
import type { SearchResult } from '../../design-system/composites/CommandPalette/types'
|
||||
import { ShortcutsBar } from '../../design-system/composites/ShortcutsBar/ShortcutsBar'
|
||||
import { ProcessorTimeline } from '../../design-system/composites/ProcessorTimeline/ProcessorTimeline'
|
||||
|
||||
@@ -24,10 +20,11 @@ import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
||||
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
||||
|
||||
// Global filters
|
||||
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
||||
|
||||
// Mock data
|
||||
import { exchanges, type Exchange } from '../../mocks/exchanges'
|
||||
import { routes } from '../../mocks/routes'
|
||||
import { agents } from '../../mocks/agents'
|
||||
import { kpiMetrics } from '../../mocks/metrics'
|
||||
import { SIDEBAR_APPS } from '../../mocks/sidebar'
|
||||
|
||||
@@ -137,58 +134,6 @@ function durationClass(ms: number, status: Exchange['status']): string {
|
||||
return styles.durBreach
|
||||
}
|
||||
|
||||
// ─── Build CommandPalette search data ────────────────────────────────────────
|
||||
function buildSearchData(
|
||||
exs: Exchange[],
|
||||
rts: typeof routes,
|
||||
ags: typeof agents,
|
||||
): SearchResult[] {
|
||||
const results: SearchResult[] = []
|
||||
|
||||
for (const exec of exs) {
|
||||
results.push({
|
||||
id: exec.id,
|
||||
category: 'exchange',
|
||||
title: `${exec.orderId} — ${exec.route}`,
|
||||
badges: [{ label: statusLabel(exec.status), color: statusToVariant(exec.status) }],
|
||||
meta: `${exec.correlationId} · ${formatDuration(exec.durationMs)} · ${exec.customer}`,
|
||||
timestamp: formatTimestamp(exec.timestamp),
|
||||
})
|
||||
}
|
||||
|
||||
for (const route of rts) {
|
||||
results.push({
|
||||
id: route.id,
|
||||
category: 'route',
|
||||
title: route.name,
|
||||
badges: [{ label: route.group }],
|
||||
meta: `${route.exchangeCount.toLocaleString()} exchanges · ${route.successRate}% success`,
|
||||
})
|
||||
}
|
||||
|
||||
for (const agent of ags) {
|
||||
results.push({
|
||||
id: agent.id,
|
||||
category: 'agent',
|
||||
title: agent.name,
|
||||
badges: [{ label: agent.status }],
|
||||
meta: `${agent.service} ${agent.version} · ${agent.tps} · ${agent.lastSeen}`,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
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' },
|
||||
{ keys: '↑↓', label: 'Navigate rows' },
|
||||
@@ -199,12 +144,11 @@ 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>()
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null)
|
||||
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||
|
||||
const { isInTimeRange, statusFilters } = useGlobalFilters()
|
||||
|
||||
// Build set of route IDs belonging to the selected app (if any)
|
||||
const appRouteIds = useMemo(() => {
|
||||
@@ -214,55 +158,26 @@ export function Dashboard() {
|
||||
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)
|
||||
// Filter exchanges (scoped + global filters)
|
||||
const filteredExchanges = useMemo(() => {
|
||||
let data = scopedExchanges
|
||||
|
||||
const statusFilter = activeFilters.find((f) =>
|
||||
['completed', 'failed', 'running', 'warning', 'all'].includes(f.value),
|
||||
)
|
||||
if (statusFilter && statusFilter.value !== 'all') {
|
||||
data = data.filter((e) => e.status === statusFilter.value)
|
||||
}
|
||||
// Time range filter
|
||||
data = data.filter((e) => isInTimeRange(e.timestamp))
|
||||
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase()
|
||||
data = data.filter(
|
||||
(e) =>
|
||||
e.orderId.toLowerCase().includes(q) ||
|
||||
e.route.toLowerCase().includes(q) ||
|
||||
e.customer.toLowerCase().includes(q) ||
|
||||
e.correlationId.toLowerCase().includes(q) ||
|
||||
(e.errorMessage?.toLowerCase().includes(q) ?? false),
|
||||
)
|
||||
// Status filter
|
||||
if (statusFilters.size > 0) {
|
||||
data = data.filter((e) => statusFilters.has(e.status))
|
||||
}
|
||||
|
||||
return data
|
||||
}, [activeFilters, search, scopedExchanges])
|
||||
|
||||
const searchData = useMemo(
|
||||
() => buildSearchData(scopedExchanges, scopedRoutes, scopedAgents),
|
||||
[scopedExchanges, scopedRoutes, scopedAgents],
|
||||
)
|
||||
}, [scopedExchanges, isInTimeRange, statusFilters])
|
||||
|
||||
function handleRowClick(row: Exchange) {
|
||||
setSelectedId(row.id)
|
||||
@@ -392,7 +307,6 @@ export function Dashboard() {
|
||||
}
|
||||
environment="PRODUCTION"
|
||||
user={{ name: 'hendrik' }}
|
||||
onSearchClick={() => setPaletteOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Scrollable content */}
|
||||
@@ -414,17 +328,6 @@ export function Dashboard() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter bar */}
|
||||
<FilterBar
|
||||
filters={buildStatusFilters(scopedExchanges)}
|
||||
activeFilters={activeFilters}
|
||||
onFilterChange={setActiveFilters}
|
||||
searchPlaceholder="Search by Order ID, correlation ID, error message..."
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
className={styles.filterBar}
|
||||
/>
|
||||
|
||||
{/* Exchanges table */}
|
||||
<div className={styles.tableSection}>
|
||||
<div className={styles.tableHeader}>
|
||||
@@ -459,15 +362,6 @@ export function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Command palette */}
|
||||
<CommandPalette
|
||||
open={paletteOpen}
|
||||
onClose={() => setPaletteOpen(false)}
|
||||
onSelect={() => setPaletteOpen(false)}
|
||||
data={searchData}
|
||||
onOpen={() => setPaletteOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Shortcuts bar */}
|
||||
<ShortcutsBar shortcuts={SHORTCUTS} />
|
||||
</AppShell>
|
||||
|
||||
@@ -122,7 +122,7 @@ export function ExchangeDetail() {
|
||||
{ label: id ?? 'Unknown' },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
@@ -149,7 +149,6 @@ export function ExchangeDetail() {
|
||||
{ label: exchange.id },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -120,9 +120,8 @@ export function LayoutSection() {
|
||||
{ label: 'order-ingest' },
|
||||
]}
|
||||
environment="production"
|
||||
shift="Morning"
|
||||
|
||||
user={{ name: 'Hendrik' }}
|
||||
onSearchClick={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
</DemoCard>
|
||||
|
||||
@@ -7,24 +7,12 @@
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
/* Date range picker bar */
|
||||
.dateRangeBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.refreshIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.refreshDot {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styles from './Metrics.module.css'
|
||||
|
||||
@@ -16,7 +15,6 @@ import type { Column } from '../../design-system/composites/DataTable/types'
|
||||
|
||||
// Primitives
|
||||
import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
|
||||
import { DateRangePicker } from '../../design-system/primitives/DateRangePicker/DateRangePicker'
|
||||
import { Sparkline } from '../../design-system/primitives/Sparkline/Sparkline'
|
||||
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
||||
@@ -192,10 +190,6 @@ function convertSeries(series: typeof throughputSeries) {
|
||||
// ─── Metrics page ─────────────────────────────────────────────────────────────
|
||||
export function Metrics() {
|
||||
const navigate = useNavigate()
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: new Date('2026-03-18T06:00:00'),
|
||||
end: new Date('2026-03-18T09:15:00'),
|
||||
})
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@@ -210,20 +204,16 @@ export function Metrics() {
|
||||
{ label: 'Metrics' },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className={styles.content}>
|
||||
|
||||
{/* Date range picker bar */}
|
||||
<div className={styles.dateRangeBar}>
|
||||
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
||||
<div className={styles.refreshIndicator}>
|
||||
<span className={styles.refreshDot} />
|
||||
<span className={styles.refreshText}>Auto-refresh: 30s</span>
|
||||
</div>
|
||||
{/* Auto-refresh indicator */}
|
||||
<div className={styles.refreshIndicator}>
|
||||
<span className={styles.refreshDot} />
|
||||
<span className={styles.refreshText}>Auto-refresh: 30s</span>
|
||||
</div>
|
||||
|
||||
{/* KPI stat cards (5) */}
|
||||
|
||||
@@ -210,7 +210,7 @@ export function RouteDetail() {
|
||||
{ label: id ?? 'Unknown' },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
@@ -236,7 +236,7 @@ export function RouteDetail() {
|
||||
{ label: route.name },
|
||||
]}
|
||||
environment="PRODUCTION"
|
||||
shift="Day (06:00-18:00)"
|
||||
|
||||
user={{ name: 'hendrik' }}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user