diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/hooks/useCustomEditor.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/hooks/useCustomEditor.ts index d985a63ff012..031a7299730f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/hooks/useCustomEditor.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/hooks/useCustomEditor.ts @@ -135,6 +135,15 @@ export const useCustomEditor = ( return () => { isMounted = false; + // Without this the ProseMirror view, its plugin state and its + // MutationObserver outlive the component. Tables mount one editor per + // description cell, so every pagination click, search keystroke or tab + // switch used to abandon N live instances for the rest of the session. + // The isDestroyed guard mirrors @tiptap/react's own useEditor cleanup — + // StrictMode double-invokes effects, so this can run twice. + if (!instance.isDestroyed) { + instance.destroy(); + } }; }, deps); diff --git a/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx index a74febea4626..41e9f275f028 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx @@ -131,8 +131,8 @@ import { import { centerNodePosition, getViewportForLineageExport, - positionNodesUsingElk, } from '../../utils/EntityLineageLayoutUtils'; +import { positionNodesUsingElk } from '../../utils/Lineage/Layout/ElkLayoutUtils'; import { createNodes, getConnectedNodesEdges, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageLayoutUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageLayoutUtils.ts index db3b9ffc2aaf..037e64f1d2ee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageLayoutUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageLayoutUtils.ts @@ -12,7 +12,6 @@ */ import { graphlib, layout } from '@dagrejs/dagre'; -import type { ElkExtendedEdge, ElkNode } from 'elkjs/lib/elk.bundled.js'; import type { Edge, Node, ReactFlowInstance } from 'reactflow'; import { Position } from 'reactflow'; import type { ExportViewport } from '../components/Entity/EntityExportModalProvider/EntityExportModalProvider.interface'; @@ -24,10 +23,6 @@ import { ZOOM_VALUE, } from '../constants/Lineage.constants'; import { EntityLineageDirection } from '../enums/entity.enum'; -import { useLineageStore } from '../hooks/useLineageStore'; -import { getNodeHeight } from './CanvasUtils'; -import { getEntityChildrenAndLabel } from './EntityLineageNodeUtils'; -import ELKLayout from './Lineage/Layout/ELKUtil/ELKUtil'; interface LayoutedElements { node: Array; @@ -101,70 +96,6 @@ export const getLayoutedElements = ( return { node: uNode, edge: edgesRequired }; }; -export const getELKLayoutedElements = async ( - nodes: Node[], - edges: Edge[], - columnsHavingLineage: Map> = new Map() -) => { - const { nodeFilterState, isColumnLevelLineage, isEditMode } = - useLineageStore.getState(); - const elkNodes: ElkNode[] = nodes.map((node) => { - const isColumnOnlyFilterActive = - (isColumnLevelLineage || nodeFilterState.get(node.id)) ?? false; - const columns = isEditMode - ? getEntityChildrenAndLabel(node.data.node).children.length - : columnsHavingLineage.get(node.id)?.size ?? 0; - - const nodeHeight = getNodeHeight(node, isColumnOnlyFilterActive, columns); - - return { - id: node.id, - width: NODE_WIDTH, - height: nodeHeight, - }; - }); - - const elkEdges: ElkExtendedEdge[] = edges.map((edge) => ({ - id: edge.id, - sources: [edge.source], - targets: [edge.target], - })); - - try { - const layoutedGraph = await ELKLayout.layoutGraph(elkNodes, elkEdges); - const layoutedMap = new Map( - (layoutedGraph?.children ?? []).map((n) => [n.id, n]) - ); - const updatedNodes: Node[] = nodes.map((node) => { - const layoutedNode = layoutedMap.get(node.id); - - return { - ...node, - position: { x: layoutedNode?.x ?? 0, y: layoutedNode?.y ?? 0 }, - height: layoutedNode?.height ?? node.height, - hidden: false, - }; - }); - - return { nodes: updatedNodes, edges: edges ?? [] }; - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error occurred while layouting graph:', error); - - return { nodes: [], edges: [] }; - } -}; - -export const positionNodesUsingElk = async ( - nodes: Node[], - edges: Edge[], - columnsHavingLineage: Map> -) => { - const obj = await getELKLayoutedElements(nodes, edges, columnsHavingLineage); - - return obj; -}; - export const getNodesBoundsReactFlow = (nodes: Node[]) => { const bounds = { xMin: Infinity, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Lineage/Layout/ElkLayoutUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Lineage/Layout/ElkLayoutUtils.ts new file mode 100644 index 000000000000..360086c97f87 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Lineage/Layout/ElkLayoutUtils.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ElkExtendedEdge, ElkNode } from 'elkjs/lib/elk.bundled.js'; +import type { Edge, Node } from 'reactflow'; +import { NODE_WIDTH } from '../../../constants/Lineage.constants'; +import { useLineageStore } from '../../../hooks/useLineageStore'; +import { getNodeHeight } from '../../CanvasUtils'; +import { getEntityChildrenAndLabel } from '../../EntityLineageNodeUtils'; + +/** + * Lazy boundary for the ELK engine. + * + * elkjs is ~1.37MB in the built bundle and this is the only module that needs + * it. Loading it here — rather than at module scope — keeps it out of whatever + * chunk imports this file. `LineageProvider` is statically imported by eight + * lineage components, so a top-level import would put the engine in the shared + * chunk that every authenticated route pulls, including glossary pages that + * render with G6/antv-dagre and never call ELK. + */ +const loadElkLayout = async () => (await import('./ELKUtil/ELKUtil')).default; + +export const getELKLayoutedElements = async ( + nodes: Node[], + edges: Edge[], + columnsHavingLineage: Map> = new Map() +) => { + const { nodeFilterState, isColumnLevelLineage, isEditMode } = + useLineageStore.getState(); + const elkNodes: ElkNode[] = nodes.map((node) => { + const isColumnOnlyFilterActive = + (isColumnLevelLineage || nodeFilterState.get(node.id)) ?? false; + const columns = isEditMode + ? getEntityChildrenAndLabel(node.data.node).children.length + : columnsHavingLineage.get(node.id)?.size ?? 0; + + const nodeHeight = getNodeHeight(node, isColumnOnlyFilterActive, columns); + + return { + id: node.id, + width: NODE_WIDTH, + height: nodeHeight, + }; + }); + + const elkEdges: ElkExtendedEdge[] = edges.map((edge) => ({ + id: edge.id, + sources: [edge.source], + targets: [edge.target], + })); + + try { + const ELKLayout = await loadElkLayout(); + const layoutedGraph = await ELKLayout.layoutGraph(elkNodes, elkEdges); + const layoutedMap = new Map( + (layoutedGraph?.children ?? []).map((n) => [n.id, n]) + ); + const updatedNodes: Node[] = nodes.map((node) => { + const layoutedNode = layoutedMap.get(node.id); + + return { + ...node, + position: { x: layoutedNode?.x ?? 0, y: layoutedNode?.y ?? 0 }, + height: layoutedNode?.height ?? node.height, + hidden: false, + }; + }); + + return { nodes: updatedNodes, edges: edges ?? [] }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error occurred while layouting graph:', error); + + return { nodes: [], edges: [] }; + } +}; + +export const positionNodesUsingElk = async ( + nodes: Node[], + edges: Edge[], + columnsHavingLineage: Map> +) => { + const obj = await getELKLayoutedElements(nodes, edges, columnsHavingLineage); + + return obj; +};