feat: Dashboard page composing full design system
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,10 @@
|
|||||||
|
import { Routes, Route } from 'react-router-dom'
|
||||||
|
import { Dashboard } from './pages/Dashboard/Dashboard'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return <div>Cameleer3 Design System</div>
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
231
src/pages/Dashboard/Dashboard.module.css
Normal file
231
src/pages/Dashboard/Dashboard.module.css
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
/* Scrollable content area */
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px 24px 40px;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Health strip */
|
||||||
|
.healthStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter bar spacing */
|
||||||
|
.filterBar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table section */
|
||||||
|
.tableSection {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableMeta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status cell */
|
||||||
|
.statusCell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Route cells */
|
||||||
|
.routeName {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.routeGroup {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customer text */
|
||||||
|
.customerText {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Duration color classes */
|
||||||
|
.durFast {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.durNormal {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.durSlow {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.durBreach {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agent badge in table */
|
||||||
|
.agentBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentDot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #5db866;
|
||||||
|
box-shadow: 0 0 4px rgba(93, 184, 102, 0.4);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline error preview below row */
|
||||||
|
.inlineError {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--error-bg);
|
||||||
|
border-left: 3px solid var(--error-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineErrorIcon {
|
||||||
|
color: var(--error);
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineErrorText {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--error);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineErrorHint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail panel: overview tab */
|
||||||
|
.overviewTab {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overviewRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overviewLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorBlock {
|
||||||
|
background: var(--error-bg);
|
||||||
|
border: 1px solid var(--error-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorClass {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--error);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail panel: processors tab */
|
||||||
|
.processorsTab {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail panel: exchange tab */
|
||||||
|
.exchangeTab {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail panel: error tab */
|
||||||
|
.errorTab {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyTabMsg {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorPre {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--error);
|
||||||
|
background: var(--error-bg);
|
||||||
|
border: 1px solid var(--error-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
460
src/pages/Dashboard/Dashboard.tsx
Normal file
460
src/pages/Dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import styles from './Dashboard.module.css'
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
import { AppShell } from '../../design-system/layout/AppShell/AppShell'
|
||||||
|
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'
|
||||||
|
|
||||||
|
// Primitives
|
||||||
|
import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
|
||||||
|
import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
||||||
|
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
||||||
|
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
import { executions, type Execution } from '../../mocks/executions'
|
||||||
|
import { routes } from '../../mocks/routes'
|
||||||
|
import { agents } from '../../mocks/agents'
|
||||||
|
import { kpiMetrics } from '../../mocks/metrics'
|
||||||
|
|
||||||
|
// ─── Sidebar app list (static) ───────────────────────────────────────────────
|
||||||
|
const APPS = [
|
||||||
|
{ id: 'order-service', name: 'order-service', agentCount: 2, health: 'live' as const, execCount: 1433 },
|
||||||
|
{ id: 'payment-svc', name: 'payment-svc', agentCount: 1, health: 'live' as const, execCount: 912 },
|
||||||
|
{ id: 'shipment-tracker', name: 'shipment-tracker', agentCount: 2, health: 'live' as const, execCount: 471 },
|
||||||
|
{ id: 'notification-hub', name: 'notification-hub', agentCount: 1, health: 'stale' as const, execCount: 128 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Sidebar routes (top 3) ───────────────────────────────────────────────────
|
||||||
|
const SIDEBAR_ROUTES = routes.slice(0, 3).map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
execCount: r.execCount,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
||||||
|
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
||||||
|
return `${ms}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(date: Date): string {
|
||||||
|
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusToVariant(status: Execution['status']): 'success' | 'error' | 'running' | 'warning' {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'success'
|
||||||
|
case 'failed': return 'error'
|
||||||
|
case 'running': return 'running'
|
||||||
|
case 'warning': return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: Execution['status']): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'OK'
|
||||||
|
case 'failed': return 'ERR'
|
||||||
|
case 'running': return 'RUN'
|
||||||
|
case 'warning': return 'WARN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Table columns ────────────────────────────────────────────────────────────
|
||||||
|
const COLUMNS: Column<Execution>[] = [
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
width: '80px',
|
||||||
|
render: (_, row) => (
|
||||||
|
<span className={styles.statusCell}>
|
||||||
|
<StatusDot variant={statusToVariant(row.status)} />
|
||||||
|
<MonoText size="xs">{statusLabel(row.status)}</MonoText>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'route',
|
||||||
|
header: 'Route',
|
||||||
|
sortable: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div>
|
||||||
|
<div className={styles.routeName}>{row.route}</div>
|
||||||
|
<div className={styles.routeGroup}>{row.routeGroup}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orderId',
|
||||||
|
header: 'Order ID',
|
||||||
|
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>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'timestamp',
|
||||||
|
header: 'Started',
|
||||||
|
sortable: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<MonoText size="xs">{formatTimestamp(row.timestamp)}</MonoText>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'durationMs',
|
||||||
|
header: 'Duration',
|
||||||
|
sortable: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<MonoText size="sm" className={durationClass(row.durationMs, row.status)}>
|
||||||
|
{formatDuration(row.durationMs)}
|
||||||
|
</MonoText>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'agent',
|
||||||
|
header: 'Agent',
|
||||||
|
render: (_, row) => (
|
||||||
|
<span className={styles.agentBadge}>
|
||||||
|
<span className={styles.agentDot} />
|
||||||
|
{row.agent}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function durationClass(ms: number, status: Execution['status']): string {
|
||||||
|
if (status === 'failed') return styles.durBreach
|
||||||
|
if (ms < 100) return styles.durFast
|
||||||
|
if (ms < 200) return styles.durNormal
|
||||||
|
if (ms < 300) return styles.durSlow
|
||||||
|
return styles.durBreach
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Build CommandPalette search data ────────────────────────────────────────
|
||||||
|
function buildSearchData(): SearchResult[] {
|
||||||
|
const results: SearchResult[] = []
|
||||||
|
|
||||||
|
for (const exec of executions) {
|
||||||
|
results.push({
|
||||||
|
id: exec.id,
|
||||||
|
category: 'execution',
|
||||||
|
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 routes) {
|
||||||
|
results.push({
|
||||||
|
id: route.id,
|
||||||
|
category: 'route',
|
||||||
|
title: route.name,
|
||||||
|
badges: [{ label: route.group }],
|
||||||
|
meta: `${route.execCount.toLocaleString()} executions · ${route.successRate}% success`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const agent of agents) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEARCH_DATA = buildSearchData()
|
||||||
|
|
||||||
|
// ─── Filter options ───────────────────────────────────────────────────────────
|
||||||
|
const STATUS_FILTERS = [
|
||||||
|
{ label: 'All', value: 'all', count: executions.length },
|
||||||
|
{ label: 'OK', value: 'completed', count: executions.filter((e) => e.status === 'completed').length, color: 'success' as const },
|
||||||
|
{ label: 'Warn', value: 'warning', count: executions.filter((e) => e.status === 'warning').length },
|
||||||
|
{ label: 'Error', value: 'failed', count: executions.filter((e) => e.status === 'failed').length, color: 'error' as const },
|
||||||
|
{ label: 'Running', value: 'running', count: executions.filter((e) => e.status === 'running').length, color: 'running' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
const SHORTCUTS = [
|
||||||
|
{ keys: 'Ctrl+K', label: 'Search' },
|
||||||
|
{ keys: '↑↓', label: 'Navigate rows' },
|
||||||
|
{ keys: 'Enter', label: 'Open detail' },
|
||||||
|
{ keys: 'Esc', label: 'Close panel' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Dashboard component ──────────────────────────────────────────────────────
|
||||||
|
export function Dashboard() {
|
||||||
|
const [activeItem, setActiveItem] = useState('order-service')
|
||||||
|
const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([])
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [selectedId, setSelectedId] = useState<string | undefined>()
|
||||||
|
const [panelOpen, setPanelOpen] = useState(false)
|
||||||
|
const [selectedExecution, setSelectedExecution] = useState<Execution | null>(null)
|
||||||
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||||
|
|
||||||
|
// Filter executions
|
||||||
|
const filteredExecutions = useMemo(() => {
|
||||||
|
let data = executions
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}, [activeFilters, search])
|
||||||
|
|
||||||
|
function handleRowClick(row: Execution) {
|
||||||
|
setSelectedId(row.id)
|
||||||
|
setSelectedExecution(row)
|
||||||
|
setPanelOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRowAccent(row: Execution): 'error' | 'warning' | undefined {
|
||||||
|
if (row.status === 'failed') return 'error'
|
||||||
|
if (row.status === 'warning') return 'warning'
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build detail panel tabs for selected execution
|
||||||
|
const detailTabs = selectedExecution
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Overview',
|
||||||
|
value: 'overview',
|
||||||
|
content: (
|
||||||
|
<div className={styles.overviewTab}>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Order ID</span>
|
||||||
|
<MonoText size="sm">{selectedExecution.orderId}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Route</span>
|
||||||
|
<span>{selectedExecution.route}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Status</span>
|
||||||
|
<span className={styles.statusCell}>
|
||||||
|
<StatusDot variant={statusToVariant(selectedExecution.status)} />
|
||||||
|
<span>{statusLabel(selectedExecution.status)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Duration</span>
|
||||||
|
<MonoText size="sm">{formatDuration(selectedExecution.durationMs)}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Customer</span>
|
||||||
|
<MonoText size="sm">{selectedExecution.customer}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Agent</span>
|
||||||
|
<MonoText size="sm">{selectedExecution.agent}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Correlation ID</span>
|
||||||
|
<MonoText size="xs">{selectedExecution.correlationId}</MonoText>
|
||||||
|
</div>
|
||||||
|
<div className={styles.overviewRow}>
|
||||||
|
<span className={styles.overviewLabel}>Timestamp</span>
|
||||||
|
<MonoText size="xs">{selectedExecution.timestamp.toISOString()}</MonoText>
|
||||||
|
</div>
|
||||||
|
{selectedExecution.errorMessage && (
|
||||||
|
<div className={styles.errorBlock}>
|
||||||
|
<div className={styles.errorClass}>{selectedExecution.errorClass}</div>
|
||||||
|
<div className={styles.errorMessage}>{selectedExecution.errorMessage}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Processors',
|
||||||
|
value: 'processors',
|
||||||
|
content: (
|
||||||
|
<div className={styles.processorsTab}>
|
||||||
|
<ProcessorTimeline
|
||||||
|
processors={selectedExecution.processors}
|
||||||
|
totalMs={selectedExecution.durationMs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Exchange',
|
||||||
|
value: 'exchange',
|
||||||
|
content: (
|
||||||
|
<div className={styles.exchangeTab}>
|
||||||
|
<div className={styles.emptyTabMsg}>Exchange snapshot not available in mock mode.</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Error',
|
||||||
|
value: 'error',
|
||||||
|
content: (
|
||||||
|
<div className={styles.errorTab}>
|
||||||
|
{selectedExecution.errorMessage ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.errorClass}>{selectedExecution.errorClass}</div>
|
||||||
|
<pre className={styles.errorPre}>{selectedExecution.errorMessage}</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className={styles.emptyTabMsg}>No error for this execution.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
sidebar={
|
||||||
|
<Sidebar
|
||||||
|
apps={APPS}
|
||||||
|
routes={SIDEBAR_ROUTES}
|
||||||
|
agents={agents}
|
||||||
|
activeItem={activeItem}
|
||||||
|
onItemClick={setActiveItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
detail={
|
||||||
|
selectedExecution ? (
|
||||||
|
<DetailPanel
|
||||||
|
open={panelOpen}
|
||||||
|
onClose={() => setPanelOpen(false)}
|
||||||
|
title={`${selectedExecution.orderId} — ${selectedExecution.route}`}
|
||||||
|
tabs={detailTabs}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Top bar */}
|
||||||
|
<TopBar
|
||||||
|
breadcrumb={[
|
||||||
|
{ label: 'Applications', href: '/' },
|
||||||
|
{ label: 'order-service' },
|
||||||
|
]}
|
||||||
|
environment="PRODUCTION"
|
||||||
|
shift="Day (06:00-18:00)"
|
||||||
|
user={{ name: 'hendrik' }}
|
||||||
|
onSearchClick={() => setPaletteOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Scrollable content */}
|
||||||
|
<div className={styles.content}>
|
||||||
|
|
||||||
|
{/* Health strip */}
|
||||||
|
<div className={styles.healthStrip}>
|
||||||
|
{kpiMetrics.map((kpi, i) => (
|
||||||
|
<StatCard
|
||||||
|
key={i}
|
||||||
|
label={kpi.label}
|
||||||
|
value={kpi.value}
|
||||||
|
detail={kpi.detail}
|
||||||
|
trend={kpi.trend}
|
||||||
|
trendValue={kpi.trendValue}
|
||||||
|
accent={kpi.accent}
|
||||||
|
sparkline={kpi.sparkline}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter bar */}
|
||||||
|
<FilterBar
|
||||||
|
filters={STATUS_FILTERS}
|
||||||
|
activeFilters={activeFilters}
|
||||||
|
onFilterChange={setActiveFilters}
|
||||||
|
searchPlaceholder="Search by Order ID, correlation ID, error message..."
|
||||||
|
searchValue={search}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
className={styles.filterBar}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Executions table */}
|
||||||
|
<div className={styles.tableSection}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<span className={styles.tableTitle}>Recent Executions</span>
|
||||||
|
<div className={styles.tableRight}>
|
||||||
|
<span className={styles.tableMeta}>
|
||||||
|
{filteredExecutions.length.toLocaleString()} of {executions.length.toLocaleString()} executions
|
||||||
|
</span>
|
||||||
|
<Badge label="LIVE" color="success" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={COLUMNS}
|
||||||
|
data={filteredExecutions}
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
selectedId={selectedId}
|
||||||
|
sortable
|
||||||
|
rowAccent={handleRowAccent}
|
||||||
|
expandedContent={(row) =>
|
||||||
|
row.errorMessage ? (
|
||||||
|
<div className={styles.inlineError}>
|
||||||
|
<span className={styles.inlineErrorIcon}>⚠</span>
|
||||||
|
<div>
|
||||||
|
<div className={styles.inlineErrorText}>{row.errorMessage}</div>
|
||||||
|
<div className={styles.inlineErrorHint}>Click to view full stack trace</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Command palette */}
|
||||||
|
<CommandPalette
|
||||||
|
open={paletteOpen}
|
||||||
|
onClose={() => setPaletteOpen(false)}
|
||||||
|
onSelect={() => setPaletteOpen(false)}
|
||||||
|
data={SEARCH_DATA}
|
||||||
|
onOpen={() => setPaletteOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Shortcuts bar */}
|
||||||
|
<ShortcutsBar shortcuts={SHORTCUTS} />
|
||||||
|
</AppShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user