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:
parent
ded3c22bb1
commit
7480085698
@ -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': {
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -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
|
||||||
|
@ -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: {
|
||||||
|
@ -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' {
|
||||||
|
Loading…
Reference in New Issue
Block a user