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:
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,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
|
||||
|
@ -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