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;