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;