Skip to content

Commit ecbc127

Browse files
authored
feat: handler reference panel and UI component preview (#1449)
* feat: add handler reference panel and UI component detail view Add collapsible handler reference panel to pattern detail page that extracts service names from Starlark content using the star-parser and filters the HandlerReference component to show relevant handlers. Add ComponentDetail view for registry:ui items showing configurable props, usage context, registry dependencies, source files, and a preview placeholder. Update detail page routing to show ComponentDetail for UI components and the existing tabbed view with handler reference for patterns. Extend ComponentMeta type with tenant_configurable and configurable_props fields to match component.json schema. * fix: update detail page test for component detail view Update test assertion to match new ComponentDetail output instead of the old placeholder text. * fix: address review feedback for handler ref and component detail Render tenant_configurable field in ComponentDetail usage context card. Make HandlerReference onInsert prop optional and hide insert buttons when omitted, so the read-only cookbook panel does not show non-functional buttons. * fix: use serviceNames array prop for multi-service handler filtering Replace space-joined filter string with a dedicated serviceNames prop on HandlerReference that filters by exact service name match. The space-joined approach broke when multiple services were present since includes() checked the entire joined string. * fix: include tenant_configurable in usage context card guard Ensure the Usage Context card renders when only tenant_configurable is set, even without feature_module or used_by. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 3f8d0c2 commit ecbc127

6 files changed

Lines changed: 289 additions & 78 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Link } from 'react-router-dom'
2+
import { Badge } from '@/components/ui/badge'
3+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
4+
import type { CookbookItem, ComponentMeta } from '../hooks/use-cookbook'
5+
6+
interface ComponentDetailProps {
7+
item: CookbookItem
8+
}
9+
10+
export function ComponentDetail({ item }: ComponentDetailProps) {
11+
const meta = item.meta as ComponentMeta | undefined
12+
13+
return (
14+
<div className="space-y-6">
15+
{/* Configurable Props */}
16+
{meta?.configurable_props && meta.configurable_props.length > 0 && (
17+
<Card>
18+
<CardHeader className="pb-3">
19+
<CardTitle className="text-base">Configurable Props</CardTitle>
20+
</CardHeader>
21+
<CardContent>
22+
<div className="rounded border">
23+
<table className="w-full text-sm">
24+
<thead>
25+
<tr className="border-b bg-muted/50">
26+
<th className="px-3 py-2 text-left font-medium">Prop</th>
27+
<th className="px-3 py-2 text-left font-medium">Source</th>
28+
</tr>
29+
</thead>
30+
<tbody>
31+
{meta.configurable_props.map((prop) => (
32+
<tr key={prop} className="border-b last:border-0">
33+
<td className="px-3 py-2 font-mono text-xs">{prop}</td>
34+
<td className="px-3 py-2 text-muted-foreground">component.json</td>
35+
</tr>
36+
))}
37+
</tbody>
38+
</table>
39+
</div>
40+
</CardContent>
41+
</Card>
42+
)}
43+
44+
{/* Feature Module Context */}
45+
{(meta?.feature_module || meta?.tenant_configurable != null || (meta?.used_by && meta.used_by.length > 0)) && (
46+
<Card>
47+
<CardHeader className="pb-3">
48+
<CardTitle className="text-base">Usage Context</CardTitle>
49+
</CardHeader>
50+
<CardContent className="space-y-3">
51+
{meta?.feature_module && (
52+
<div className="flex items-center gap-2 text-sm">
53+
<span className="text-muted-foreground">Feature module:</span>
54+
<Badge variant="outline">{meta.feature_module}</Badge>
55+
</div>
56+
)}
57+
{meta?.tenant_configurable != null && (
58+
<div className="flex items-center gap-2 text-sm">
59+
<span className="text-muted-foreground">Tenant configurable:</span>
60+
<Badge variant={meta.tenant_configurable ? 'default' : 'secondary'}>
61+
{meta.tenant_configurable ? 'Yes' : 'No'}
62+
</Badge>
63+
</div>
64+
)}
65+
{meta?.used_by && meta.used_by.length > 0 && (
66+
<div className="flex items-center gap-2 text-sm">
67+
<span className="text-muted-foreground">Used by:</span>
68+
<div className="flex flex-wrap gap-1.5">
69+
{meta.used_by.map((name) => (
70+
<Badge key={name} variant="secondary" className="text-xs">
71+
{name}
72+
</Badge>
73+
))}
74+
</div>
75+
</div>
76+
)}
77+
</CardContent>
78+
</Card>
79+
)}
80+
81+
{/* Registry Dependencies */}
82+
<Card>
83+
<CardHeader className="pb-3">
84+
<CardTitle className="text-base">Registry Dependencies</CardTitle>
85+
</CardHeader>
86+
<CardContent>
87+
{item.registryDependencies && item.registryDependencies.length > 0 ? (
88+
<div className="flex flex-wrap gap-1.5">
89+
{item.registryDependencies.map((dep) => (
90+
<Link key={dep} to={`/cookbook/${encodeURIComponent(dep)}`}>
91+
<Badge variant="outline" className="cursor-pointer hover:bg-accent">
92+
{dep}
93+
</Badge>
94+
</Link>
95+
))}
96+
</div>
97+
) : (
98+
<p className="text-sm text-muted-foreground">No registry dependencies.</p>
99+
)}
100+
</CardContent>
101+
</Card>
102+
103+
{/* Source Files */}
104+
{item.files && item.files.length > 0 && (
105+
<Card>
106+
<CardHeader className="pb-3">
107+
<CardTitle className="text-base">Source Files</CardTitle>
108+
</CardHeader>
109+
<CardContent>
110+
<ul className="space-y-1">
111+
{item.files.map((file) => (
112+
<li key={file.path} className="flex items-center gap-2 text-sm">
113+
<span className="font-mono text-xs text-muted-foreground">{file.path}</span>
114+
{file.type && (
115+
<Badge variant="secondary" className="text-xs">
116+
{file.type}
117+
</Badge>
118+
)}
119+
</li>
120+
))}
121+
</ul>
122+
</CardContent>
123+
</Card>
124+
)}
125+
126+
{/* Preview Placeholder */}
127+
<Card>
128+
<CardHeader className="pb-3">
129+
<CardTitle className="text-base">Live Preview</CardTitle>
130+
</CardHeader>
131+
<CardContent>
132+
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
133+
<p className="text-sm font-medium text-muted-foreground">Preview not available</p>
134+
<p className="mt-1 text-xs text-muted-foreground">
135+
Live component rendering requires a sandboxed runtime environment.
136+
</p>
137+
</div>
138+
</CardContent>
139+
</Card>
140+
</div>
141+
)
142+
}

