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 };