mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-22 01:16:07 +02:00
Virtualize project toggles list (#1043)
* fix: virtualize project toggles list * refactor: improve type for feature dialogs * fix: formatting - prettier
This commit is contained in:
parent
c6f0a20fd6
commit
24cb1b21ef
@ -23,15 +23,16 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||||
|
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||||
import { FeatureSchema } from 'openapi';
|
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 { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||||
@ -100,8 +101,6 @@ 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 = () => {
|
||||||
@ -194,20 +193,8 @@ 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);
|
const [firstRenderedIndex, lastRenderedIndex] =
|
||||||
useEffect(() => {
|
useVirtualizedRange(rowHeight);
|
||||||
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
|
||||||
@ -256,8 +243,8 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
>
|
>
|
||||||
{rows.map((row, index) => {
|
{rows.map((row, index) => {
|
||||||
const isVirtual =
|
const isVirtual =
|
||||||
index > scrollOffset + scrollIndex ||
|
index < firstRenderedIndex ||
|
||||||
index + scrollOffset < scrollIndex;
|
index > lastRenderedIndex;
|
||||||
|
|
||||||
if (isVirtual) {
|
if (isVirtual) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -137,7 +137,7 @@ const Project = () => {
|
|||||||
show={
|
show={
|
||||||
<ApiError
|
<ApiError
|
||||||
data-loading
|
data-loading
|
||||||
style={{ maxWidth: '400px', marginTop: '1rem' }}
|
style={{ maxWidth: '400px', margin: '1rem' }}
|
||||||
onClick={refetch}
|
onClick={refetch}
|
||||||
text="Could not fetch project"
|
text="Could not fetch project"
|
||||||
/>
|
/>
|
||||||
|
@ -22,9 +22,6 @@ import {
|
|||||||
DELETE_FEATURE,
|
DELETE_FEATURE,
|
||||||
UPDATE_FEATURE,
|
UPDATE_FEATURE,
|
||||||
} from 'component/providers/AccessProvider/permissions';
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
|
||||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
|
||||||
|
|
||||||
interface IActionsCellProps {
|
interface IActionsCellProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -34,13 +31,17 @@ interface IActionsCellProps {
|
|||||||
stale?: boolean;
|
stale?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
onOpenArchiveDialog: (featureId: string) => void;
|
||||||
|
onOpenStaleDialog: (props: { featureId: string; stale: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionsCell: VFC<IActionsCellProps> = ({ projectId, row }) => {
|
export const ActionsCell: VFC<IActionsCellProps> = ({
|
||||||
|
projectId,
|
||||||
|
row,
|
||||||
|
onOpenArchiveDialog,
|
||||||
|
onOpenStaleDialog,
|
||||||
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
|
||||||
const [openArchiveDialog, setOpenArchiveDialog] = useState(false);
|
|
||||||
const { refetch } = useProject(projectId);
|
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const {
|
const {
|
||||||
original: { name: featureId, stale },
|
original: { name: featureId, stale },
|
||||||
@ -120,7 +121,7 @@ export const ActionsCell: VFC<IActionsCellProps> = ({ projectId, row }) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
className={classes.item}
|
className={classes.item}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenArchiveDialog(true);
|
onOpenArchiveDialog(featureId);
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
disabled={!hasAccess}
|
disabled={!hasAccess}
|
||||||
@ -145,7 +146,10 @@ export const ActionsCell: VFC<IActionsCellProps> = ({ projectId, row }) => {
|
|||||||
className={classes.item}
|
className={classes.item}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
setOpenStaleDialog(true);
|
onOpenStaleDialog({
|
||||||
|
featureId,
|
||||||
|
stale: stale === true,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!hasAccess}
|
disabled={!hasAccess}
|
||||||
>
|
>
|
||||||
@ -162,25 +166,6 @@ export const ActionsCell: VFC<IActionsCellProps> = ({ projectId, row }) => {
|
|||||||
</PermissionHOC>
|
</PermissionHOC>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Popover>
|
</Popover>
|
||||||
<FeatureStaleDialog
|
|
||||||
isStale={stale === true}
|
|
||||||
isOpen={openStaleDialog}
|
|
||||||
onClose={() => {
|
|
||||||
setOpenStaleDialog(false);
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
featureId={featureId}
|
|
||||||
projectId={projectId}
|
|
||||||
/>
|
|
||||||
<FeatureArchiveDialog
|
|
||||||
isOpen={openArchiveDialog}
|
|
||||||
onConfirm={() => {
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
onClose={() => setOpenArchiveDialog(false)}
|
|
||||||
featureId={featureId}
|
|
||||||
projectId={projectId}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,6 @@ interface IFeatureToggleSwitchProps {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check React.memo performance
|
|
||||||
export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
||||||
projectId,
|
projectId,
|
||||||
featureName,
|
featureName,
|
||||||
|
@ -17,11 +17,6 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
'& th': {
|
'& th': {
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
lineHeight: '1rem',
|
lineHeight: '1rem',
|
||||||
// fix for padding with different font size in hovered column header
|
|
||||||
'span[data-tooltip] span': {
|
|
||||||
padding: '4px 0',
|
|
||||||
display: 'block',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bodyClass: {
|
bodyClass: {
|
||||||
@ -65,4 +60,16 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
button: {
|
button: {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
},
|
},
|
||||||
|
row: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexShrink: 0,
|
||||||
|
'& > *': {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTheme } from '@mui/system';
|
||||||
import { Add } from '@mui/icons-material';
|
import { Add } from '@mui/icons-material';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useGlobalFilter, useSortBy, useTable } from 'react-table';
|
import {
|
||||||
|
useGlobalFilter,
|
||||||
|
useFlexLayout,
|
||||||
|
useSortBy,
|
||||||
|
useTable,
|
||||||
|
} from 'react-table';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
@ -29,6 +35,7 @@ import {
|
|||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
|
||||||
@ -38,6 +45,8 @@ import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
|
|||||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
|
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||||
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
interface IProjectFeatureTogglesProps {
|
||||||
features: IProject['features'];
|
features: IProject['features'];
|
||||||
@ -58,8 +67,6 @@ 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,
|
||||||
@ -72,6 +79,13 @@ export const ProjectFeatureToggles = ({
|
|||||||
featureId: '',
|
featureId: '',
|
||||||
environmentName: '',
|
environmentName: '',
|
||||||
});
|
});
|
||||||
|
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
||||||
|
featureId?: string;
|
||||||
|
stale?: boolean;
|
||||||
|
}>({});
|
||||||
|
const [featureArchiveState, setFeatureArchiveState] = useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
@ -80,6 +94,8 @@ export const ProjectFeatureToggles = ({
|
|||||||
);
|
);
|
||||||
const { refetch } = useProject(projectId);
|
const { refetch } = useProject(projectId);
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const theme = useTheme();
|
||||||
|
const rowHeight = theme.shape.tableRowHeight;
|
||||||
|
|
||||||
const data = useMemo<ListItemType[]>(() => {
|
const data = useMemo<ListItemType[]>(() => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -93,9 +109,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
}) as ListItemType[];
|
}) as ListItemType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
return features
|
return features.map(
|
||||||
.slice(0, features.length > limit ? pageSize : limit)
|
|
||||||
.map(
|
|
||||||
({
|
({
|
||||||
name,
|
name,
|
||||||
lastSeenAt,
|
lastSeenAt,
|
||||||
@ -181,12 +195,14 @@ export const ProjectFeatureToggles = ({
|
|||||||
Cell: FeatureSeenCell,
|
Cell: FeatureSeenCell,
|
||||||
sortType: 'date',
|
sortType: 'date',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
maxWidth: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Type',
|
Header: 'Type',
|
||||||
accessor: 'type',
|
accessor: 'type',
|
||||||
Cell: FeatureTypeCell,
|
Cell: FeatureTypeCell,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
maxWidth: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Feature toggle name',
|
Header: 'Feature toggle name',
|
||||||
@ -197,9 +213,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
to={`/projects/${projectId}/features/${value}`}
|
to={`/projects/${projectId}/features/${value}`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
width: '99%',
|
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxWidth: 200,
|
|
||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
disableGlobalFilter: false,
|
disableGlobalFilter: false,
|
||||||
},
|
},
|
||||||
@ -213,7 +227,6 @@ export const ProjectFeatureToggles = ({
|
|||||||
...environments.map(name => ({
|
...environments.map(name => ({
|
||||||
Header: loading ? () => '' : name,
|
Header: loading ? () => '' : name,
|
||||||
maxWidth: 90,
|
maxWidth: 90,
|
||||||
minWidth: 90,
|
|
||||||
accessor: `environments.${name}`,
|
accessor: `environments.${name}`,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
Cell: ({
|
Cell: ({
|
||||||
@ -242,7 +255,12 @@ export const ProjectFeatureToggles = ({
|
|||||||
maxWidth: 56,
|
maxWidth: 56,
|
||||||
width: 56,
|
width: 56,
|
||||||
Cell: (props: { row: { original: ListItemType } }) => (
|
Cell: (props: { row: { original: ListItemType } }) => (
|
||||||
<ActionsCell projectId={projectId} {...props} />
|
<ActionsCell
|
||||||
|
projectId={projectId}
|
||||||
|
onOpenArchiveDialog={setFeatureArchiveState}
|
||||||
|
onOpenStaleDialog={setFeatureStaleDialogState}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
@ -319,6 +337,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
disableGlobalFilter: true,
|
disableGlobalFilter: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
useFlexLayout,
|
||||||
useGlobalFilter,
|
useGlobalFilter,
|
||||||
useSortBy
|
useSortBy
|
||||||
);
|
);
|
||||||
@ -357,6 +376,10 @@ export const ProjectFeatureToggles = ({
|
|||||||
},
|
},
|
||||||
[setStoredParams]
|
[setStoredParams]
|
||||||
);
|
);
|
||||||
|
const [firstRenderedIndex, lastRenderedIndex] = useVirtualizedRange(
|
||||||
|
rowHeight,
|
||||||
|
20
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
@ -366,11 +389,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
title={`Project feature toggles (${
|
title={`Project feature toggles (${rows.length})`}
|
||||||
features?.length > limit
|
|
||||||
? `first ${rows.length} of ${features.length}`
|
|
||||||
: data.length
|
|
||||||
})`}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<TableSearch
|
<TableSearch
|
||||||
@ -410,19 +429,51 @@ export const ProjectFeatureToggles = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SearchHighlightProvider value={globalFilter}>
|
<SearchHighlightProvider value={globalFilter}>
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()} rowHeight={rowHeight}>
|
||||||
<SortableTableHeader
|
<SortableTableHeader
|
||||||
// @ts-expect-error -- verify after `react-table` v8
|
// @ts-expect-error -- verify after `react-table` v8
|
||||||
headerGroups={headerGroups}
|
headerGroups={headerGroups}
|
||||||
className={styles.headerClass}
|
className={styles.headerClass}
|
||||||
|
flex
|
||||||
/>
|
/>
|
||||||
<TableBody {...getTableBodyProps()}>
|
<TableBody
|
||||||
{rows.map(row => {
|
{...getTableBodyProps()}
|
||||||
|
style={{
|
||||||
|
height: `${rowHeight * rows.length}px`,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rows.map((row, index) => {
|
||||||
|
const isVirtual =
|
||||||
|
index < firstRenderedIndex ||
|
||||||
|
index > lastRenderedIndex;
|
||||||
|
|
||||||
|
if (isVirtual) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
return (
|
return (
|
||||||
<TableRow hover {...row.getRowProps()}>
|
<TableRow
|
||||||
|
hover
|
||||||
|
{...row.getRowProps()}
|
||||||
|
className={styles.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={styles.cell}
|
||||||
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render('Cell')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
@ -460,6 +511,27 @@ export const ProjectFeatureToggles = ({
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
{...strategiesDialogState}
|
{...strategiesDialogState}
|
||||||
/>
|
/>
|
||||||
|
<FeatureStaleDialog
|
||||||
|
isStale={featureStaleDialogState.stale === true}
|
||||||
|
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||||
|
onClose={() => {
|
||||||
|
setFeatureStaleDialogState({});
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
featureId={featureStaleDialogState.featureId || ''}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
<FeatureArchiveDialog
|
||||||
|
isOpen={Boolean(featureArchiveState)}
|
||||||
|
onConfirm={() => {
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setFeatureArchiveState(undefined);
|
||||||
|
}}
|
||||||
|
featureId={featureArchiveState || ''}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -68,10 +68,10 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
fontSize: '0.8rem',
|
fontSize: '0.8rem',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
padding: '0.8rem',
|
padding: '0.8rem',
|
||||||
['&:first-child']: {
|
['&:first-of-type']: {
|
||||||
marginLeft: '0',
|
marginLeft: '0',
|
||||||
},
|
},
|
||||||
['&:last-child']: {
|
['&:last-of-type']: {
|
||||||
marginRight: '0',
|
marginRight: '0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
38
frontend/src/hooks/useVirtualizedRange.ts
Normal file
38
frontend/src/hooks/useVirtualizedRange.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get index of first and last displayed item in current window scroll offset.
|
||||||
|
* This is done to optimize performance for large lists.
|
||||||
|
*
|
||||||
|
* @param rowHeight height of single item in pixels
|
||||||
|
* @param scrollOffset how many items above and below to render -- TODO: calculate from window height
|
||||||
|
* @param dampening cause less re-renders -- only after jumping this x of elements, "staircase" effect
|
||||||
|
* @returns [firstIndex, lastIndex]
|
||||||
|
*/
|
||||||
|
export const useVirtualizedRange = (
|
||||||
|
rowHeight: number,
|
||||||
|
scrollOffset = 40,
|
||||||
|
dampening = 5
|
||||||
|
) => {
|
||||||
|
const [scrollIndex, setScrollIndex] = useState(
|
||||||
|
Math.floor(window.pageYOffset / rowHeight)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setScrollIndex(
|
||||||
|
Math.floor(window.pageYOffset / (rowHeight * dampening)) *
|
||||||
|
dampening
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [rowHeight, dampening]);
|
||||||
|
|
||||||
|
return [scrollIndex - scrollOffset, scrollIndex + scrollOffset] as const;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user