Skip to content

Commit 8e4c9bc

Browse files
authored
feat: implement reference data hub landing page (#1285)
* feat: implement reference data hub landing page Replace placeholder with a hub showing cards for Instruments, Account Types, and Nodes. Each card displays a count fetched via list RPCs with pageSize 1, description, icon, and a navigation link to the detail page. Grid is 3-column on desktop and single-column on mobile. * fix: fetch all items for accurate count on reference data hub The list RPCs return no totalCount field. pageSize: 1 always shows 0 or 1. Fetch up to 1000 items (reference data collections are small) and display the actual array length as the count. * fix: add isError state to reference data hub cards Show 'Failed to load' when a count query errors rather than silently displaying '—', which was indistinguishable from an empty collection. * fix: remove invalid dimensionFilter field from listInstruments request ListInstrumentsRequest only accepts statusFilter, pageSize, and pageToken. The dimensionFilter field is not part of the proto schema. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent d912fc8 commit 8e4c9bc

2 files changed

Lines changed: 143 additions & 1 deletion

File tree

frontend/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { PositionsPage } from '@/pages/positions'
2626
import { PositionDetailPage } from '@/pages/positions/detail'
2727
import { MappingsPage } from '@/pages/mappings'
2828
import { MappingDetailPage } from '@/pages/mappings/[mappingId]'
29+
import { ReferenceDataHubPage } from '@/pages/reference-data'
2930
import { InstrumentsPage } from '@/pages/reference-data/instruments'
3031
import { AccountTypesPage } from '@/pages/reference-data/account-types'
3132
import { NodesPage } from '@/pages/reference-data/nodes'
@@ -258,7 +259,7 @@ function AppShellLayout() {
258259
<Route path="/market-data" element={guarded(<MarketDataPage />)} />
259260
<Route path="/market-data/:datasetCode" element={guarded(<DatasetDetailPage />)} />
260261
<Route path="/forecasting" element={guarded(<ForecastingPage />)} />
261-
<Route path="/reference-data" element={guarded(<PlaceholderPage title="Reference Data" />)} />
262+
<Route path="/reference-data" element={guarded(<ReferenceDataHubPage />)} />
262263
<Route path="/reference-data/instruments" element={guarded(<InstrumentsPage />)} />
263264
<Route path="/reference-data/account-types" element={guarded(<AccountTypesPage />)} />
264265
<Route path="/reference-data/nodes" element={guarded(<NodesPage />)} />
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as React from 'react'
2+
import { Link } from 'react-router-dom'
3+
import { useQuery } from '@tanstack/react-query'
4+
import { ChevronRight, Layers, Tag, GitBranch } from 'lucide-react'
5+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6+
import { useApiClients } from '@/api/context'
7+
import { referenceKeys } from '@/lib/query-keys'
8+
import { InstrumentStatus } from '@/api/gen/meridian/reference_data/v1/instrument_pb'
9+
import { BehaviorClass } from '@/api/gen/meridian/reference_data/v1/account_type_pb'
10+
11+
interface ReferenceDataCardProps {
12+
title: string
13+
description: string
14+
count: number | undefined
15+
isLoading: boolean
16+
isError: boolean
17+
href: string
18+
icon: React.ReactNode
19+
}
20+
21+
function ReferenceDataCard({ title, description, count, isLoading, isError, href, icon }: ReferenceDataCardProps) {
22+
return (
23+
<Link to={href} className="group block focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg">
24+
<Card className="h-full transition-colors group-hover:border-primary/50 group-focus-visible:border-primary/50">
25+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
26+
<CardTitle className="text-sm font-medium">{title}</CardTitle>
27+
<div className="text-muted-foreground">{icon}</div>
28+
</CardHeader>
29+
<CardContent>
30+
<div className="mb-2">
31+
{isLoading ? (
32+
<div className="h-8 w-16 animate-pulse rounded bg-muted" />
33+
) : isError ? (
34+
<div className="text-sm text-destructive">Failed to load</div>
35+
) : (
36+
<div className="text-2xl font-bold">
37+
{count !== undefined ? count.toLocaleString() : '—'}
38+
</div>
39+
)}
40+
</div>
41+
<p className="text-xs text-muted-foreground">{description}</p>
42+
<div className="mt-4 flex items-center text-xs font-medium text-primary">
43+
View all
44+
<ChevronRight className="ml-1 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
45+
</div>
46+
</CardContent>
47+
</Card>
48+
</Link>
49+
)
50+
}
51+
52+
export function ReferenceDataHubPage() {
53+
const clients = useApiClients()
54+
55+
// Reference data APIs return no totalCount field, so we fetch all items to count them.
56+
// These collections are typically small (< 200 items), so fetching all is acceptable.
57+
const instrumentsQuery = useQuery({
58+
queryKey: [...referenceKeys.instruments(), 'hub-count'],
59+
queryFn: async () => {
60+
const res = await clients.referenceData.listInstruments({
61+
statusFilter: InstrumentStatus.UNSPECIFIED,
62+
pageSize: 1000,
63+
pageToken: '',
64+
})
65+
return res.instruments
66+
},
67+
staleTime: 60_000,
68+
})
69+
70+
const accountTypesQuery = useQuery({
71+
queryKey: [...referenceKeys.accountTypes(), 'hub-count'],
72+
queryFn: async () => {
73+
const res = await clients.accountTypeRegistry.listActive({
74+
behaviorClassFilter: BehaviorClass.UNSPECIFIED,
75+
pageSize: 1000,
76+
pageToken: '',
77+
})
78+
return res.definitions
79+
},
80+
staleTime: 60_000,
81+
})
82+
83+
const nodesQuery = useQuery({
84+
queryKey: [...referenceKeys.nodeChildren(''), 'hub-count'],
85+
queryFn: async () => {
86+
const res = await clients.node.getChildren({
87+
parentId: '',
88+
activeOnly: true,
89+
})
90+
return res.nodes
91+
},
92+
staleTime: 60_000,
93+
})
94+
95+
const cards = [
96+
{
97+
title: 'Instruments',
98+
description: 'Asset classes and financial instrument definitions with CEL validation.',
99+
count: instrumentsQuery.data?.length,
100+
isLoading: instrumentsQuery.isLoading,
101+
isError: instrumentsQuery.isError,
102+
href: '/reference-data/instruments',
103+
icon: <Tag className="h-4 w-4" />,
104+
},
105+
{
106+
title: 'Account Types',
107+
description: 'Account type registry with behavior classes and CEL policy configuration.',
108+
count: accountTypesQuery.data?.length,
109+
isLoading: accountTypesQuery.isLoading,
110+
isError: accountTypesQuery.isError,
111+
href: '/reference-data/account-types',
112+
icon: <Layers className="h-4 w-4" />,
113+
},
114+
{
115+
title: 'Nodes',
116+
description: 'Hierarchical reference data nodes with bi-temporal query support.',
117+
count: nodesQuery.data?.length,
118+
isLoading: nodesQuery.isLoading,
119+
isError: nodesQuery.isError,
120+
href: '/reference-data/nodes',
121+
icon: <GitBranch className="h-4 w-4" />,
122+
},
123+
]
124+
125+
return (
126+
<div className="space-y-6">
127+
<div>
128+
<h1 className="text-3xl font-bold tracking-tight">Reference Data</h1>
129+
<p className="mt-2 text-muted-foreground">
130+
Manage instruments, account types, and hierarchical reference data nodes.
131+
</p>
132+
</div>
133+
134+
<div className="grid gap-4 md:grid-cols-3">
135+
{cards.map((card) => (
136+
<ReferenceDataCard key={card.href} {...card} />
137+
))}
138+
</div>
139+
</div>
140+
)
141+
}

0 commit comments

Comments
 (0)