Skip to content

Commit 36b6352

Browse files
authored
feat: Add Economy breadcrumbs and Valuation Rules page (#1655)
* feat: Add Economy breadcrumbs to all sub-pages Add breadcrumb navigation tracing back to Economy on Reference Data, Instruments, Account Types, Nodes, Starlark Config, Market Data, and Forecasting pages. Updates existing breadcrumbs on saga detail and dataset detail pages to include Economy as parent. * feat: Add Valuation Rules page under Reference Data Create ValuationRulesPage that queries the current manifest for valuation rules and displays them in a table with columns for rule name, from/to instruments, method, and source. Add route, export, and 4th card to Reference Data hub with updated grid layout. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent d6caf2a commit 36b6352

13 files changed

Lines changed: 389 additions & 10 deletions

File tree

frontend/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { ReconciliationPage, ReconciliationDetailPage } from '@/features/reconci
2929
import { AuditLogPage } from '@/features/audit'
3030
import { StarlarkConfigPage, StarlarkDetailPage } from '@/features/sagas'
3131
import { MappingsPage, MappingDetailPage } from '@/features/mappings'
32-
import { ReferenceDataHubPage, InstrumentsPage, AccountTypesPage, NodesPage } from '@/features/reference-data'
32+
import { ReferenceDataHubPage, InstrumentsPage, AccountTypesPage, NodesPage, ValuationRulesPage } from '@/features/reference-data'
3333
import { InternalAccountsPage, InternalAccountDetailPage } from '@/features/internal-accounts'
3434
import { MarketDataPage, DatasetDetailPage } from '@/features/market-data'
3535
import { ForecastingPage } from '@/features/forecasting'
@@ -309,6 +309,7 @@ function AppShellLayout() {
309309
<Route path="/reference-data/instruments" element={<FeatureGuard feature="reference-data">{guarded(<InstrumentsPage />)}</FeatureGuard>} />
310310
<Route path="/reference-data/account-types" element={<FeatureGuard feature="reference-data">{guarded(<AccountTypesPage />)}</FeatureGuard>} />
311311
<Route path="/reference-data/nodes" element={<FeatureGuard feature="reference-data">{guarded(<NodesPage />)}</FeatureGuard>} />
312+
<Route path="/reference-data/valuation-rules" element={<FeatureGuard feature="reference-data">{guarded(<ValuationRulesPage />)}</FeatureGuard>} />
312313
<Route path="/gateway-mappings" element={<FeatureGuard feature="mappings">{guarded(<MappingsPage />)}</FeatureGuard>} />
313314
<Route path="/gateway-mappings/:mappingId" element={<FeatureGuard feature="mappings">{guarded(<MappingDetailPage />)}</FeatureGuard>} />
314315
<Route path="/economy" element={<FeatureGuard feature="economy">{guarded(<EconomyOverviewPage />)}</FeatureGuard>} />

frontend/src/features/forecasting/pages/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { format } from 'date-fns'
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
55
import { Button } from '@/components/ui/button'
66
import { Input } from '@/components/ui/input'
7-
import { PageShell, PageHeader } from '@/shared'
7+
import { PageShell, PageHeader, Breadcrumbs } from '@/shared'
88
import { useApiClients } from '@/api/context'
99
import { usePageTitle } from '@/hooks/use-page-title'
1010
import { useTenantContext } from '@/contexts/tenant-context'
@@ -178,6 +178,11 @@ export function ForecastingPage() {
178178

179179
return (
180180
<PageShell>
181+
<Breadcrumbs items={[
182+
{ label: 'Economy', href: '/economy' },
183+
{ label: 'Forecasting' },
184+
]} />
185+
181186
<PageHeader
182187
title="Forecasting"
183188
description="Compute forward curves by executing a forecasting strategy."

frontend/src/features/market-data/pages/[datasetCode].tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,10 @@ export function DatasetDetailPage() {
171171
if (!tenantSlug) {
172172
return (
173173
<PageShell>
174-
<Breadcrumbs items={[{ label: 'Market Data', href: '/market-data' }]} />
174+
<Breadcrumbs items={[
175+
{ label: 'Economy', href: '/economy' },
176+
{ label: 'Market Data', href: '/market-data' },
177+
]} />
175178
<p className="text-muted-foreground">No tenant selected.</p>
176179
</PageShell>
177180
)
@@ -180,7 +183,10 @@ export function DatasetDetailPage() {
180183
if (!datasetCode) {
181184
return (
182185
<PageShell>
183-
<Breadcrumbs items={[{ label: 'Market Data', href: '/market-data' }]} />
186+
<Breadcrumbs items={[
187+
{ label: 'Economy', href: '/economy' },
188+
{ label: 'Market Data', href: '/market-data' },
189+
]} />
184190
<p className="text-muted-foreground">No dataset selected.</p>
185191
</PageShell>
186192
)
@@ -222,6 +228,7 @@ export function DatasetDetailPage() {
222228
<PageShell>
223229
<Breadcrumbs
224230
items={[
231+
{ label: 'Economy', href: '/economy' },
225232
{ label: 'Market Data', href: '/market-data' },
226233
{ label: dataset?.displayName || datasetCode },
227234
]}

frontend/src/features/market-data/pages/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'
44
import { DataTable } from '@/shared/data-table'
55
import { StatusBadge } from '@/shared/status-badge'
66
import { TimeDisplay } from '@/shared/time-display'
7+
import { Breadcrumbs } from '@/shared/breadcrumbs'
78
import { PageShell } from '@/shared/page-shell'
89
import { PageHeader } from '@/shared/page-header'
910
import { Card } from '@/components/ui/card'
@@ -124,6 +125,11 @@ export function MarketDataPage() {
124125

125126
return (
126127
<PageShell>
128+
<Breadcrumbs items={[
129+
{ label: 'Economy', href: '/economy' },
130+
{ label: 'Market Data' },
131+
]} />
132+
127133
<PageHeader
128134
title="Market Data"
129135
description="Market data sets with price observations for FX rates, interest rates, energy prices, and more."

frontend/src/features/reference-data/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { ReferenceDataHubPage } from './pages/index'
33
export { InstrumentsPage } from './pages/instruments/index'
44
export { AccountTypesPage } from './pages/account-types/index'
55
export { NodesPage } from './pages/nodes/index'
6+
export { ValuationRulesPage } from './pages/valuation-rules/index'

frontend/src/features/reference-data/pages/account-types/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react'
22
import type { ColumnDef } from '@tanstack/react-table'
33
import { DataTable } from '@/shared/data-table'
44
import { StatusBadge } from '@/shared/status-badge'
5+
import { Breadcrumbs } from '@/shared/breadcrumbs'
56
import { CELEditor } from '@/features/sagas/components/cel-editor'
67
import { useApiClients } from '@/api/context'
78
import { PageShell } from '@/shared/page-shell'
@@ -107,6 +108,12 @@ export function AccountTypesPage() {
107108

108109
return (
109110
<PageShell>
111+
<Breadcrumbs items={[
112+
{ label: 'Economy', href: '/economy' },
113+
{ label: 'Reference Data', href: '/reference-data' },
114+
{ label: 'Account Types' },
115+
]} />
116+
110117
<PageHeader
111118
title="Account Types"
112119
description="Account type registry with CEL policy configuration."

frontend/src/features/reference-data/pages/index.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as React from 'react'
22
import { Link } from 'react-router-dom'
33
import { useQuery } from '@tanstack/react-query'
4-
import { ChevronRight, Layers, Tag, GitBranch } from 'lucide-react'
4+
import { ChevronRight, Layers, Tag, GitBranch, Scale } from 'lucide-react'
55
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6+
import { Breadcrumbs } from '@/shared/breadcrumbs'
67
import { useApiClients } from '@/api/context'
7-
import { referenceKeys } from '@/lib/query-keys'
8+
import { referenceKeys, manifestKeys } from '@/lib/query-keys'
89
import { InstrumentStatus } from '@/api/gen/meridian/reference_data/v1/instrument_pb'
910
import { BehaviorClass } from '@/api/gen/meridian/reference_data/v1/account_type_pb'
1011
import { usePageTitle } from '@/hooks/use-page-title'
@@ -94,6 +95,15 @@ export function ReferenceDataHubPage() {
9495
staleTime: 60_000,
9596
})
9697

98+
const valuationRulesQuery = useQuery({
99+
queryKey: [...manifestKeys.current(), 'hub-valuation-rules-count'],
100+
queryFn: async () => {
101+
const res = await clients.manifestHistory.getCurrentManifest({})
102+
return res.version?.manifest?.valuationRules ?? []
103+
},
104+
staleTime: 60_000,
105+
})
106+
97107
const cards = [
98108
{
99109
title: 'Instruments',
@@ -122,18 +132,32 @@ export function ReferenceDataHubPage() {
122132
href: '/reference-data/nodes',
123133
icon: <GitBranch className="h-4 w-4" />,
124134
},
135+
{
136+
title: 'Valuation Rules',
137+
description: 'Instrument conversion rules for cross-asset valuation.',
138+
count: valuationRulesQuery.data?.length,
139+
isLoading: valuationRulesQuery.isLoading,
140+
isError: valuationRulesQuery.isError,
141+
href: '/reference-data/valuation-rules',
142+
icon: <Scale className="h-4 w-4" />,
143+
},
125144
]
126145

127146
return (
128147
<div className="space-y-6">
148+
<Breadcrumbs items={[
149+
{ label: 'Economy', href: '/economy' },
150+
{ label: 'Reference Data' },
151+
]} />
152+
129153
<div>
130154
<h1 className="text-3xl font-bold tracking-tight">Reference Data</h1>
131155
<p className="mt-2 text-muted-foreground">
132156
Manage instruments, account types, and hierarchical reference data nodes.
133157
</p>
134158
</div>
135159

136-
<div className="grid gap-4 md:grid-cols-3">
160+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
137161
{cards.map((card) => (
138162
<ReferenceDataCard key={card.href} {...card} />
139163
))}

frontend/src/features/reference-data/pages/instruments/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ColumnDef } from '@tanstack/react-table'
33
import { useMutation } from '@tanstack/react-query'
44
import { DataTable } from '@/shared/data-table'
55
import { StatusBadge } from '@/shared/status-badge'
6+
import { Breadcrumbs } from '@/shared/breadcrumbs'
67
import { CELEditor } from '@/features/sagas/components/cel-editor'
78
import { useApiClients } from '@/api/context'
89
import { PageShell } from '@/shared/page-shell'
@@ -185,6 +186,12 @@ export function InstrumentsPage() {
185186

186187
return (
187188
<PageShell>
189+
<Breadcrumbs items={[
190+
{ label: 'Economy', href: '/economy' },
191+
{ label: 'Reference Data', href: '/reference-data' },
192+
{ label: 'Instruments' },
193+
]} />
194+
188195
<PageHeader
189196
title="Instruments"
190197
description="Reference data instrument definitions with CEL validation expressions."

frontend/src/features/reference-data/pages/nodes/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react'
22
import { useQuery } from '@tanstack/react-query'
33
import { useApiClients } from '@/api/context'
4+
import { Breadcrumbs } from '@/shared/breadcrumbs'
45
import { PageShell } from '@/shared/page-shell'
56
import { PageHeader } from '@/shared/page-header'
67
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -127,6 +128,12 @@ export function NodesPage() {
127128

128129
return (
129130
<PageShell>
131+
<Breadcrumbs items={[
132+
{ label: 'Economy', href: '/economy' },
133+
{ label: 'Reference Data', href: '/reference-data' },
134+
{ label: 'Nodes' },
135+
]} />
136+
130137
<PageHeader
131138
title="Nodes"
132139
description="Hierarchical reference data node browser with bi-temporal query support."
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { screen, waitFor } from '@testing-library/react'
3+
import { MemoryRouter } from 'react-router-dom'
4+
import { renderWithProviders } from '@/test/test-utils'
5+
import { ValuationRulesPage } from './index'
6+
7+
vi.mock('@/api/context', () => ({
8+
useApiClients: vi.fn(),
9+
ApiClientProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
10+
}))
11+
12+
import { useApiClients } from '@/api/context'
13+
14+
const mockManifestVersion = {
15+
id: 'mv-1',
16+
version: '2.0',
17+
manifest: {
18+
version: '2.0',
19+
metadata: { name: 'Test Economy' },
20+
instruments: [],
21+
accountTypes: [],
22+
valuationRules: [
23+
{ fromInstrument: 'KWH', toInstrument: 'GBP', method: 1, source: 'nordpool_spot' },
24+
{ fromInstrument: 'USD', toInstrument: 'EUR', method: 2, source: 'admin_override' },
25+
],
26+
sagas: [],
27+
seedData: undefined,
28+
paymentRails: [],
29+
partyTypes: [],
30+
mappings: [],
31+
},
32+
}
33+
34+
function mockApiClients(overrides: Record<string, unknown> = {}) {
35+
vi.mocked(useApiClients).mockReturnValue({
36+
manifestHistory: {
37+
getCurrentManifest: vi.fn().mockResolvedValue({ version: mockManifestVersion }),
38+
...overrides,
39+
},
40+
} as unknown as ReturnType<typeof useApiClients>)
41+
}
42+
43+
function renderPage() {
44+
return renderWithProviders(
45+
<MemoryRouter>
46+
<ValuationRulesPage />
47+
</MemoryRouter>,
48+
)
49+
}
50+
51+
describe('ValuationRulesPage', () => {
52+
beforeEach(() => {
53+
vi.clearAllMocks()
54+
})
55+
56+
it('renders breadcrumbs with Economy and Reference Data links', async () => {
57+
mockApiClients()
58+
renderPage()
59+
60+
const breadcrumb = await waitFor(() => screen.getByRole('navigation', { name: 'Breadcrumb' }))
61+
expect(breadcrumb).toBeInTheDocument()
62+
63+
const economyLink = screen.getByText('Economy').closest('a')
64+
expect(economyLink).toHaveAttribute('href', '/economy')
65+
66+
const refDataLink = screen.getByText('Reference Data').closest('a')
67+
expect(refDataLink).toHaveAttribute('href', '/reference-data')
68+
})
69+
70+
it('renders valuation rules in a table', async () => {
71+
mockApiClients()
72+
renderPage()
73+
74+
await waitFor(() => {
75+
expect(screen.getByText('KWH')).toBeInTheDocument()
76+
})
77+
78+
expect(screen.getByText('GBP')).toBeInTheDocument()
79+
expect(screen.getByText('USD')).toBeInTheDocument()
80+
expect(screen.getByText('EUR')).toBeInTheDocument()
81+
expect(screen.getByText('nordpool_spot')).toBeInTheDocument()
82+
expect(screen.getByText('admin_override')).toBeInTheDocument()
83+
expect(screen.getByText('Spot Rate')).toBeInTheDocument()
84+
expect(screen.getByText('Fixed')).toBeInTheDocument()
85+
})
86+
87+
it('renders loading skeleton', () => {
88+
vi.mocked(useApiClients).mockReturnValue({
89+
manifestHistory: {
90+
getCurrentManifest: vi.fn().mockReturnValue(new Promise(() => {})),
91+
},
92+
} as unknown as ReturnType<typeof useApiClients>)
93+
94+
renderPage()
95+
const skeletons = document.querySelectorAll('.animate-pulse')
96+
expect(skeletons.length).toBeGreaterThan(0)
97+
})
98+
99+
it('renders empty state when no valuation rules exist', async () => {
100+
vi.mocked(useApiClients).mockReturnValue({
101+
manifestHistory: {
102+
getCurrentManifest: vi.fn().mockResolvedValue({
103+
version: {
104+
...mockManifestVersion,
105+
manifest: { ...mockManifestVersion.manifest, valuationRules: [] },
106+
},
107+
}),
108+
},
109+
} as unknown as ReturnType<typeof useApiClients>)
110+
111+
renderPage()
112+
113+
await waitFor(() => {
114+
expect(screen.getByTestId('empty-state')).toBeInTheDocument()
115+
})
116+
117+
expect(screen.getByText('No valuation rules')).toBeInTheDocument()
118+
})
119+
120+
it('renders error state on fetch failure', async () => {
121+
vi.mocked(useApiClients).mockReturnValue({
122+
manifestHistory: {
123+
getCurrentManifest: vi.fn().mockRejectedValue(new Error('Network error')),
124+
},
125+
} as unknown as ReturnType<typeof useApiClients>)
126+
127+
renderPage()
128+
129+
await waitFor(() => {
130+
expect(screen.getByText('Failed to load valuation rules.')).toBeInTheDocument()
131+
})
132+
})
133+
134+
it('renders rule name column as from → to', async () => {
135+
mockApiClients()
136+
renderPage()
137+
138+
await waitFor(() => {
139+
expect(screen.getByText(/KWH GBP/)).toBeInTheDocument()
140+
expect(screen.getByText(/USD EUR/)).toBeInTheDocument()
141+
})
142+
})
143+
})

0 commit comments

Comments
 (0)