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;