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