diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts index e7e9c88560..3a7ffa4823 100644 --- a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts @@ -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': { diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx index 59b7c423e5..08eb23bc0a 100644 --- a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx @@ -23,6 +23,8 @@ interface ICellSortableProps { minWidth?: number | string; maxWidth?: number | string; align?: 'left' | 'center' | 'right'; + isFlex?: boolean; + isFlexGrow?: boolean; onClick?: MouseEventHandler; } @@ -36,6 +38,8 @@ export const CellSortable: FC = ({ maxWidth, align, ariaTitle, + isFlex, + isFlexGrow, onClick = () => {}, }) => { const { setAnnouncement } = useContext(AnnouncerContext); @@ -92,7 +96,12 @@ export const CellSortable: FC = ({ []; className?: string; + flex?: boolean; } export const SortableTableHeader: VFC = ({ headerGroups, className, + flex, }) => { const { classes: styles } = useStyles(); @@ -43,6 +45,8 @@ export const SortableTableHeader: VFC = ({ 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} > diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index a5ca137517..54207fe238 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -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 = [ ), 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 = { 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 ( { {/* @ts-expect-error -- fix in react-table v8 */} - - - {rows.map(row => { + + + {rows.map((row, index) => { + const isVirtual = + index > scrollOffset + scrollIndex || + index + scrollOffset < scrollIndex; + + if (isVirtual) { + return null; + } + prepareRow(row); return ( - + {row.cells.map(cell => ( - + {cell.render('Cell')} ))} diff --git a/frontend/src/component/feature/FeatureToggleList/styles.ts b/frontend/src/component/feature/FeatureToggleList/styles.ts index adee1300d7..cd6593f4c3 100644 --- a/frontend/src/component/feature/FeatureToggleList/styles.ts +++ b/frontend/src/component/feature/FeatureToggleList/styles.ts @@ -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, + }, + }, })); diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 70abc92201..201be258b0 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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={ limit + ? `first ${rows.length} of ${features.length}` + : data.length + })`} actions={ <>