github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/hooks/useCheckGrouping.tsx (about) 1 import BenchmarkNode from "../components/dashboards/check/common/node/BenchmarkNode"; 2 import ControlEmptyResultNode from "../components/dashboards/check/common/node/ControlEmptyResultNode"; 3 import ControlErrorNode from "../components/dashboards/check/common/node/ControlErrorNode"; 4 import ControlNode from "../components/dashboards/check/common/node/ControlNode"; 5 import ControlResultNode from "../components/dashboards/check/common/node/ControlResultNode"; 6 import ControlRunningNode from "../components/dashboards/check/common/node/ControlRunningNode"; 7 import KeyValuePairNode from "../components/dashboards/check/common/node/KeyValuePairNode"; 8 import RootNode from "../components/dashboards/check/common/node/RootNode"; 9 import usePrevious from "./usePrevious"; 10 import { 11 CheckDisplayGroup, 12 CheckDisplayGroupType, 13 CheckNode, 14 CheckResult, 15 CheckSummary, 16 findDimension, 17 } from "../components/dashboards/check/common"; 18 import { 19 createContext, 20 useContext, 21 useEffect, 22 useMemo, 23 useReducer, 24 } from "react"; 25 import { default as BenchmarkType } from "../components/dashboards/check/common/Benchmark"; 26 import { ElementType, IActions, PanelDefinition } from "../types"; 27 import { useDashboard } from "./useDashboard"; 28 import { useSearchParams } from "react-router-dom"; 29 30 type CheckGroupingActionType = ElementType<typeof checkGroupingActions>; 31 32 export type CheckGroupNodeState = { 33 expanded: boolean; 34 }; 35 36 export type CheckGroupNodeStates = { 37 [name: string]: CheckGroupNodeState; 38 }; 39 40 export type CheckGroupingAction = { 41 type: CheckGroupingActionType; 42 [key: string]: any; 43 }; 44 45 type ICheckGroupingContext = { 46 benchmark: BenchmarkType | null; 47 definition: PanelDefinition; 48 grouping: CheckNode | null; 49 groupingsConfig: CheckDisplayGroup[]; 50 firstChildSummaries: CheckSummary[]; 51 nodeStates: CheckGroupNodeStates; 52 dispatch(action: CheckGroupingAction): void; 53 }; 54 55 const CheckGroupingActions: IActions = { 56 COLLAPSE_ALL_NODES: "collapse_all_nodes", 57 COLLAPSE_NODE: "collapse_node", 58 EXPAND_ALL_NODES: "expand_all_nodes", 59 EXPAND_NODE: "expand_node", 60 UPDATE_NODES: "update_nodes", 61 }; 62 63 const checkGroupingActions = Object.values(CheckGroupingActions); 64 65 const CheckGroupingContext = createContext<ICheckGroupingContext | null>(null); 66 67 const addBenchmarkTrunkNode = ( 68 benchmark_trunk: BenchmarkType[], 69 children: CheckNode[], 70 benchmarkChildrenLookup: { [name: string]: CheckNode[] } 71 ): CheckNode => { 72 const currentNode = benchmark_trunk.length > 0 ? benchmark_trunk[0] : null; 73 let newChildren: CheckNode[]; 74 if (benchmark_trunk.length > 1) { 75 newChildren = [ 76 addBenchmarkTrunkNode( 77 benchmark_trunk.slice(1), 78 children, 79 benchmarkChildrenLookup 80 ), 81 ]; 82 } else { 83 newChildren = children; 84 } 85 if (!!currentNode?.name) { 86 const existingChildren = 87 benchmarkChildrenLookup[currentNode?.name || "Other"]; 88 if (existingChildren) { 89 // We only want to add children that are not already in the list, 90 // else we end up with duplicate nodes in the tree 91 for (const child of newChildren) { 92 if ( 93 existingChildren && 94 existingChildren.find((c) => c.name === child.name) 95 ) { 96 continue; 97 } 98 existingChildren.push(child); 99 } 100 } else { 101 benchmarkChildrenLookup[currentNode?.name || "Other"] = newChildren; 102 } 103 } 104 return new BenchmarkNode( 105 currentNode?.sort || "Other", 106 currentNode?.name || "Other", 107 currentNode?.title || "Other", 108 newChildren 109 ); 110 }; 111 112 const getCheckGroupingKey = ( 113 checkResult: CheckResult, 114 group: CheckDisplayGroup 115 ) => { 116 switch (group.type) { 117 case "dimension": 118 const foundDimension = findDimension(checkResult.dimensions, group.value); 119 return foundDimension ? foundDimension.value : "Other"; 120 case "tag": 121 return group.value ? checkResult.tags[group.value] || "Other" : "Other"; 122 case "reason": 123 return checkResult.reason || "Other"; 124 case "resource": 125 return checkResult.resource || "Other"; 126 case "severity": 127 return checkResult.control.severity || "Other"; 128 case "status": 129 return checkResult.status === "empty" ? "Other" : checkResult.status; 130 case "benchmark": 131 if (checkResult.benchmark_trunk.length <= 1) { 132 return null; 133 } 134 return checkResult.benchmark_trunk[checkResult.benchmark_trunk.length - 1] 135 .name; 136 case "control": 137 return checkResult.control.name; 138 default: 139 return "Other"; 140 } 141 }; 142 143 const getCheckGroupingNode = ( 144 checkResult: CheckResult, 145 group: CheckDisplayGroup, 146 children: CheckNode[], 147 benchmarkChildrenLookup: { [name: string]: CheckNode[] } 148 ): CheckNode => { 149 switch (group.type) { 150 case "dimension": 151 const foundDimension = findDimension(checkResult.dimensions, group.value); 152 const dimensionValue = foundDimension ? foundDimension.value : "Other"; 153 return new KeyValuePairNode( 154 "dimension", 155 group.value || "Other", 156 dimensionValue, 157 children 158 ); 159 case "tag": 160 return new KeyValuePairNode( 161 "tag", 162 group.value || "Other", 163 group.value ? checkResult.tags[group.value] || "Other" : "Other", 164 children 165 ); 166 case "reason": 167 return new KeyValuePairNode( 168 "reason", 169 "reason", 170 checkResult.reason || "Other", 171 children 172 ); 173 case "resource": 174 return new KeyValuePairNode( 175 "resource", 176 "resource", 177 checkResult.resource || "Other", 178 children 179 ); 180 case "severity": 181 return new KeyValuePairNode( 182 "severity", 183 "severity", 184 checkResult.control.severity || "Other", 185 children 186 ); 187 case "status": 188 return new KeyValuePairNode( 189 "status", 190 "status", 191 checkResult.status === "empty" ? "Other" : checkResult.status, 192 children 193 ); 194 case "benchmark": 195 return checkResult.benchmark_trunk.length > 1 196 ? addBenchmarkTrunkNode( 197 checkResult.benchmark_trunk.slice(1), 198 children, 199 benchmarkChildrenLookup 200 ) 201 : children[0]; 202 case "control": 203 return new ControlNode( 204 checkResult.control.sort, 205 checkResult.control.name, 206 checkResult.control.title, 207 children 208 ); 209 default: 210 throw new Error(`Unknown group type ${group.type}`); 211 } 212 }; 213 214 const addBenchmarkGroupingNode = ( 215 existingGroups: CheckNode[], 216 groupingNode: CheckNode 217 ) => { 218 const existingGroup = existingGroups.find( 219 (existingGroup) => existingGroup.name === groupingNode.name 220 ); 221 if (existingGroup) { 222 (existingGroup as BenchmarkNode).merge(groupingNode); 223 } else { 224 existingGroups.push(groupingNode); 225 } 226 }; 227 228 const groupCheckItems = ( 229 temp: { _: CheckNode[] }, 230 checkResult: CheckResult, 231 groupingsConfig: CheckDisplayGroup[], 232 checkNodeStates: CheckGroupNodeStates, 233 benchmarkChildrenLookup: { [name: string]: CheckNode[] } 234 ) => { 235 return groupingsConfig 236 .filter((groupConfig) => groupConfig.type !== "result") 237 .reduce((cumulativeGrouping, currentGroupingConfig) => { 238 // Get this items grouping key - e.g. control or benchmark name 239 const groupKey = getCheckGroupingKey(checkResult, currentGroupingConfig); 240 241 if (!groupKey) { 242 return cumulativeGrouping; 243 } 244 245 // Collapse all benchmark trunk nodes 246 if (currentGroupingConfig.type === "benchmark") { 247 checkResult.benchmark_trunk.forEach( 248 (benchmark) => 249 (checkNodeStates[benchmark.name] = { 250 expanded: false, 251 }) 252 ); 253 } else { 254 checkNodeStates[groupKey] = { 255 expanded: false, 256 }; 257 } 258 259 if (!cumulativeGrouping[groupKey]) { 260 cumulativeGrouping[groupKey] = { _: [] }; 261 262 const groupingNode = getCheckGroupingNode( 263 checkResult, 264 currentGroupingConfig, 265 cumulativeGrouping[groupKey]._, 266 benchmarkChildrenLookup 267 ); 268 269 if (groupingNode) { 270 if (currentGroupingConfig.type === "benchmark") { 271 // For benchmarks we need to get the benchmark nodes including the trunk 272 addBenchmarkGroupingNode(cumulativeGrouping._, groupingNode); 273 } else { 274 cumulativeGrouping._.push(groupingNode); 275 } 276 } 277 } 278 279 // If the grouping key for this has already been logged by another result, 280 // use the existing children from that - this covers cases where we may have 281 // benchmark 1 -> benchmark 2 -> control 1 282 // benchmark 1 -> control 2 283 // ...when we build the benchmark grouping node for control 1, its key will be 284 // for benchmark 2, but we'll add a hierarchical grouping node for benchmark 1 -> benchmark 2 285 // When we come to get the benchmark grouping node for control 2, we'll need to add 286 // the control to the existing children of benchmark 1 287 if ( 288 currentGroupingConfig.type === "benchmark" && 289 benchmarkChildrenLookup[groupKey] 290 ) { 291 const groupingEntry = cumulativeGrouping[groupKey]; 292 const { _, ...rest } = groupingEntry || {}; 293 cumulativeGrouping[groupKey] = { 294 _: benchmarkChildrenLookup[groupKey], 295 ...rest, 296 }; 297 } 298 299 return cumulativeGrouping[groupKey]; 300 }, temp); 301 }; 302 303 const getCheckResultNode = (checkResult: CheckResult) => { 304 if (checkResult.type === "loading") { 305 return new ControlRunningNode(checkResult); 306 } else if (checkResult.type === "error") { 307 return new ControlErrorNode(checkResult); 308 } else if (checkResult.type === "empty") { 309 return new ControlEmptyResultNode(checkResult); 310 } 311 return new ControlResultNode(checkResult); 312 }; 313 314 const reducer = (state: CheckGroupNodeStates, action) => { 315 switch (action.type) { 316 case CheckGroupingActions.COLLAPSE_ALL_NODES: { 317 const newNodes = {}; 318 for (const [name, node] of Object.entries(state)) { 319 newNodes[name] = { 320 ...node, 321 expanded: false, 322 }; 323 } 324 return { 325 ...state, 326 nodes: newNodes, 327 }; 328 } 329 case CheckGroupingActions.COLLAPSE_NODE: 330 return { 331 ...state, 332 [action.name]: { 333 ...(state[action.name] || {}), 334 expanded: false, 335 }, 336 }; 337 case CheckGroupingActions.EXPAND_ALL_NODES: { 338 const newNodes = {}; 339 Object.entries(state).forEach(([name, node]) => { 340 newNodes[name] = { 341 ...node, 342 expanded: true, 343 }; 344 }); 345 return newNodes; 346 } 347 case CheckGroupingActions.EXPAND_NODE: { 348 return { 349 ...state, 350 [action.name]: { 351 ...(state[action.name] || {}), 352 expanded: true, 353 }, 354 }; 355 } 356 case CheckGroupingActions.UPDATE_NODES: 357 return action.nodes; 358 default: 359 return state; 360 } 361 }; 362 363 type CheckGroupingProviderProps = { 364 children: null | JSX.Element | JSX.Element[]; 365 definition: PanelDefinition; 366 }; 367 368 const CheckGroupingProvider = ({ 369 children, 370 definition, 371 }: CheckGroupingProviderProps) => { 372 const { panelsMap } = useDashboard(); 373 const [nodeStates, dispatch] = useReducer(reducer, { nodes: {} }); 374 const [searchParams] = useSearchParams(); 375 376 const groupingsConfig = useMemo(() => { 377 const rawGrouping = searchParams.get("grouping"); 378 if (rawGrouping) { 379 const groupings: CheckDisplayGroup[] = []; 380 const groupingParts = rawGrouping.split(","); 381 for (const groupingPart of groupingParts) { 382 const typeValueParts = groupingPart.split("|"); 383 if (typeValueParts.length > 1) { 384 groupings.push({ 385 type: typeValueParts[0] as CheckDisplayGroupType, 386 value: typeValueParts[1], 387 }); 388 } else { 389 groupings.push({ 390 type: typeValueParts[0] as CheckDisplayGroupType, 391 }); 392 } 393 } 394 return groupings; 395 } else { 396 return [ 397 { type: "benchmark" }, 398 { type: "control" }, 399 { type: "result" }, 400 ] as CheckDisplayGroup[]; 401 } 402 }, [searchParams]); 403 404 const [ 405 benchmark, 406 panelDefinition, 407 grouping, 408 firstChildSummaries, 409 tempNodeStates, 410 ] = useMemo(() => { 411 if (!definition) { 412 return [null, null, null, [], {}]; 413 } 414 415 // @ts-ignore 416 const nestedBenchmarks = definition.children?.filter( 417 (child) => child.panel_type === "benchmark" 418 ); 419 const nestedControls = 420 definition.panel_type === "control" 421 ? [definition] 422 : // @ts-ignore 423 definition.children?.filter( 424 (child) => child.panel_type === "control" 425 ); 426 427 const rootBenchmarkPanel = panelsMap[definition.name]; 428 const b = new BenchmarkType( 429 "0", 430 rootBenchmarkPanel.name, 431 rootBenchmarkPanel.title, 432 rootBenchmarkPanel.description, 433 nestedBenchmarks, 434 nestedControls, 435 panelsMap, 436 [] 437 ); 438 439 const checkNodeStates: CheckGroupNodeStates = {}; 440 const result: CheckNode[] = []; 441 const temp = { _: result }; 442 const benchmarkChildrenLookup = {}; 443 444 // We'll loop over each control result and build up the grouped nodes from there 445 b.all_control_results.forEach((checkResult) => { 446 // Build a grouping node - this will be the leaf node down from the root group 447 // e.g. benchmark -> control (where control is the leaf) 448 const grouping = groupCheckItems( 449 temp, 450 checkResult, 451 groupingsConfig, 452 checkNodeStates, 453 benchmarkChildrenLookup 454 ); 455 // Build and add a check result node to the children of the trailing group. 456 // This will be used to calculate totals and severity, amongst other things. 457 const node = getCheckResultNode(checkResult); 458 grouping._.push(node); 459 }); 460 461 const results = new RootNode(result); 462 463 const firstChildSummaries: CheckSummary[] = []; 464 for (const child of results.children) { 465 firstChildSummaries.push(child.summary); 466 } 467 468 return [ 469 b, 470 { ...rootBenchmarkPanel, children: definition.children }, 471 results, 472 firstChildSummaries, 473 checkNodeStates, 474 ] as const; 475 }, [definition, groupingsConfig, panelsMap]); 476 477 const previousGroupings = usePrevious({ groupingsConfig }); 478 479 useEffect(() => { 480 if ( 481 previousGroupings && 482 // @ts-ignore 483 previousGroupings.groupingsConfig === groupingsConfig 484 ) { 485 return; 486 } 487 dispatch({ 488 type: CheckGroupingActions.UPDATE_NODES, 489 nodes: tempNodeStates, 490 }); 491 }, [previousGroupings, groupingsConfig, tempNodeStates]); 492 493 return ( 494 <CheckGroupingContext.Provider 495 value={{ 496 benchmark, 497 // @ts-ignore 498 definition: panelDefinition, 499 dispatch, 500 firstChildSummaries, 501 grouping, 502 groupingsConfig, 503 nodeStates, 504 }} 505 > 506 {children} 507 </CheckGroupingContext.Provider> 508 ); 509 }; 510 511 const useCheckGrouping = () => { 512 const context = useContext(CheckGroupingContext); 513 if (context === undefined) { 514 throw new Error( 515 "useCheckGrouping must be used within a CheckGroupingContext" 516 ); 517 } 518 return context as ICheckGroupingContext; 519 }; 520 521 export { 522 CheckGroupingActions, 523 CheckGroupingContext, 524 CheckGroupingProvider, 525 useCheckGrouping, 526 }; 527 528 // https://stackoverflow.com/questions/50737098/multi-level-grouping-in-javascript 529 // keys = ['level1', 'level2'], 530 // result = [], 531 // temp = { _: result }; 532 // 533 // data.forEach(function (a) { 534 // keys.reduce(function (r, k) { 535 // if (!r[a[k]]) { 536 // r[a[k]] = { _: [] }; 537 // r._.push({ [k]: a[k], [k + 'list']: r[a[k]]._ }); 538 // } 539 // return r[a[k]]; 540 // }, temp)._.push({ Id: a.Id }); 541 // });