Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d968793
fix: update year to 2026
ethanluc7 Feb 3, 2026
3709c14
feat: add subtitle prop to modal component
ethanluc7 Feb 3, 2026
cc905ad
feat: create diagram view button
ethanluc7 Feb 3, 2026
1b0a3d5
feat: add dictionary view button to toolbar
ethanluc7 Feb 3, 2026
bd02d6b
fix: format fles
ethanluc7 Feb 3, 2026
f729b69
feat: implement entity relationship diagram
ethanluc7 Feb 10, 2026
2d61856
feat: add icons for diagram
ethanluc7 Feb 10, 2026
f840404
feat: add entity relationship diagram to diagram view button
ethanluc7 Feb 10, 2026
1479f36
feat: add entity relationship diagram story
ethanluc7 Feb 10, 2026
36476f5
Merge branch 'main' into 395-render-dictionary-schema
ethanluc7 Feb 10, 2026
2abb827
Merge branch 'main' into 395-render-dictionary-schema
ethanluc7 Feb 10, 2026
288d961
Merge branch '395-render-dictionary-schema' of https://github.com/ove…
ethanluc7 Feb 10, 2026
f66e39f
fix: removed unused imports
ethanluc7 Feb 10, 2026
d69ef67
fix: use empty dictionary instead of full dictionary
ethanluc7 Feb 10, 2026
1448234
feat: add separate file for schema node css
ethanluc7 Feb 11, 2026
e2ec2ed
fix: import types and components in one line
ethanluc7 Feb 11, 2026
6eba979
fix: remove spread
ethanluc7 Feb 11, 2026
7700a63
fix: remove type casting
ethanluc7 Feb 11, 2026
e083d73
fix: reduce height of modal to remove vertical scrolling on container
ethanluc7 Feb 11, 2026
04e0314
fix: remove unused type import
ethanluc7 Feb 11, 2026
ffb4767
refactor: remove max-height from nodes
ethanluc7 Feb 12, 2026
3a9efd9
docs: add docs for functions and props
ethanluc7 Feb 12, 2026
cb0e9d4
feat: add row hover states
ethanluc7 Feb 12, 2026
82b93f8
feat: add edge hover states
ethanluc7 Feb 12, 2026
0554d37
Merge branch 'main' into 396-field-edge-interactions
ethanluc7 Feb 12, 2026
a75c21e
feat: allow modal to take in reactnode as subtitle
ethanluc7 Feb 18, 2026
5c814cf
feat: add highlight states to schema node styles
ethanluc7 Feb 18, 2026
e53cac7
feat: add relationship provider to create relationship to determine h…
ethanluc7 Feb 18, 2026
2941455
feat: add diagram subtitle component to render scheam chain
ethanluc7 Feb 18, 2026
897ca27
feat: update schema node to activate / deactive edges on click
ethanluc7 Feb 18, 2026
f9c88f3
feat: add active and inactive marker
ethanluc7 Feb 18, 2026
f3d58d1
refactor: update index with new utils
ethanluc7 Feb 18, 2026
3ea658c
feat: add dictionaries for testing
ethanluc7 Feb 18, 2026
6fe4d87
Merge branch 'main' into 397-highlight-states
ethanluc7 Feb 19, 2026
8bafc25
test
ethanluc7 Feb 19, 2026
8349593
revert test
ethanluc7 Feb 19, 2026
52da977
fix: replace div element with fragment
ethanluc7 Feb 20, 2026
355fe54
refactor: use css selectors with hover text variable
ethanluc7 Feb 20, 2026
d67e0aa
fix: align schema title and rows text
ethanluc7 Feb 20, 2026
020ba8b
fix: remove union with null
ethanluc7 Feb 20, 2026
d5bdcc5
fix: use optional fields instead of union with null
ethanluc7 Feb 20, 2026
65f8ac3
fix: replace one line if statements
ethanluc7 Feb 20, 2026
cfdd84a
refactor: rename variable names for clarity
ethanluc7 Feb 20, 2026
6029fdb
fix: simplify conditional logic
ethanluc7 Feb 20, 2026
3a6160a
fix: change from null to undefined for now optional fields
ethanluc7 Feb 20, 2026
84f1b7a
refactor: remove isEvenRow property from rows
ethanluc7 Feb 20, 2026
1131100
refactor: move styling for marker outside of function
ethanluc7 Feb 25, 2026
46de40e
refactor: remove unnecessary duplicate check on sets
ethanluc7 Feb 25, 2026
27a6cb7
refactor: use useMemo to prevent extra re-renders
ethanluc7 Feb 25, 2026
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
6 changes: 4 additions & 2 deletions packages/ui/src/common/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type ModalProps = {
onAfterOpen?: () => void;
children?: ReactNode;
title: string;
subtitle?: string;
subtitle?: ReactNode;
};

