Browse Source

feat: fit role chart into window (#804)

* fix: role chart size issue

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix theme change update

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 3 months ago
parent
commit
f3a507133d
2 changed files with 105 additions and 37 deletions
  1. 104 37
      client/src/pages/user/D3PrivilegeTree.tsx
  2. 1 0
      client/src/pages/user/Roles.tsx

+ 104 - 37
client/src/pages/user/D3PrivilegeTree.tsx

@@ -54,9 +54,7 @@ const D3PrivilegeTree: React.FC<Props> = ({
   );
   );
 
 
   // Transform privileges data into d3.hierarchy structure
   // Transform privileges data into d3.hierarchy structure
-  const transformData = (
-    privileges: DBCollectionsPrivileges
-  ): { nodeCount: number; treeNode: TreeNode } => {
+  const transformData = (privileges: DBCollectionsPrivileges): TreeNode => {
     let nodeCount = 0;
     let nodeCount = 0;
 
 
     const res = {
     const res = {
@@ -91,22 +89,47 @@ const D3PrivilegeTree: React.FC<Props> = ({
       })),
       })),
     };
     };
 
 
-    return {
-      nodeCount,
-      treeNode: res,
-    };
+    return res;
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {
     if (!privileges) return;
     if (!privileges) return;
 
 
-    const transformedData = transformData(privileges);
+    const treeNode = transformData(privileges);
+
+    // get svg width and height by accessing dom element
+    const svgWidth = svgRef.current?.clientWidth || 0;
+    const svgHeight = svgRef.current?.clientHeight || 0;
+
+    // Calculate height based on tree structure rather than total node count
+    // Max nodes at any level would be a better indication for vertical space needed
+    const maxNodesAtLevel = Math.max(
+      1, // Role level
+      Object.keys(privileges).length, // Database level
+      ...Object.values(privileges).map(
+        db => Object.keys(db.collections).length
+      ), // Collection level
+      ...Object.values(privileges).reduce(
+        (acc, db) =>
+          acc.concat(
+            Object.values(db.collections).map(col => Object.keys(col).length)
+          ),
+        [] as number[] // Privilege level
+      )
+    );
+
+    // Increase the multiplier to provide more space between nodes
+    const nodeSpacing = 30;
+    let height =
+      maxNodesAtLevel * nodeSpacing + margin.top + margin.bottom + 120; // Added extra padding
 
 
-    const width = 1000; // Fixed SVG width
-    const defaultHeight = 580; // Default SVG height
-    // calculate height based on number of nodes
-    let height = transformedData.nodeCount * 15 + margin.top + margin.bottom;
-    if (height < 500) height = defaultHeight; // Set a minimum height
+    // Ensure minimum height for better visualization
+    height = Math.max(height, svgHeight);
+
+    // Add additional padding for large datasets
+    if (maxNodesAtLevel > 15) {
+      height += maxNodesAtLevel * 10; // Extra space for very large datasets
+    }
 
 
     const fontSize = 12; // Font size for text labels
     const fontSize = 12; // Font size for text labels
     const marginLeft = role.length * fontSize; // Adjust margin left based on role length
     const marginLeft = role.length * fontSize; // Adjust margin left based on role length
@@ -115,10 +138,10 @@ const D3PrivilegeTree: React.FC<Props> = ({
     d3.select(svgRef.current).selectAll('*').remove();
     d3.select(svgRef.current).selectAll('*').remove();
 
 
     // Create hierarchy and layout
     // Create hierarchy and layout
-    const root = d3.hierarchy<TreeNode>(transformedData.treeNode);
+    const root = d3.hierarchy<TreeNode>(treeNode);
     const treeLayout = d3
     const treeLayout = d3
       .tree<TreeNode>()
       .tree<TreeNode>()
-      .size([height, width / 2])
+      .size([height - margin.top - margin.bottom, svgWidth / 2])
       .separation((a: any, b: any) => {
       .separation((a: any, b: any) => {
         return a.parent === b.parent ? 3 : 4;
         return a.parent === b.parent ? 3 : 4;
       }); // Swap width and height for vertical layout
       }); // Swap width and height for vertical layout
@@ -136,22 +159,11 @@ const D3PrivilegeTree: React.FC<Props> = ({
       if (d.y < y0) y0 = d.y;
       if (d.y < y0) y0 = d.y;
     });
     });
 
 
-    // Calculate translateY to center the tree vertically
-    const treeHeight = x1 - x0;
-    const translateY = (height - treeHeight) / 2 - x0;
-
-    // Create SVG container
-    const svg = d3
-      .select(svgRef.current)
-      .attr('width', width)
-      .attr('height', height)
-      .attr('viewBox', [0, 0, width, height].join(' ')) // Add viewBox for scaling
-      .attr('style', 'max-width: 100%; height: auto;'); // Make SVG responsive
+    // Create SVG container with expanded height
+    const svg = d3.select(svgRef.current);
 
 
     // Add a group for zoomable content
     // Add a group for zoomable content
-    const g = svg
-      .append('g')
-      .attr('transform', `translate(${marginLeft}, ${translateY})`);
+    const g = svg.append('g').attr('transform', `translate(0, ${margin.top})`); // Add top margin
     gRef.current = g.node();
     gRef.current = g.node();
 
 
     const colorMap: { [key: string]: any } = {
     const colorMap: { [key: string]: any } = {
@@ -220,21 +232,74 @@ const D3PrivilegeTree: React.FC<Props> = ({
           : d.data.name
           : d.data.name
       );
       );
 
 
+    // Calculate scale to fit the entire tree
+    const treeWidth = y1 - y0 + marginLeft + 50; // Add some padding
+    const treeHeight = x1 - x0 + 60; // Increased padding
+    const scaleX = svgWidth / treeWidth;
+    const scaleY = svgHeight / treeHeight;
+    const scale = Math.min(scaleX, scaleY, 0.95); // Slightly reduce to ensure visibility
+
+    // Calculate translation to center the tree
+    const centerX = (svgWidth - treeWidth * scale) / 2 - 60;
+    const centerY =
+      (svgHeight - treeHeight * scale) / 2 - x0 * scale + margin.top;
+
     // Add zoom functionality
     // Add zoom functionality
     const zoom: any = d3
     const zoom: any = d3
       .zoom()
       .zoom()
-      .scaleExtent([0.5, 3]) // Set zoom limits
+      .scaleExtent([scale, 4]) // Set minimum zoom to fit entire tree
       .on('zoom', event => {
       .on('zoom', event => {
-        g.attr(
-          'transform',
-          `translate(${marginLeft}, ${translateY}) ${event.transform}`
-        );
+        g.attr('transform', event.transform);
       });
       });
 
 
     svg.call(zoom); // Apply zoom to the SVG
     svg.call(zoom); // Apply zoom to the SVG
 
 
-    svg.transition().duration(0).call(zoom.transform, d3.zoomIdentity);
-  }, [JSON.stringify({ privileges, margin, theme, groupPrivileges, role })]);
+    // Apply initial transform to show the entire tree
+    svg
+      .transition()
+      .duration(250)
+      .call(
+        zoom.transform,
+        d3.zoomIdentity.translate(centerX, centerY).scale(scale)
+      );
+  }, [JSON.stringify({ privileges, margin, groupPrivileges, role })]);
+
+  // Update colors on theme change
+  useEffect(() => {
+    const updateColors = () => {
+      const colorMap: { [key: string]: any } = {
+        role: theme.palette.primary.dark,
+        database: theme.palette.primary.dark,
+        collection: theme.palette.primary.dark,
+      };
+
+      // Update link colors
+      d3.select(svgRef.current)
+        .selectAll('.link')
+        .attr('stroke', (d: any) => colorMap[d.source.data.type!]);
+
+      // Update node colors
+      d3.select(svgRef.current)
+        .selectAll('.node circle')
+        .attr('stroke', theme.palette.primary.main)
+        .attr('fill', (d: any, index) =>
+          d.children || index == 0 || groupPrivileges.has(d.data.name)
+            ? `${theme.palette.primary.main}`
+            : `transparent`
+        );
+
+      // Update text colors
+      d3.select(svgRef.current)
+        .selectAll('.node text')
+        .attr('fill', (d: any) =>
+          groupPrivileges.has(d.data.name)
+            ? `${theme.palette.primary.dark}`
+            : `${theme.palette.text.primary}`
+        );
+    };
+
+    updateColors();
+  }, [theme]);
 
 
   // UI handler
   // UI handler
   const handleDownload = () => {
   const handleDownload = () => {
@@ -264,6 +329,8 @@ const D3PrivilegeTree: React.FC<Props> = ({
         flexDirection: 'column',
         flexDirection: 'column',
         gap: '16px',
         gap: '16px',
         position: 'relative',
         position: 'relative',
+        width: '100%',
+        height: '100%',
       }}
       }}
     >
     >
       <CustomButton
       <CustomButton
@@ -278,7 +345,7 @@ const D3PrivilegeTree: React.FC<Props> = ({
       >
       >
         {btnTrans('downloadChart')}
         {btnTrans('downloadChart')}
       </CustomButton>
       </CustomButton>
-      <svg ref={svgRef}></svg>
+      <svg ref={svgRef} style={{ width: '100%', height: '100%' }}></svg>
     </Box>
     </Box>
   );
   );
 };
 };

+ 1 - 0
client/src/pages/user/Roles.tsx

@@ -35,6 +35,7 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
   },
   tree: {
   tree: {
     overflow: 'auto',
     overflow: 'auto',
+    width: 'calc(84% - 16px)',
   },
   },
   chip: {
   chip: {
     marginBottom: theme.spacing(0.5),
     marginBottom: theme.spacing(0.5),