github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/components/dashboards/graphs/common/useGraph.tsx (about)

     1  import difference from "lodash/difference";
     2  import useDeepCompareEffect from "use-deep-compare-effect";
     3  import usePrevious from "../../../../hooks/usePrevious";
     4  import useTemplateRender from "../../../../hooks/useTemplateRender";
     5  import {
     6    createContext,
     7    ReactNode,
     8    useContext,
     9    useEffect,
    10    useState,
    11  } from "react";
    12  import { Edge, Node, useReactFlow } from "reactflow";
    13  import { FoldedNode, RowRenderResult } from "../../common/types";
    14  import { noop } from "../../../../utils/func";
    15  import { useDashboard } from "../../../../hooks/useDashboard";
    16  import { v4 as uuid } from "uuid";
    17  
    18  export type ExpandedNodeInfo = {
    19    category: string;
    20    foldedNodes: FoldedNode[];
    21  };
    22  
    23  export type ExpandedNodes = {
    24    [nodeId: string]: ExpandedNodeInfo;
    25  };
    26  
    27  type IGraphContext = {
    28    collapseNodes: (foldedNodes: FoldedNode[]) => void;
    29    expandNode: (foldedNodes: FoldedNode[], category: string) => void;
    30    expandedNodes: ExpandedNodes;
    31    layoutId: string;
    32    recalcLayout: () => void;
    33    renderResults: RowRenderResult;
    34    setGraphEdges: (edges: Edge[]) => void;
    35    setGraphNodes: (nodes: Node[]) => void;
    36  };
    37  
    38  const GraphContext = createContext<IGraphContext>({
    39    collapseNodes: noop,
    40    expandNode: noop,
    41    expandedNodes: {},
    42    layoutId: "",
    43    recalcLayout: noop,
    44    renderResults: {},
    45    setGraphEdges: noop,
    46    setGraphNodes: noop,
    47  });
    48  
    49  type PreviousNodesAndEdges = {
    50    nodes: Node[];
    51    edges: Edge[];
    52  };
    53  
    54  type CategoryNodeMap = {
    55    [category: string]: Node[];
    56  };
    57  
    58  const GraphProvider = ({ children }: { children: ReactNode }) => {
    59    const {
    60      themeContext: { theme },
    61    } = useDashboard();
    62    const { fitView } = useReactFlow();
    63    const { ready: templateRenderReady, renderTemplates } = useTemplateRender();
    64    const [layoutId, setLayoutId] = useState(uuid());
    65    const [graphEdges, setGraphEdges] = useState<Edge[]>([]);
    66    const [graphNodes, setGraphNodes] = useState<Node[]>([]);
    67    const [expandedNodes, setExpandedNodes] = useState<ExpandedNodes>({});
    68    const [renderResults, setRenderResults] = useState<RowRenderResult>({});
    69  
    70    const previousNodesAndEdges = usePrevious<PreviousNodesAndEdges>({
    71      nodes: graphNodes,
    72      edges: graphEdges,
    73    });
    74  
    75    useDeepCompareEffect(() => {
    76      if (!templateRenderReady) {
    77        return;
    78      }
    79  
    80      const doRender = async () => {
    81        const nodesWithHrefs = graphNodes.filter(
    82          (n) => n.data && !n.data.isFolded && !!n.data.href
    83        );
    84        const nodesByCategory: CategoryNodeMap = {};
    85        for (const node of nodesWithHrefs) {
    86          const category = node?.data?.category?.name || null;
    87          if (!category) {
    88            // What to do? We have no category for this node
    89            continue;
    90          }
    91          nodesByCategory[category] = nodesByCategory[category] || [];
    92          nodesByCategory[category].push(node);
    93        }
    94  
    95        const renderResults: RowRenderResult = {};
    96  
    97        for (const [category, nodes] of Object.entries(nodesByCategory)) {
    98          const hrefTemplate = nodes[0].data.href;
    99          const results = await renderTemplates(
   100            { [category]: hrefTemplate },
   101            nodes.map((n) => n.data.row_data || {})
   102          );
   103          for (let nodeIdx = 0; nodeIdx < nodes.length; nodeIdx++) {
   104            const node = nodes[nodeIdx];
   105            if (!node.id) {
   106              continue;
   107            }
   108            renderResults[node.id] = results[nodeIdx][category];
   109          }
   110        }
   111        setRenderResults(renderResults);
   112      };
   113  
   114      doRender();
   115    }, [graphNodes, renderTemplates, templateRenderReady]);
   116  
   117    // When the edges or nodes change, update the layout
   118    useEffect(() => {
   119      if (!fitView || (!graphEdges && !graphNodes) || !previousNodesAndEdges) {
   120        return;
   121      }
   122      const previousNodeIds = previousNodesAndEdges.nodes.map((n) => n.id);
   123      const currentNodeIds = graphNodes.map((n) => n.id);
   124      const previousEdgeIds = previousNodesAndEdges.edges.map((e) => e.id);
   125      const currentEdgeIds = graphEdges.map((e) => e.id);
   126      const expandedNodesKeys = Object.keys(expandedNodes);
   127      const differentNodeIdsOldToNew = difference(
   128        previousNodeIds,
   129        currentNodeIds
   130      );
   131      const differentNodeIdsOldToNewAllFoldNodes =
   132        differentNodeIdsOldToNew.length > 0 &&
   133        differentNodeIdsOldToNew.every((n) => n.startsWith("fold-node."));
   134      const differentNodeIdsNewToOld = difference(
   135        currentNodeIds,
   136        previousNodeIds
   137      );
   138      const differentNodeIdsNewToOldAllFoldNodes =
   139        differentNodeIdsNewToOld.length > 0 &&
   140        differentNodeIdsNewToOld.every((n) => n.startsWith("fold-node."));
   141      const differentNodeIdsNewToOldWithoutExpanded = difference(
   142        differentNodeIdsNewToOld,
   143        expandedNodesKeys
   144      );
   145      const differentEdgeIdsOldToNew = difference(
   146        previousEdgeIds,
   147        currentEdgeIds
   148      );
   149      const differentEdgeIdsNewToOld = difference(
   150        currentEdgeIds,
   151        previousEdgeIds
   152      );
   153      if (
   154        !differentNodeIdsOldToNewAllFoldNodes &&
   155        !differentNodeIdsNewToOldAllFoldNodes &&
   156        (differentNodeIdsOldToNew.length > 0 ||
   157          differentNodeIdsNewToOldWithoutExpanded.length > 0 ||
   158          differentEdgeIdsOldToNew.length > 0 ||
   159          differentEdgeIdsNewToOld.length > 0)
   160      ) {
   161        fitView();
   162      }
   163    }, [previousNodesAndEdges, expandedNodes, graphEdges, graphNodes, fitView]);
   164  
   165    // This is annoying, but unless I force a refresh the theme doesn't stay in sync when you switch
   166    useEffect(() => setLayoutId(uuid()), [theme.name]);
   167  
   168    const recalcLayout = () => {
   169      setExpandedNodes({});
   170      setLayoutId(uuid());
   171    };
   172  
   173    const collapseNodes = (foldedNodes: FoldedNode[] = []) => {
   174      setExpandedNodes((current) => {
   175        const newExpandedNodes = { ...current };
   176        for (const foldedNode of foldedNodes) {
   177          delete newExpandedNodes[foldedNode.id];
   178        }
   179        return newExpandedNodes;
   180      });
   181      setLayoutId(uuid());
   182    };
   183  
   184    const expandNode = (foldedNodes: FoldedNode[] = [], category: string) => {
   185      setExpandedNodes((current) => {
   186        const newExpandedNodes = { ...current };
   187        for (const foldedNode of foldedNodes) {
   188          newExpandedNodes[foldedNode.id] = {
   189            category,
   190            foldedNodes,
   191          };
   192        }
   193        return newExpandedNodes;
   194      });
   195      setLayoutId(uuid());
   196    };
   197  
   198    return (
   199      <GraphContext.Provider
   200        value={{
   201          collapseNodes,
   202          expandNode,
   203          expandedNodes,
   204          layoutId,
   205          recalcLayout,
   206          renderResults,
   207          setGraphEdges,
   208          setGraphNodes,
   209        }}
   210      >
   211        {children}
   212      </GraphContext.Provider>
   213    );
   214  };
   215  
   216  const useGraph = () => {
   217    const context = useContext(GraphContext);
   218    if (context === undefined) {
   219      throw new Error("useGraph must be used within a GraphContext");
   220    }
   221    return context as IGraphContext;
   222  };
   223  
   224  export { GraphProvider, useGraph };