// Using react-modal's built-in styling system instead of emotion css for modal configuration
Expand Down Expand Up @@ -73,6 +73,8 @@ const headerStyle = (theme: Theme) => css`
border-bottom: 1px solid ${theme.colors.border_subtle};
background: ${theme.colors.white};
box-shadow: 0px 1px 6px 0px #00000026;
position: relative;
z-index: 1;
`;

const bodyStyle = css`
Expand Down Expand Up @@ -117,7 +119,7 @@ const ModalComponent = ({ children, setIsOpen, isOpen, onAfterOpen, title, subti
<div css={headerStyle(theme)}>
<div>
<span css={titleStyle(theme)}>{title}</span>
{subtitle && <p css={subtitleStyle(theme)}>{subtitle}</p>}
{subtitle && <div css={subtitleStyle(theme)}>{subtitle}</div>}
</div>
<Button iconOnly onClick={() => setIsOpen(false)} icon={<Cancel />} />
</div>
Expand Down
50 changes: 33 additions & 17 deletions packages/ui/src/theme/emotion/schemaNodeStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,36 @@
import { css } from '@emotion/react';
import type { Theme } from '../';

export const fieldRowStyles = (theme: Theme, isForeignKey: boolean, isEven: boolean) => css`
padding: 12px 12px;
display: flex;
align-items: center;
justify-content: space-between;
transition: background-color 0.2s;
position: relative;
background-color: ${isEven ? theme.colors.accent_1 : 'transparent'};
border-block: 1.5px solid ${isEven ? theme.colors.accent_2 : 'transparent'};
${isForeignKey ? 'cursor: pointer;' : ''}

