src/pages/Home/Home.state.ts (154 lines of code) (raw):
import { Run } from '@/types';
import { sortRuns } from '@pages/Home/Home.utils';
export type HomeState = {
initialised: boolean;
// Loader that we show when we are about to replace existing data.
showLoader: boolean;
// Current page number. Get overriden by placeholderParameter on reordering queries
page: number;
// Keep track if we are on last page.
isLastPage: boolean;
// Groups to render {[groupName]: data}
rungroups: Record<string, Run[]>;
// New runs that are not yet added to list
// TODO: Explain better here why this is
newRuns: Run[];
// Active parameters
params: Record<string, string>;
// WARNING: These placeHolderParameters are workaround for one special case. Generally when we are changing filters, we want to
// reset page to start (page = 1). BUT when ordering stuff again, we want to keep same amount items as before. We don't
// want to interfere pagination overall, but we need to fetch all new stuff in one request, so we store these placeHolderParameters here
// for one request.
//
// For example if we are in page 5 with limit 15, when we reorder fakeparams would be set to page=1&limit=75. However url params are kept
// in page=5&limit15 so that when user scrolls down, we can fetch sixth page easily.
placeHolderParameters: { _limit: string; _page: string } | null;
// Track if we are scrolled from top. This will cause new runs to go "newData" pool instead of straight to view
isScrolledFromTop: boolean;
};
type HomeAction =
| { type: 'setLoader'; show: boolean }
| { type: 'setPage'; page: number }
| { type: 'setLastPage'; isLast: boolean }
| { type: 'data'; data: Run[]; replace: boolean; isLastPage?: boolean }
| { type: 'realtimeData'; data: Run[] }
| { type: 'setParams'; params: Record<string, string>; cachedResult: boolean }
| { type: 'groupReset' }
| { type: 'setScroll'; isScrolledFromTop: boolean };
const HomeReducer = (state: HomeState, action: HomeAction): HomeState => {
switch (action.type) {
case 'setPage':
return { ...state, page: action.page, placeHolderParameters: null };
case 'setLastPage':
return { ...state, isLastPage: action.isLast };
case 'setLoader':
return { ...state, showLoader: action.show };
case 'setParams': {
const shouldGoToPageOne =
state.params.flow_id !== action.params.flow_id ||
state.params._tags !== action.params._tags ||
state.params.status !== action.params.status ||
state.params._group !== action.params._group;
const reordering = state.params._order !== action.params._order && state.page > 1;
return {
...state,
params: action.params,
placeHolderParameters:
reordering || action.cachedResult
? { _limit: String(parseInt(action.params._limit) * state.page), _page: '1' }
: null,
page: action.cachedResult ? state.page : shouldGoToPageOne ? 1 : reordering ? state.page : 1,
showLoader: true,
initialised: true,
};
}
case 'data':
return {
...state,
isLastPage: typeof action.isLastPage === 'boolean' ? action.isLastPage : state.isLastPage,
// Add / merge incoming stuff to existing, if replace parameter is given, clear old stuff
rungroups: mergeTo(action.data, action.replace ? {} : state.rungroups, state.params),
// If we have replace parameters, it means that old websocket updates are not relevant anymore so
// we can clear new runs here.
newRuns: action.replace ? [] : state.newRuns,
showLoader: false,
};
case 'realtimeData': {
if (state.isScrolledFromTop && !state.params._group) {
return {
...state,
...mergeWithSeparatePool(action.data, { rungroups: state.rungroups, newRuns: state.newRuns }, state.params),
};
}
return {
...state,
rungroups: mergeTo(action.data, state.rungroups, state.params),
};
}
case 'groupReset':
return { ...state, rungroups: {}, newRuns: [], showLoader: true, page: 1 };
case 'setScroll': {
if (!action.isScrolledFromTop && state.isScrolledFromTop && Object.keys(state.rungroups).length > 0) {
return {
...state,
isScrolledFromTop: action.isScrolledFromTop,
rungroups: mergeTo(state.newRuns, state.rungroups, state.params),
newRuns: [],
};
}
return { ...state, isScrolledFromTop: action.isScrolledFromTop };
}
}
};
/**
* Add or merge incoming runs to existing run groups
* @param runs Incoming runs
* @param initialData Existing run groups
* @param params
*/
export function mergeTo(
runs: Run[],
initialData: Record<string, Run[]>,
params: Record<string, string>,
): Record<string, Run[]> {
return runs.reduce((data, item) => {
const groupKey = item[params._group as keyof Run] || 'undefined';
if (typeof groupKey === 'string') {
if (data[groupKey]) {
const index = data[groupKey].findIndex((r) => r.run_number === item.run_number);
return {
...data,
[groupKey]:
index > -1
? sortRuns(
data[groupKey].map((r) => (r.run_number === item.run_number ? item : r)),
params._order,
)
: sortRuns([...data[groupKey], item], params._order),
};
} else {
return {
...data,
[groupKey]: [item],
};
}
}
return data;
}, initialData);
}
type DataAndNew = {
rungroups: Record<string, Run[]>;
newRuns: Run[];
};
/**
* Adds new runs to existing run groups OR in some cases add them to separate pool to be added to run groups later.
* If we are not grouping and user has scrolled the view, we don't want to add new runs to the list since it would cause
* layout shifts. We only add new runs to view when we scroll up. BUT if incoming run already exists in view, we will update it.
* @param runs Incoming runs
* @param initialData Existing runs that are visible in UI and runs that are not visible, but are queued to be shown
* @param params
*/
export function mergeWithSeparatePool(
runs: Run[],
initialData: DataAndNew,
params: Record<string, string>,
): DataAndNew {
return runs.reduce((data, item): DataAndNew => {
const groupKey = item[params._group as keyof Run] || 'undefined';
if (typeof groupKey === 'string') {
// If we already have same group, we need to check if we can add current item there
if (data.rungroups[groupKey]) {
const index = data.rungroups[groupKey].findIndex((r) => r.run_number === item.run_number);
// If we already have this run, we can update it....
if (index > -1) {
return {
rungroups: {
...data.rungroups,
[groupKey]: sortRuns(
data.rungroups[groupKey].map((r) => (r.run_number === item.run_number ? item : r)),
params._order,
),
},
newRuns: data.newRuns,
};
}
}
//...else we need to merge or add it to newData
const indexInNewData = data.newRuns.findIndex((r) => r.run_number === item.run_number);
return {
rungroups: data.rungroups,
newRuns:
indexInNewData > -1
? data.newRuns.map((r) => (r.run_number === item.run_number ? item : r))
: [...data.newRuns, item],
};
}
return data;
}, initialData);
}
export default HomeReducer;