github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/hooks/useDashboardState.tsx (about)

     1  import get from "lodash/get";
     2  import useDashboardVersionCheck from "./useDashboardVersionCheck";
     3  import {
     4    buildDashboards,
     5    buildPanelsLog,
     6    buildSelectedDashboardInputsFromSearchParams,
     7    updatePanelsLogFromCompletedPanels,
     8    updateSelectedDashboard,
     9    wrapDefinitionInArtificialDashboard,
    10  } from "../utils/state";
    11  import {
    12    controlsUpdatedEventHandler,
    13    leafNodesUpdatedEventHandler,
    14    migratePanelStatuses,
    15  } from "../utils/dashboardEventHandlers";
    16  import {
    17    DashboardActions,
    18    DashboardDataModeCLISnapshot,
    19    DashboardDataModeCloudSnapshot,
    20    DashboardDataModeLive,
    21    DashboardDataOptions,
    22    DashboardRenderOptions,
    23    IDashboardContext,
    24  } from "../types";
    25  import {
    26    EXECUTION_SCHEMA_VERSION_20220929,
    27    EXECUTION_SCHEMA_VERSION_20221222,
    28  } from "../constants/versions";
    29  import {
    30    ExecutionCompleteSchemaMigrator,
    31    ExecutionStartedSchemaMigrator,
    32  } from "../utils/schema";
    33  import { useCallback, useReducer } from "react";
    34  
    35  const reducer = (state: IDashboardContext, action) => {
    36    switch (action.type) {
    37      case DashboardActions.DASHBOARD_METADATA:
    38        return {
    39          ...state,
    40          metadata: {
    41            mod: {},
    42            ...action.metadata,
    43          },
    44        };
    45      case DashboardActions.AVAILABLE_DASHBOARDS:
    46        const { dashboards, dashboardsMap } = buildDashboards(
    47          action.dashboards,
    48          action.benchmarks,
    49          action.snapshots
    50        );
    51        const selectedDashboard = updateSelectedDashboard(
    52          state.selectedDashboard,
    53          dashboards
    54        );
    55        return {
    56          ...state,
    57          error: null,
    58          availableDashboardsLoaded: true,
    59          dashboards,
    60          dashboardsMap,
    61          selectedDashboard:
    62            state.dataMode === DashboardDataModeCLISnapshot ||
    63            state.dataMode === DashboardDataModeCloudSnapshot
    64              ? state.selectedDashboard
    65              : selectedDashboard,
    66          dashboard:
    67            state.dataMode === DashboardDataModeCLISnapshot ||
    68            state.dataMode === DashboardDataModeCloudSnapshot
    69              ? state.dashboard
    70              : selectedDashboard &&
    71                state.dashboard &&
    72                state.dashboard.name === selectedDashboard.full_name
    73              ? state.dashboard
    74              : null,
    75        };
    76      case DashboardActions.EXECUTION_STARTED: {
    77        const rootLayoutPanel = action.layout;
    78        const rootPanel = action.panels[rootLayoutPanel.name];
    79        let dashboard;
    80        // For benchmarks and controls that are run directly from a mod,
    81        // we need to wrap these in an artificial dashboard, so we can treat
    82        // it just like any other dashboard
    83        if (rootPanel.panel_type !== "dashboard") {
    84          dashboard = wrapDefinitionInArtificialDashboard(
    85            rootPanel,
    86            action.layout
    87          );
    88        } else {
    89          dashboard = {
    90            ...rootPanel,
    91            ...action.layout,
    92          };
    93        }
    94  
    95        const eventMigrator = new ExecutionStartedSchemaMigrator();
    96        const migratedEvent = eventMigrator.toLatest(action);
    97  
    98        return {
    99          ...state,
   100          error: null,
   101          panelsLog: buildPanelsLog(
   102            migratedEvent.panels,
   103            migratedEvent.start_time
   104          ),
   105          panelsMap: migratedEvent.panels,
   106          dashboard,
   107          execution_id: migratedEvent.execution_id,
   108          refetchDashboard: false,
   109          progress: 0,
   110          snapshot: null,
   111          state: "running",
   112        };
   113      }
   114      case DashboardActions.EXECUTION_COMPLETE: {
   115        // If we're in live mode and not expecting execution events for this ID
   116        if (
   117          state.dataMode === DashboardDataModeLive &&
   118          action.execution_id !== state.execution_id
   119        ) {
   120          return state;
   121        }
   122  
   123        const eventMigrator = new ExecutionCompleteSchemaMigrator();
   124        const migratedEvent = eventMigrator.toLatest(action);
   125        const layout = migratedEvent.snapshot.layout;
   126        const panels = migratedEvent.snapshot.panels;
   127        const rootLayoutPanel = migratedEvent.snapshot.layout;
   128        const rootPanel = panels[rootLayoutPanel.name];
   129        let dashboard;
   130  
   131        if (rootPanel.panel_type !== "dashboard") {
   132          dashboard = wrapDefinitionInArtificialDashboard(rootPanel, layout);
   133        } else {
   134          dashboard = {
   135            ...rootPanel,
   136            ...layout,
   137          };
   138        }
   139  
   140        const panelsMap = migratePanelStatuses(panels, action.schema_version);
   141  
   142        // Replace the whole dashboard as this event contains everything
   143        return {
   144          ...state,
   145          error: null,
   146          panelsLog: updatePanelsLogFromCompletedPanels(
   147            state.panelsLog,
   148            panels,
   149            action.snapshot.end_time
   150          ),
   151          panelsMap,
   152          dashboard,
   153          progress: 100,
   154          snapshot: action.snapshot,
   155          state: "complete",
   156        };
   157      }
   158      case DashboardActions.EXECUTION_ERROR:
   159        return { ...state, error: action.error, progress: 100, state: "error" };
   160      case DashboardActions.CONTROLS_UPDATED:
   161        return controlsUpdatedEventHandler(action, state);
   162      case DashboardActions.LEAF_NODES_COMPLETE:
   163        return leafNodesUpdatedEventHandler(
   164          action,
   165          EXECUTION_SCHEMA_VERSION_20220929,
   166          state
   167        );
   168      case DashboardActions.LEAF_NODES_UPDATED:
   169        return leafNodesUpdatedEventHandler(
   170          action,
   171          EXECUTION_SCHEMA_VERSION_20221222,
   172          state
   173        );
   174      case DashboardActions.SELECT_PANEL:
   175        return { ...state, selectedPanel: action.panel };
   176      case DashboardActions.SET_DATA_MODE:
   177        const newState = {
   178          ...state,
   179          dataMode: action.dataMode,
   180        };
   181        if (action.dataMode === DashboardDataModeCLISnapshot) {
   182          newState.snapshotFileName = action.snapshotFileName;
   183        } else if (
   184          state.dataMode !== DashboardDataModeLive &&
   185          action.dataMode === DashboardDataModeLive
   186        ) {
   187          newState.snapshot = null;
   188          newState.snapshotFileName = null;
   189          newState.snapshotId = null;
   190        }
   191        return newState;
   192      case DashboardActions.SET_REFETCH_DASHBOARD:
   193        return {
   194          ...state,
   195          refetchDashboard: true,
   196        };
   197      case DashboardActions.SET_DASHBOARD:
   198        return {
   199          ...state,
   200          dashboard: action.dashboard,
   201        };
   202      case DashboardActions.SELECT_DASHBOARD:
   203        if (action.dashboard && action.dashboard.type === "snapshot") {
   204          return {
   205            ...state,
   206            dataMode: DashboardDataModeCLISnapshot,
   207            selectedDashboard: action.dashboard,
   208          };
   209        }
   210  
   211        if (
   212          action.dataMode === DashboardDataModeCLISnapshot ||
   213          action.dataMode === DashboardDataModeCloudSnapshot
   214        ) {
   215          return {
   216            ...state,
   217            dataMode: action.dataMode,
   218            selectedDashboard: action.dashboard,
   219          };
   220        }
   221  
   222        return {
   223          ...state,
   224          dataMode: DashboardDataModeLive,
   225          dashboard: null,
   226          execution_id: null,
   227          panelsMap: {},
   228          snapshot: null,
   229          snapshotFileName: null,
   230          snapshotId: null,
   231          state: null,
   232          selectedDashboard: action.dashboard,
   233          selectedPanel: null,
   234          lastChangedInput: null,
   235        };
   236      case DashboardActions.CLEAR_DASHBOARD_INPUTS:
   237        return {
   238          ...state,
   239          selectedDashboardInputs: {},
   240          lastChangedInput: null,
   241          recordInputsHistory: !!action.recordInputsHistory,
   242        };
   243      case DashboardActions.DELETE_DASHBOARD_INPUT:
   244        const { [action.name]: toDelete, ...rest } =
   245          state.selectedDashboardInputs;
   246        return {
   247          ...state,
   248          selectedDashboardInputs: {
   249            ...rest,
   250          },
   251          lastChangedInput: action.name,
   252          recordInputsHistory: !!action.recordInputsHistory,
   253        };
   254      case DashboardActions.SET_DASHBOARD_INPUT:
   255        return {
   256          ...state,
   257          selectedDashboardInputs: {
   258            ...state.selectedDashboardInputs,
   259            [action.name]: action.value,
   260          },
   261          lastChangedInput: action.name,
   262          recordInputsHistory: !!action.recordInputsHistory,
   263        };
   264      case DashboardActions.SET_DASHBOARD_INPUTS:
   265        return {
   266          ...state,
   267          selectedDashboardInputs: action.value,
   268          lastChangedInput: null,
   269          recordInputsHistory: !!action.recordInputsHistory,
   270        };
   271      case DashboardActions.INPUT_VALUES_CLEARED: {
   272        // We're not expecting execution events for this ID
   273        if (action.execution_id !== state.execution_id) {
   274          return state;
   275        }
   276        const newSelectedDashboardInputs = { ...state.selectedDashboardInputs };
   277        const newPanelsMap = { ...state.panelsMap };
   278        const panelsMapKeys = Object.keys(newPanelsMap);
   279        for (const input of action.cleared_inputs || []) {
   280          delete newSelectedDashboardInputs[input];
   281          const matchingPanelKey = panelsMapKeys.find((key) =>
   282            key.endsWith(input)
   283          );
   284          if (!matchingPanelKey) {
   285            continue;
   286          }
   287          const panel = newPanelsMap[matchingPanelKey];
   288          newPanelsMap[matchingPanelKey] = {
   289            ...panel,
   290            status: "initialized",
   291          };
   292        }
   293        return {
   294          ...state,
   295          panelsMap: newPanelsMap,
   296          selectedDashboardInputs: newSelectedDashboardInputs,
   297          lastChangedInput: null,
   298          recordInputsHistory: false,
   299        };
   300      }
   301      case DashboardActions.SET_DASHBOARD_SEARCH_VALUE:
   302        return {
   303          ...state,
   304          search: {
   305            ...state.search,
   306            value: action.value,
   307          },
   308        };
   309      case DashboardActions.SET_DASHBOARD_SEARCH_GROUP_BY:
   310        return {
   311          ...state,
   312          search: {
   313            ...state.search,
   314            groupBy: {
   315              value: action.value,
   316              tag: action.tag,
   317            },
   318          },
   319        };
   320      case DashboardActions.SET_DASHBOARD_TAG_KEYS:
   321        return {
   322          ...state,
   323          dashboardTags: {
   324            ...state.dashboardTags,
   325            keys: action.keys,
   326          },
   327        };
   328      case DashboardActions.WORKSPACE_ERROR:
   329        return { ...state, error: action.error };
   330      default:
   331        console.warn(`Unsupported action ${action.type}`, action);
   332        return state;
   333    }
   334  };
   335  
   336  const getInitialState = (searchParams, defaults: any = {}) => {
   337    return {
   338      versionMismatchCheck: defaults.versionMismatchCheck,
   339      availableDashboardsLoaded: false,
   340      metadata: null,
   341      dashboards: [],
   342      dashboardTags: {
   343        keys: [],
   344      },
   345      dataMode: defaults.dataMode || DashboardDataModeLive,
   346      snapshotId: defaults.snapshotId ? defaults.snapshotId : null,
   347      refetchDashboard: false,
   348      error: null,
   349      panelsLog: {},
   350      panelsMap: {},
   351      dashboard: null,
   352      selectedPanel: null,
   353      selectedDashboard: null,
   354      selectedDashboardInputs:
   355        buildSelectedDashboardInputsFromSearchParams(searchParams),
   356      snapshot: null,
   357      lastChangedInput: null,
   358  
   359      search: {
   360        value: searchParams.get("search") || "",
   361        groupBy: {
   362          value:
   363            searchParams.get("group_by") ||
   364            get(defaults, "search.groupBy.value", "tag"),
   365          tag:
   366            searchParams.get("tag") ||
   367            get(defaults, "search.groupBy.value", "service"),
   368        },
   369      },
   370  
   371      execution_id: null,
   372  
   373      progress: 0,
   374    };
   375  };
   376  
   377  type DashboardStateProps = {
   378    dataOptions: DashboardDataOptions;
   379    renderOptions: DashboardRenderOptions;
   380    searchParams: URLSearchParams;
   381    stateDefaults: {};
   382    versionMismatchCheck: boolean;
   383  };
   384  
   385  const useDashboardState = ({
   386    dataOptions = {
   387      dataMode: DashboardDataModeLive,
   388    },
   389    renderOptions = {
   390      headless: false,
   391    },
   392    searchParams,
   393    stateDefaults = {},
   394    versionMismatchCheck,
   395  }: DashboardStateProps) => {
   396    const [state, dispatchInner] = useReducer(
   397      reducer,
   398      getInitialState(searchParams, {
   399        ...stateDefaults,
   400        ...dataOptions,
   401        ...renderOptions,
   402        versionMismatchCheck,
   403      })
   404    );
   405    useDashboardVersionCheck(state);
   406    const dispatch = useCallback((action) => {
   407      // console.log(action.type, action);
   408      dispatchInner(action);
   409    }, []);
   410    return [state, dispatch];
   411  };
   412  
   413  export default useDashboardState;