Browse Source

improve algorithm performance

AykutSarac 2 years ago
parent
commit
80a02d79ba

+ 32 - 55
src/components/Graph/index.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
-import { Canvas, Edge, ElkRoot } from "reaflow";
+import { Canvas, Edge, ElkRoot, getParentsForNodeId } from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
 import useGraph from "src/store/useGraph";
 import useUser from "src/store/useUser";
@@ -12,9 +12,6 @@ import { PremiumView } from "./PremiumView";
 interface GraphProps {
   isWidget?: boolean;
   openNodeModal: () => void;
-  setSelectedNode: (node: [string, string][]) => void;
-  openEdgeModal: () => void;
-  setSelectedEdge: (edgeInfo: string) => void;
 }
 
 const StyledEditorWrapper = styled.div<{ widget: boolean }>`
@@ -44,17 +41,12 @@ const StyledEditorWrapper = styled.div<{ widget: boolean }>`
   }
 `;
 
-const GraphComponent = ({
-  isWidget = false,
-  openNodeModal,
-  setSelectedNode,
-  openEdgeModal,
-  setSelectedEdge,
-}: GraphProps) => {
+const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
   const isPremium = useUser(state => state.isPremium);
   const setLoading = useGraph(state => state.setLoading);
   const setZoomPanPinch = useGraph(state => state.setZoomPanPinch);
   const centerView = useGraph(state => state.centerView);
+  const setSelectedNode = useGraph(state => state.setSelectedNode);
 
   const loading = useGraph(state => state.loading);
   const direction = useGraph(state => state.direction);
@@ -67,70 +59,57 @@ const GraphComponent = ({
   });
 
   const handleNodeClick = React.useCallback(
-    (e: React.MouseEvent<SVGElement>, data: NodeData) => {
-      if (setSelectedNode) setSelectedNode(data.text);
-      if (openNodeModal) openNodeModal();
-    },
-    [openNodeModal, setSelectedNode]
-  );
+    (_: React.MouseEvent<SVGElement>, data: NodeData) => {
+      let resolvedPath = "";
+      const parentIds = getParentsForNodeId(nodes, edges, data.id).map(n => n.id);
+      const path = parentIds.reverse().concat(data.id);
+      const rootArrayElementIds = ["1"];
+      const edgesMap = new Map();
 
-  const handleEdgeClick = React.useCallback(
-    (e: React.MouseEvent<SVGElement>, data: EdgeData) => {
-      let curFromId = data.from;
-      let curToId = data.to;
-      const path = [curFromId, curToId];
-      while (curFromId !== "1") {
-        curToId = curFromId;
-        curFromId = edges.find(({ to }) => to === curFromId)?.from;
-        if (curFromId === undefined) {
-          break;
+      for (const edge of edges) {
+        if (!edgesMap.has(edge.from!)) {
+          edgesMap.set(edge.from!, []);
         }
-        path.unshift(curFromId);
+        edgesMap.get(edge.from!).push(edge.to);
       }
-      const rootArrayElementIds = ["1"];
-      const edgeLength = edges.length;
-      for (let i = 1; i < edgeLength; i++) {
+
+      for (let i = 1; i < edges.length; i++) {
         const curNodeId = edges[i].from!;
-        if (rootArrayElementIds.find(id => id === curNodeId)) continue;
-        let isRootArrayElement = true;
-        for (let j = i - 1; j >= 0; j--) {
-          if (curNodeId === edges[j]?.to) {
-            isRootArrayElement = false;
-          }
-        }
-        if (isRootArrayElement) {
+        if (rootArrayElementIds.includes(curNodeId)) continue;
+        if (!edgesMap.has(curNodeId)) {
           rootArrayElementIds.push(curNodeId);
         }
       }
-      const pathLength = path.length;
-      let resolvedPath = "";
+
       if (rootArrayElementIds.length > 1) {
         resolvedPath += `Root[${rootArrayElementIds.findIndex(id => id === path[0])}]`;
       } else {
         resolvedPath += "{Root}";
       }
-      for (let i = 1; i < pathLength; i++) {
+
+      for (let i = 1; i < path.length; i++) {
         const curId = path[i];
-        if (curId === undefined) break;
         const curNode = nodes[+curId - 1];
-        if (curNode?.data.parent === "array") {
+
+        if (!curNode) break;
+        if (curNode.data.parent === "array") {
           resolvedPath += `.${curNode.text}`;
-          if (i !== pathLength - 1) {
-            const idx = edges
-              .filter(({ from }) => from === curId)
-              .findIndex(({ to }) => to === path[i + 1]);
+          if (i !== path.length - 1) {
+            const toNodeId = path[i + 1];
+            const idx = edgesMap.get(curId).indexOf(toNodeId);
             resolvedPath += `[${idx}]`;
           }
         }
-        if (curNode?.data.parent === "object") {
+
+        if (curNode.data.parent === "object") {
           resolvedPath += `.${curNode.text}`;
         }
       }
 
-      if (setSelectedEdge) setSelectedEdge(resolvedPath);
-      if (openEdgeModal) openEdgeModal();
+      if (setSelectedNode) setSelectedNode({ node: data.text, path: resolvedPath });
+      if (openNodeModal) openNodeModal();
     },
-    [nodes, edges]
+    [edges, nodes, openNodeModal, setSelectedNode]
   );
 
   const onInit = React.useCallback(
@@ -214,9 +193,7 @@ const GraphComponent = ({
             fit={true}
             key={direction}
             node={props => <CustomNode {...props} onClick={handleNodeClick} />}
-            edge={props => (
-              <Edge {...props} onClick={handleEdgeClick} containerClassName={`edge-${props.id}`} />
-            )}
+            edge={props => <Edge {...props} containerClassName={`edge-${props.id}`} />}
           />
         </TransformComponent>
       </TransformWrapper>

+ 2 - 22
src/containers/Editor/LiveEditor/GraphCanvas.tsx

@@ -1,20 +1,15 @@
 import React from "react";
 import { Graph } from "src/components/Graph";
-import { EdgeModal } from "src/containers/Modals/EdgeModal";
 import { NodeModal } from "src/containers/Modals/NodeModal";
 import useGraph from "src/store/useGraph";
 
 export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
   const [isNodeModalVisible, setNodeModalVisible] = React.useState(false);
-  const [selectedNode, setSelectedNode] = React.useState<[string, string][]>([]);
-  const [isEdgeModalVisible, setEdgeModalVisible] = React.useState(false);
-  const [selectedEdge, setSelectedEdge] = React.useState("");
 
   const collapsedNodes = useGraph(state => state.collapsedNodes);
   const collapsedEdges = useGraph(state => state.collapsedEdges);
 
   const openNodeModal = React.useCallback(() => setNodeModalVisible(true), []);
-  const openEdgeModal = React.useCallback(() => setEdgeModalVisible(true), []);
 
   React.useEffect(() => {
     const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
@@ -36,23 +31,8 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
 
   return (
     <>
-      <Graph
-        openNodeModal={openNodeModal}
-        setSelectedNode={setSelectedNode}
-        openEdgeModal={openEdgeModal}
-        setSelectedEdge={setSelectedEdge}
-        isWidget={isWidget}
-      />
-      <NodeModal
-        selectedNode={selectedNode}
-        visible={isNodeModalVisible}
-        closeModal={() => setNodeModalVisible(false)}
-      />
-      <EdgeModal
-        selectedEdge={selectedEdge}
-        visible={isEdgeModalVisible}
-        closeModal={() => setEdgeModalVisible(false)}
-      />
+      <Graph openNodeModal={openNodeModal} isWidget={isWidget} />
+      <NodeModal visible={isNodeModalVisible} closeModal={() => setNodeModalVisible(false)} />
     </>
   );
 };

+ 0 - 55
src/containers/Modals/EdgeModal/index.tsx

@@ -1,55 +0,0 @@
-import React from "react";
-import dynamic from "next/dynamic";
-import toast from "react-hot-toast";
-import { FiCopy } from "react-icons/fi";
-import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
-import { vs } from "react-syntax-highlighter/dist/cjs/styles/prism";
-import { Button } from "src/components/Button";
-import { Modal } from "src/components/Modal";
-import useStored from "src/store/useStored";
-
-const SyntaxHighlighter = dynamic(
-  () => import("react-syntax-highlighter/dist/esm/prism-async").then(c => c),
-  {
-    ssr: false,
-  }
-);
-
-interface EdgeModalProps {
-  selectedEdge: string;
-  visible: boolean;
-  closeModal: () => void;
-}
-
-export const EdgeModal = ({ selectedEdge, visible, closeModal }: EdgeModalProps) => {
-  const lightmode = useStored(state => state.lightmode);
-  const handleClipboard = () => {
-    toast.success("Content copied to clipboard!");
-    navigator.clipboard.writeText(selectedEdge);
-    closeModal();
-  };
-
-  return (
-    <Modal visible={visible} setVisible={closeModal}>
-      <Modal.Header>Edge Content</Modal.Header>
-      <Modal.Content>
-        <SyntaxHighlighter
-          style={lightmode ? vs : vscDarkPlus}
-          customStyle={{
-            borderRadius: "4px",
-            fontSize: "14px",
-            margin: "0",
-          }}
-          language="javascript"
-        >
-          {selectedEdge}
-        </SyntaxHighlighter>
-      </Modal.Content>
-      <Modal.Controls setVisible={closeModal}>
-        <Button status="SECONDARY" onClick={handleClipboard}>
-          <FiCopy size={18} /> Clipboard
-        </Button>
-      </Modal.Controls>
-    </Modal>
-  );
-};

+ 43 - 8
src/containers/Modals/NodeModal/index.tsx

@@ -6,7 +6,9 @@ import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
 import { vs } from "react-syntax-highlighter/dist/cjs/styles/prism";
 import { Button } from "src/components/Button";
 import { Modal } from "src/components/Modal";
+import useGraph from "src/store/useGraph";
 import useStored from "src/store/useStored";
+import styled from "styled-components";
 
 const SyntaxHighlighter = dynamic(
   () => import("react-syntax-highlighter/dist/esm/prism-async").then(c => c),
@@ -16,25 +18,43 @@ const SyntaxHighlighter = dynamic(
 );
 
 interface NodeModalProps {
-  selectedNode: object;
   visible: boolean;
   closeModal: () => void;
 }
 
-export const NodeModal = ({ selectedNode, visible, closeModal }: NodeModalProps) => {
+const StyledTitle = styled.div`
+  line-height: 16px;
+  font-size: 12px;
+  font-weight: 600;
+  padding: 16px 0;
+
+  &:first-of-type {
+    padding-top: 0;
+  }
+`;
+
+export const NodeModal = ({ visible, closeModal }: NodeModalProps) => {
   const lightmode = useStored(state => state.lightmode);
-  const nodeData = Array.isArray(selectedNode) ? Object.fromEntries(selectedNode) : selectedNode;
+  const path = useGraph(state => state.path);
+  const nodeData = useGraph(state =>
+    Array.isArray(state.selectedNode) ? Object.fromEntries(state.selectedNode) : state.selectedNode
+  );
 
-  const handleClipboard = () => {
-    toast.success("Content copied to clipboard!");
-    navigator.clipboard?.writeText(JSON.stringify(nodeData));
-    closeModal();
+  const handleClipboard = (content: string) => {
+    try {
+      navigator.clipboard?.writeText(content);
+      toast.success("Content copied to clipboard!");
+      closeModal();
+    } catch (error) {
+      toast.error("Failed to save to clipboard.");
+    }
   };
 
   return (
     <Modal visible={visible} setVisible={closeModal} size="lg">
       <Modal.Header>Node Content</Modal.Header>
       <Modal.Content>
+        <StyledTitle>Content</StyledTitle>
         <SyntaxHighlighter
           style={lightmode ? vs : vscDarkPlus}
           customStyle={{
@@ -54,11 +74,26 @@ export const NodeModal = ({ selectedNode, visible, closeModal }: NodeModalProps)
             2
           )}
         </SyntaxHighlighter>
+        <StyledTitle>Node Path</StyledTitle>
+        <SyntaxHighlighter
+          style={lightmode ? vs : vscDarkPlus}
+          customStyle={{
+            borderRadius: "4px",
+            fontSize: "14px",
+            margin: "0",
+          }}
+          language="javascript"
+        >
+          {path}
+        </SyntaxHighlighter>
       </Modal.Content>
       <Modal.Controls setVisible={closeModal}>
-        <Button status="SECONDARY" onClick={handleClipboard}>
+        <Button status="SECONDARY" onClick={() => handleClipboard(JSON.stringify(nodeData))}>
           <FiCopy size={18} /> Clipboard
         </Button>
+        <Button status="SECONDARY" onClick={() => handleClipboard(path)}>
+          <FiCopy size={18} /> Copy Path
+        </Button>
       </Modal.Controls>
     </Modal>
   );

+ 4 - 0
src/store/useGraph.tsx

@@ -19,6 +19,8 @@ const initialStates = {
   collapsedNodes: [] as string[],
   collapsedEdges: [] as string[],
   collapsedParents: [] as string[],
+  selectedNode: [] as string | object,
+  path: "",
 };
 
 export type Graph = typeof initialStates;
@@ -37,10 +39,12 @@ interface GraphActions {
   zoomIn: () => void;
   zoomOut: () => void;
   centerView: () => void;
+  setSelectedNode: ({ node, path }: { node?: string | string[]; path?: string }) => void;
 }
 
 const useGraph = create<Graph & GraphActions>((set, get) => ({
   ...initialStates,
+  setSelectedNode: ({ node, path }) => set({ selectedNode: node, path }),
   setGraph: (data, options) => {
     const { nodes, edges } = parser(data ?? useJson.getState().json);