Merge branch 'feat/236-aurora-1843-svod2-server-paging-web' into feat/236-aurora-1843-svod2-server-paging-perf
This commit is contained in:
commit
0a74c9ebc0
@ -1,5 +1,5 @@
|
|||||||
import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';
|
import RefreshRoundedIcon from '@mui/icons-material/RefreshRounded';
|
||||||
import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material';
|
import { Alert, Box, Button, Paper, Skeleton, Tab, Tabs, Typography } from '@mui/material';
|
||||||
import { MRT_TableHead, MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
import { MRT_TableHead, MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import { MRT_Localization_RU } from 'material-react-table/locales/ru';
|
import { MRT_Localization_RU } from 'material-react-table/locales/ru';
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -13,7 +13,7 @@ import DenominationSelect from '../../components/common/DenominationSelect';
|
|||||||
import useDenomination from '../../components/common/useDenomination';
|
import useDenomination from '../../components/common/useDenomination';
|
||||||
import { useSvod } from '../SvodPage/SvodContext';
|
import { useSvod } from '../SvodPage/SvodContext';
|
||||||
import { SvodFilters } from '../SvodPage/SvodFilters';
|
import { SvodFilters } from '../SvodPage/SvodFilters';
|
||||||
import { EMPTY_FACET_MAP, getSvod2RowId, isPlaceholderRow, shouldShowFound } from './svod2Paging';
|
import { EMPTY_FACET_MAP, getSkeletonWidth, getSvod2RowId, isPlaceholderRow, shouldShowFound } from './svod2Paging';
|
||||||
import { SVOD2_LAYOUT_OPTIONS, SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles';
|
import { SVOD2_LAYOUT_OPTIONS, SVOD2_TABLE_CONTAINER_SX, createColumnClassMap, getEstimateTypeClassName } from './svod2Styles';
|
||||||
import { useSvod2Pages } from './useSvod2Pages';
|
import { useSvod2Pages } from './useSvod2Pages';
|
||||||
|
|
||||||
@ -402,7 +402,17 @@ export default function Svod2Page() {
|
|||||||
},
|
},
|
||||||
muiTableHeadCellProps: ({ column }) => ({ className: columnClassNames.get(column.id) }),
|
muiTableHeadCellProps: ({ column }) => ({ className: columnClassNames.get(column.id) }),
|
||||||
muiTableHeadRowProps: { sx: { boxShadow: 'none' } },
|
muiTableHeadRowProps: { sx: { boxShadow: 'none' } },
|
||||||
muiTableBodyCellProps: ({ column }) => ({ className: columnClassNames.get(column.id) }),
|
muiTableBodyCellProps: ({ column, row }) => ({
|
||||||
|
className: columnClassNames.get(column.id),
|
||||||
|
// Ячейки строк-заглушек: полосы загрузки того же вида, что MRT рисует при «Обновить» (Skeleton wave, высота 20).
|
||||||
|
...(isPlaceholderRow(row.original)
|
||||||
|
? {
|
||||||
|
children: (
|
||||||
|
<Skeleton animation='wave' height={20} width={getSkeletonWidth(column.getSize(), row.original.index, column.getIndex())} />
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}),
|
||||||
localization: {
|
localization: {
|
||||||
...MRT_Localization_RU,
|
...MRT_Localization_RU,
|
||||||
noRecordsToDisplay: 'Нет данных для выбранных параметров',
|
noRecordsToDisplay: 'Нет данных для выбранных параметров',
|
||||||
|
|||||||
@ -111,6 +111,21 @@ export function shouldRefine({ hasCurrentData, loading, pendingQueryKey, queryKe
|
|||||||
return Boolean(loading) && pendingQueryKey === queryKey;
|
return Boolean(loading) && pendingQueryKey === queryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ширина полосы загрузки в ячейке строки-заглушки: детерминированная (без Math.random, чтобы полосы не «прыгали»
|
||||||
|
// при перерисовке окна), от трети до полной ширины колонки, как у штатных полос MRT.
|
||||||
|
export function getSkeletonWidth(columnSize, rowIndex, columnIndex) {
|
||||||
|
const hash = ((rowIndex + 1) * 2654435761 + (columnIndex + 1) * 40503) >>> 0;
|
||||||
|
const min = Math.round(columnSize / 3);
|
||||||
|
return (min + ((hash % 1000) / 1000) * (columnSize - min)) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// При смене поиска/фильтров на глубине первая запрошенная страница (по прокрутке) может оказаться за концом нового,
|
||||||
|
// короткого списка: сервер ответит только total. Тогда страницу 0 и общее число берём из запасного запроса offset=0,
|
||||||
|
// не дожидаясь (и не ожидая) ответа на страницу за пределами списка.
|
||||||
|
export function shouldFetchFirstPageFallback({ candidate, total }) {
|
||||||
|
return candidate > 0 && candidate >= getPageCount(total);
|
||||||
|
}
|
||||||
|
|
||||||
export function isPlaceholderRow(row) {
|
export function isPlaceholderRow(row) {
|
||||||
return Boolean(row?.__placeholder);
|
return Boolean(row?.__placeholder);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,14 @@ import {
|
|||||||
facetsToMaps,
|
facetsToMaps,
|
||||||
getMissingPages,
|
getMissingPages,
|
||||||
getPageCount,
|
getPageCount,
|
||||||
|
getSkeletonWidth,
|
||||||
getSvod2RowId,
|
getSvod2RowId,
|
||||||
getVirtualizerPadding,
|
getVirtualizerPadding,
|
||||||
getVisiblePage,
|
getVisiblePage,
|
||||||
getWindowPages,
|
getWindowPages,
|
||||||
getWindowRange,
|
getWindowRange,
|
||||||
isPlaceholderRow,
|
isPlaceholderRow,
|
||||||
|
shouldFetchFirstPageFallback,
|
||||||
shouldRefine,
|
shouldRefine,
|
||||||
shouldShowFound,
|
shouldShowFound,
|
||||||
svod2PagingReducer,
|
svod2PagingReducer,
|
||||||
@ -307,3 +309,22 @@ test('shouldRefine: первое монтаж и смена вкладки бе
|
|||||||
assert.equal(shouldRefine({ ...base, dataRefineKey: undefined }), false);
|
assert.equal(shouldRefine({ ...base, dataRefineKey: undefined }), false);
|
||||||
assert.equal(shouldRefine({ ...base, hasCurrentData: true, dataRefineKey: 'a' }), false);
|
assert.equal(shouldRefine({ ...base, hasCurrentData: true, dataRefineKey: 'a' }), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('полосы загрузки: ширина детерминирована и в пределах колонки', () => {
|
||||||
|
for (const size of [60, 150, 400]) {
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const w = getSkeletonWidth(size, i, 3);
|
||||||
|
assert.equal(w, getSkeletonWidth(size, i, 3));
|
||||||
|
assert.ok(w >= Math.round(size / 3) && w <= size, `${size}:${i}:${w}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.notEqual(getSkeletonWidth(400, 1, 0), getSkeletonWidth(400, 2, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('первая страница при смене условий: запасной offset=0, если запрошенная страница за концом нового списка', () => {
|
||||||
|
assert.equal(shouldFetchFirstPageFallback({ candidate: 0, total: 43 }), false);
|
||||||
|
assert.equal(shouldFetchFirstPageFallback({ candidate: 37, total: 43 }), true);
|
||||||
|
assert.equal(shouldFetchFirstPageFallback({ candidate: 37, total: 0 }), true);
|
||||||
|
assert.equal(shouldFetchFirstPageFallback({ candidate: 37, total: 22419 }), false);
|
||||||
|
assert.equal(shouldFetchFirstPageFallback({ candidate: 75, total: 22419 }), true);
|
||||||
|
});
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
getVisiblePage,
|
getVisiblePage,
|
||||||
getWindowPages,
|
getWindowPages,
|
||||||
getWindowRange,
|
getWindowRange,
|
||||||
|
shouldFetchFirstPageFallback,
|
||||||
shouldRefine,
|
shouldRefine,
|
||||||
svod2PagingReducer,
|
svod2PagingReducer,
|
||||||
} from './svod2Paging';
|
} from './svod2Paging';
|
||||||
@ -59,7 +60,7 @@ export function useSvod2Pages({ sheet, year, orgIds, queryKey, search, columnFil
|
|||||||
if (mountedRef.current) setState(next);
|
if (mountedRef.current) setState(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchPage = useCallback(async (ctx, pageIndex) => {
|
const fetchPage = useCallback(async (ctx, pageIndex, signal = ctx.controller.signal) => {
|
||||||
ctx.inflight.add(pageIndex);
|
ctx.inflight.add(pageIndex);
|
||||||
try {
|
try {
|
||||||
const response = await SummaryApi.getSvod2Page(
|
const response = await SummaryApi.getSvod2Page(
|
||||||
@ -71,7 +72,7 @@ export function useSvod2Pages({ sheet, year, orgIds, queryKey, search, columnFil
|
|||||||
columnFilters: ctx.columnFilters,
|
columnFilters: ctx.columnFilters,
|
||||||
offset: pageIndex * PAGE_SIZE,
|
offset: pageIndex * PAGE_SIZE,
|
||||||
}),
|
}),
|
||||||
{ signal: ctx.controller.signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
const rows = response.result || [];
|
const rows = response.result || [];
|
||||||
return { rows, total: response.total ?? rows.length };
|
return { rows, total: response.total ?? rows.length };
|
||||||
@ -138,14 +139,43 @@ export function useSvod2Pages({ sheet, year, orgIds, queryKey, search, columnFil
|
|||||||
|
|
||||||
const loadPages = async () => {
|
const loadPages = async () => {
|
||||||
try {
|
try {
|
||||||
const first = await fetchPage(ctx, candidate);
|
// Страница по прокрутке и (на глубине) запасная offset=0 летят параллельно: если страница по прокрутке
|
||||||
|
// за концом нового списка, сервер отвечает на неё долго и без строк, а страница 0 уже даёт total.
|
||||||
|
const candidateController = new AbortController();
|
||||||
|
const zeroController = new AbortController();
|
||||||
|
const abortRequests = () => {
|
||||||
|
candidateController.abort();
|
||||||
|
zeroController.abort();
|
||||||
|
};
|
||||||
|
ctx.controller.signal.addEventListener('abort', abortRequests);
|
||||||
|
const settle = (request, index) =>
|
||||||
|
request.then(
|
||||||
|
(result) => ({ result, index }),
|
||||||
|
(error) => ({ error, index }),
|
||||||
|
);
|
||||||
|
const candidateRequest = settle(fetchPage(ctx, candidate, candidateController.signal), candidate);
|
||||||
|
const zeroRequest = candidate > 0 ? settle(fetchPage(ctx, 0, zeroController.signal), 0) : null;
|
||||||
|
let answer = zeroRequest ? await Promise.race([candidateRequest, zeroRequest]) : await candidateRequest;
|
||||||
|
if (
|
||||||
|
answer.index === 0 &&
|
||||||
|
candidate > 0 &&
|
||||||
|
!answer.error &&
|
||||||
|
shouldFetchFirstPageFallback({ candidate, total: answer.result.total })
|
||||||
|
) {
|
||||||
|
candidateController.abort();
|
||||||
|
} else {
|
||||||
|
zeroController.abort();
|
||||||
|
answer = await candidateRequest;
|
||||||
|
if (answer.error) throw answer.error;
|
||||||
|
}
|
||||||
if (ctx.controller.signal.aborted) return;
|
if (ctx.controller.signal.aborted) return;
|
||||||
|
const first = answer.result;
|
||||||
const { total } = first;
|
const { total } = first;
|
||||||
const pages = getWindowFor(total);
|
const pages = getWindowFor(total);
|
||||||
const pageCount = getPageCount(total);
|
const pageCount = getPageCount(total);
|
||||||
let cache = new Map();
|
let cache = new Map();
|
||||||
if (total === 0 || candidate < pageCount) {
|
if (total === 0 || answer.index < pageCount) {
|
||||||
const pageIndex = total === 0 ? 0 : candidate;
|
const pageIndex = total === 0 ? 0 : answer.index;
|
||||||
dispatch({ type: 'page', sheet: sheetKey, generation, pageIndex, rows: first.rows, total, keepPages: pages });
|
dispatch({ type: 'page', sheet: sheetKey, generation, pageIndex, rows: first.rows, total, keepPages: pages });
|
||||||
cache = stateRef.current[sheetKey].pages;
|
cache = stateRef.current[sheetKey].pages;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user