mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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',
 | 
			
		||||
        fontWeight: theme.fontWeight.medium,
 | 
			
		||||
    },
 | 
			
		||||
    flex: {
 | 
			
		||||
        justifyContent: 'stretch',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        flexShrink: 0,
 | 
			
		||||
        '& > *': {
 | 
			
		||||
            flexGrow: 1,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    flexGrow: {
 | 
			
		||||
        flexGrow: 1,
 | 
			
		||||
    },
 | 
			
		||||
    sortable: {
 | 
			
		||||
        padding: 0,
 | 
			
		||||
        '&:hover, &:focus': {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
                            >
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
                                    ))}
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
@ -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,7 +93,9 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
            }) as ListItemType[];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return features.map(
 | 
			
		||||
        return features
 | 
			
		||||
            .slice(0, features.length > limit ? pageSize : limit)
 | 
			
		||||
            .map(
 | 
			
		||||
                ({
 | 
			
		||||
                    name,
 | 
			
		||||
                    lastSeenAt,
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,8 @@ export default createTheme({
 | 
			
		||||
        borderRadiusMedium: '8px',
 | 
			
		||||
        borderRadiusLarge: '12px',
 | 
			
		||||
        borderRadiusExtraLarge: '20px',
 | 
			
		||||
        tableRowHeight: 64,
 | 
			
		||||
        tableRowHeightDense: 48,
 | 
			
		||||
    },
 | 
			
		||||
    palette: {
 | 
			
		||||
        primary: {
 | 
			
		||||
 | 
			
		||||
@ -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' {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user