Skip to content

Commit 6801aa3

Browse files
Merge pull request #8 from NHSDigital/change-to-resolve-schema
Change to resolve schema
2 parents 1411b74 + f233ca0 commit 6801aa3

File tree

9 files changed

+2554
-62
lines changed

9 files changed

+2554
-62
lines changed

.astro/data-store.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.15.3","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false}}"]
1+
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.16.0","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"]

.astro/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"_variables": {
3-
"lastUpdateCheck": 1763653789860
3+
"lastUpdateCheck": 1765300660767
44
}
55
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
// Props passed from MDX
3+
const { definition, src, filePath, id = 'fhir-viewer', title } = Astro.props;
4+
import fs from 'node:fs/promises';
5+
import { existsSync } from 'fs';
6+
import FHIRSchemaViewer from '@components/SchemaExplorer/FHIRSchemaViewer'; // <-- path to your React component
7+
import { resolveProjectPath, getAbsoluteFilePathForAstroFile } from '@utils/files';
8+
9+
let schema;
10+
11+
try {
12+
const schemaPath = getAbsoluteFilePathForAstroFile(filePath, src);
13+
const exists = existsSync(schemaPath);
14+
if (exists) {
15+
schema = await fs.readFile(schemaPath, 'utf-8');
16+
schema = JSON.parse(schema);
17+
if (!schema) {
18+
throw new Error(`<FHIRViewer> needs either a "definition" object or a "src" JSON file path`);
19+
}
20+
}
21+
} catch (error) {
22+
console.log('Failed to process schemas');
23+
console.log(error);
24+
}
25+
---
26+
27+
<div class="my-4">
28+
<FHIRSchemaViewer structureDefinition={schema} title={title} client:only="react" />
29+
</div>

eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ try {
1515
const absoluteFilePath = resolveProjectPath(filePath);
1616
const file = await fs.readFile(absoluteFilePath, 'utf-8');
1717
const schemaViewers = getMDXComponentsByName(file, 'SchemaViewer');
18-
1918
// Loop around all the possible SchemaViewers in the file.
2019
const getAllComponents = schemaViewers.map(async (schemaViewerProps: any, index: number) => {
2120
const schemaPath = getAbsoluteFilePathForAstroFile(filePath, schemaViewerProps.file);

eventcatalog/src/components/MDX/components.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import DrawIO from '@components/MDX/DrawIO/DrawIO.astro';
2727
import FigJam from '@components/MDX/FigJam/FigJam.astro';
2828
import Design from '@components/MDX/Design/Design.astro';
2929
import MermaidFileLoader from '@components/MDX/MermaidFileLoader/MermaidFileLoader.astro';
30+
import FHIRViewer from '@components/MDX/FHIRViewer/FHIRViewer.astro';
3031
// Portals: required for server/client components
3132
import NodeGraphPortal from '@components/MDX/NodeGraph/NodeGraphPortal';
3233
import SchemaViewerPortal from '@components/MDX/SchemaViewer/SchemaViewerPortal';
@@ -66,6 +67,7 @@ const components = (props: any) => {
6667
DrawIO: (mdxProp: any) => jsx(DrawIO, { ...props, ...mdxProp }),
6768
FigJam: (mdxProp: any) => jsx(FigJam, { ...props, ...mdxProp }),
6869
MermaidFileLoader: (mdxProp: any) => jsx(MermaidFileLoader, { ...props, ...mdxProp }),
70+
FHIRViewer: (mdxProp: any) => jsx(FHIRViewer, { ...props, ...mdxProp }),
6971
};
7072
};
7173

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import React, { useMemo } from 'react';
2+
3+
interface FHIRSchemaViewerProps {
4+
structureDefinition: any;
5+
title?: string;
6+
}
7+
8+
interface FhirElementNode {
9+
key: string;
10+
id: string;
11+
path: string;
12+
name: string;
13+
min: number;
14+
max: string;
15+
types: string[];
16+
sliceName?: string;
17+
18+
short?: string;
19+
definition?: string;
20+
comment?: string;
21+
alias?: string[];
22+
binding?: {
23+
strength?: string;
24+
valueSet?: string;
25+
};
26+
mapping?: {
27+
identity: string;
28+
map: string;
29+
}[];
30+
31+
children: FhirElementNode[];
32+
}
33+
/**
34+
* Take snapshot.element[] and transform the flat list
35+
* into a nested tree based on the element paths.
36+
*/
37+
function buildElementTree(elements: any[]): FhirElementNode[] {
38+
const roots: FhirElementNode[] = [];
39+
const stack: { depth: number; node: FhirElementNode }[] = [];
40+
41+
elements.forEach((el: any, index: number) => {
42+
const depth = el.path.split('.').length;
43+
44+
const node: FhirElementNode = {
45+
key: `${index}`,
46+
id: el.id,
47+
path: el.path,
48+
name: el.name,
49+
min: el.min ?? 0,
50+
max: el.max ?? '1',
51+
types: Array.isArray(el.type) ? el.type.map((t: any) => t.code) : [],
52+
sliceName: el.sliceName,
53+
54+
short: el.short,
55+
definition: el.definition,
56+
comment: el.comment,
57+
alias: el.alias,
58+
binding: el.binding
59+
? {
60+
strength: el.binding.strength,
61+
valueSet: el.binding.valueSet,
62+
}
63+
: undefined,
64+
mapping: el.mapping,
65+
66+
children: [],
67+
};
68+
69+
// Pop stack until we find the correct parent depth
70+
while (stack.length && stack[stack.length - 1].depth >= depth) {
71+
stack.pop();
72+
}
73+
74+
if (stack.length === 0) {
75+
roots.push(node);
76+
} else {
77+
stack[stack.length - 1].node.children.push(node);
78+
}
79+
80+
stack.push({ depth, node });
81+
});
82+
83+
return roots;
84+
}
85+
/**
86+
* Simple recursive UI renderer for FHIR element tree.
87+
*/
88+
import { useState } from 'react';
89+
90+
function ElementDetails({ node }: { node: FhirElementNode }) {
91+
return (
92+
<div
93+
style={{
94+
marginTop: 6,
95+
padding: '10px 14px',
96+
background: '#f9fafb',
97+
borderLeft: '3px solid #3b82f6',
98+
fontSize: '0.85em',
99+
borderRadius: 4,
100+
}}
101+
>
102+
{node.short && (
103+
<div>
104+
<strong>Short description</strong>
105+
<br />
106+
{node.short}
107+
</div>
108+
)}
109+
110+
{node.alias?.length && (
111+
<div style={{ marginTop: 6 }}>
112+
<strong>Alternate names</strong>
113+
<br />
114+
{node.alias.join(', ')}
115+
</div>
116+
)}
117+
118+
{node.definition && (
119+
<div style={{ marginTop: 6 }}>
120+
<strong>Definition</strong>
121+
<br />
122+
{node.definition}
123+
</div>
124+
)}
125+
126+
{node.comment && (
127+
<div style={{ marginTop: 6 }}>
128+
<strong>Comments</strong>
129+
<br />
130+
{node.comment}
131+
</div>
132+
)}
133+
134+
{node.binding && (
135+
<div style={{ marginTop: 6 }}>
136+
<strong>Binding</strong>
137+
<br />
138+
{node.binding.strength}{' '}
139+
<a href={node.binding.valueSet} target="_blank" rel="noreferrer">
140+
{node.binding.valueSet}
141+
</a>
142+
</div>
143+
)}
144+
145+
{node.mapping?.length && (
146+
<div style={{ marginTop: 6 }}>
147+
<strong>Mappings</strong>
148+
<ul style={{ marginLeft: 16 }}>
149+
{node.mapping.map((m, i) => (
150+
<li key={i}>
151+
<strong>{m.identity}:</strong> {m.map}
152+
</li>
153+
))}
154+
</ul>
155+
</div>
156+
)}
157+
</div>
158+
);
159+
}
160+
161+
function ElementNodeView({ node, depth }: { node: FhirElementNode; depth: number }) {
162+
const [expanded, setExpanded] = useState(depth === 0);
163+
const [selected, setSelected] = useState(false);
164+
165+
const hasChildren = node.children.length > 0;
166+
167+
return (
168+
<div style={{ marginLeft: depth * 18 }}>
169+
<div
170+
style={{
171+
display: 'flex',
172+
alignItems: 'center',
173+
cursor: 'pointer',
174+
lineHeight: '1.6em',
175+
padding: '2px 4px',
176+
borderRadius: 4,
177+
}}
178+
onMouseEnter={(e) => {
179+
e.currentTarget.style.background = '#f3f4f6';
180+
}}
181+
onMouseLeave={(e) => {
182+
e.currentTarget.style.background = 'transparent';
183+
}}
184+
onClick={(e) => {
185+
e.stopPropagation(); // 🔑 critical
186+
setSelected(!selected);
187+
}}
188+
>
189+
{hasChildren ? (
190+
<span
191+
onClick={(e) => {
192+
e.stopPropagation();
193+
setExpanded(!expanded);
194+
}}
195+
style={{ width: 14, display: 'inline-block', fontSize: 12 }}
196+
>
197+
{expanded ? '▼' : '▶'}
198+
</span>
199+
) : (
200+
<span style={{ width: 14 }} />
201+
)}
202+
203+
<span style={{ fontWeight: depth === 0 ? 'bold' : 'normal' }}>{node.path}</span>
204+
205+
<span style={{ opacity: 0.6, marginLeft: 6 }}>
206+
({node.min}..{node.max})
207+
</span>
208+
209+
{node.types.length > 0 && (
210+
<span style={{ marginLeft: 8 }}>
211+
:
212+
{node.types.map((t, i) => (
213+
<a
214+
key={t}
215+
href={`/fhir/datatypes/${t}`}
216+
style={{
217+
marginLeft: 4,
218+
color: '#2563eb',
219+
textDecoration: 'none',
220+
fontSize: '0.9em',
221+
}}
222+
>
223+
{t}
224+
{i < node.types.length - 1 ? ' |' : ''}
225+
</a>
226+
))}
227+
</span>
228+
)}
229+
</div>
230+
231+
{selected && <ElementDetails node={node} />}
232+
233+
{expanded && node.children.map((child) => <ElementNodeView key={child.key} node={child} depth={depth + 1} />)}
234+
</div>
235+
);
236+
}
237+
/**
238+
* Main viewer component.
239+
*/
240+
export default function FHIRSchemaViewer({ structureDefinition, title }: FHIRSchemaViewerProps) {
241+
if (!structureDefinition) {
242+
return <div>No StructureDefinition provided.</div>;
243+
}
244+
245+
return (
246+
<div style={{ padding: '1rem', fontFamily: 'Arial' }}>
247+
<h3 style={{ marginBottom: '0.5rem' }}>{title ?? structureDefinition.title ?? structureDefinition.name}</h3>
248+
249+
<div style={{ fontSize: '0.9rem', color: '#555', marginBottom: '1rem' }}>{structureDefinition.description}</div>
250+
251+
{/* Render root elements */}
252+
{buildElementTree(structureDefinition.snapshot.element).map((rootNode) => (
253+
<ElementNodeView key={rootNode.path} node={rootNode} depth={0} />
254+
))}
255+
</div>
256+
);
257+
}

0 commit comments

Comments
 (0)