github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/redux/reducers/tracing.ts (about) 1 import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 import type { Profile } from '@pyroscope/models/src'; 3 import { 4 MergeOutput, 5 mergeWithQueryID, 6 HeatmapOutput, 7 getHeatmap, 8 SelectionProfileOutput, 9 getHeatmapSelectionProfile, 10 Heatmap, 11 getHeatmapProps, 12 selectionProfileProps, 13 } from '@webapp/services/render'; 14 import type { RootState } from '@webapp/redux/store'; 15 import { RequestAbortedError } from '@webapp/services/base'; 16 import { addNotification } from './notifications'; 17 import { createAsyncThunk } from '../async-thunk'; 18 19 type MergeMetadata = { 20 appName: string; 21 startTime: string; 22 endTime: string; 23 profilesLength: number; 24 }; 25 26 type SingleView = 27 | { type: 'pristine'; profile?: Profile; mergeMetadata?: MergeMetadata } 28 | { type: 'loading'; profile?: Profile; mergeMetadata?: MergeMetadata } 29 | { 30 type: 'loaded'; 31 profile: Profile; 32 mergeMetadata: MergeMetadata; 33 } 34 | { 35 type: 'reloading'; 36 profile: Profile; 37 mergeMetadata: MergeMetadata; 38 }; 39 // TODO 40 41 type ExemplarsSingleView = 42 | { 43 type: 'pristine'; 44 heatmap?: Heatmap | null; 45 profile?: Profile; 46 selectionProfile?: Profile; 47 } 48 | { 49 type: 'loading'; 50 heatmap?: Heatmap | null; 51 profile?: Profile; 52 selectionProfile?: Profile; 53 } 54 | { 55 type: 'loaded'; 56 heatmap: Heatmap | null; 57 profile?: Profile; 58 selectionProfile?: Profile; 59 } 60 | { 61 type: 'reloading'; 62 heatmap: Heatmap | null; 63 profile?: Profile; 64 selectionProfile?: Profile; 65 }; 66 67 interface TracingState { 68 queryID: string; 69 maxNodes: string; 70 refreshToken?: string; 71 72 exemplarsSingleView: ExemplarsSingleView; 73 singleView: SingleView; 74 } 75 76 let singleViewAbortController: AbortController | undefined; 77 let exemplarsSingleViewAbortController: AbortController | undefined; 78 let selectionProfileAbortController: AbortController | undefined; 79 80 const initialState: TracingState = { 81 queryID: '', 82 maxNodes: '1024', 83 84 exemplarsSingleView: { type: 'pristine' }, 85 singleView: { type: 'pristine' }, 86 }; 87 88 export const fetchSingleView = createAsyncThunk< 89 MergeOutput, 90 null, 91 { state: { tracing: TracingState } } 92 >('tracing/singleView', async (_, thunkAPI) => { 93 if (singleViewAbortController) { 94 singleViewAbortController.abort(); 95 } 96 97 singleViewAbortController = new AbortController(); 98 thunkAPI.signal = singleViewAbortController.signal; 99 100 const state = thunkAPI.getState(); 101 const res = await mergeWithQueryID(state.tracing, singleViewAbortController); 102 103 if (res.isOk) { 104 return Promise.resolve(res.value); 105 } 106 107 if (res.isErr && res.error instanceof RequestAbortedError) { 108 return thunkAPI.rejectWithValue({ rejectedWithValue: 'reloading' }); 109 } 110 111 thunkAPI.dispatch( 112 addNotification({ 113 type: 'danger', 114 title: 'Failed to load single view data', 115 message: res.error.message, 116 }) 117 ); 118 119 return Promise.reject(res.error); 120 }); 121 122 export const fetchExemplarsSingleView = createAsyncThunk< 123 HeatmapOutput, 124 getHeatmapProps, 125 { state: { tracing: TracingState } } 126 >('tracing/exemplarsSingleView', async (heatmapProps, thunkAPI) => { 127 if (exemplarsSingleViewAbortController) { 128 exemplarsSingleViewAbortController.abort(); 129 } 130 131 exemplarsSingleViewAbortController = new AbortController(); 132 thunkAPI.signal = exemplarsSingleViewAbortController.signal; 133 134 const res = await getHeatmap( 135 heatmapProps, 136 exemplarsSingleViewAbortController 137 ); 138 139 if (res.isOk) { 140 return Promise.resolve(res.value); 141 } 142 143 if (res.isErr && res.error instanceof RequestAbortedError) { 144 return thunkAPI.rejectWithValue({ rejectedWithValue: 'reloading' }); 145 } 146 147 thunkAPI.dispatch( 148 addNotification({ 149 type: 'danger', 150 title: 'Failed to load heatmap', 151 message: res.error.message, 152 }) 153 ); 154 155 return Promise.reject(res.error); 156 }); 157 158 export const fetchSelectionProfile = createAsyncThunk< 159 SelectionProfileOutput, 160 selectionProfileProps, 161 { state: { tracing: TracingState } } 162 >('tracing/fetchSelectionProfile', async (selectionProfileProps, thunkAPI) => { 163 if (selectionProfileAbortController) { 164 selectionProfileAbortController.abort(); 165 } 166 167 selectionProfileAbortController = new AbortController(); 168 thunkAPI.signal = selectionProfileAbortController.signal; 169 170 const res = await getHeatmapSelectionProfile( 171 selectionProfileProps, 172 selectionProfileAbortController 173 ); 174 175 if (res.isOk) { 176 return Promise.resolve(res.value); 177 } 178 179 if (res.isErr && res.error instanceof RequestAbortedError) { 180 return thunkAPI.rejectWithValue({ rejectedWithValue: 'reloading' }); 181 } 182 183 thunkAPI.dispatch( 184 addNotification({ 185 type: 'danger', 186 title: 'Failed to load profile', 187 message: res.error.message, 188 }) 189 ); 190 191 return Promise.reject(res.error); 192 }); 193 194 export const tracingSlice = createSlice({ 195 name: 'tracing', 196 initialState, 197 reducers: { 198 setMaxNodes(state, action: PayloadAction<string>) { 199 state.maxNodes = action.payload; 200 }, 201 setQueryID(state, action: PayloadAction<string>) { 202 state.queryID = action.payload; 203 }, 204 refresh(state) { 205 state.refreshToken = Math.random().toString(); 206 }, 207 }, 208 extraReducers: (builder) => { 209 /*************************/ 210 /* Single View */ 211 /*************************/ 212 builder.addCase(fetchSingleView.pending, (state) => { 213 switch (state.singleView.type) { 214 // if we are fetching but there's already data 215 // it's considered a 'reload' 216 case 'reloading': 217 case 'loaded': { 218 state.singleView = { 219 ...state.singleView, 220 type: 'reloading', 221 }; 222 break; 223 } 224 225 default: { 226 state.singleView = { type: 'loading' }; 227 } 228 } 229 }); 230 231 builder.addCase(fetchSingleView.fulfilled, (state, action) => { 232 state.singleView = { 233 ...action.payload, 234 mergeMetadata: action.payload.mergeMetadata, 235 type: 'loaded', 236 }; 237 }); 238 239 builder.addCase(fetchSingleView.rejected, (state, action) => { 240 switch (state.singleView.type) { 241 // if previous state is loaded, let's continue displaying data 242 case 'reloading': { 243 let type: SingleView['type'] = 'reloading'; 244 if (action.meta.rejectedWithValue) { 245 type = ( 246 action?.payload as { rejectedWithValue: SingleView['type'] } 247 )?.rejectedWithValue; 248 } else if (action.error.message === 'cancel') { 249 type = 'loaded'; 250 } 251 state.singleView = { 252 ...state.singleView, 253 type, 254 }; 255 break; 256 } 257 258 default: { 259 // it failed to load for the first time, so far all effects it's pristine 260 state.singleView = { 261 type: 'pristine', 262 }; 263 } 264 } 265 }); 266 267 /***********************************/ 268 /* Exemplars Single View */ 269 /***********************************/ 270 271 builder.addCase(fetchExemplarsSingleView.pending, (state) => { 272 switch (state.exemplarsSingleView.type) { 273 // if we are fetching but there's already data 274 // it's considered a 'reload' 275 case 'reloading': 276 case 'loaded': { 277 state.exemplarsSingleView = { 278 ...state.exemplarsSingleView, 279 type: 'reloading', 280 }; 281 break; 282 } 283 284 default: { 285 state.exemplarsSingleView = { type: 'loading' }; 286 } 287 } 288 }); 289 290 builder.addCase(fetchExemplarsSingleView.fulfilled, (state, action) => { 291 state.exemplarsSingleView = { 292 ...action.payload, 293 type: 'loaded', 294 }; 295 }); 296 297 builder.addCase(fetchExemplarsSingleView.rejected, (state, action) => { 298 switch (state.exemplarsSingleView.type) { 299 // if previous state is loaded, let's continue displaying data 300 case 'reloading': { 301 let type: ExemplarsSingleView['type'] = 'reloading'; 302 if (action.meta.rejectedWithValue) { 303 type = ( 304 action?.payload as { 305 rejectedWithValue: ExemplarsSingleView['type']; 306 } 307 )?.rejectedWithValue; 308 } else if (action.error.message === 'cancel') { 309 type = 'loaded'; 310 } 311 state.exemplarsSingleView = { 312 ...state.exemplarsSingleView, 313 type, 314 }; 315 break; 316 } 317 318 default: { 319 // it failed to load for the first time, so far all effects it's pristine 320 state.exemplarsSingleView = { 321 type: 'pristine', 322 }; 323 } 324 } 325 }); 326 327 /**************************************/ 328 /* Heatmap Selection Profile */ 329 /**************************************/ 330 331 builder.addCase(fetchSelectionProfile.pending, (state) => { 332 switch (state.exemplarsSingleView.type) { 333 // if we are fetching but there's already data 334 // it's considered a 'reload' 335 case 'reloading': 336 case 'loaded': { 337 state.exemplarsSingleView = { 338 ...state.exemplarsSingleView, 339 type: 'reloading', 340 }; 341 break; 342 } 343 344 default: { 345 state.exemplarsSingleView = { type: 'loading' }; 346 } 347 } 348 }); 349 350 builder.addCase(fetchSelectionProfile.fulfilled, (state, action) => { 351 state.exemplarsSingleView.type = 'loaded'; 352 state.exemplarsSingleView.selectionProfile = 353 action.payload.selectionProfile; 354 }); 355 356 builder.addCase(fetchSelectionProfile.rejected, (state, action) => { 357 switch (state.exemplarsSingleView.type) { 358 // if previous state is loaded, let's continue displaying data 359 case 'reloading': { 360 let type: ExemplarsSingleView['type'] = 'reloading'; 361 if (action.meta.rejectedWithValue) { 362 type = ( 363 action?.payload as { 364 rejectedWithValue: ExemplarsSingleView['type']; 365 } 366 )?.rejectedWithValue; 367 } else if (action.error.message === 'cancel') { 368 type = 'loaded'; 369 } 370 state.exemplarsSingleView = { 371 ...state.exemplarsSingleView, 372 type, 373 }; 374 break; 375 } 376 377 default: { 378 // it failed to load for the first time, so far all effects it's pristine 379 state.exemplarsSingleView = { 380 type: 'pristine', 381 }; 382 } 383 } 384 }); 385 }, 386 }); 387 388 export const selectTracingState = (state: RootState) => state.tracing; 389 390 export default tracingSlice.reducer; 391 export const { actions } = tracingSlice;