From abeed0093551301c5043470e7fdfd6de3e4ed413 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Sat, 14 Jun 2025 17:09:08 +0530 Subject: [PATCH] #21232: fix the lineage export image being cropped (#21765) * fix the lineage export image being cropped * fix the nodes children column not being able to trace well * minor removal as not needed (cherry picked from commit da25cd1a3968fc9a890f79f9badf3149702b8a95) --- .../ui/src/utils/EntityLineageUtils.test.tsx | 68 +++++++++---------- .../ui/src/utils/EntityLineageUtils.tsx | 46 ++++++++++--- .../ui/src/utils/Export/ExportUtils.ts | 2 + 3 files changed, 71 insertions(+), 45 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.test.tsx index fad304997d8..b5f73b020a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.test.tsx @@ -670,10 +670,10 @@ describe('Test EntityLineageUtils utility', () => { const bounds = getNodesBoundsReactFlow(nodes); expect(bounds).toEqual({ - xMin: 100, - yMin: 200, - xMax: 150, // x + width - yMax: 230, // y + height + xMin: 80, // x - padding + yMin: 180, // y - padding + xMax: 170, // x + width + padding + yMax: 250, // y + height + padding }); }); @@ -705,10 +705,10 @@ describe('Test EntityLineageUtils utility', () => { const bounds = getNodesBoundsReactFlow(nodes); expect(bounds).toEqual({ - xMin: 0, - yMin: 0, - xMax: 260, // rightmost x (200) + width (60) - yMax: 340, // bottom y (300) + height (40) + xMin: -20, // x - padding + yMin: -20, // y - padding + xMax: 280, // rightmost x (200) + width (60) + padding + yMax: 360, // bottom y (300) + height (40) + padding }); }); @@ -729,10 +729,10 @@ describe('Test EntityLineageUtils utility', () => { const bounds = getNodesBoundsReactFlow(nodes); expect(bounds).toEqual({ - xMin: 100, - yMin: 200, - xMax: 200, - yMax: 300, + xMin: 80, + yMin: 180, + xMax: 220, + yMax: 320, }); }); @@ -770,10 +770,10 @@ describe('Test EntityLineageUtils utility', () => { const bounds = getNodesBoundsReactFlow(nodes); expect(bounds).toEqual({ - xMin: -100, - yMin: -200, - xMax: 140, // rightmost x (100) + width (40) - yMax: 220, // bottom y (200) + height (20) + xMin: -120, // x - padding + yMin: -220, // y - padding + xMax: 160, // rightmost x (100) + width (40) + padding + yMax: 240, // bottom y (200) + height (20) + padding }); }); }); @@ -796,9 +796,9 @@ describe('Test EntityLineageUtils utility', () => { ); expect(viewport).toEqual({ - x: 0, - y: 0, - zoom: 2, + x: 20, + y: 20, + zoom: 1.1428571428571428, }); }); @@ -821,9 +821,9 @@ describe('Test EntityLineageUtils utility', () => { ); expect(viewport).toEqual({ - x: 100, - y: 100, - zoom: 1, + x: 110, + y: 97.5, + zoom: 0.75, }); }); @@ -844,9 +844,9 @@ describe('Test EntityLineageUtils utility', () => { ); expect(viewport).toEqual({ - x: 200, - y: 200, - zoom: 2, + x: 170, + y: 170, + zoom: 1.5, }); }); @@ -867,9 +867,9 @@ describe('Test EntityLineageUtils utility', () => { ); expect(viewport).toEqual({ - x: 0, - y: 50, - zoom: 0.5, + x: 20, + y: 56.36363636363636, + zoom: 0.36363636363636365, }); }); @@ -890,9 +890,9 @@ describe('Test EntityLineageUtils utility', () => { ); expect(viewport).toEqual({ - x: NaN, - y: NaN, - zoom: Infinity, + x: 20, + y: 20, + zoom: 4, }); }); @@ -915,9 +915,9 @@ describe('Test EntityLineageUtils utility', () => { ); expect(viewport).toEqual({ - x: -100, - y: -100, - zoom: 4, + x: -60, + y: -60, + zoom: 2.2857142857142856, }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx index 258b01c0e85..32df137bd49 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityLineageUtils.tsx @@ -278,12 +278,15 @@ export const getELKLayoutedElements = async ( return { ...node, position: { x: layoutedNode?.x ?? 0, y: layoutedNode?.y ?? 0 }, + // layoutedNode contains the total height of the node including the children height + // Needed to calculate the bounds height of the nodes in the export + height: layoutedNode?.height ?? node.height, hidden: false, }; }); return { nodes: updatedNodes, edges: edges ?? [] }; - } catch (error) { + } catch { return { nodes: [], edges: [] }; } }; @@ -1725,10 +1728,16 @@ export const getNodesBoundsReactFlow = (nodes: Node[]) => { nodes.forEach((node) => { const { x, y } = node.position; - bounds.xMin = Math.min(bounds.xMin, x); - bounds.yMin = Math.min(bounds.yMin, y); - bounds.xMax = Math.max(bounds.xMax, x + (node.width ?? 0)); - bounds.yMax = Math.max(bounds.yMax, y + (node.height ?? 0)); + const width = node.width ?? 0; + const height = node.height ?? 0; + + // Add padding to ensure nodes are fully visible + const padding = 20; + + bounds.xMin = Math.min(bounds.xMin, x - padding); + bounds.yMin = Math.min(bounds.yMin, y - padding); + bounds.xMax = Math.max(bounds.xMax, x + width + padding); + bounds.yMax = Math.max(bounds.yMax, y + height + padding); }); return bounds; @@ -1744,13 +1753,23 @@ export const getViewportForBoundsReactFlow = ( const width = bounds.xMax - bounds.xMin; const height = bounds.yMax - bounds.yMin; - // Scale the image to fit the container + // Add extra padding to ensure content is fully visible + const padding = 20; + const paddedWidth = width + padding * 2; + const paddedHeight = height + padding * 2; + + // Scale the image to fit the container while maintaining aspect ratio const scale = - Math.min(imageWidth / width, imageHeight / height) * scaleFactor; + Math.min( + (imageWidth - padding * 2) / paddedWidth, + (imageHeight - padding * 2) / paddedHeight + ) * scaleFactor; // Calculate translation to center the flow - const translateX = (imageWidth - width * scale) / 2 - bounds.xMin * scale; - const translateY = (imageHeight - height * scale) / 2 - bounds.yMin * scale; + const translateX = + (imageWidth - paddedWidth * scale) / 2 - bounds.xMin * scale; + const translateY = + (imageHeight - paddedHeight * scale) / 2 - bounds.yMin * scale; return { x: translateX, y: translateY, zoom: scale }; }; @@ -1766,8 +1785,13 @@ export const getViewportForLineageExport = ( const nodesBounds = getNodesBoundsReactFlow(nodes); - // Calculate the viewport to fit all nodes - return getViewportForBoundsReactFlow(nodesBounds, imageWidth, imageHeight); + // Calculate the viewport to fit all nodes with padding + return getViewportForBoundsReactFlow( + nodesBounds, + imageWidth, + imageHeight, + 0.9 + ); // Scale down slightly to ensure padding }; export const getLineageEntityExclusionFilter = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Export/ExportUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Export/ExportUtils.ts index d48b3d1a804..01b2b57de07 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Export/ExportUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Export/ExportUtils.ts @@ -54,6 +54,8 @@ export const exportPNGImageFromElement = async (exportData: ExportData) => { backgroundColor: '#ffffff', width: imageWidth + padding * 2, height: imageHeight + padding * 2, + pixelRatio: 3, + quality: 1.0, style: { width: imageWidth.toString(), height: imageHeight.toString(),