github.com/grafana/pyroscope@v1.18.0/public/app/redux/reducers/continuous/index.ts (about) 1 import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 import { Query } from '@pyroscope/models/query'; 3 import { addNotification } from '../notifications'; 4 import { createAsyncThunk } from '../../async-thunk'; 5 import { ContinuousState, TagsState } from './state'; 6 import { fetchTagValues, fetchTags } from './tags.thunks'; 7 import { fetchSingleView } from './singleView.thunks'; 8 import { fetchSideTimelines } from './timelines.thunks'; 9 import { fetchApps } from '@pyroscope/services/apps'; 10 import { formatAsOBject } from '@pyroscope/util/formatDate'; 11 import { App } from '@pyroscope/models/app'; 12 13 const initialState: ContinuousState = { 14 from: 'now-1h', 15 until: 'now', 16 leftFrom: 'now-1h', 17 leftUntil: 'now-30m', 18 rightFrom: 'now-30m', 19 rightUntil: 'now', 20 maxNodes: '1024', 21 aggregation: 'sum', 22 23 singleView: { type: 'pristine' }, 24 tags: {}, 25 26 apps: { 27 type: 'loaded', 28 data: [], 29 }, 30 31 query: '', 32 33 leftTimeline: { 34 type: 'pristine', 35 timeline: { 36 startTime: 0, 37 samples: [], 38 durationDelta: 0, 39 }, 40 }, 41 rightTimeline: { 42 type: 'pristine', 43 timeline: { 44 startTime: 0, 45 samples: [], 46 durationDelta: 0, 47 }, 48 }, 49 }; 50 51 export const reloadAppNames = createAsyncThunk< 52 App[], 53 void, 54 { state: { continuous: ContinuousState } } 55 >('names/reloadAppNames', async (_, thunkAPI) => { 56 const state = thunkAPI.getState(); 57 const fromMs = formatAsOBject(state.continuous.from).getTime(); 58 const untilMs = formatAsOBject(state.continuous.until).getTime(); 59 60 // TODO, retries? 61 const res = await fetchApps(fromMs, untilMs); 62 63 if (res.isOk) { 64 return Promise.resolve(res.value); 65 } 66 67 thunkAPI.dispatch( 68 addNotification({ 69 type: 'danger', 70 title: 'Failed to load app names', 71 message: res.error.message, 72 }) 73 ); 74 75 return Promise.reject(res.error); 76 }); 77 78 export const continuousSlice = createSlice({ 79 name: 'continuous', 80 initialState, 81 reducers: { 82 setFrom(state, action: PayloadAction<string>) { 83 state.from = action.payload; 84 }, 85 setUntil(state, action: PayloadAction<string>) { 86 state.until = action.payload; 87 }, 88 setFromAndUntil( 89 state, 90 action: PayloadAction<{ from: string; until: string }> 91 ) { 92 state.from = action.payload.from; 93 state.until = action.payload.until; 94 }, 95 setQuery(state, action: PayloadAction<Query>) { 96 // TODO: figure out why is being dispatched as undefined 97 state.query = action.payload || ''; 98 }, 99 setLeftQuery(state, action: PayloadAction<Query>) { 100 state.leftQuery = action.payload; 101 }, 102 setRightQuery(state, action: PayloadAction<Query>) { 103 state.rightQuery = action.payload; 104 }, 105 setLeftFrom(state, action: PayloadAction<string>) { 106 state.leftFrom = action.payload; 107 }, 108 setLeftUntil(state, action: PayloadAction<string>) { 109 state.leftUntil = action.payload; 110 }, 111 setLeft(state, action: PayloadAction<{ from: string; until: string }>) { 112 state.leftFrom = action.payload.from; 113 state.leftUntil = action.payload.until; 114 }, 115 setRightFrom(state, action: PayloadAction<string>) { 116 state.rightFrom = action.payload; 117 }, 118 setRightUntil(state, action: PayloadAction<string>) { 119 state.rightUntil = action.payload; 120 }, 121 setRight(state, action: PayloadAction<{ from: string; until: string }>) { 122 state.rightFrom = action.payload.from; 123 state.rightUntil = action.payload.until; 124 }, 125 setMaxNodes(state, action: PayloadAction<string>) { 126 state.maxNodes = action.payload; 127 }, 128 setAggregation(state, action: PayloadAction<string>) { 129 state.aggregation = action.payload; 130 }, 131 132 setDateRange( 133 state, 134 action: PayloadAction<Pick<ContinuousState, 'from' | 'until'>> 135 ) { 136 state.from = action.payload.from; 137 state.until = action.payload.until; 138 }, 139 140 refresh(state) { 141 state.refreshToken = Math.random().toString(); 142 }, 143 }, 144 145 extraReducers: (builder) => { 146 /** ******************* */ 147 /* GENERAL GUIDELINES */ 148 /** ******************* */ 149 150 // There are (currently) only 2 ways an action can be aborted: 151 // 1. The component is unmounting, eg when changing route 152 // 2. New data is loading, which means previous request is going to be superseeded 153 // In both cases, not doing state transitions is fine 154 // Specially in the second case, where a 'rejected' may happen AFTER a 'pending' is dispatched 155 // https://redux-toolkit.js.org/api/createAsyncThunk#checking-if-a-promise-rejection-was-from-an-error-or-cancellation 156 157 /** ********************** */ 158 /* Single View */ 159 /** ********************** */ 160 builder.addCase(fetchSingleView.pending, (state) => { 161 switch (state.singleView.type) { 162 // if we are fetching but there's already data 163 // it's considered a 'reload' 164 case 'reloading': 165 case 'loaded': { 166 state.singleView = { 167 ...state.singleView, 168 type: 'reloading', 169 }; 170 break; 171 } 172 173 default: { 174 state.singleView = { type: 'loading' }; 175 } 176 } 177 }); 178 179 builder.addCase(fetchSingleView.fulfilled, (state, action) => { 180 state.singleView = { 181 ...action.payload, 182 type: 'loaded', 183 }; 184 }); 185 186 builder.addCase(fetchSingleView.rejected, (state, action) => { 187 if (action.meta.aborted) { 188 return; 189 } 190 191 state.singleView = { 192 type: 'pristine', 193 }; 194 }); 195 196 /** ************************** */ 197 /* Timeline Sides */ 198 /** ************************** */ 199 builder.addCase(fetchSideTimelines.pending, (state) => { 200 state.leftTimeline = { 201 ...state.leftTimeline, 202 type: getNextStateFromPending(state.leftTimeline.type), 203 }; 204 state.rightTimeline = { 205 ...state.rightTimeline, 206 type: getNextStateFromPending(state.leftTimeline.type), 207 }; 208 }); 209 builder.addCase(fetchSideTimelines.fulfilled, (state, action) => { 210 state.leftTimeline = { 211 type: 'loaded', 212 timeline: action.payload.left.timeline, 213 }; 214 state.rightTimeline = { 215 type: 'loaded', 216 timeline: action.payload.right.timeline, 217 }; 218 }); 219 220 // TODO 221 builder.addCase(fetchSideTimelines.rejected, () => {}); 222 223 /** ************** */ 224 /* Tags */ 225 /** ************** */ 226 227 // TODO: 228 builder.addCase(fetchTags.pending, () => {}); 229 230 builder.addCase(fetchTags.fulfilled, (state, action) => { 231 // convert each 232 // TODO(eh-am): don't delete tags if we already have them 233 const tags = action.payload.tags.reduce((acc, tag) => { 234 acc[tag] = { type: 'pristine' }; 235 return acc; 236 }, {} as TagsState['tags']); 237 238 state.tags[action.payload.appName] = { 239 type: 'loaded', 240 from: action.payload.from, 241 until: action.payload.until, 242 tags, 243 }; 244 }); 245 246 // TODO 247 builder.addCase(fetchTags.rejected, () => {}); 248 249 // TODO other cases 250 builder.addCase(fetchTagValues.fulfilled, (state, action) => { 251 state.tags[action.payload.appName].tags[action.payload.label] = { 252 type: 'loaded', 253 values: action.payload.values, 254 }; 255 }); 256 257 /** ******************** */ 258 /* App Names */ 259 /** ******************** */ 260 builder.addCase(reloadAppNames.fulfilled, (state, action) => { 261 state.apps = { type: 'loaded', data: action.payload }; 262 }); 263 builder.addCase(reloadAppNames.pending, (state) => { 264 state.apps = { type: 'reloading', data: state.apps.data }; 265 }); 266 builder.addCase(reloadAppNames.rejected, (state) => { 267 state.apps = { type: 'failed', data: state.apps.data }; 268 }); 269 }, 270 }); 271 272 export const { actions } = continuousSlice; 273 export const { setDateRange, setQuery, setMaxNodes } = continuousSlice.actions; 274 275 function getNextStateFromPending( 276 prevState: 'pristine' | 'loading' | 'reloading' | 'loaded' 277 ) { 278 if (prevState === 'pristine' || prevState === 'loading') { 279 return 'loading'; 280 } 281 282 return 'reloading'; 283 } 284 285 export * from './selectors'; 286 287 export * from './state'; 288 export * from './tags.thunks'; 289 export * from './singleView.thunks'; 290 export * from './timelines.thunks'; 291 292 export const continuousReducer = continuousSlice.reducer;