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