Skip to content

Commit 8b11f8a

Browse files
authored
Merge pull request #41 from ainetx/feature/schema-examples-display
Add schema examples preview feature
2 parents 1901c9c + 5260f78 commit 8b11f8a

10 files changed

Lines changed: 324 additions & 31 deletions

apps/web/src/components/SchemaDiagram.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,9 @@ export const SchemaDiagram = forwardRef<SchemaDiagramHandle, SchemaDiagramProps>
529529
const expanded = !!sn.expansion?.expanded
530530
const sections = sn.expansion?.sections || {}
531531
const rawView = (sn.extra?.rawView as boolean | undefined) ?? model.rawView
532+
const showExamples = (sn.extra?.showExamples as boolean | undefined) ?? model.showExamples
532533

533-
model.initLayout({ x: sn.position.x, y: sn.position.y }, expanded, rawView ?? false, sections)
534+
model.initLayout({ x: sn.position.x, y: sn.position.y }, expanded, rawView ?? false, sections, showExamples)
534535

535536
return {
536537
...n,

apps/web/src/components/SchemaNodeModel.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ export class SchemaNodeModel {
1111
position: Position
1212
expanded: boolean
1313
rawView: boolean
14+
showExamples: boolean
1415
sections: Record<string, boolean>
1516
properties?: any[]
17+
examplesProperties?: any[]
1618
edges: SchemaEdgeModel[]
1719
isMaximized: boolean
1820
snapshotChecked: boolean
1921

2022
// Dirtiness detection
2123
origExpanded: boolean
2224
origRawView: boolean
25+
origShowExamples: boolean
2326
origPosition: Position
2427
origSections: Record<string, boolean>
2528

@@ -28,6 +31,7 @@ export class SchemaNodeModel {
2831
position?: Position
2932
expanded?: boolean
3033
rawView?: boolean
34+
showExamples?: boolean
3135
sections?: Record<string, boolean>
3236
isMaximized?: boolean
3337
}) {
@@ -36,32 +40,48 @@ export class SchemaNodeModel {
3640
this.position = params.position ? { x: params.position.x, y: params.position.y } : { x: 0, y: 0 }
3741
this.expanded = params.expanded ?? true
3842
this.rawView = params.rawView ?? false
43+
this.showExamples = params.showExamples ?? false
3944
this.sections = params.sections || {}
4045
this.edges = []
4146
this.isMaximized = params.isMaximized ?? false
4247
this.snapshotChecked = false
4348
this.properties = params.entity.isSchema
4449
? parseSchemaToProperties(params.entity.content)
4550
: parseJsonToProperties(params.entity.content)
51+
52+
// Extract examples from schema if available
53+
if (params.entity.isSchema && params.entity.content?.examples) {
54+
const examples = params.entity.content.examples
55+
if (Array.isArray(examples) && examples.length > 0) {
56+
this.examplesProperties = examples.map((example: any, index: number) => ({
57+
name: `Example ${index + 1}`,
58+
type: 'object',
59+
children: parseJsonToProperties(example, `example-${index}`)
60+
}))
61+
}
62+
}
4663

4764
// Dirtiness calculation assistance
4865
this.origExpanded = params.expanded ?? true
4966
this.origRawView = params.rawView ?? false
67+
this.origShowExamples = params.showExamples ?? false
5068
this.origPosition = params.position ? { x: params.position.x, y: params.position.y } : { x: 0, y: 0 }
5169
this.origSections = params.sections || {}
5270
debug.node("constructor node {" + this.entity.id + "} constructor", this.position.x, this.position.y, "orig:", this.origPosition.x, this.origPosition.y)
5371
}
5472

5573
// apply layout from diagram storage
56-
initLayout(position: Position, expanded: boolean, rawView: boolean, sections: Record<string, boolean>) {
74+
initLayout(position: Position, expanded: boolean, rawView: boolean, sections: Record<string, boolean>, showExamples?: boolean) {
5775
// clone to avoid shared references
5876
this.position = { x: position.x, y: position.y }
5977
this.expanded = expanded
6078
this.rawView = rawView
79+
this.showExamples = showExamples ?? false
6180
this.sections = { ...sections }
6281
this.snapshotChecked = true
6382
this.origExpanded = expanded
6483
this.origRawView = rawView
84+
this.origShowExamples = showExamples ?? false
6585
this.origPosition = { x: position.x, y: position.y }
6686
this.origSections = { ...sections }
6787
debug.node("initLayout node {" + this.entity.id + "} applyLayoutFromSnapshot", position.x, position.y, "orig:", this.origPosition.x, this.origPosition.y)
@@ -76,6 +96,7 @@ export class SchemaNodeModel {
7696

7797
isDirty(): boolean {
7898
if (this.rawView !== this.origRawView) return true
99+
if (this.showExamples !== this.origShowExamples) return true
79100
if (this.position.x !== this.origPosition.x || this.position.y !== this.origPosition.y) return true
80101
if (this.expanded !== this.origExpanded) return true
81102

@@ -103,6 +124,7 @@ export class SchemaNodeModel {
103124
resetDirtyBaseline() {
104125
this.origExpanded = this.expanded
105126
this.origRawView = this.rawView
127+
this.origShowExamples = this.showExamples
106128
this.origPosition = { x: this.position.x, y: this.position.y }
107129
this.origSections = { ...this.sections }
108130
debug.node("resetDirtyBaseline node {" + this.entity.id + "} resetDirtyBaseline", this.origRawView, this.origPosition.x, this.origPosition.y, this.origSections)
@@ -120,7 +142,7 @@ export class SchemaNodeModel {
120142
expanded: this.expanded,
121143
sections: this.sections,
122144
},
123-
extra: { rawView: this.rawView },
145+
extra: { rawView: this.rawView, showExamples: this.showExamples },
124146
}
125147
}
126148
}

apps/web/src/components/SchemaNodeView.tsx

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, createRef } from 'react'
22
import { createPortal } from 'react-dom'
33
import { Handle, Position, type NodeProps } from 'reactflow'
4-
import { ChevronDown, ChevronUp, CheckCircle, AlertCircle, Code2, List, X } from 'lucide-react'
4+
import { ChevronDown, ChevronUp, CheckCircle, AlertCircle, Code2, List, X, FileJson } from 'lucide-react'
55
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
66
import { Button } from '@/components/ui/button'
77
import { PropertyViewer } from './PropertyViewer'
@@ -125,14 +125,20 @@ export class SchemaNodeView extends Component<NodeProps<any>, {}> {
125125
const next = !diagramRegistry.getViewState().globalRawViewPreference
126126
this.onMaximizeRawJson?.(next)
127127
} else {
128-
// notify diagram (e.g., to mark dirty)
129128
const next = !this.model.rawView
130129
this.model.rawView = next
131130
}
132131
this.onNodeChange?.()
133132
this.forceUpdate()
134133
}
135134

135+
private handleToggleShowExamples = () => {
136+
const next = !this.model.showExamples
137+
this.model.showExamples = next
138+
this.onNodeChange?.()
139+
this.forceUpdate()
140+
}
141+
136142
private findPropertyInJson(code: string, instancePath: string, rootObj?: any): { lineStart: number; lineEnd: number; charStart: number; charEnd: number } | null {
137143
// Parse instancePath like "/retention2" or "/nested/property"
138144
if (!instancePath || instancePath === '/') return null
@@ -444,15 +450,28 @@ export class SchemaNodeView extends Component<NodeProps<any>, {}> {
444450
</Popup>
445451
)}
446452
</div>
447-
<Button
448-
variant="ghost"
449-
size="sm"
450-
className="h-6 px-2 bg-gray-200"
451-
onClick={this.handleToggleRawView}
452-
title={rawView ? 'Switch to formatted view' : 'Switch to raw JSON'}
453-
>
454-
{rawView ? <List className="h-3.5 w-3.5" /> : <Code2 className="h-3.5 w-3.5" />}
455-
</Button>
453+
<div className="flex gap-1">
454+
{this.model?.examplesProperties && this.model.examplesProperties.length > 0 && !rawView && (
455+
<Button
456+
variant="ghost"
457+
size="sm"
458+
className="h-6 px-2 bg-gray-200"
459+
onClick={this.handleToggleShowExamples}
460+
title={this.model.showExamples ? 'Show schema' : 'Show examples'}
461+
>
462+
<FileJson className="h-3.5 w-3.5" />
463+
</Button>
464+
)}
465+
<Button
466+
variant="ghost"
467+
size="sm"
468+
className="h-6 px-2 bg-gray-200"
469+
onClick={this.handleToggleRawView}
470+
title={rawView ? 'Switch to formatted view' : 'Switch to raw JSON'}
471+
>
472+
{rawView ? <List className="h-3.5 w-3.5" /> : <Code2 className="h-3.5 w-3.5" />}
473+
</Button>
474+
</div>
456475
</div>
457476
)}
458477
{(this.model?.entity?.validation || d.entity?.validation) && (this.model?.entity?.validation || d.entity?.validation).errors.length > 0 && (
@@ -473,7 +492,7 @@ export class SchemaNodeView extends Component<NodeProps<any>, {}> {
473492
{this.renderCodeWithErrors(JSON.stringify((this.model?.entity?.content ?? d.entity?.content), null, 2))}
474493
</div>
475494
) : (
476-
this.model?.properties && (
495+
(this.model?.showExamples && this.model?.examplesProperties ? this.model.examplesProperties : this.model?.properties) && (
477496
<div
478497
className="max-h-96 overflow-y-auto overflow-x-hidden pr-2"
479498
onWheel={(e) => {
@@ -495,7 +514,7 @@ export class SchemaNodeView extends Component<NodeProps<any>, {}> {
495514
}}
496515
>
497516
<PropertyViewer
498-
properties={this.model.properties}
517+
properties={(this.model.showExamples && this.model.examplesProperties ? this.model.examplesProperties : this.model.properties) || []}
499518
sectionStates={sectionStates}
500519
onToggleSection={this.handleToggleSection}
501520
validationErrors={this.model?.entity?.validation?.errors}
@@ -552,15 +571,28 @@ export class SchemaNodeView extends Component<NodeProps<any>, {}> {
552571
) : (
553572
<span className="truncate cursor-default overflow-hidden">{this.model?.entity?.file?.name}</span>
554573
)}
555-
<Button
556-
variant="ghost"
557-
size="sm"
558-
className="h-6 px-2 bg-gray-200"
559-
onClick={(e) => { e.stopPropagation(); this.handleToggleRawView() }}
560-
title={(this.model?.rawView ? true : false) ? 'Switch to formatted view' : 'Switch to raw JSON'}
561-
>
562-
{(this.model?.rawView ? true : false) ? <List className="h-3.5 w-3.5" /> : <Code2 className="h-3.5 w-3.5" />}
563-
</Button>
574+
<div className="flex gap-1">
575+
{this.model?.examplesProperties && this.model.examplesProperties.length > 0 && !rawView && (
576+
<Button
577+
variant="ghost"
578+
size="sm"
579+
className="h-6 px-2 bg-gray-200"
580+
onClick={(e) => { e.stopPropagation(); this.handleToggleShowExamples() }}
581+
title={this.model.showExamples ? 'Show schema' : 'Show examples'}
582+
>
583+
<FileJson className="h-3.5 w-3.5" />
584+
</Button>
585+
)}
586+
<Button
587+
variant="ghost"
588+
size="sm"
589+
className="h-6 px-2 bg-gray-200"
590+
onClick={(e) => { e.stopPropagation(); this.handleToggleRawView() }}
591+
title={(this.model?.rawView ? true : false) ? 'Switch to formatted view' : 'Switch to raw JSON'}
592+
>
593+
{(this.model?.rawView ? true : false) ? <List className="h-3.5 w-3.5" /> : <Code2 className="h-3.5 w-3.5" />}
594+
</Button>
595+
</div>
564596
</div>
565597
)}
566598
{this.model?.entity?.validation && this.model.entity.validation.errors.length > 0 && (
@@ -581,10 +613,10 @@ export class SchemaNodeView extends Component<NodeProps<any>, {}> {
581613
{this.renderCodeWithErrors(JSON.stringify(this.model?.entity?.content, null, 2))}
582614
</div>
583615
) : (
584-
this.model?.properties && (
616+
(this.model?.showExamples && this.model?.examplesProperties ? this.model.examplesProperties : this.model?.properties) && (
585617
<div className="h-[calc(100%-2rem)] overflow-y-auto overflow-x-hidden pr-2">
586618
<PropertyViewer
587-
properties={this.model.properties}
619+
properties={(this.model.showExamples && this.model.examplesProperties ? this.model.examplesProperties : this.model.properties) || []}
588620
sectionStates={this.model.sections}
589621
onToggleSection={this.handleToggleSection}
590622
validationErrors={this.model?.entity?.validation?.errors}

examples/events/schemas/gts.x.commerce.orders.order.v1.0~.schema.json

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,5 +260,78 @@
260260
},
261261
"required": ["firstName", "lastName", "addressLine1", "city", "state", "postalCode", "country"]
262262
}
263-
}
263+
},
264+
"examples": [
265+
{
266+
"id": "550e8400-e29b-41d4-a716-446655440000",
267+
"orderNumber": "ORD-2024-001234",
268+
"status": "confirmed",
269+
"customerId": "660e8400-e29b-41d4-a716-446655440001",
270+
"customerEmail": "john.doe@example.com",
271+
"subtotal": 149.97,
272+
"taxAmount": 14.50,
273+
"discountAmount": 15.00,
274+
"totalAmount": 159.47,
275+
"currency": "USD",
276+
"items": [
277+
{
278+
"id": "770e8400-e29b-41d4-a716-446655440010",
279+
"productId": "880e8400-e29b-41d4-a716-446655440011",
280+
"productName": "Wireless Mouse",
281+
"productSku": "WM-2024-BLK",
282+
"quantity": 2,
283+
"unitPrice": 29.99,
284+
"totalPrice": 59.98,
285+
"discountAmount": 0,
286+
"taxAmount": 5.80
287+
},
288+
{
289+
"id": "770e8400-e29b-41d4-a716-446655440012",
290+
"productId": "880e8400-e29b-41d4-a716-446655440013",
291+
"productName": "USB-C Cable",
292+
"productSku": "USBC-2M-WHT",
293+
"quantity": 3,
294+
"unitPrice": 19.99,
295+
"totalPrice": 59.97,
296+
"discountAmount": 5.00,
297+
"taxAmount": 5.30,
298+
"metadata": {
299+
"length": "2m",
300+
"color": "white"
301+
}
302+
}
303+
],
304+
"shippingAddress": {
305+
"id": "990e8400-e29b-41d4-a716-446655440020",
306+
"firstName": "John",
307+
"lastName": "Doe",
308+
"addressLine1": "123 Main Street",
309+
"addressLine2": "Apt 4B",
310+
"city": "New York",
311+
"state": "NY",
312+
"postalCode": "10001",
313+
"country": "US",
314+
"phone": "+1-555-123-4567"
315+
},
316+
"billingAddress": {
317+
"id": "990e8400-e29b-41d4-a716-446655440021",
318+
"firstName": "John",
319+
"lastName": "Doe",
320+
"addressLine1": "123 Main Street",
321+
"city": "New York",
322+
"state": "NY",
323+
"postalCode": "10001",
324+
"country": "US",
325+
"phone": "+1-555-123-4567"
326+
},
327+
"shippingMethod": "standard",
328+
"shippingCost": 10.00,
329+
"paymentMethod": "credit_card",
330+
"paymentStatus": "completed",
331+
"paymentReference": "ch_1234567890abcdef",
332+
"tags": ["priority", "new-customer"],
333+
"createdAt": "2024-01-15T10:30:00Z",
334+
"updatedAt": "2024-01-15T10:35:00Z"
335+
}
336+
]
264337
}

examples/events/schemas/gts.x.core.events.topic.v1~.schema.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,42 @@
7171
},
7272
"tags": { "type": "array", "items": { "type": "string" } }
7373
},
74-
"additionalProperties": false
74+
"additionalProperties": false,
75+
"examples": [
76+
{
77+
"id": "gts://gts.x.core.events.topic.v1~x.core.commerce.orders.v1~",
78+
"$schema": "gts://gts.x.core.events.topic.v1~",
79+
"name": "Orders Topic",
80+
"description": "Stream for all order-related events",
81+
"retention": "P90D",
82+
"ordering": "by-partition-key",
83+
"partitions": 16,
84+
"partitionKeyPath": "/payload/orderId",
85+
"dedup": {
86+
"strategy": "by-key",
87+
"keyPaths": ["/id"]
88+
},
89+
"storage": {
90+
"kind": "db",
91+
"config": {
92+
"table": "events_orders"
93+
}
94+
},
95+
"tags": ["commerce", "orders"]
96+
},
97+
{
98+
"id": "gts://gts.x.core.events.topic.v1~x.core.idp.contacts.v1~",
99+
"$schema": "gts://gts.x.core.events.topic.v1~",
100+
"name": "Contacts Topic",
101+
"description": "Stream for contact lifecycle events",
102+
"retention": "P365D",
103+
"ordering": "global",
104+
"dedup": {
105+
"strategy": "time-window",
106+
"window": "PT5M",
107+
"keyPaths": ["/id", "/type"]
108+
},
109+
"tags": ["idp", "contacts"]
110+
}
111+
]
75112
}

0 commit comments

Comments
 (0)