Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .astro/data-store.json
Original file line number Diff line number Diff line change
@@ -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}}"]
[["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}}"]
2 changes: 1 addition & 1 deletion .astro/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1763653789860
"lastUpdateCheck": 1765300660767
}
}
29 changes: 29 additions & 0 deletions eventcatalog/src/components/MDX/FHIRViewer/FHIRViewer.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
// Props passed from MDX
const { definition, src, filePath, id = 'fhir-viewer', title } = Astro.props;
import fs from 'node:fs/promises';
import { existsSync } from 'fs';
import FHIRSchemaViewer from '@components/SchemaExplorer/FHIRSchemaViewer'; // <-- path to your React component
import { resolveProjectPath, getAbsoluteFilePathForAstroFile } from '@utils/files';

let schema;

try {
const schemaPath = getAbsoluteFilePathForAstroFile(filePath, src);
const exists = existsSync(schemaPath);
if (exists) {
schema = await fs.readFile(schemaPath, 'utf-8');
schema = JSON.parse(schema);
if (!schema) {
throw new Error(`<FHIRViewer> needs either a "definition" object or a "src" JSON file path`);
}
}
} catch (error) {
console.log('Failed to process schemas');
console.log(error);
}
---

<div class="my-4">
<FHIRSchemaViewer structureDefinition={schema} title={title} client:only="react" />
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ try {
const absoluteFilePath = resolveProjectPath(filePath);
const file = await fs.readFile(absoluteFilePath, 'utf-8');
const schemaViewers = getMDXComponentsByName(file, 'SchemaViewer');

// Loop around all the possible SchemaViewers in the file.
const getAllComponents = schemaViewers.map(async (schemaViewerProps: any, index: number) => {
const schemaPath = getAbsoluteFilePathForAstroFile(filePath, schemaViewerProps.file);
Expand Down
2 changes: 2 additions & 0 deletions eventcatalog/src/components/MDX/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import DrawIO from '@components/MDX/DrawIO/DrawIO.astro';
import FigJam from '@components/MDX/FigJam/FigJam.astro';
import Design from '@components/MDX/Design/Design.astro';
import MermaidFileLoader from '@components/MDX/MermaidFileLoader/MermaidFileLoader.astro';
import FHIRViewer from '@components/MDX/FHIRViewer/FHIRViewer.astro';
// Portals: required for server/client components
import NodeGraphPortal from '@components/MDX/NodeGraph/NodeGraphPortal';
import SchemaViewerPortal from '@components/MDX/SchemaViewer/SchemaViewerPortal';
Expand Down Expand Up @@ -66,6 +67,7 @@ const components = (props: any) => {
DrawIO: (mdxProp: any) => jsx(DrawIO, { ...props, ...mdxProp }),
FigJam: (mdxProp: any) => jsx(FigJam, { ...props, ...mdxProp }),
MermaidFileLoader: (mdxProp: any) => jsx(MermaidFileLoader, { ...props, ...mdxProp }),
FHIRViewer: (mdxProp: any) => jsx(FHIRViewer, { ...props, ...mdxProp }),
};
};

Expand Down
257 changes: 257 additions & 0 deletions eventcatalog/src/components/SchemaExplorer/FHIRSchemaViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import React, { useMemo } from 'react';

interface FHIRSchemaViewerProps {
structureDefinition: any;
title?: string;
}

interface FhirElementNode {
key: string;
id: string;
path: string;
name: string;
min: number;
max: string;
types: string[];
sliceName?: string;

short?: string;
definition?: string;
comment?: string;
alias?: string[];
binding?: {
strength?: string;
valueSet?: string;
};
mapping?: {
identity: string;
map: string;
}[];

children: FhirElementNode[];
}
/**
* Take snapshot.element[] and transform the flat list
* into a nested tree based on the element paths.
*/
function buildElementTree(elements: any[]): FhirElementNode[] {
const roots: FhirElementNode[] = [];
const stack: { depth: number; node: FhirElementNode }[] = [];

elements.forEach((el: any, index: number) => {
const depth = el.path.split('.').length;

const node: FhirElementNode = {
key: `${index}`,
id: el.id,
path: el.path,
name: el.name,
min: el.min ?? 0,
max: el.max ?? '1',
types: Array.isArray(el.type) ? el.type.map((t: any) => t.code) : [],
sliceName: el.sliceName,

short: el.short,
definition: el.definition,
comment: el.comment,
alias: el.alias,
binding: el.binding
? {
strength: el.binding.strength,
valueSet: el.binding.valueSet,
}
: undefined,
mapping: el.mapping,

children: [],
};

// Pop stack until we find the correct parent depth
while (stack.length && stack[stack.length - 1].depth >= depth) {
stack.pop();
}

if (stack.length === 0) {
roots.push(node);
} else {
stack[stack.length - 1].node.children.push(node);
}

stack.push({ depth, node });
});

return roots;
}
/**
* Simple recursive UI renderer for FHIR element tree.
*/
import { useState } from 'react';

function ElementDetails({ node }: { node: FhirElementNode }) {
return (
<div
style={{
marginTop: 6,
padding: '10px 14px',
background: '#f9fafb',
borderLeft: '3px solid #3b82f6',
fontSize: '0.85em',
borderRadius: 4,
}}
>
{node.short && (
<div>
<strong>Short description</strong>
<br />
{node.short}
</div>
)}

{node.alias?.length && (
<div style={{ marginTop: 6 }}>
<strong>Alternate names</strong>
<br />
{node.alias.join(', ')}
</div>
)}

{node.definition && (
<div style={{ marginTop: 6 }}>
<strong>Definition</strong>
<br />
{node.definition}
</div>
)}

{node.comment && (
<div style={{ marginTop: 6 }}>
<strong>Comments</strong>
<br />
{node.comment}
</div>
)}

{node.binding && (
<div style={{ marginTop: 6 }}>
<strong>Binding</strong>
<br />
{node.binding.strength} —{' '}
<a href={node.binding.valueSet} target="_blank" rel="noreferrer">
{node.binding.valueSet}
</a>
</div>
)}

{node.mapping?.length && (
<div style={{ marginTop: 6 }}>
<strong>Mappings</strong>
<ul style={{ marginLeft: 16 }}>
{node.mapping.map((m, i) => (
<li key={i}>
<strong>{m.identity}:</strong> {m.map}
</li>
))}
</ul>
</div>
)}
</div>
);
}

function ElementNodeView({ node, depth }: { node: FhirElementNode; depth: number }) {
const [expanded, setExpanded] = useState(depth === 0);
const [selected, setSelected] = useState(false);

const hasChildren = node.children.length > 0;

return (
<div style={{ marginLeft: depth * 18 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
lineHeight: '1.6em',
padding: '2px 4px',
borderRadius: 4,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f3f4f6';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
onClick={(e) => {
e.stopPropagation(); // 🔑 critical
setSelected(!selected);
}}
>
{hasChildren ? (
<span
onClick={(e) => {
e.stopPropagation();
setExpanded(!expanded);
}}
style={{ width: 14, display: 'inline-block', fontSize: 12 }}
>
{expanded ? '▼' : '▶'}
</span>
) : (
<span style={{ width: 14 }} />
)}

<span style={{ fontWeight: depth === 0 ? 'bold' : 'normal' }}>{node.path}</span>

<span style={{ opacity: 0.6, marginLeft: 6 }}>
({node.min}..{node.max})
</span>

{node.types.length > 0 && (
<span style={{ marginLeft: 8 }}>
:
{node.types.map((t, i) => (
<a
key={t}
href={`/fhir/datatypes/${t}`}
style={{
marginLeft: 4,
color: '#2563eb',
textDecoration: 'none',
fontSize: '0.9em',
}}
>
{t}
{i < node.types.length - 1 ? ' |' : ''}
</a>
))}
</span>
)}
</div>

{selected && <ElementDetails node={node} />}

{expanded && node.children.map((child) => <ElementNodeView key={child.key} node={child} depth={depth + 1} />)}
</div>
);
}
/**
* Main viewer component.
*/
export default function FHIRSchemaViewer({ structureDefinition, title }: FHIRSchemaViewerProps) {
if (!structureDefinition) {
return <div>No StructureDefinition provided.</div>;
}

return (
<div style={{ padding: '1rem', fontFamily: 'Arial' }}>
<h3 style={{ marginBottom: '0.5rem' }}>{title ?? structureDefinition.title ?? structureDefinition.name}</h3>

<div style={{ fontSize: '0.9rem', color: '#555', marginBottom: '1rem' }}>{structureDefinition.description}</div>

{/* Render root elements */}
{buildElementTree(structureDefinition.snapshot.element).map((rootNode) => (
<ElementNodeView key={rootNode.path} node={rootNode} depth={0} />
))}
</div>
);
}
Loading