1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +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',
fontWeight: theme.fontWeight.medium,
},
flex: {
justifyContent: 'stretch',
alignItems: 'center',
display: 'flex',
flexShrink: 0,
'& > *': {
flexGrow: 1,
},
},
flexGrow: {
flexGrow: 1,
},
sortable: {
padding: 0,
'&:hover, &:focus': {

View File

@ -23,6 +23,8 @@ interface ICellSortableProps {
minWidth?: number | string;
maxWidth?: number | string;
align?: 'left' | 'center' | 'right';
isFlex?: boolean;
isFlexGrow?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>;
}
@ -36,6 +38,8 @@ export const CellSortable: FC<ICellSortableProps> = ({
maxWidth,
align,
ariaTitle,
isFlex,
isFlexGrow,
onClick = () => {},
}) => {
const { setAnnouncement } = useContext(AnnouncerContext);
@ -92,7 +96,12 @@ export const CellSortable: FC<ICellSortableProps> = ({
<TableCell
component="th"
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 }}
>
<ConditionallyRender

View File

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

View File

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

View File

@ -35,4 +35,17 @@ export const useStyles = makeStyles()(theme => ({
justifyContent: 'space-between',
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 limit = 300; // if above limit, render only `pageSize` of items
const pageSize = 100;
export const ProjectFeatureToggles = ({
features,
@ -91,34 +93,36 @@ export const ProjectFeatureToggles = ({
}) as ListItemType[];
}
return features.map(
({
name,
lastSeenAt,
createdAt,
type,
stale,
environments: featureEnvironments,
}) => ({
name,
lastSeenAt,
createdAt,
type,
stale,
environments: Object.fromEntries(
environments.map(env => [
env,
{
name: env,
enabled:
featureEnvironments?.find(
feature => feature?.name === env
)?.enabled || false,
},
])
),
})
);
return features
.slice(0, features.length > limit ? pageSize : limit)
.map(
({
name,
lastSeenAt,
createdAt,
type,
stale,
environments: featureEnvironments,
}) => ({
name,
lastSeenAt,
createdAt,
type,
stale,
environments: Object.fromEntries(
environments.map(env => [
env,
{
name: env,
enabled:
featureEnvironments?.find(
feature => feature?.name === env
)?.enabled || false,
},
])
),
})
);
}, [features, loading]); // eslint-disable-line react-hooks/exhaustive-deps
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
@ -366,7 +370,11 @@ export const ProjectFeatureToggles = ({
header={
<PageHeader
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={
<>
<TableSearch

View File

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

View File

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