frontend/src/features/cookbook/hooks/use-cookbook.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface ComponentMeta {
3333
feature_module?: string
3434
used_by?: string[]
3535
configurable?: boolean
36+
tenant_configurable?: boolean
37+
configurable_props?: string[]
3638
}
3739

3840
export interface CookbookFile {

frontend/src/features/cookbook/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type { CookbookItem, CookbookFile, CookbookRegistry, PatternMeta, Compone
66
export { useCookbook } from './hooks/use-cookbook'
77
export { usePatternFiles } from './hooks/use-pattern-files'
88
export { ManifestViewer } from './components/manifest-viewer'
9+
export { ComponentDetail } from './components/component-detail'
910
export { SagaFlowDiagram } from './components/saga-flow'
1011
export { PreviewSourceTabs } from './components/preview-source-tabs'
1112
export { parseStarlarkSaga } from './lib/star-parser'

frontend/src/features/cookbook/pages/detail.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,10 @@ describe('CookbookDetailPage', () => {
127127
expect(screen.getByRole('tab', { name: 'Composition' })).toBeInTheDocument()
128128
})
129129

130-
it('shows placeholder for UI component type', () => {
130+
it('shows component detail for UI component type', () => {
131131
renderDetail('transaction-table')
132-
expect(screen.getByText(/UI component preview/)).toBeInTheDocument()
132+
expect(screen.getByText('Preview not available')).toBeInTheDocument()
133+
expect(screen.getByText('Registry Dependencies')).toBeInTheDocument()
133134
expect(screen.queryByRole('tab')).not.toBeInTheDocument()
134135
})
135136

frontend/src/features/cookbook/pages/detail.tsx

Lines changed: 115 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { useMemo } from 'react'
1+
import { useState, useMemo } from 'react'
22
import { Link, useParams } from 'react-router-dom'
3+
import { ChevronDown, ChevronRight } from 'lucide-react'
34
import { Badge } from '@/components/ui/badge'
45
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
56
import { Breadcrumbs } from '@/shared/breadcrumbs'
67
import { DetailSkeleton } from '@/shared/detail-skeleton'
8+
import { HandlerReference } from '@/shared/handler-reference'
79
import { StarlarkEditor } from '@/features/sagas/components/starlark-editor'
810
import { ManifestViewer } from '../components/manifest-viewer'
11+
import { ComponentDetail } from '../components/component-detail'
912
import { SagaFlowDiagram } from '../components/saga-flow'
1013
import { PreviewSourceTabs } from '../components/preview-source-tabs'
1114
import { parseStarlarkSaga } from '../lib/star-parser'
@@ -142,6 +145,55 @@ function CompositionSection({ meta }: { meta: PatternMeta }) {
142145
)
143146
}
144147

148+
function HandlerReferencePanel({ starlarkContent }: { starlarkContent: string | null }) {
149+
const [expanded, setExpanded] = useState(false)
150+
151+
const serviceNames = useMemo(() => {
152+
if (!starlarkContent) return []
153+
const flow = parseStarlarkSaga(starlarkContent)
154+
const names = new Set<string>()
155+
for (const step of flow.steps) {
156+
for (const call of step.serviceCalls) {
157+
names.add(call.service)
158+
}
159+
}
160+
return Array.from(names)
161+
}, [starlarkContent])
162+
163+
return (
164+
<div className="rounded-lg border">
165+
<button
166+
type="button"
167+
onClick={() => setExpanded(!expanded)}
168+
className="flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium hover:bg-muted/50"
169+
>
170+
{expanded ? (
171+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
172+
) : (
173+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
174+
)}
175+
Handler Reference
176+
{serviceNames.length > 0 && (
177+
<span className="ml-auto flex gap-1.5">
178+
{serviceNames.map((name) => (
179+
<Badge key={name} variant="secondary" className="text-xs">
180+
{name}
181+
</Badge>
182+
))}
183+
</span>
184+
)}
185+
</button>
186+
{expanded && (
187+
<div className="border-t px-4 py-3">
188+
<HandlerReference
189+
serviceNames={serviceNames.length > 0 ? serviceNames : undefined}
190+
/>
191+
</div>
192+
)}
193+
</div>
194+
)
195+
}
196+
145197
export function CookbookDetailPage() {
146198
const { name } = useParams<{ name: string }>()
147199
const { items, isLoading: catalogueLoading } = useCookbook()
@@ -185,70 +237,70 @@ export function CookbookDetailPage() {
185237
<PatternInfoSection item={item} />
186238

187239
{isPattern ? (
188-
<Tabs defaultValue="manifest">
189-
<TabsList>
190-
<TabsTrigger value="manifest">Manifest</TabsTrigger>
191-
<TabsTrigger value="starlark">Starlark</TabsTrigger>
192-
<TabsTrigger value="flow">Flow</TabsTrigger>
193-
<TabsTrigger value="composition">Composition</TabsTrigger>
194-
</TabsList>
195-
196-
<TabsContent value="manifest" className="mt-4">
197-
{isLoading ? (
198-
<div className="h-[200px] animate-pulse rounded border bg-muted" />
199-
) : manifestContent ? (
200-
<ManifestViewer content={manifestContent} />
201-
) : (
202-
<p className="text-sm text-muted-foreground">No manifest file found.</p>
203-
)}
204-
</TabsContent>
205-
206-
<TabsContent value="starlark" className="mt-4">
207-
{isLoading ? (
208-
<div className="h-[200px] animate-pulse rounded border bg-muted" />
209-
) : starlarkContent ? (
210-
<StarlarkEditor
211-
value={starlarkContent}
212-
onChange={() => {}}
213-
readOnly
214-
/>
215-
) : (
216-
<p className="text-sm text-muted-foreground">No Starlark file found.</p>
217-
)}
218-
</TabsContent>
219-
220-
<TabsContent value="flow" className="mt-4">
221-
{isLoading ? (
222-
<div className="h-[400px] animate-pulse rounded border bg-muted" />
223-
) : sagaFlow && sagaFlow.steps.length > 0 ? (
224-
<PreviewSourceTabs
225-
preview={
226-
<div className="h-[500px] rounded-lg border">
227-
<SagaFlowDiagram flow={sagaFlow} />
228-
</div>
229-
}
230-
source={mermaidMarkup}
231-
sourceLabel="Mermaid"
232-
/>
233-
) : (
234-
<p className="text-sm text-muted-foreground">No saga flow detected in Starlark source.</p>
235-
)}
236-
</TabsContent>
240+
<>
241+
<Tabs defaultValue="manifest">
242+
<TabsList>
243+
<TabsTrigger value="manifest">Manifest</TabsTrigger>
244+
<TabsTrigger value="starlark">Starlark</TabsTrigger>
245+
<TabsTrigger value="flow">Flow</TabsTrigger>
246+
<TabsTrigger value="composition">Composition</TabsTrigger>
247+
</TabsList>
237248

238-
<TabsContent value="composition" className="mt-4">
239-
{meta ? (
240-
<CompositionSection meta={meta} />
241-
) : (
242-
<p className="text-sm text-muted-foreground">No composition metadata available.</p>
243-
)}
244-
</TabsContent>
245-
</Tabs>
249+
<TabsContent value="manifest" className="mt-4">
250+
{isLoading ? (
251+
<div className="h-[200px] animate-pulse rounded border bg-muted" />
252+
) : manifestContent ? (
253+
<ManifestViewer content={manifestContent} />
254+
) : (
255+
<p className="text-sm text-muted-foreground">No manifest file found.</p>
256+
)}
257+
</TabsContent>
258+
259+
<TabsContent value="starlark" className="mt-4">
260+
{isLoading ? (
261+
<div className="h-[200px] animate-pulse rounded border bg-muted" />
262+
) : starlarkContent ? (
263+
<StarlarkEditor
264+
value={starlarkContent}
265+
onChange={() => {}}
266+
readOnly
267+
/>
268+
) : (
269+
<p className="text-sm text-muted-foreground">No Starlark file found.</p>
270+
)}
271+
</TabsContent>
272+
273+
<TabsContent value="flow" className="mt-4">
274+
{isLoading ? (
275+
<div className="h-[400px] animate-pulse rounded border bg-muted" />
276+
) : sagaFlow && sagaFlow.steps.length > 0 ? (
277+
<PreviewSourceTabs
278+
preview={
279+
<div className="h-[500px] rounded-lg border">
280+
<SagaFlowDiagram flow={sagaFlow} />
281+
</div>
282+
}
283+
source={mermaidMarkup}
284+
sourceLabel="Mermaid"
285+
/>
286+
) : (
287+
<p className="text-sm text-muted-foreground">No saga flow detected in Starlark source.</p>
288+
)}
289+
</TabsContent>
290+
291+
<TabsContent value="composition" className="mt-4">
292+
{meta ? (
293+
<CompositionSection meta={meta} />
294+
) : (
295+
<p className="text-sm text-muted-foreground">No composition metadata available.</p>
296+
)}
297+
</TabsContent>
298+
</Tabs>
299+
300+
<HandlerReferencePanel starlarkContent={starlarkContent} />
301+
</>
246302
) : (
247-
<div className="rounded-lg border border-dashed p-8 text-center">
248-
<p className="text-sm text-muted-foreground">
249-
UI component preview will be available in a future update.
250-
</p>
251-
</div>
303+
<ComponentDetail item={item} />
252304
)}
253305
</div>
254306
)

0 commit comments

Comments
 (0)