github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/redux/reducers/adhoc.ts (about) 1 import { Profile } from '@pyroscope/models/src'; 2 import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; 3 import { 4 upload, 5 retrieve, 6 retrieveAll, 7 retrieveDiff, 8 } from '@webapp/services/adhoc'; 9 import type { RootState } from '@webapp/redux/store'; 10 import { Maybe } from '@webapp/util/fp'; 11 import { AllProfiles } from '@webapp/models/adhoc'; 12 import { addNotification } from './notifications'; 13 14 type uploadState = 15 | { type: 'pristine' } 16 | { type: 'loading'; fileName: string } 17 | { type: 'loaded' }; 18 19 type Upload = { 20 left: uploadState; 21 right: uploadState; 22 }; 23 24 type Shared = { 25 profilesList: 26 | { type: 'pristine' } 27 | { type: 'loading' } 28 | { type: 'loaded'; profilesList: AllProfiles }; 29 30 left: { 31 type: 'pristine' | 'loading' | 'loaded'; 32 profile?: Profile; 33 id?: string; 34 }; 35 36 right: { 37 type: 'pristine' | 'loading' | 'loaded'; 38 profile?: Profile; 39 id?: string; 40 }; 41 }; 42 43 type DiffState = { 44 type: 'pristine' | 'loading' | 'loaded'; 45 profile?: Profile; 46 }; 47 48 type side = 'left' | 'right'; 49 50 interface AdhocState { 51 // Upload refers to the files being uploaded 52 upload: Upload; 53 // Shared refers to the list of already uploaded files 54 shared: Shared; 55 diff: DiffState; 56 } 57 58 const initialState: AdhocState = { 59 shared: { 60 profilesList: { type: 'pristine' }, 61 left: { type: 'pristine' }, 62 right: { type: 'pristine' }, 63 }, 64 upload: { left: { type: 'pristine' }, right: { type: 'pristine' } }, 65 diff: { type: 'pristine' }, 66 }; 67 68 export const uploadFile = createAsyncThunk( 69 'adhoc/uploadFile', 70 async ( 71 { 72 file, 73 spyName, 74 units, 75 ...args 76 }: { file: File; spyName?: string; units?: string } & { side: side }, 77 thunkAPI 78 ) => { 79 const res = await upload( 80 file, 81 spyName && units ? { spyName, units } : undefined 82 ); 83 84 if (res.isOk) { 85 // Since we just uploaded a file, let's reload to see it on the file list 86 thunkAPI.dispatch(fetchAllProfiles()); 87 88 return Promise.resolve({ profile: res.value, fileName: file.name }); 89 } 90 91 thunkAPI.dispatch( 92 addNotification({ 93 type: 'danger', 94 title: 'Failed to upload adhoc file', 95 message: res.error.message, 96 }) 97 ); 98 99 // Since the file is invalid, let's remove it 100 thunkAPI.dispatch(removeFile(args)); 101 102 return Promise.reject(res.error); 103 } 104 ); 105 106 export const fetchAllProfiles = createAsyncThunk( 107 'adhoc/fetchAllProfiles', 108 async (_, thunkAPI) => { 109 const res = await retrieveAll(); 110 if (res.isOk) { 111 return Promise.resolve(res.value); 112 } 113 114 thunkAPI.dispatch( 115 addNotification({ 116 type: 'danger', 117 title: 'Failed to load list of adhoc files', 118 message: res.error.message, 119 }) 120 ); 121 122 return Promise.reject(res.error); 123 } 124 ); 125 126 export const fetchProfile = createAsyncThunk( 127 'adhoc/fetchProfile', 128 async ({ id, side }: { id: string; side: side }, thunkAPI) => { 129 const res = await retrieve(id); 130 131 if (res.isOk) { 132 return Promise.resolve({ profile: res.value, side, id }); 133 } 134 135 thunkAPI.dispatch( 136 addNotification({ 137 type: 'danger', 138 title: 'Failed to load adhoc file', 139 message: res.error.message, 140 }) 141 ); 142 143 return Promise.reject(res.error); 144 } 145 ); 146 147 export const fetchDiffProfile = createAsyncThunk( 148 'adhoc/fetchDiffProfile', 149 async ( 150 { leftId, rightId }: { leftId: string; rightId: string }, 151 thunkAPI 152 ) => { 153 const res = await retrieveDiff(leftId, rightId); 154 155 if (res.isOk) { 156 return Promise.resolve({ profile: res.value }); 157 } 158 159 thunkAPI.dispatch( 160 addNotification({ 161 type: 'danger', 162 title: 'Failed to load adhoc diff', 163 message: res.error.message, 164 }) 165 ); 166 167 return Promise.reject(res.error); 168 } 169 ); 170 171 export const adhocSlice = createSlice({ 172 name: 'adhoc', 173 initialState, 174 reducers: { 175 removeFile(state, action: PayloadAction<{ side: side }>) { 176 state.upload[action.payload.side] = { 177 type: 'pristine', 178 }; 179 }, 180 }, 181 extraReducers: (builder) => { 182 builder.addCase(uploadFile.pending, (state, action) => { 183 state.upload[action.meta.arg.side] = { 184 type: 'loading', 185 fileName: action.meta.arg.file.name, 186 }; 187 }); 188 builder.addCase(uploadFile.rejected, (state, action) => { 189 // Since the file is invalid, let's remove it 190 state.upload[action.meta.arg.side] = { 191 type: 'pristine', 192 }; 193 }); 194 195 builder.addCase(uploadFile.fulfilled, (state, action) => { 196 const s = action.meta.arg; 197 198 // state.upload[s.side] = { type: 'loaded', fileName: s.file.name }; 199 state.upload[s.side] = { type: 'pristine' }; 200 201 state.shared[s.side] = { 202 type: 'loaded', 203 profile: action.payload.profile.flamebearer, 204 id: action.payload.profile.id, 205 }; 206 }); 207 208 builder.addCase(fetchProfile.fulfilled, (state, action) => { 209 const { side } = action.meta.arg; 210 211 // After loading a profile, there's no uploaded profile 212 state.upload[side] = { 213 type: 'pristine', 214 }; 215 216 state.shared[side] = { 217 type: 'loaded', 218 profile: action.payload.profile, 219 id: action.payload.id, 220 }; 221 }); 222 223 builder.addCase(fetchAllProfiles.fulfilled, (state, action) => { 224 state.shared.profilesList = { 225 type: 'loaded', 226 profilesList: action.payload, 227 }; 228 }); 229 230 builder.addCase(fetchDiffProfile.pending, (state) => { 231 state.diff = { 232 // Keep previous value 233 ...state.diff, 234 type: 'loading', 235 }; 236 }); 237 238 builder.addCase(fetchDiffProfile.fulfilled, (state, action) => { 239 state.diff = { 240 type: 'loaded', 241 profile: action.payload.profile, 242 }; 243 }); 244 }, 245 }); 246 247 const selectAdhocState = (state: RootState) => { 248 return state.adhoc; 249 }; 250 251 export const selectShared = (state: RootState) => { 252 return selectAdhocState(state).shared; 253 }; 254 255 export const selectProfilesList = (state: RootState) => { 256 return selectShared(state).profilesList; 257 }; 258 259 export const selectedSelectedProfileId = (side: side) => (state: RootState) => { 260 return Maybe.of(selectShared(state)[side].id); 261 }; 262 263 export const selectProfile = (side: side) => (state: RootState) => { 264 return Maybe.of(selectShared(state)[side].profile); 265 }; 266 267 export const selectDiffProfile = (state: RootState) => { 268 return Maybe.of(selectAdhocState(state).diff.profile); 269 }; 270 271 export const selectProfileId = (side: side) => (state: RootState) => { 272 return Maybe.of(selectShared(state)[side].id); 273 }; 274 275 export const { removeFile } = adhocSlice.actions; 276 export default adhocSlice.reducer;