&:hover {
export const fieldRowStyles = (theme: Theme, isForeignKey: boolean, isHighlighted: boolean = false) => {
const hoverText = css`
background-color: ${isForeignKey ? theme.colors.secondary_1 : theme.colors.grey_3};
border-block: 1.5px solid ${isForeignKey ? theme.colors.secondary_dark : theme.colors.grey_4};
}
`;
`;

return css`
padding: 12px 12px;
display: flex;
align-items: center;
justify-content: space-between;
transition: background-color 0.2s;
position: relative;
border-block: 1.5px solid ${isHighlighted ? theme.colors.secondary_dark : 'transparent'};
${isForeignKey ? 'cursor: pointer;' : ''}

&:hover {
${hoverText}
}

&:nth-child(even) {
background-color: ${theme.colors.accent_1};
border-block: 1.5px solid ${theme.colors.accent_2};
}

&:nth-child(even):hover {
${hoverText}
}
Comment on lines +41 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

`;
};

export const fieldContentStyles = css`
display: flex;
Expand Down Expand Up @@ -69,23 +83,25 @@ export const dataTypeBadgeStyles = (theme: Theme) => css`
}
`;

export const nodeContainerStyles = css`
export const nodeContainerStyles = (isInactive: boolean = false) => css`
background: white;
border: 1px solid black;
border-radius: 8px;
box-shadow:
0 10px 20px -3px rgba(0, 0, 0, 0.30),
0 10px 20px -3px rgba(0, 0, 0, 0.3),
0 4px 10px -2px rgba(0, 0, 0, 0.35);
min-width: 280px;
max-width: 350px;
overflow: hidden;
transition: opacity 0.2s;
${isInactive ? 'opacity: 0.4;' : ''}
`;

export const nodeHeaderStyles = (theme: Theme) => css`
${theme.typography.subtitleSecondary}
background: ${theme.colors.accent};
color: white;
padding: 16px 24px;
padding: 16px 12px;
text-align: left;
border-bottom: 1px solid black;
letter-spacing: 0.05em;
Expand Down
16 changes: 15 additions & 1 deletion packages/ui/src/theme/icons/OneCardinalityMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
/** @jsxImportSource @emotion/react */

export const ONE_CARDINALITY_MARKER_ID = 'one-cardinality-marker';
export const ONE_CARDINALITY_MARKER_ACTIVE_ID = 'one-cardinality-marker-active';

type OneCardinalityMarkerProps = {
color?: string;
activeColor: string;
};

const OneCardinalityMarker = ({ color = '#374151' }: OneCardinalityMarkerProps) => {
const OneCardinalityMarker = ({ color = '#374151', activeColor }: OneCardinalityMarkerProps) => {
return (
<svg style={{ position: 'absolute', top: 0, left: 0 }}>
<defs>
Expand All @@ -43,6 +45,18 @@ const OneCardinalityMarker = ({ color = '#374151' }: OneCardinalityMarkerProps)
>
<line x1="0" y1="-6" x2="0" y2="6" stroke={color} strokeWidth="2" />
</marker>
<marker
id={ONE_CARDINALITY_MARKER_ACTIVE_ID}
markerWidth="20"
markerHeight="20"
viewBox="-10 -10 20 20"
markerUnits="userSpaceOnUse"
orient="auto-start-reverse"
refX="0"
refY="0"
>
<line x1="0" y1="-6" x2="0" y2="6" stroke={activeColor} strokeWidth="2" />
</marker>
</defs>
</svg>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
*
* Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved
*
* This program and the accompanying materials are made available under the terms of
* the GNU Affero General Public License v3.0. You should have received a copy of the
* GNU Affero General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/

import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
import { traceChain, type RelationshipMap } from './diagramUtils';

type ActiveRelationshipState = {
edgeIds: Set<string>;
fieldKeys: Set<string>;
schemaChain: string[];
};

type ActiveRelationshipContextValue = {
activeEdgeIds?: Set<string>;
activeFieldKeys?: Set<string>;
activeSchemaNames?: Set<string>;
activeSchemaChain?: string[];
activateRelationship: (chainStartingIndex: number) => void;
deactivateRelationship: () => void;
relationshipMap: RelationshipMap;
isFieldInActiveRelationship: (schemaName: string, fieldName: string) => boolean;
};

const ActiveRelationshipContext = createContext<ActiveRelationshipContextValue | null>(null);

type ActiveRelationshipProviderProps = {
relationshipMap: RelationshipMap;
children: ReactNode;
};

/**
* Provides active relationship state and actions to the ERD component tree.
* Wraps children with context that tracks which FK chain is currently highlighted,
* and exposes methods to activate/deactivate highlighting via traceChain.
*
* @param {RelationshipMap} relationshipMap — The FK adjacency graph used for chain tracing
* @param {ReactNode} children — Child components that can consume the active relationship context
*/
export function ActiveRelationshipProvider({ relationshipMap, children }: ActiveRelationshipProviderProps) {
const [activeState, setActiveState] = useState<ActiveRelationshipState | null>(null);

const activateRelationship = useCallback(
(chainStartingIndex: number) => {
const result = traceChain(chainStartingIndex, relationshipMap);
setActiveState(result);
},
[relationshipMap],
);

const deactivateRelationship = useCallback(() => {
setActiveState(null);
}, []);

const isFieldInActiveRelationship = useCallback(
(schemaName: string, fieldName: string): boolean => {
if (!activeState) {
return false;
}
return activeState.fieldKeys.has(`${schemaName}::${fieldName}`);
},
[activeState],
);

const activeSchemaNames = useMemo(() => {
if (!activeState) {
return undefined;
}
const names = new Set<string>();
for (const key of activeState.fieldKeys) {
const schemaName = key.split('::')[0];
if (schemaName) {
names.add(schemaName);
}
}
return names;
}, [activeState]);

return (
<ActiveRelationshipContext.Provider
value={{
activeEdgeIds: activeState?.edgeIds,
activeFieldKeys: activeState?.fieldKeys,
activeSchemaNames,
activeSchemaChain: activeState?.schemaChain,
activateRelationship,
deactivateRelationship,
relationshipMap,
isFieldInActiveRelationship,
}}
>
{children}
</ActiveRelationshipContext.Provider>
);
}

/**
* Consumes the active relationship context. Must be called from a component
* that is a descendant of ActiveRelationshipProvider.
*
* @returns {ActiveRelationshipContextValue} The current active relationship state and actions
*/
export function useActiveRelationship(): ActiveRelationshipContextValue {
const context = useContext(ActiveRelationshipContext);
if (!context) {
throw new Error('useActiveRelationship must be used within an ActiveRelationshipProvider');
}
return context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,26 @@
import { css } from '@emotion/react';
import { type Theme, useThemeContext } from '../../theme';
import type { Dictionary } from '@overture-stack/lectern-dictionary';
import { useCallback, useMemo } from 'react';
import ReactFlow, {
Background,
BackgroundVariant,
Controls,
useEdgesState,
useNodesState,
type Edge,
type NodeTypes,
} from 'reactflow';
import 'reactflow/dist/style.css';
import OneCardinalityMarker from '../../theme/icons/OneCardinalityMarker';
import { getEdgesForDictionary, getNodesForDictionary, type SchemaNodeLayout } from './diagramUtils';
import {
getEdgesFromMap,
getEdgesWithHighlight,
getNodesForDictionary,
type RelationshipEdgeData,
type SchemaNodeLayout,
} from './diagramUtils';
import { useActiveRelationship } from './ActiveRelationshipContext';
import { SchemaNode } from './SchemaNode';

const nodeTypes: NodeTypes = {
Expand All @@ -56,31 +65,69 @@ const edgeHoverStyles = (theme: Theme) => css`
.react-flow__edge:hover .react-flow__edge-path {
stroke: ${theme.colors.secondary_dark};
}

.react-flow__edge.edge-active .react-flow__edge-path {
stroke: ${theme.colors.secondary_dark};
stroke-width: 3;
}

.react-flow__edge.edge-inactive .react-flow__edge-path {
stroke: ${theme.colors.grey_5};
stroke-width: 1.5;
opacity: 0.9;
}

.react-flow__edge.edge-inactive .react-flow__edge-path:hover {
stroke: ${theme.colors.grey_4};
}
`;

/**
* Entity Relationship Diagram visualizing schemas and their foreign key relationships.
* Must be rendered inside an `ActiveRelationshipProvider`.
*
* @param {Dictionary} dictionary — The Lectern dictionary whose schemas and relationships to visualize
* @param {Partial<SchemaNodeLayout>} layout — Optional overrides for the grid layout of schema nodes.
* maxColumns controls the number of nodes per row before wrapping (default 4),
* columnWidth sets horizontal spacing in pixels between column left edges (default 500),
* and rowHeight sets vertical spacing in pixels between row top edges (default 500)
*/
export function EntityRelationshipDiagram({ dictionary, layout }: EntityRelationshipDiagramProps) {
export function EntityRelationshipDiagramContent({ dictionary, layout }: EntityRelationshipDiagramProps) {
const [nodes, , onNodesChange] = useNodesState(getNodesForDictionary(dictionary, layout));
const [edges, , onEdgesChange] = useEdgesState(getEdgesForDictionary(dictionary));
const { activeEdgeIds, activateRelationship, deactivateRelationship, relationshipMap } = useActiveRelationship();
const [edges, , onEdgesChange] = useEdgesState(getEdgesFromMap(relationshipMap));
const theme = useThemeContext();

const highlightedEdges = useMemo(
() => getEdgesWithHighlight(edges, activeEdgeIds, theme.colors.secondary_dark),
[edges, activeEdgeIds],
);

const onEdgeClick = useCallback(
(_event: React.MouseEvent, edge: Edge) => {
const edgeData = edge.data as RelationshipEdgeData | undefined;
if (edgeData?.fkIndex !== undefined) {
activateRelationship(edgeData.fkIndex);
}
},
[activateRelationship],
);

const onPaneClick = useCallback(() => {
deactivateRelationship();
}, [deactivateRelationship]);

return (
<>
<OneCardinalityMarker />
<OneCardinalityMarker activeColor={theme.colors.secondary_dark} />
<div css={edgeHoverStyles(theme)} style={{ width: '100%', height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
edges={highlightedEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onEdgeClick={onEdgeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 20, maxZoom: 1.5, minZoom: 0.5 }}
Expand Down
Loading