From d96879343ca875e435b07f0d8257ebfe3f12578c Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 3 Feb 2026 13:43:48 -0500 Subject: [PATCH 01/21] fix: update year to 2026 --- packages/ui/src/common/Modal.tsx | 2 +- packages/ui/src/viewer-table/Toolbar/Toolbar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/common/Modal.tsx b/packages/ui/src/common/Modal.tsx index a19bc3c7..3c8519c9 100644 --- a/packages/ui/src/common/Modal.tsx +++ b/packages/ui/src/common/Modal.tsx @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * 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 diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index 1b19ae99..cc2de232 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * 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 From 3709c14f77e8a4cdab24198d714f3a75fcea1024 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 3 Feb 2026 13:44:07 -0500 Subject: [PATCH 02/21] feat: add subtitle prop to modal component --- packages/ui/src/common/Modal.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/common/Modal.tsx b/packages/ui/src/common/Modal.tsx index 3c8519c9..a4149958 100644 --- a/packages/ui/src/common/Modal.tsx +++ b/packages/ui/src/common/Modal.tsx @@ -36,6 +36,7 @@ export type ModalProps = { onAfterOpen?: () => void; children?: ReactNode; title: string; + subtitle?: string; }; // Using react-modal's built-in styling system instead of emotion css for modal configuration @@ -85,8 +86,16 @@ const titleStyle = (theme: Theme) => css` ${theme.typography.subtitleBold}; color: ${theme.colors.accent}; `; + +const subtitleStyle = (theme: Theme) => css` + ${theme.typography.data}; + color: ${theme.colors.black}; + margin-top: 8px; + margin-bottom: 0; +`; + Modal.setAppElement('body'); -const ModalComponent = ({ children, setIsOpen, isOpen, onAfterOpen, title }: ModalProps) => { +const ModalComponent = ({ children, setIsOpen, isOpen, onAfterOpen, title, subtitle }: ModalProps) => { const theme: Theme = useThemeContext(); return ( <> @@ -106,7 +115,10 @@ const ModalComponent = ({ children, setIsOpen, isOpen, onAfterOpen, title }: Mod bodyOpenClassName="modal-open" >
- {title} +
+ {title} + {subtitle &&

{subtitle}

} +
{children}
From cc905ada9f210784dda099a79507c07c8ee876d5 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 3 Feb 2026 13:44:26 -0500 Subject: [PATCH 03/21] feat: create diagram view button --- .../Toolbar/DiagramViewButton.tsx | 50 +++++++++++++++++++ .../Toolbar/DiagramViewButton.stories.tsx | 45 +++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx create mode 100644 packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx diff --git a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx new file mode 100644 index 00000000..cd300408 --- /dev/null +++ b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx @@ -0,0 +1,50 @@ +/* + * + * 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 . + * + * 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 { useState } from 'react'; + +import Button from '../../common/Button'; +import Modal from '../../common/Modal'; +import { useDictionaryDataContext } from '../../dictionary-controller/DictionaryDataContext'; +import { useThemeContext } from '../../theme/index'; + +const DiagramViewButton = () => { + const [isOpen, setIsOpen] = useState(false); + const theme = useThemeContext(); + const { Eye } = theme.icons; + const { loading, errors } = useDictionaryDataContext(); + + return ( + <> + + + + ); +}; + +export default DiagramViewButton; diff --git a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx new file mode 100644 index 00000000..32a81d08 --- /dev/null +++ b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx @@ -0,0 +1,45 @@ +/* + * + * 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 . + * + * 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 type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/test'; + +import DiagramViewButton from '../../../src/viewer-table/Toolbar/DiagramViewButton'; + +import { multipleDictionaryData, withDictionaryContext, withForeverLoading } from '../../dictionaryDecorator'; +import themeDecorator from '../../themeDecorator'; + +const meta = { + component: DiagramViewButton, + title: 'Viewer - Table/Toolbar/DiagramViewButton', + tags: ['autodocs'], + decorators: [themeDecorator(), withDictionaryContext(multipleDictionaryData)], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Loading: Story = { + decorators: [themeDecorator(), withForeverLoading()], +}; \ No newline at end of file From 1b0a3d54e33a56edfb76f4fa782126a6e36e3d1d Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 3 Feb 2026 13:44:47 -0500 Subject: [PATCH 04/21] feat: add dictionary view button to toolbar --- packages/ui/src/viewer-table/Toolbar/Toolbar.tsx | 2 ++ packages/ui/src/viewer-table/Toolbar/index.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index cc2de232..5b40d696 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -29,6 +29,7 @@ import { ToolbarSkeleton } from '../Loading'; import AttributeFilterDropdown from './AttributeFilterDropdown'; import CollapseAllButton from './CollapseAllButton'; +import DiagramViewButton from './DiagramViewButton'; import DictionaryDownloadButton from './DictionaryDownloadButton'; import ExpandAllButton from './ExpandAllButton'; import TableOfContentsDropdown from './TableOfContentsDropdown'; @@ -84,6 +85,7 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed }: ToolbarProps) => { : setIsCollapsed(true)} />}
+
diff --git a/packages/ui/src/viewer-table/Toolbar/index.ts b/packages/ui/src/viewer-table/Toolbar/index.ts index 7bac6020..9028ed3b 100644 --- a/packages/ui/src/viewer-table/Toolbar/index.ts +++ b/packages/ui/src/viewer-table/Toolbar/index.ts @@ -1,5 +1,6 @@ export { default as AttributeFilterDropdown } from './AttributeFilterDropdown.js'; export { default as CollapseAllButton, type CollapseAllButtonProps } from './CollapseAllButton.js'; +export { default as DiagramViewButton } from './DiagramViewButton.js'; export { default as DictionaryDownloadButton, type DictionaryDownloadButtonProps } from './DictionaryDownloadButton.js'; export { default as ExpandAllButton, type ExpandAllButtonProps } from './ExpandAllButton.js'; export { default as TableOfContentsDropdown, type TableOfContentsDropdownProps } from './TableOfContentsDropdown.js'; From bd02d6b4f3811fd9650c0e6cd57482bd8ccdc0d3 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 3 Feb 2026 13:49:27 -0500 Subject: [PATCH 05/21] fix: format fles --- packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx | 2 +- .../stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx index cd300408..97f2100d 100644 --- a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx +++ b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx @@ -35,7 +35,7 @@ const DiagramViewButton = () => { return ( <> Date: Tue, 10 Feb 2026 09:21:19 -0500 Subject: [PATCH 06/21] feat: implement entity relationship diagram --- .../EntityRelationshipDiagram.tsx | 71 +++++++ .../EntityRelationshipDiagram/SchemaNode.tsx | 199 ++++++++++++++++++ .../EntityRelationshipDiagram/diagramUtils.ts | 92 ++++++++ .../EntityRelationshipDiagram/index.ts | 22 ++ 4 files changed, 384 insertions(+) create mode 100644 packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx create mode 100644 packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx create mode 100644 packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts create mode 100644 packages/ui/src/viewer-table/EntityRelationshipDiagram/index.ts diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx b/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx new file mode 100644 index 00000000..f43c1cd7 --- /dev/null +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx @@ -0,0 +1,71 @@ +/* + * + * 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 . + * + * 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. + * + */ + +/** @jsxImportSource @emotion/react */ +import type { Dictionary } from '@overture-stack/lectern-dictionary'; +import ReactFlow, { + Background, + BackgroundVariant, + Controls, + useEdgesState, + useNodesState, + type NodeTypes, +} from 'reactflow'; +import 'reactflow/dist/style.css'; +import OneCardinalityMarker from '../../theme/icons/OneCardinalityMarker'; +import { getEdgesForDictionary, getNodesForDictionary, type SchemaNodeLayout } from './diagramUtils'; +import { SchemaNode } from './SchemaNode'; + +const nodeTypes: NodeTypes = { + schema: SchemaNode, +}; + +type EntityRelationshipDiagramProps = { + dictionary: Dictionary; + layout?: Partial; +}; + +export function EntityRelationshipDiagram({ dictionary, layout }: EntityRelationshipDiagramProps) { + const [nodes, , onNodesChange] = useNodesState(getNodesForDictionary(dictionary, layout)); + const [edges, , onEdgesChange] = useEdgesState(getEdgesForDictionary(dictionary)); + + return ( + <> + + + + + + + ); +} diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx b/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx new file mode 100644 index 00000000..ddec5ac3 --- /dev/null +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx @@ -0,0 +1,199 @@ +/* + * + * 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 . + * + * 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. + * + */ + +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import type { Schema } from '@overture-stack/lectern-dictionary'; +import { Handle, Position } from 'reactflow'; +import 'reactflow/dist/style.css'; +import Key from '../../theme/icons/Key'; +import { type Theme, useThemeContext } from '../../theme'; +import { createFieldHandleId } from './diagramUtils'; + +const fieldRowStyles = css` + padding: 12px 12px; + display: flex; + align-items: center; + justify-content: space-between; + transition: background-color 0.2s; + position: relative; +`; + +const fieldContentStyles = css` + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; +`; + +const fieldNameStyles = (theme: Theme) => css` + ${theme.typography.subtitleSecondary} + font-size: 14px; + color: #1f2937; + line-height: 1.5; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +`; + +const dataTypeBadgeStyles = (theme: Theme) => css` + ${theme.typography.regular} + font-size: 12px; + color: #374151; + flex-shrink: 0; + + &:first-letter { + text-transform: uppercase; + } +`; + +const nodeContainerStyles = css` + background: white; + border: 1px solid black; + border-radius: 8px; + box-shadow: + 0 10px 20px -3px rgba(0, 0, 0, 0.30), + 0 4px 10px -2px rgba(0, 0, 0, 0.35); + min-width: 280px; + max-width: 350px; + overflow: hidden; +`; + +const nodeHeaderStyles = (theme: Theme) => css` + ${theme.typography.subtitleSecondary} + background: ${theme.colors.accent}; + color: white; + padding: 16px 24px; + text-align: left; + border-bottom: 1px solid black; + letter-spacing: 0.05em; + margin: 0; + gap: 4px; + display: flex; + flex-direction: column; + align-items: left; +`; + +const nodeTitleTextStyle = css` + font-size: 20px; + ::first-letter { + text-transform: uppercase; + } +`; + +const nodeSubtitleTextStyle = css` + font-size: 16px; +`; + +const fieldsListStyles = css` + background: #f8fafc; + max-height: 350px; + overflow-y: auto; + & > div:nth-child(even) { + background-color: #e5edf3; + border-block: 1.5px solid #d4dce2; + } +`; + +const fieldNameContainerStyles = css` + display: flex; + align-items: center; + gap: 4px; +`; + +const baseHandleStyles = css` + position: absolute; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + width: 8px; + height: 8px; +`; + +const sourceHandleStyles = css` + ${baseHandleStyles} + right: -10px; +`; + +const targetHandleStyles = css` + ${baseHandleStyles} + left: -10px; +`; + +export function SchemaNode(props: { data: Schema }) { + const { data: schema } = props; + const theme = useThemeContext(); + + return ( +
+
+ {schema.name} + Schema +
+ +
+ {schema.fields.map((field, index) => { + const isUniqueKey = schema.restrictions?.uniqueKey?.includes(field.name) || field.unique === true; + const isForeignKey = + schema.restrictions?.foreignKey?.some((fk) => + fk.mappings.some((mapping) => mapping.local === field.name), + ) || false; + + const valueType = field.isArray ? `${field.valueType}[]` : field.valueType; + + return ( +
+
+ {field.name} +
+ +
+ {(isUniqueKey || isForeignKey) && } + {valueType} +
+ + {isUniqueKey && ( + + )} + + {isForeignKey && ( + + )} +
+ ); + })} +
+
+ ); +} diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts new file mode 100644 index 00000000..59845872 --- /dev/null +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts @@ -0,0 +1,92 @@ +/* + * + * 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 . + * + * 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 type { Dictionary, Schema } from '@overture-stack/lectern-dictionary'; +import type { Edge, Node } from 'reactflow'; +import { MarkerType } from 'reactflow'; +import { ONE_CARDINALITY_MARKER_ID } from '../../theme/icons/OneCardinalityMarker'; + +export type SchemaFlowNode = Node; + +export type SchemaNodeLayout = { + maxColumns: number; + columnWidth: number; + rowHeight: number; +}; + +function buildSchemaNode(schema: Schema): Omit { + return { + id: schema.name, + type: 'schema', + data: { ...schema }, + }; +} + +export function getNodesForDictionary(dictionary: Dictionary, layout?: Partial): Node[] { + const maxColumns = layout?.maxColumns ?? 4; + const columnWidth = layout?.columnWidth ?? 500; + const rowHeight = layout?.rowHeight ?? 500; + + return dictionary.schemas.map((schema, index) => { + const partialNode = buildSchemaNode(schema); + + const row = Math.floor(index / maxColumns); + const col = index % maxColumns; + + const position: Node['position'] = { + x: col * columnWidth, + y: row * rowHeight, + }; + + return { ...partialNode, position }; + }); +} + +export const createFieldHandleId = (schemaName: string, fieldName: string, type: 'source' | 'target'): string => + `${schemaName}-${fieldName}-${type}`; + +export function getEdgesForDictionary(dictionary: Dictionary): Edge[] { + return dictionary.schemas.flatMap((schema) => { + if (!schema.restrictions?.foreignKey) return []; + + return schema.restrictions.foreignKey.flatMap((foreignKey) => { + return foreignKey.mappings.map((mapping) => ({ + id: `${schema.name}-${mapping.local}-to-${foreignKey.schema}-${mapping.foreign}`, + source: foreignKey.schema, + sourceHandle: createFieldHandleId(foreignKey.schema, mapping.foreign, 'source'), + target: schema.name, + targetHandle: createFieldHandleId(schema.name, mapping.local, 'target'), + type: 'smoothstep', + style: { stroke: '#374151', strokeWidth: 2 }, + pathOptions: { + offset: -20, + }, + markerEnd: { + type: MarkerType.Arrow, + width: 20, + height: 20, + color: '#374151', + }, + markerStart: ONE_CARDINALITY_MARKER_ID, + })); + }); + }); +} diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/index.ts b/packages/ui/src/viewer-table/EntityRelationshipDiagram/index.ts new file mode 100644 index 00000000..f13bdc44 --- /dev/null +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/index.ts @@ -0,0 +1,22 @@ +/* + * + * 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 . + * + * 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. + * + */ + +export { EntityRelationshipDiagram } from './EntityRelationshipDiagram'; From 2d6185673a253f30c489de68bd41e60e19ee924e Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 10 Feb 2026 09:21:52 -0500 Subject: [PATCH 07/21] feat: add icons for diagram --- packages/ui/src/theme/icons/Key.tsx | 49 ++++++++++++++++++ .../src/theme/icons/OneCardinalityMarker.tsx | 51 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 packages/ui/src/theme/icons/Key.tsx create mode 100644 packages/ui/src/theme/icons/OneCardinalityMarker.tsx diff --git a/packages/ui/src/theme/icons/Key.tsx b/packages/ui/src/theme/icons/Key.tsx new file mode 100644 index 00000000..6489ac16 --- /dev/null +++ b/packages/ui/src/theme/icons/Key.tsx @@ -0,0 +1,49 @@ +/* + * + * 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 . + * + * 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. + * + */ + +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; + +import IconProps from './IconProps'; + +const Key = ({ fill, width, height, style }: IconProps) => { + return ( + + + + ); +}; + +export default Key; diff --git a/packages/ui/src/theme/icons/OneCardinalityMarker.tsx b/packages/ui/src/theme/icons/OneCardinalityMarker.tsx new file mode 100644 index 00000000..c8fb74a1 --- /dev/null +++ b/packages/ui/src/theme/icons/OneCardinalityMarker.tsx @@ -0,0 +1,51 @@ +/* + * + * 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 . + * + * 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. + * + */ + +/** @jsxImportSource @emotion/react */ + +export const ONE_CARDINALITY_MARKER_ID = 'one-cardinality-marker'; + +type OneCardinalityMarkerProps = { + color?: string; +}; + +const OneCardinalityMarker = ({ color = '#374151' }: OneCardinalityMarkerProps) => { + return ( + + + + + + + + ); +}; + +export default OneCardinalityMarker; From f8404049fedf44cb2cf13a89f9349ce0b9af565f Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 10 Feb 2026 09:22:12 -0500 Subject: [PATCH 08/21] feat: add entity relationship diagram to diagram view button --- .../src/viewer-table/Toolbar/DiagramViewButton.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx index 97f2100d..83ecab68 100644 --- a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx +++ b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx @@ -19,18 +19,21 @@ * */ +import type { Dictionary } from '@overture-stack/lectern-dictionary'; import { useState } from 'react'; import Button from '../../common/Button'; import Modal from '../../common/Modal'; -import { useDictionaryDataContext } from '../../dictionary-controller/DictionaryDataContext'; +import { useDictionaryDataContext, useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; import { useThemeContext } from '../../theme/index'; +import { EntityRelationshipDiagram } from '../EntityRelationshipDiagram'; const DiagramViewButton = () => { const [isOpen, setIsOpen] = useState(false); const theme = useThemeContext(); const { Eye } = theme.icons; const { loading, errors } = useDictionaryDataContext(); + const { selectedDictionary } = useDictionaryStateContext(); return ( <> @@ -42,7 +45,13 @@ const DiagramViewButton = () => { subtitle="Select any key field or edge to highlight a relation." isOpen={isOpen} setIsOpen={setIsOpen} - /> + > + {selectedDictionary && ( +
+ +
+ )} +
); }; From 1479f36ff7196ddc34e12fb10f43d9ac80921939 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 10 Feb 2026 09:22:23 -0500 Subject: [PATCH 09/21] feat: add entity relationship diagram story --- .../EntityRelationshipDiagram.stories.tsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx diff --git a/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx b/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx new file mode 100644 index 00000000..a1e5676d --- /dev/null +++ b/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx @@ -0,0 +1,52 @@ +/* + * + * 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 . + * + * 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 type { Meta, StoryObj } from '@storybook/react'; +import type { Dictionary } from '@overture-stack/lectern-dictionary'; + +import { EntityRelationshipDiagram } from '../../src/viewer-table/EntityRelationshipDiagram'; +import DictionarySample from '../fixtures/pcgl.json'; +import SimpleERDiagram from '../fixtures/simpleERDiagram.json'; +import themeDecorator from '../themeDecorator'; +import React from 'react'; + +const meta = { + component: EntityRelationshipDiagram, + title: 'Viewer - Table/Entity Relationship Diagram', + decorators: [themeDecorator()], + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + dictionary: DictionarySample as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; \ No newline at end of file From f66e39f327a29795dc5f530b543d989deb5b7277 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 10 Feb 2026 09:51:58 -0500 Subject: [PATCH 10/21] fix: removed unused imports --- .../stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx index 145be087..a6d8b9b8 100644 --- a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx @@ -20,7 +20,6 @@ */ import type { Meta, StoryObj } from '@storybook/react'; -import { userEvent, within } from '@storybook/test'; import DiagramViewButton from '../../../src/viewer-table/Toolbar/DiagramViewButton'; From d69ef677777e971fccf9272384edaa1b03ec0981 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 10 Feb 2026 09:52:23 -0500 Subject: [PATCH 11/21] fix: use empty dictionary instead of full dictionary --- .../viewer-table/Toolbar/DiagramViewButton.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx index a6d8b9b8..6879d9f7 100644 --- a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx @@ -23,14 +23,14 @@ import type { Meta, StoryObj } from '@storybook/react'; import DiagramViewButton from '../../../src/viewer-table/Toolbar/DiagramViewButton'; -import { multipleDictionaryData, withDictionaryContext, withForeverLoading } from '../../dictionaryDecorator'; +import { emptyDictionaryData, withDictionaryContext, withForeverLoading } from '../../dictionaryDecorator'; import themeDecorator from '../../themeDecorator'; const meta = { component: DiagramViewButton, title: 'Viewer - Table/Toolbar/DiagramViewButton', tags: ['autodocs'], - decorators: [themeDecorator(), withDictionaryContext(multipleDictionaryData)], + decorators: [themeDecorator(), withDictionaryContext(emptyDictionaryData)], } satisfies Meta; export default meta; From 1448234300b6da12c92919ef2061ee234206b0f1 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 11 Feb 2026 14:07:30 -0500 Subject: [PATCH 12/21] feat: add separate file for schema node css --- .../ui/src/theme/emotion/schemaNodeStyles.ts | 136 ++++++++++++++++++ .../EntityRelationshipDiagram/SchemaNode.tsx | 130 ++--------------- 2 files changed, 151 insertions(+), 115 deletions(-) create mode 100644 packages/ui/src/theme/emotion/schemaNodeStyles.ts diff --git a/packages/ui/src/theme/emotion/schemaNodeStyles.ts b/packages/ui/src/theme/emotion/schemaNodeStyles.ts new file mode 100644 index 00000000..302aebf0 --- /dev/null +++ b/packages/ui/src/theme/emotion/schemaNodeStyles.ts @@ -0,0 +1,136 @@ +/* + * + * 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 . + * + * 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 { css } from '@emotion/react'; +import type { Theme } from '../'; + +export const fieldRowStyles = css` + padding: 12px 12px; + display: flex; + align-items: center; + justify-content: space-between; + transition: background-color 0.2s; + position: relative; +`; + +export const fieldContentStyles = css` + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; +`; + +export const fieldNameStyles = (theme: Theme) => css` + ${theme.typography.subtitleSecondary} + font-size: 14px; + color: #1f2937; + line-height: 1.5; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +`; + +export const dataTypeBadgeStyles = (theme: Theme) => css` + ${theme.typography.regular} + font-size: 12px; + color: #374151; + flex-shrink: 0; + + &:first-letter { + text-transform: uppercase; + } +`; + +export const nodeContainerStyles = css` + background: white; + border: 1px solid black; + border-radius: 8px; + box-shadow: + 0 10px 20px -3px rgba(0, 0, 0, 0.30), + 0 4px 10px -2px rgba(0, 0, 0, 0.35); + min-width: 280px; + max-width: 350px; + overflow: hidden; +`; + +export const nodeHeaderStyles = (theme: Theme) => css` + ${theme.typography.subtitleSecondary} + background: ${theme.colors.accent}; + color: white; + padding: 16px 24px; + text-align: left; + border-bottom: 1px solid black; + letter-spacing: 0.05em; + margin: 0; + gap: 4px; + display: flex; + flex-direction: column; + align-items: left; +`; + +export const nodeTitleTextStyle = css` + font-size: 20px; + ::first-letter { + text-transform: uppercase; + } +`; + +export const nodeSubtitleTextStyle = css` + font-size: 16px; +`; + +export const fieldsListStyles = css` + background: #f8fafc; + max-height: 350px; + overflow-y: auto; + & > div:nth-child(even) { + background-color: #e5edf3; + border-block: 1.5px solid #d4dce2; + } +`; + +export const fieldNameContainerStyles = css` + display: flex; + align-items: center; + gap: 4px; +`; + +const baseHandleStyles = css` + position: absolute; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + width: 8px; + height: 8px; +`; + +export const sourceHandleStyles = css` + ${baseHandleStyles} + right: -10px; +`; + +export const targetHandleStyles = css` + ${baseHandleStyles} + left: -10px; +`; diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx b/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx index ddec5ac3..036609f2 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx @@ -20,127 +20,27 @@ */ /** @jsxImportSource @emotion/react */ -import { css } from '@emotion/react'; import type { Schema } from '@overture-stack/lectern-dictionary'; import { Handle, Position } from 'reactflow'; import 'reactflow/dist/style.css'; import Key from '../../theme/icons/Key'; -import { type Theme, useThemeContext } from '../../theme'; +import { useThemeContext } from '../../theme'; +import { + fieldRowStyles, + fieldContentStyles, + fieldNameStyles, + dataTypeBadgeStyles, + nodeContainerStyles, + nodeHeaderStyles, + nodeTitleTextStyle, + nodeSubtitleTextStyle, + fieldsListStyles, + fieldNameContainerStyles, + sourceHandleStyles, + targetHandleStyles, +} from '../../theme/emotion/schemaNodeStyles'; import { createFieldHandleId } from './diagramUtils'; -const fieldRowStyles = css` - padding: 12px 12px; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.2s; - position: relative; -`; - -const fieldContentStyles = css` - display: flex; - align-items: center; - gap: 8px; - flex: 1; - min-width: 0; -`; - -const fieldNameStyles = (theme: Theme) => css` - ${theme.typography.subtitleSecondary} - font-size: 14px; - color: #1f2937; - line-height: 1.5; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -`; - -const dataTypeBadgeStyles = (theme: Theme) => css` - ${theme.typography.regular} - font-size: 12px; - color: #374151; - flex-shrink: 0; - - &:first-letter { - text-transform: uppercase; - } -`; - -const nodeContainerStyles = css` - background: white; - border: 1px solid black; - border-radius: 8px; - box-shadow: - 0 10px 20px -3px rgba(0, 0, 0, 0.30), - 0 4px 10px -2px rgba(0, 0, 0, 0.35); - min-width: 280px; - max-width: 350px; - overflow: hidden; -`; - -const nodeHeaderStyles = (theme: Theme) => css` - ${theme.typography.subtitleSecondary} - background: ${theme.colors.accent}; - color: white; - padding: 16px 24px; - text-align: left; - border-bottom: 1px solid black; - letter-spacing: 0.05em; - margin: 0; - gap: 4px; - display: flex; - flex-direction: column; - align-items: left; -`; - -const nodeTitleTextStyle = css` - font-size: 20px; - ::first-letter { - text-transform: uppercase; - } -`; - -const nodeSubtitleTextStyle = css` - font-size: 16px; -`; - -const fieldsListStyles = css` - background: #f8fafc; - max-height: 350px; - overflow-y: auto; - & > div:nth-child(even) { - background-color: #e5edf3; - border-block: 1.5px solid #d4dce2; - } -`; - -const fieldNameContainerStyles = css` - display: flex; - align-items: center; - gap: 4px; -`; - -const baseHandleStyles = css` - position: absolute; - top: 50%; - transform: translateY(-50%); - background: transparent; - border: none; - width: 8px; - height: 8px; -`; - -const sourceHandleStyles = css` - ${baseHandleStyles} - right: -10px; -`; - -const targetHandleStyles = css` - ${baseHandleStyles} - left: -10px; -`; - export function SchemaNode(props: { data: Schema }) { const { data: schema } = props; const theme = useThemeContext(); From e2ec2ed52dc104c5a07320001c6b5199379b4a81 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 11 Feb 2026 14:08:05 -0500 Subject: [PATCH 13/21] fix: import types and components in one line --- .../src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts index 59845872..3d6fd0ef 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts @@ -20,8 +20,7 @@ */ import type { Dictionary, Schema } from '@overture-stack/lectern-dictionary'; -import type { Edge, Node } from 'reactflow'; -import { MarkerType } from 'reactflow'; +import { type Edge, type Node, MarkerType } from 'reactflow'; import { ONE_CARDINALITY_MARKER_ID } from '../../theme/icons/OneCardinalityMarker'; export type SchemaFlowNode = Node; From 6eba979267f1d5773a7965f06d5ac3c9e641b44b Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 11 Feb 2026 14:08:14 -0500 Subject: [PATCH 14/21] fix: remove spread --- .../src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts index 3d6fd0ef..6b0b2d65 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts @@ -35,7 +35,7 @@ function buildSchemaNode(schema: Schema): Omit { return { id: schema.name, type: 'schema', - data: { ...schema }, + data: schema, }; } From 7700a6316770d8c0cc7bcd141a186b60bb8aa028 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 11 Feb 2026 14:10:01 -0500 Subject: [PATCH 15/21] fix: remove type casting --- packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx index 2ea77628..6b9f1449 100644 --- a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx +++ b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx @@ -21,7 +21,6 @@ import type { Dictionary } from '@overture-stack/lectern-dictionary'; import { useState } from 'react'; - import Button from '../../common/Button'; import Modal from '../../common/Modal'; import { useDictionaryDataContext, useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; @@ -48,7 +47,7 @@ const DiagramViewButton = () => { > {selectedDictionary && (
- +
)}
From e083d7319820703695c14cef044c321faee7e8fa Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 11 Feb 2026 14:10:23 -0500 Subject: [PATCH 16/21] fix: reduce height of modal to remove vertical scrolling on container --- packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx index 6b9f1449..2ee914db 100644 --- a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx +++ b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx @@ -46,7 +46,7 @@ const DiagramViewButton = () => { setIsOpen={setIsOpen} > {selectedDictionary && ( -
+
)} From 04e03140b7ab1dc1bf1b9dcfb9c85b8509b90e99 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 11 Feb 2026 14:10:54 -0500 Subject: [PATCH 17/21] fix: remove unused type import --- packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx index 2ee914db..dc5c1bc7 100644 --- a/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx +++ b/packages/ui/src/viewer-table/Toolbar/DiagramViewButton.tsx @@ -19,7 +19,6 @@ * */ -import type { Dictionary } from '@overture-stack/lectern-dictionary'; import { useState } from 'react'; import Button from '../../common/Button'; import Modal from '../../common/Modal'; From ffb47673c349860050092e26f9e882066bcf9361 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 12 Feb 2026 11:17:49 -0500 Subject: [PATCH 18/21] refactor: remove max-height from nodes --- packages/ui/src/theme/emotion/schemaNodeStyles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/theme/emotion/schemaNodeStyles.ts b/packages/ui/src/theme/emotion/schemaNodeStyles.ts index 302aebf0..e8830f61 100644 --- a/packages/ui/src/theme/emotion/schemaNodeStyles.ts +++ b/packages/ui/src/theme/emotion/schemaNodeStyles.ts @@ -101,7 +101,6 @@ export const nodeSubtitleTextStyle = css` export const fieldsListStyles = css` background: #f8fafc; - max-height: 350px; overflow-y: auto; & > div:nth-child(even) { background-color: #e5edf3; From 3a9efd9d6b81d71ad9b14d3077dbc8e125049fa6 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 12 Feb 2026 11:20:07 -0500 Subject: [PATCH 19/21] docs: add docs for functions and props --- .../EntityRelationshipDiagram.tsx | 9 +++++++++ .../EntityRelationshipDiagram/diagramUtils.ts | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx b/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx index f43c1cd7..99657009 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx @@ -43,6 +43,15 @@ type EntityRelationshipDiagramProps = { layout?: Partial; }; +/** + * Entity Relationship Diagram visualizing schemas and their foreign key relationships. + * + * @param {Dictionary} dictionary — The Lectern dictionary whose schemas and relationships to visualize + * @param {Partial} 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) { const [nodes, , onNodesChange] = useNodesState(getNodesForDictionary(dictionary, layout)); const [edges, , onEdgesChange] = useEdgesState(getEdgesForDictionary(dictionary)); diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts index 6b0b2d65..2a79af6a 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts @@ -39,6 +39,13 @@ function buildSchemaNode(schema: Schema): Omit { }; } +/** + * Converts a dictionary's schemas into positioned ReactFlow nodes arranged in a grid layout. + * + * @param {Dictionary} dictionary — The Lectern dictionary containing schemas to visualize + * @param {Partial} layout — Optional overrides for grid layout configuration + * @returns {Node[]} Array of positioned ReactFlow nodes + */ export function getNodesForDictionary(dictionary: Dictionary, layout?: Partial): Node[] { const maxColumns = layout?.maxColumns ?? 4; const columnWidth = layout?.columnWidth ?? 500; @@ -62,6 +69,12 @@ export function getNodesForDictionary(dictionary: Dictionary, layout?: Partial `${schemaName}-${fieldName}-${type}`; +/** + * Converts a dictionary's foreign key relationships into ReactFlow edges connecting schema nodes. + * + * @param {Dictionary} dictionary — The Lectern dictionary containing schemas with foreign key restrictions + * @returns {Edge[]} Array of ReactFlow edges representing foreign key relationships + */ export function getEdgesForDictionary(dictionary: Dictionary): Edge[] { return dictionary.schemas.flatMap((schema) => { if (!schema.restrictions?.foreignKey) return []; From cb0e9d427816753e5c63081fd73ade04c8eb932f Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 12 Feb 2026 13:53:59 -0500 Subject: [PATCH 20/21] feat: add row hover states --- packages/ui/src/theme/emotion/schemaNodeStyles.ts | 14 +++++++++----- .../EntityRelationshipDiagram/SchemaNode.tsx | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/theme/emotion/schemaNodeStyles.ts b/packages/ui/src/theme/emotion/schemaNodeStyles.ts index e8830f61..1891a410 100644 --- a/packages/ui/src/theme/emotion/schemaNodeStyles.ts +++ b/packages/ui/src/theme/emotion/schemaNodeStyles.ts @@ -22,13 +22,21 @@ import { css } from '@emotion/react'; import type { Theme } from '../'; -export const fieldRowStyles = css` +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 ? '#e5edf3' : 'transparent'}; + border-block: 1.5px solid ${isEven ? '#d4dce2' : 'transparent'}; + ${isForeignKey ? 'cursor: pointer;' : ''} + + &:hover { + 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}; + } `; export const fieldContentStyles = css` @@ -102,10 +110,6 @@ export const nodeSubtitleTextStyle = css` export const fieldsListStyles = css` background: #f8fafc; overflow-y: auto; - & > div:nth-child(even) { - background-color: #e5edf3; - border-block: 1.5px solid #d4dce2; - } `; export const fieldNameContainerStyles = css` diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx b/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx index 036609f2..208e3ae7 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/SchemaNode.tsx @@ -59,11 +59,11 @@ export function SchemaNode(props: { data: Schema }) { schema.restrictions?.foreignKey?.some((fk) => fk.mappings.some((mapping) => mapping.local === field.name), ) || false; - + const isEvenRow = index % 2 === 1; const valueType = field.isArray ? `${field.valueType}[]` : field.valueType; return ( -
+
{field.name}
From 82b93f835cd105aca4c3b7900c91006647fe1666 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 12 Feb 2026 13:54:25 -0500 Subject: [PATCH 21/21] feat: add edge hover states --- .../EntityRelationshipDiagram.tsx | 50 +++++++++++++------ .../EntityRelationshipDiagram/diagramUtils.ts | 1 - 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx b/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx index 99657009..529eb63f 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.tsx @@ -20,6 +20,8 @@ */ /** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import { type Theme, useThemeContext } from '../../theme'; import type { Dictionary } from '@overture-stack/lectern-dictionary'; import ReactFlow, { Background, @@ -52,29 +54,45 @@ type EntityRelationshipDiagramProps = { * 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) */ +const edgeHoverStyles = (theme: Theme) => css` + .react-flow__edge { + cursor: pointer; + } + .react-flow__edge-path { + stroke: ${theme.colors.black}; + stroke-width: 2; + } + .react-flow__edge:hover .react-flow__edge-path { + stroke: ${theme.colors.secondary_dark}; + } +`; + export function EntityRelationshipDiagram({ dictionary, layout }: EntityRelationshipDiagramProps) { const [nodes, , onNodesChange] = useNodesState(getNodesForDictionary(dictionary, layout)); const [edges, , onEdgesChange] = useEdgesState(getEdgesForDictionary(dictionary)); + const theme = useThemeContext(); return ( <> - - - - +
+ + + + +
); } diff --git a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts index 2a79af6a..bdf95ef1 100644 --- a/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts +++ b/packages/ui/src/viewer-table/EntityRelationshipDiagram/diagramUtils.ts @@ -87,7 +87,6 @@ export function getEdgesForDictionary(dictionary: Dictionary): Edge[] { target: schema.name, targetHandle: createFieldHandleId(schema.name, mapping.local, 'target'), type: 'smoothstep', - style: { stroke: '#374151', strokeWidth: 2 }, pathOptions: { offset: -20, },