Add comprehensive slot visualization to LinkML viewer
- Add standalone Slots section in visual view alongside Classes and Enums - Display slot_uri, range, identifier badge, description, pattern - Show examples with value/description pairs - Color-coded SKOS mapping tags (exact/close/narrow/broad/related) - Yellow highlighted comments section - Custodian type filtering works with slots - Shared renderSlotDetails() function for consistency
This commit is contained in:
parent
f8421a2903
commit
81da4ede50
4 changed files with 298 additions and 0 deletions
|
|
@ -18,6 +18,11 @@ function debugLog(...args: unknown[]): void {
|
|||
}
|
||||
}
|
||||
|
||||
export interface SlotExample {
|
||||
value: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SlotDefinition {
|
||||
name: string;
|
||||
description?: string;
|
||||
|
|
@ -29,6 +34,7 @@ export interface SlotDefinition {
|
|||
identifier?: boolean;
|
||||
comments?: string[];
|
||||
annotations?: Record<string, unknown>;
|
||||
examples?: SlotExample[];
|
||||
// SKOS semantic mappings for ontology alignment
|
||||
exact_mappings?: string[];
|
||||
close_mappings?: string[];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export interface SlotExample {
|
||||
value: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface LinkMLSlot {
|
||||
name: string;
|
||||
description?: string;
|
||||
|
|
@ -14,6 +19,15 @@ export interface LinkMLSlot {
|
|||
multivalued?: boolean;
|
||||
slot_uri?: string;
|
||||
pattern?: string;
|
||||
identifier?: boolean;
|
||||
comments?: string[];
|
||||
examples?: SlotExample[];
|
||||
// SKOS semantic mappings for ontology alignment
|
||||
exact_mappings?: string[];
|
||||
close_mappings?: string[];
|
||||
narrow_mappings?: string[];
|
||||
broad_mappings?: string[];
|
||||
related_mappings?: string[];
|
||||
}
|
||||
|
||||
export interface LinkMLClass {
|
||||
|
|
|
|||
|
|
@ -1617,6 +1617,162 @@
|
|||
color: var(--info-dark, #1565c0);
|
||||
}
|
||||
|
||||
/* Additional mapping type styles for SKOS semantic mappings */
|
||||
.linkml-viewer__tag--exact {
|
||||
background: var(--success-light, #e8f5e9);
|
||||
border-color: var(--success-color, #4caf50);
|
||||
color: var(--success-dark, #2e7d32);
|
||||
}
|
||||
|
||||
.linkml-viewer__tag--narrow {
|
||||
background: #fff3e0;
|
||||
border-color: #ff9800;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.linkml-viewer__tag--broad {
|
||||
background: #fce4ec;
|
||||
border-color: #e91e63;
|
||||
color: #ad1457;
|
||||
}
|
||||
|
||||
.linkml-viewer__tag--related {
|
||||
background: #f3e5f5;
|
||||
border-color: #9c27b0;
|
||||
color: #6a1b9a;
|
||||
}
|
||||
|
||||
/* Identifier badge */
|
||||
.linkml-viewer__badge--identifier {
|
||||
background: var(--primary-color, #1976d2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Slot URI display */
|
||||
.linkml-viewer__slot-uri {
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.linkml-viewer__uri-value {
|
||||
background: var(--surface-secondary, #f5f5f5);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary-color, #1976d2);
|
||||
}
|
||||
|
||||
/* Examples section */
|
||||
.linkml-viewer__examples {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.linkml-viewer__examples-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.linkml-viewer__example-item {
|
||||
padding: 0.375rem 0.5rem;
|
||||
margin: 0.25rem 0;
|
||||
background: var(--surface-secondary, #f5f5f5);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--primary-color, #1976d2);
|
||||
}
|
||||
|
||||
.linkml-viewer__example-value {
|
||||
background: white;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
color: var(--primary-dark, #0d47a1);
|
||||
}
|
||||
|
||||
.linkml-viewer__example-description {
|
||||
color: var(--text-secondary, #616161);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Comments section */
|
||||
.linkml-viewer__comments {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.linkml-viewer__comments-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.linkml-viewer__comment-item {
|
||||
padding: 0.5rem;
|
||||
margin: 0.25rem 0;
|
||||
background: var(--warning-light, #fff8e1);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--warning-color, #ffc107);
|
||||
color: var(--text-primary, #212121);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Dark mode support for new slot elements */
|
||||
[data-theme="dark"] .linkml-viewer__uri-value {
|
||||
background: #374151;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__example-item {
|
||||
background: #374151;
|
||||
border-left-color: #60a5fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__example-value {
|
||||
background: #1f2937;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__example-description {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__comment-item {
|
||||
background: #422006;
|
||||
border-left-color: #f59e0b;
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__tag--exact {
|
||||
background: #064e3b;
|
||||
border-color: #10b981;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__tag--narrow {
|
||||
background: #451a03;
|
||||
border-color: #f59e0b;
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__tag--broad {
|
||||
background: #500724;
|
||||
border-color: #ec4899;
|
||||
color: #f9a8d4;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__tag--related {
|
||||
background: #4a044e;
|
||||
border-color: #a855f7;
|
||||
color: #d8b4fe;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .linkml-viewer__badge--identifier {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Enum Values */
|
||||
.linkml-viewer__enum-values {
|
||||
margin-top: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -960,6 +960,14 @@ const TEXT = {
|
|||
en: 'Shows all slots (properties) defined for this class'
|
||||
},
|
||||
noClassSlots: { nl: 'Deze klasse heeft geen slots.', en: 'This class has no slots.' },
|
||||
// Slot details - semantic URI and mappings
|
||||
slotUri: { nl: 'Slot URI:', en: 'Slot URI:' },
|
||||
identifier: { nl: 'identifier', en: 'identifier' },
|
||||
examples: { nl: 'Voorbeelden:', en: 'Examples:' },
|
||||
narrowMappings: { nl: 'Specifiekere mappings:', en: 'Narrow Mappings:' },
|
||||
broadMappings: { nl: 'Bredere mappings:', en: 'Broad Mappings:' },
|
||||
relatedMappings: { nl: 'Gerelateerde mappings:', en: 'Related Mappings:' },
|
||||
comments: { nl: 'Opmerkingen:', en: 'Comments:' },
|
||||
};
|
||||
|
||||
// Dynamically discover schema files from the modules directory
|
||||
|
|
@ -2587,6 +2595,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
{slot.name}
|
||||
{slot.required && <span className="linkml-viewer__badge linkml-viewer__badge--required">{t('required')}</span>}
|
||||
{slot.multivalued && <span className="linkml-viewer__badge linkml-viewer__badge--multi">{t('multivalued')}</span>}
|
||||
{slot.identifier && <span className="linkml-viewer__badge linkml-viewer__badge--identifier">{t('identifier')}</span>}
|
||||
{/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
|
||||
{use3DIndicator ? (
|
||||
<CustodianTypeIndicator3D
|
||||
|
|
@ -2609,6 +2618,13 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
)
|
||||
)}
|
||||
</h4>
|
||||
{/* Slot URI - semantic predicate */}
|
||||
{slot.slot_uri && (
|
||||
<div className="linkml-viewer__slot-uri">
|
||||
<span className="linkml-viewer__label">{t('slotUri')}</span>
|
||||
<code className="linkml-viewer__uri-value">{slot.slot_uri}</code>
|
||||
</div>
|
||||
)}
|
||||
{slot.range && (
|
||||
<div className="linkml-viewer__range">
|
||||
<span className="linkml-viewer__label">{t('range')}</span>
|
||||
|
|
@ -2639,6 +2655,84 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
<code>{slot.pattern}</code>
|
||||
</div>
|
||||
)}
|
||||
{/* Examples section */}
|
||||
{slot.examples && slot.examples.length > 0 && (
|
||||
<div className="linkml-viewer__examples">
|
||||
<span className="linkml-viewer__label">{t('examples')}</span>
|
||||
<ul className="linkml-viewer__examples-list">
|
||||
{slot.examples.map((example, idx) => (
|
||||
<li key={idx} className="linkml-viewer__example-item">
|
||||
<code className="linkml-viewer__example-value">{example.value}</code>
|
||||
{example.description && (
|
||||
<span className="linkml-viewer__example-description"> - {example.description}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{/* Semantic Mappings - 5 types per LinkML spec */}
|
||||
{slot.exact_mappings && slot.exact_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('exactMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.exact_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--exact">{mapping}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{slot.close_mappings && slot.close_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('closeMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.close_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--close">{mapping}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{slot.narrow_mappings && slot.narrow_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('narrowMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.narrow_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--narrow">{mapping}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{slot.broad_mappings && slot.broad_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('broadMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.broad_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--broad">{mapping}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{slot.related_mappings && slot.related_mappings.length > 0 && (
|
||||
<div className="linkml-viewer__mappings">
|
||||
<span className="linkml-viewer__label">{t('relatedMappings')}</span>
|
||||
<div className="linkml-viewer__tag-list">
|
||||
{slot.related_mappings.map(mapping => (
|
||||
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--related">{mapping}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Comments section */}
|
||||
{slot.comments && slot.comments.length > 0 && (
|
||||
<div className="linkml-viewer__comments">
|
||||
<span className="linkml-viewer__label">{t('comments')}</span>
|
||||
<ul className="linkml-viewer__comments-list">
|
||||
{slot.comments.map((comment, idx) => (
|
||||
<li key={idx} className="linkml-viewer__comment-item">{comment}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -2788,6 +2882,7 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
|
||||
const classes = extractClasses(schema);
|
||||
const enums = extractEnums(schema);
|
||||
const slots = extractSlots(schema);
|
||||
|
||||
// Count matching items when filter is active (for display purposes)
|
||||
const matchingClassCount = custodianTypeFilter.size > 0
|
||||
|
|
@ -2804,6 +2899,13 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
}).length
|
||||
: enums.length;
|
||||
|
||||
const matchingSlotCount = custodianTypeFilter.size > 0
|
||||
? slots.filter(slot => {
|
||||
const types = slotCustodianTypes[slot.name] || getCustodianTypesForSlot(slot.name);
|
||||
return types.some(t => custodianTypeFilter.has(t));
|
||||
}).length
|
||||
: slots.length;
|
||||
|
||||
return (
|
||||
<div className="linkml-viewer__visual">
|
||||
{/* Schema Metadata */}
|
||||
|
|
@ -2873,6 +2975,26 @@ const LinkMLViewerPage: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Slots */}
|
||||
{slots.length > 0 && (
|
||||
<div className="linkml-viewer__section">
|
||||
<button
|
||||
className="linkml-viewer__section-header"
|
||||
onClick={() => toggleSection('slots')}
|
||||
>
|
||||
<span className="linkml-viewer__section-icon">
|
||||
{expandedSections.has('slots') ? '▼' : '▶'}
|
||||
</span>
|
||||
{t('slots')} ({custodianTypeFilter.size > 0 ? `${matchingSlotCount}/${slots.length}` : slots.length})
|
||||
</button>
|
||||
{expandedSections.has('slots') && (
|
||||
<div className="linkml-viewer__section-content">
|
||||
{slots.map(renderSlotDetails)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enums */}
|
||||
{enums.length > 0 && (
|
||||
<div className="linkml-viewer__section">
|
||||
|
|
|
|||
Loading…
Reference in a new issue