github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/redux/reducers/settings.ts (about) 1 import { createSlice, combineReducers } from '@reduxjs/toolkit'; 2 import { Users, type User } from '@webapp/models/users'; 3 import { APIKey, APIKeys } from '@webapp/models/apikeys'; 4 import { App } from '@webapp/models/app'; 5 6 import { 7 fetchUsers, 8 createUser as createUserAPI, 9 enableUser as enableUserAPI, 10 disableUser as disableUserAPI, 11 changeUserRole as changeUserRoleAPI, 12 deleteUser as deleteUserAPI, 13 } from '@webapp/services/users'; 14 import { 15 fetchAPIKeys, 16 createAPIKey as createAPIKeyAPI, 17 deleteAPIKey as deleteAPIKeyAPI, 18 } from '@webapp/services/apiKeys'; 19 import { fetchApps, deleteApp as deleteAppAPI } from '@webapp/services/apps'; 20 import type { RootState } from '@webapp/redux/store'; 21 import { addNotification } from './notifications'; 22 import { createAsyncThunk } from '../async-thunk'; 23 24 enum FetchStatus { 25 pristine = 'pristine', 26 loading = 'loading', 27 loaded = 'loaded', 28 failed = 'failed', 29 } 30 type DataWithStatus<T> = { type: FetchStatus; data?: T }; 31 32 const usersInitialState: DataWithStatus<Users> = { 33 type: FetchStatus.pristine, 34 data: undefined, 35 }; 36 37 const apiKeysInitialState: DataWithStatus<APIKeys> = { 38 type: FetchStatus.pristine, 39 data: undefined, 40 }; 41 42 const appsInitialState: DataWithStatus<App[]> = { 43 type: FetchStatus.pristine, 44 data: undefined, 45 }; 46 47 export const reloadApiKeys = createAsyncThunk( 48 'newRoot/reloadAPIKeys', 49 async (_, thunkAPI) => { 50 const res = await fetchAPIKeys(); 51 if (res.isOk) { 52 return Promise.resolve(res.value); 53 } 54 55 thunkAPI.dispatch( 56 addNotification({ 57 type: 'danger', 58 title: 'Failed to load api keys', 59 message: res.error.message, 60 }) 61 ); 62 63 return Promise.reject(res.error); 64 } 65 ); 66 67 export const reloadUsers = createAsyncThunk( 68 'newRoot/reloadUsers', 69 async (_, thunkAPI) => { 70 const res = await fetchUsers(); 71 72 if (res.isOk) { 73 return Promise.resolve(res.value); 74 } 75 76 thunkAPI.dispatch( 77 addNotification({ 78 type: 'danger', 79 title: 'Failed to load users', 80 message: res.error.message, 81 }) 82 ); 83 84 return Promise.reject(res.error); 85 } 86 ); 87 88 export const reloadApps = createAsyncThunk( 89 'newRoot/reloadApps', 90 async (_, thunkAPI) => { 91 const res = await fetchApps(); 92 93 if (res.isOk) { 94 return Promise.resolve(res.value); 95 } 96 97 // eslint-disable-next-line @typescript-eslint/no-floating-promises 98 thunkAPI.dispatch( 99 addNotification({ 100 type: 'danger', 101 title: 'Failed to load apps', 102 message: res.error.message, 103 }) 104 ); 105 106 return Promise.reject(res.error); 107 } 108 ); 109 110 export const enableUser = createAsyncThunk( 111 'newRoot/enableUser', 112 async (user: User, thunkAPI) => { 113 const res = await enableUserAPI(user); 114 115 if (res.isOk) { 116 thunkAPI.dispatch(reloadUsers()); 117 return Promise.resolve(true); 118 } 119 120 thunkAPI.dispatch( 121 addNotification({ 122 type: 'danger', 123 title: 'Failed to enable a user', 124 message: res.error.message, 125 }) 126 ); 127 128 return Promise.reject(res.error); 129 } 130 ); 131 132 export const disableUser = createAsyncThunk( 133 'newRoot/disableUser', 134 async (user: User, thunkAPI) => { 135 const res = await disableUserAPI(user); 136 137 if (res.isOk) { 138 thunkAPI.dispatch(reloadUsers()); 139 return Promise.resolve(true); 140 } 141 142 thunkAPI.dispatch( 143 addNotification({ 144 type: 'danger', 145 title: 'Failed to disable a user', 146 message: res.error.message, 147 }) 148 ); 149 150 return Promise.reject(res.error); 151 } 152 ); 153 154 export const createUser = createAsyncThunk( 155 'newRoot/createUser', 156 async (user: User, thunkAPI) => { 157 const res = await createUserAPI(user); 158 159 thunkAPI.dispatch(reloadUsers()); 160 161 if (res.isOk) { 162 return Promise.resolve(true); 163 } 164 165 thunkAPI.dispatch( 166 addNotification({ 167 type: 'danger', 168 title: 'Failed to create new user', 169 message: res.error.message, 170 }) 171 ); 172 return Promise.reject(res.error); 173 } 174 ); 175 176 export const deleteUser = createAsyncThunk( 177 'newRoot/deleteUser', 178 async (user: User, thunkAPI) => { 179 const res = await deleteUserAPI({ id: user.id }); 180 181 thunkAPI.dispatch(reloadUsers()); 182 183 if (res.isOk) { 184 return Promise.resolve(true); 185 } 186 187 thunkAPI.dispatch( 188 addNotification({ 189 type: 'danger', 190 title: 'Failed to delete user', 191 message: res.error.message, 192 }) 193 ); 194 return Promise.reject(res.error); 195 } 196 ); 197 198 export const changeUserRole = createAsyncThunk( 199 'users/changeUserRole', 200 async (action: Pick<User, 'id' | 'role'>, thunkAPI) => { 201 const { id, role } = action; 202 const res = await changeUserRoleAPI(id, role); 203 204 if (res.isOk) { 205 return Promise.resolve(true); 206 } 207 208 thunkAPI.dispatch( 209 addNotification({ 210 type: 'danger', 211 title: 'Failed to change users role', 212 message: res.error.message, 213 }) 214 ); 215 return thunkAPI.rejectWithValue(res.error); 216 } 217 ); 218 219 export const createAPIKey = createAsyncThunk( 220 'newRoot/createAPIKey', 221 async (data: Parameters<typeof createAPIKeyAPI>[0], thunkAPI) => { 222 const res = await createAPIKeyAPI(data); 223 224 if (res.isOk) { 225 return Promise.resolve(res.value); 226 } 227 228 thunkAPI.dispatch( 229 addNotification({ 230 type: 'danger', 231 title: 'Failed to create API key', 232 message: res.error.message, 233 }) 234 ); 235 return thunkAPI.rejectWithValue(res.error); 236 } 237 ); 238 239 export const deleteAPIKey = createAsyncThunk( 240 'newRoot/deleteAPIKey', 241 async (data: Pick<APIKey, 'id'>, thunkAPI) => { 242 const res = await deleteAPIKeyAPI(data); 243 if (res.isOk) { 244 thunkAPI.dispatch( 245 addNotification({ 246 type: 'success', 247 title: 'Key has been deleted', 248 message: `API Key id ${data.id} has been successfully deleted`, 249 }) 250 ); 251 return Promise.resolve(true); 252 } 253 254 thunkAPI.dispatch( 255 addNotification({ 256 type: 'danger', 257 title: 'Failed to delete API key', 258 message: res.error.message, 259 }) 260 ); 261 return thunkAPI.rejectWithValue(res.error); 262 } 263 ); 264 265 export const deleteApp = createAsyncThunk( 266 'newRoot/deleteApp', 267 async (app: App, thunkAPI) => { 268 const res = await deleteAppAPI({ name: app.name }); 269 270 // eslint-disable-next-line @typescript-eslint/no-floating-promises 271 thunkAPI.dispatch(reloadApps()); 272 273 if (res.isOk) { 274 return Promise.resolve(true); 275 } 276 277 // eslint-disable-next-line @typescript-eslint/no-floating-promises 278 thunkAPI.dispatch( 279 addNotification({ 280 type: 'danger', 281 title: 'Failed to delete app', 282 message: res.error.message, 283 }) 284 ); 285 return Promise.reject(res.error); 286 } 287 ); 288 289 export const usersSlice = createSlice({ 290 name: 'users', 291 initialState: usersInitialState, 292 reducers: {}, 293 extraReducers: (builder) => { 294 builder.addCase(reloadUsers.fulfilled, (state, action) => { 295 return { type: FetchStatus.loaded, data: action.payload }; 296 }); 297 298 builder.addCase(reloadUsers.pending, (state) => { 299 return { type: FetchStatus.loading, data: state.data }; 300 }); 301 builder.addCase(reloadUsers.rejected, (state) => { 302 return { type: FetchStatus.failed, data: state.data }; 303 }); 304 }, 305 }); 306 307 export const apiKeysSlice = createSlice({ 308 name: 'apiKeys', 309 initialState: apiKeysInitialState, 310 reducers: {}, 311 extraReducers: (builder) => { 312 builder.addCase(reloadApiKeys.fulfilled, (_, action) => { 313 return { type: FetchStatus.loaded, data: action.payload }; 314 }); 315 builder.addCase(reloadApiKeys.pending, (state) => { 316 return { type: FetchStatus.loading, data: state.data }; 317 }); 318 builder.addCase(reloadApiKeys.rejected, (state) => { 319 return { type: FetchStatus.failed, data: state.data }; 320 }); 321 }, 322 }); 323 324 export const appsSlice = createSlice({ 325 name: 'apps', 326 initialState: appsInitialState, 327 reducers: {}, 328 extraReducers: (builder) => { 329 builder.addCase(reloadApps.fulfilled, (_, action) => { 330 return { type: FetchStatus.loaded, data: action.payload }; 331 }); 332 builder.addCase(reloadApps.pending, (state) => { 333 return { type: FetchStatus.loading, data: state.data }; 334 }); 335 builder.addCase(reloadApps.rejected, (state) => { 336 return { type: FetchStatus.failed, data: state.data }; 337 }); 338 }, 339 }); 340 341 export const settingsState = (state: RootState) => state.settings; 342 343 export const usersState = (state: RootState) => state.settings.users; 344 export const selectUsers = (state: RootState) => state.settings.users.data; 345 346 export const apiKeysState = (state: RootState) => state.settings.apiKeys; 347 export const selectAPIKeys = (state: RootState) => state.settings.apiKeys.data; 348 349 export const appsState = (state: RootState) => state.settings.apps; 350 export const selectApps = (state: RootState) => state.settings.apps.data; 351 export const selectIsLoadingApps = (state: RootState) => { 352 return state.settings.apps.type === FetchStatus.loading; 353 }; 354 355 export default combineReducers({ 356 users: usersSlice.reducer, 357 apiKeys: apiKeysSlice.reducer, 358 apps: appsSlice.reducer, 359 });