github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/components/dashboards/common/useNodeAndEdgeData.ts (about) 1 import has from "lodash/has"; 2 import isNumber from "lodash/isNumber"; 3 import useChartThemeColors from "../../../hooks/useChartThemeColors"; 4 import { 5 Category, 6 CategoryMap, 7 EdgeProperties, 8 KeyValueStringPairs, 9 NodeAndEdgeProperties, 10 NodeProperties, 11 } from "./types"; 12 import { 13 DashboardRunState, 14 DependencyPanelProperties, 15 PanelsMap, 16 } from "../../../types"; 17 import { getColorOverride } from "./index"; 18 import { 19 NodeAndEdgeData, 20 NodeAndEdgeDataColumn, 21 NodeAndEdgeDataFormat, 22 NodeAndEdgeDataRow, 23 NodeAndEdgeStatus, 24 WithStatusMap, 25 } from "../graphs/types"; 26 import { useDashboard } from "../../../hooks/useDashboard"; 27 import { useMemo } from "react"; 28 29 // Categories may be sourced from a node, an edge, a flow, a graph or a hierarchy 30 // A node or edge can define exactly 1 category, which covers all rows of data that don't define a category 31 // Any node/edge/flow/graph/hierarchy data row can define a category - in that event, the category must come from the category map for the composing resource 32 33 const getNodeAndEdgeDataFormat = ( 34 properties: NodeAndEdgeProperties | undefined 35 ): NodeAndEdgeDataFormat => { 36 if (!properties) { 37 return "LEGACY"; 38 } 39 40 if (!properties.nodes && !properties.edges) { 41 return "LEGACY"; 42 } 43 44 if ( 45 (properties.nodes && properties.nodes.length > 0) || 46 (properties.edges && properties.edges.length > 0) 47 ) { 48 return "NODE_AND_EDGE"; 49 } 50 51 return "LEGACY"; 52 }; 53 54 const addColumnsForResource = ( 55 columns: NodeAndEdgeDataColumn[], 56 data: NodeAndEdgeData 57 ): NodeAndEdgeDataColumn[] => { 58 // Get a union of all the columns across all nodes 59 const newColumns = [...columns]; 60 for (const column of data.columns || []) { 61 if (newColumns.some((c) => c.name === column.name)) { 62 continue; 63 } 64 newColumns.push(column); 65 } 66 return newColumns; 67 }; 68 69 const populateCategoryWithDefaults = ( 70 category: Category, 71 themeColors: KeyValueStringPairs 72 ): Category => { 73 return { 74 name: category.name, 75 color: getColorOverride(category.color, themeColors), 76 depth: category.depth, 77 properties: category.properties, 78 fold: { 79 threshold: 80 category.fold && isNumber(category.fold.threshold) 81 ? category.fold.threshold 82 : 3, 83 title: category.fold?.title || category.title, 84 icon: category.fold?.icon || category.icon, 85 }, 86 href: category.href, 87 icon: category.icon, 88 title: category.title, 89 }; 90 }; 91 92 const emptyPanels: PanelsMap = {}; 93 94 const addPanelWithsStatus = ( 95 panelsMap: PanelsMap, 96 dependencies: string[] | undefined, 97 withLookup: KeyValueStringPairs, 98 withStatuses: WithStatusMap 99 ) => { 100 for (const dependency of dependencies || []) { 101 // If we've already logged the status of this with, carry on 102 if (withLookup[dependency] || dependency.indexOf(".with.") === -1) { 103 continue; 104 } 105 106 const dependencyPanel = panelsMap[dependency]; 107 if (!dependencyPanel) { 108 continue; 109 } 110 const dependencyPanelProperties = 111 dependencyPanel.properties as DependencyPanelProperties; 112 withLookup[dependency] = dependencyPanelProperties.name; 113 withStatuses[dependencyPanelProperties.name] = { 114 id: dependencyPanelProperties.name, 115 title: dependencyPanel.title, 116 state: dependencyPanel.status || "initialized", 117 error: dependencyPanel.error, 118 }; 119 } 120 }; 121 122 // This function will normalise both the legacy and node/edge data formats into a data table. 123 // In the node/edge approach, the data will be spread out across the node and edge resources 124 // until the flow/graph/hierarchy has completed, at which point we'll have a populated data 125 // table in the parent resource. 126 const useNodeAndEdgeData = ( 127 data: NodeAndEdgeData | undefined, 128 properties: NodeAndEdgeProperties | undefined, 129 status: DashboardRunState 130 ) => { 131 const { panelsMap } = useDashboard(); 132 const themeColors = useChartThemeColors(); 133 const dataFormat = getNodeAndEdgeDataFormat(properties); 134 const panels = useMemo(() => { 135 if (dataFormat === "LEGACY") { 136 return emptyPanels; 137 } 138 return panelsMap; 139 }, [panelsMap, dataFormat]); 140 141 return useMemo(() => { 142 if (dataFormat === "LEGACY") { 143 if (status === "complete") { 144 const categories: CategoryMap = {}; 145 146 // Set defaults on categories 147 for (const [name, category] of Object.entries( 148 properties?.categories || {} 149 )) { 150 categories[name] = populateCategoryWithDefaults( 151 category, 152 themeColors 153 ); 154 } 155 156 return data ? { categories, data, dataFormat, properties } : null; 157 } 158 return null; 159 } 160 161 // We've now established that it's a NODE_AND_EDGE format data set, so let's build 162 // what we need from the component parts 163 164 let columns: NodeAndEdgeDataColumn[] = []; 165 let rows: NodeAndEdgeDataRow[] = []; 166 const categories: CategoryMap = {}; 167 const withNameLookup: KeyValueStringPairs = {}; 168 169 // Add flow/graph/hierarchy level categories 170 for (const [name, category] of Object.entries( 171 properties?.categories || {} 172 )) { 173 categories[name] = populateCategoryWithDefaults(category, themeColors); 174 } 175 176 const missingNodes = {}; 177 const missingEdges = {}; 178 const nodeAndEdgeStatus: NodeAndEdgeStatus = { 179 withs: {}, 180 nodes: [], 181 edges: [], 182 }; 183 const nodeIdLookup = {}; 184 185 // Loop over all the node names and check out their respective panel in the panels map 186 for (const nodePanelName of properties?.nodes || []) { 187 const panel = panels[nodePanelName]; 188 189 // Capture missing panels - we'll deal with that after 190 if (!panel) { 191 missingNodes[nodePanelName] = true; 192 continue; 193 } 194 195 // Capture the status of any with blocks that this node depends on 196 addPanelWithsStatus( 197 panels, 198 panel.dependencies, 199 withNameLookup, 200 nodeAndEdgeStatus.withs 201 ); 202 203 const typedPanelData = (panel.data || {}) as NodeAndEdgeData; 204 columns = addColumnsForResource(columns, typedPanelData); 205 const nodeProperties = (panel.properties || {}) as NodeProperties; 206 const nodeDataRows = typedPanelData.rows || []; 207 208 // Capture the status of this node resource 209 nodeAndEdgeStatus.nodes.push({ 210 id: panel.title || nodeProperties.name, 211 state: panel.status || "initialized", 212 category: nodeProperties.category, 213 error: panel.error, 214 title: panel.title, 215 dependencies: panel.dependencies, 216 }); 217 218 let nodeCategory: Category | null = null; 219 let nodeCategoryId: string = ""; 220 if (nodeProperties.category) { 221 nodeCategory = populateCategoryWithDefaults( 222 nodeProperties.category, 223 themeColors 224 ); 225 nodeCategoryId = `node.${nodePanelName}.${nodeCategory.name}`; 226 categories[nodeCategoryId] = nodeCategory; 227 } 228 229 // Loop over each row and ensure we have the correct category information set for it 230 for (const row of nodeDataRows) { 231 // Ensure each row has an id 232 if (row.id === null || row.id === undefined) { 233 continue; 234 } 235 236 const updatedRow = { ...row }; 237 238 // Ensure the row has a title and populate from the node if not set 239 if (!updatedRow.title && panel.title) { 240 updatedRow.title = panel.title; 241 } 242 243 // Capture the ID of each row 244 nodeIdLookup[row.id.toString()] = row; 245 246 // If the row specifies a category and it's the same now as the node specified, 247 // then update the category to the artificial node category ID 248 if (updatedRow.category && nodeCategory?.name === updatedRow.category) { 249 updatedRow.category = nodeCategoryId; 250 } 251 // Else if the row has a category, but we don't know about it, clear it 252 else if (updatedRow.category && !categories[updatedRow.category]) { 253 updatedRow.category = undefined; 254 } else if (!updatedRow.category && nodeCategoryId) { 255 updatedRow.category = nodeCategoryId; 256 } 257 rows.push(updatedRow); 258 } 259 } 260 261 // Loop over all the edge names and check out their respective panel in the panels map 262 for (const edgePanelName of properties?.edges || []) { 263 const panel = panels[edgePanelName]; 264 265 // Capture missing panels - we'll deal with that after 266 if (!panel) { 267 missingEdges[edgePanelName] = true; 268 continue; 269 } 270 271 // Capture the status of any with blocks that this edge depends on 272 addPanelWithsStatus( 273 panels, 274 panel.dependencies, 275 withNameLookup, 276 nodeAndEdgeStatus.withs 277 ); 278 279 const typedPanelData = (panel.data || {}) as NodeAndEdgeData; 280 columns = addColumnsForResource(columns, typedPanelData); 281 const edgeProperties = (panel.properties || {}) as EdgeProperties; 282 283 // Capture the status of this edge resource 284 nodeAndEdgeStatus.edges.push({ 285 id: panel.title || edgeProperties.name, 286 state: panel.status || "initialized", 287 category: edgeProperties.category, 288 error: panel.error, 289 title: panel.title, 290 dependencies: panel.dependencies, 291 }); 292 293 let edgeCategory: Category | null = null; 294 let edgeCategoryId: string = ""; 295 if (edgeProperties.category) { 296 edgeCategory = populateCategoryWithDefaults( 297 edgeProperties.category, 298 themeColors 299 ); 300 edgeCategoryId = `edge.${edgePanelName}.${edgeCategory.name}`; 301 categories[edgeCategoryId] = edgeCategory; 302 } 303 304 for (const row of typedPanelData.rows || []) { 305 // Ensure the node this edge points to exists in the data set 306 // @ts-ignore 307 const from_id = 308 has(row, "from_id") && 309 row.from_id !== null && 310 row.from_id !== undefined 311 ? row.from_id.toString() 312 : null; 313 // @ts-ignore 314 const to_id = 315 has(row, "to_id") && row.to_id !== null && row.to_id !== undefined 316 ? row.to_id.toString() 317 : null; 318 if ( 319 !from_id || 320 !to_id || 321 !nodeIdLookup[from_id] || 322 !nodeIdLookup[to_id] 323 ) { 324 continue; 325 } 326 327 const updatedRow = { ...row }; 328 329 // Ensure the row has a title and populate from the edge if not set 330 if (!updatedRow.title && panel.title) { 331 updatedRow.title = panel.title; 332 } 333 334 // If the row specifies a category and it's the same now as the edge specified, 335 // then update the category to the artificial edge category ID 336 if (updatedRow.category && edgeCategory?.name === updatedRow.category) { 337 updatedRow.category = edgeCategoryId; 338 } 339 // Else if the row has a category, but we don't know about it, clear it 340 else if (updatedRow.category && !categories[updatedRow.category]) { 341 updatedRow.category = undefined; 342 } else if (!updatedRow.category && edgeCategoryId) { 343 updatedRow.category = edgeCategoryId; 344 } 345 rows.push(updatedRow); 346 } 347 } 348 349 return { 350 categories, 351 data: { columns, rows }, 352 dataFormat, 353 properties, 354 status: nodeAndEdgeStatus, 355 }; 356 }, [data, dataFormat, panels, properties, status, themeColors]); 357 }; 358 359 export default useNodeAndEdgeData; 360 361 export { getNodeAndEdgeDataFormat };