1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

Fix: Features table performance (#1015)

* feat: persistent table query

* project overview sort query

* refactor: api methods as hook callbacks

* persitent columns in project overview

* enable new project overview

* fix: refactor feature state change in overview

* add type to sort

* update e2e tests

now takes 10% less time with use of cypress session

* prevent sort reset on features list

* fix feature toggle list loading

* fix: features table items virtualization

* project overview screen limits

* table row height in theme

* rename row index variable
This commit is contained in:
Tymoteusz Czech 2022-05-25 16:39:14 +02:00 committed by GitHub
parent ded3c22bb1
commit 7480085698
8 changed files with 152 additions and 39 deletions

View File

@ -5,6 +5,18 @@ export const useStyles = makeStyles()(theme => ({
position: 'relative', position: 'relative',
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
}, },
flex: {
justifyContent: 'stretch',
alignItems: 'center',
display: 'flex',
flexShrink: 0,
'& > *': {
flexGrow: 1,
},
},
flexGrow: {
flexGrow: 1,
},
sortable: { sortable: {
padding: 0, padding: 0,
'&:hover, &:focus': { '&:hover, &:focus': {

View File

@ -23,6 +23,8 @@ interface ICellSortableProps {
minWidth?: number | string; minWidth?: number | string;
maxWidth?: number | string; maxWidth?: number | string;
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right';
isFlex?: boolean;
isFlexGrow?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>; onClick?: MouseEventHandler<HTMLButtonElement>;
} }
@ -36,6 +38,8 @@ export const CellSortable: FC<ICellSortableProps> = ({
maxWidth, maxWidth,
align, align,
ariaTitle, ariaTitle,
isFlex,
isFlexGrow,
onClick = () => {}, onClick = () => {},
}) => { }) => {
const { setAnnouncement } = useContext(AnnouncerContext); const { setAnnouncement } = useContext(AnnouncerContext);
@ -92,7 +96,12 @@ export const CellSortable: FC<ICellSortableProps> = ({
<TableCell <TableCell
component="th" component="th"
aria-sort={ariaSort} aria-sort={ariaSort}
className={classnames(styles.header, isSortable && styles.sortable)} className={classnames(
styles.header,
isSortable && styles.sortable,
isFlex && styles.flex,
isFlexGrow && styles.flexGrow
)}
style={{ width, minWidth, maxWidth }} style={{ width, minWidth, maxWidth }}
> >
<ConditionallyRender <ConditionallyRender

View File

@ -7,11 +7,13 @@ import { CellSortable } from './CellSortable/CellSortable';
interface ISortableTableHeaderProps { interface ISortableTableHeaderProps {
headerGroups: HeaderGroup<object>[]; headerGroups: HeaderGroup<object>[];
className?: string; className?: string;
flex?: boolean;
} }
export const SortableTableHeader: VFC<ISortableTableHeaderProps> = ({ export const SortableTableHeader: VFC<ISortableTableHeaderProps> = ({
headerGroups, headerGroups,
className, className,
flex,
}) => { }) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -43,6 +45,8 @@ export const SortableTableHeader: VFC<ISortableTableHeaderProps> = ({
maxWidth={column.maxWidth} maxWidth={column.maxWidth}
minWidth={column.minWidth} minWidth={column.minWidth}
width={column.width} width={column.width}
isFlex={flex}
isFlexGrow={Boolean(column.minWidth)}
// @ts-expect-error -- check after `react-table` v8 // @ts-expect-error -- check after `react-table` v8
align={column.align} align={column.align}
> >

View File

@ -1,7 +1,13 @@
import { useEffect, useMemo, useState, VFC } from 'react'; import { useEffect, useMemo, useState, VFC } from 'react';
import { Link, useMediaQuery, useTheme } from '@mui/material'; import { Link, useMediaQuery, useTheme } from '@mui/material';
import { Link as RouterLink, useSearchParams } from 'react-router-dom'; import { Link as RouterLink, useSearchParams } from 'react-router-dom';
import { SortingRule, useGlobalFilter, useSortBy, useTable } from 'react-table'; import {
SortingRule,
useFlexLayout,
useGlobalFilter,
useSortBy,
useTable,
} from 'react-table';
import { import {
Table, Table,
SortableTableHeader, SortableTableHeader,
@ -26,6 +32,7 @@ import { FeatureSchema } from 'openapi';
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'; import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
import { useStyles } from './styles';
const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
name: 'Name of the feature', name: 'Name of the feature',
@ -44,18 +51,19 @@ const columns = [
Cell: FeatureSeenCell, Cell: FeatureSeenCell,
sortType: 'date', sortType: 'date',
align: 'center', align: 'center',
maxWidth: 85,
}, },
{ {
Header: 'Type', Header: 'Type',
accessor: 'type', accessor: 'type',
Cell: FeatureTypeCell, Cell: FeatureTypeCell,
align: 'center', align: 'center',
maxWidth: 85,
}, },
{ {
Header: 'Feature toggle name', Header: 'Feature toggle name',
accessor: 'name', accessor: 'name',
maxWidth: 300, minWidth: 150,
width: '67%',
Cell: FeatureNameCell, Cell: FeatureNameCell,
sortType: 'alphanumeric', sortType: 'alphanumeric',
}, },
@ -64,6 +72,7 @@ const columns = [
accessor: 'createdAt', accessor: 'createdAt',
Cell: DateCell, Cell: DateCell,
sortType: 'date', sortType: 'date',
maxWidth: 150,
}, },
{ {
Header: 'Project ID', Header: 'Project ID',
@ -72,12 +81,14 @@ const columns = [
<LinkCell title={value} to={`/projects/${value}`} /> <LinkCell title={value} to={`/projects/${value}`} />
), ),
sortType: 'alphanumeric', sortType: 'alphanumeric',
maxWidth: 150,
}, },
{ {
Header: 'State', Header: 'State',
accessor: 'stale', accessor: 'stale',
Cell: FeatureStaleCell, Cell: FeatureStaleCell,
sortType: 'boolean', sortType: 'boolean',
maxWidth: 120,
}, },
// Always hidden -- for search // Always hidden -- for search
{ {
@ -85,10 +96,14 @@ const columns = [
}, },
]; ];
const scrollOffset = 50;
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: false }; const defaultSort: SortingRule<string> = { id: 'createdAt', desc: false };
export const FeatureToggleListTable: VFC = () => { export const FeatureToggleListTable: VFC = () => {
const theme = useTheme(); const theme = useTheme();
const rowHeight = theme.shape.tableRowHeight;
const { classes } = useStyles();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -139,7 +154,8 @@ export const FeatureToggleListTable: VFC = () => {
disableMultiSort: true, disableMultiSort: true,
}, },
useGlobalFilter, useGlobalFilter,
useSortBy useSortBy,
useFlexLayout
); );
useEffect(() => { useEffect(() => {
@ -174,6 +190,21 @@ export const FeatureToggleListTable: VFC = () => {
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false }); setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
}, [sortBy, globalFilter, setSearchParams, setStoredParams]); }, [sortBy, globalFilter, setSearchParams, setStoredParams]);
const [scrollIndex, setScrollIndex] = useState(0);
useEffect(() => {
const handleScroll = () => {
requestAnimationFrame(() => {
const position = window.pageYOffset;
setScrollIndex(Math.floor(position / (rowHeight * 5)) * 5);
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [rowHeight]);
return ( return (
<PageContent <PageContent
isLoading={loading} isLoading={loading}
@ -207,14 +238,46 @@ export const FeatureToggleListTable: VFC = () => {
<SearchHighlightProvider value={globalFilter}> <SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}> <Table {...getTableProps()}>
{/* @ts-expect-error -- fix in react-table v8 */} {/* @ts-expect-error -- fix in react-table v8 */}
<SortableTableHeader headerGroups={headerGroups} /> <SortableTableHeader headerGroups={headerGroups} flex />
<TableBody {...getTableBodyProps()}> <TableBody
{rows.map(row => { {...getTableBodyProps()}
style={{
height: `${rowHeight * rows.length}px`,
position: 'relative',
}}
>
{rows.map((row, index) => {
const isVirtual =
index > scrollOffset + scrollIndex ||
index + scrollOffset < scrollIndex;
if (isVirtual) {
return null;
}
prepareRow(row); prepareRow(row);
return ( return (
<TableRow hover {...row.getRowProps()}> <TableRow
hover
{...row.getRowProps()}
key={row.id}
className={classes.row}
style={{
top: `${index * rowHeight}px`,
display: 'flex',
}}
>
{row.cells.map(cell => ( {row.cells.map(cell => (
<TableCell {...cell.getCellProps()}> <TableCell
{...cell.getCellProps({
style: {
flex: cell.column.minWidth
? '1 0 auto'
: undefined,
},
})}
className={classes.cell}
>
{cell.render('Cell')} {cell.render('Cell')}
</TableCell> </TableCell>
))} ))}

View File

@ -35,4 +35,17 @@ export const useStyles = makeStyles()(theme => ({
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}, },
row: {
height: theme.shape.tableRowHeight,
position: 'absolute',
width: '100%',
},
cell: {
alignItems: 'center',
display: 'flex',
flexShrink: 0,
'& > *': {
flexGrow: 1,
},
},
})); }));

View File

@ -58,6 +58,8 @@ type ListItemType = Pick<
}; };
const staticColumns = ['Actions', 'name']; const staticColumns = ['Actions', 'name'];
const limit = 300; // if above limit, render only `pageSize` of items
const pageSize = 100;
export const ProjectFeatureToggles = ({ export const ProjectFeatureToggles = ({
features, features,
@ -91,7 +93,9 @@ export const ProjectFeatureToggles = ({
}) as ListItemType[]; }) as ListItemType[];
} }
return features.map( return features
.slice(0, features.length > limit ? pageSize : limit)
.map(
({ ({
name, name,
lastSeenAt, lastSeenAt,
@ -366,7 +370,11 @@ export const ProjectFeatureToggles = ({
header={ header={
<PageHeader <PageHeader
className={styles.title} className={styles.title}
title={`Project feature toggles (${rows.length})`} title={`Project feature toggles (${
features?.length > limit
? `first ${rows.length} of ${features.length}`
: data.length
})`}
actions={ actions={
<> <>
<TableSearch <TableSearch

View File

@ -43,6 +43,8 @@ export default createTheme({
borderRadiusMedium: '8px', borderRadiusMedium: '8px',
borderRadiusLarge: '12px', borderRadiusLarge: '12px',
borderRadiusExtraLarge: '20px', borderRadiusExtraLarge: '20px',
tableRowHeight: 64,
tableRowHeightDense: 48,
}, },
palette: { palette: {
primary: { primary: {

View File

@ -90,6 +90,8 @@ declare module '@mui/system/createTheme/shape' {
borderRadiusMedium: string; borderRadiusMedium: string;
borderRadiusLarge: string; borderRadiusLarge: string;
borderRadiusExtraLarge: string; borderRadiusExtraLarge: string;
tableRowHeight: number;
tableRowHeightDense: number;
} }
} }
declare module '@mui/material/styles/zIndex' { declare module '@mui/material/styles/zIndex' {