Skip to content

Commit

Permalink
New virtualized table component
Browse files Browse the repository at this point in the history
  • Loading branch information
rawagner committed Apr 30, 2021
1 parent ef79250 commit d7bc64a
Show file tree
Hide file tree
Showing 7 changed files with 652 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const NodeDetailsPage: React.FC<React.ComponentProps<typeof DetailsPage>> = (pro
<PodsPage
showTitle={false}
fieldSelector={`spec.nodeName=${obj.metadata.name}`}
customData={{ showNamespaceOverride: true }}
showNamespaceOverride
/>
)),
events(ResourceEventStream),
Expand Down
285 changes: 285 additions & 0 deletions frontend/public/components/factory/Table/VirtualizedTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import * as React from 'react';
import * as _ from 'lodash';
import {
Table as PfTable,
TableHeader,
TableGridBreakpoint,
OnSelect,
SortByDirection,
ICell,
} from '@patternfly/react-table';
import { AutoSizer, WindowScroller } from '@patternfly/react-virtualized-extension';
import { useNamespace } from '@console/shared/src/hooks/useNamespace';

import VirtualizedTableBody from './VirtualizedTableBody';
import { history, StatusBox, WithScrollContainer } from '../../utils';

const BREAKPOINT_SM = 576;
const BREAKPOINT_MD = 768;
const BREAKPOINT_LG = 992;
const BREAKPOINT_XL = 1200;
const BREAKPOINT_XXL = 1400;
const MAX_COL_XS = 2;
const MAX_COL_SM = 4;
const MAX_COL_MD = 4;
const MAX_COL_LG = 6;
const MAX_COL_XL = 8;

const isColumnVisible = (
widthInPixels: number,
columnID: string,
columns: Set<string> = new Set(),
showNamespaceOverride: boolean,
namespace: string,
) => {
const showNamespace = columnID !== 'namespace' || !namespace || showNamespaceOverride;
if (_.isEmpty(columns) && showNamespace) {
return true;
}
if (!columns.has(columnID) || !showNamespace) {
return false;
}
const columnIndex = [...columns].indexOf(columnID);
if (widthInPixels < BREAKPOINT_SM) {
return columnIndex < MAX_COL_XS;
}
if (widthInPixels < BREAKPOINT_MD) {
return columnIndex < MAX_COL_SM;
}
if (widthInPixels < BREAKPOINT_LG) {
return columnIndex < MAX_COL_MD;
}
if (widthInPixels < BREAKPOINT_XL) {
return columnIndex < MAX_COL_LG;
}
if (widthInPixels < BREAKPOINT_XXL) {
return columnIndex < MAX_COL_XL;
}
return true;
};

const getActiveColumns = (
windowWidth: number,
allColumns: TableColumn<any>[],
activeColumns: Set<string>,
columnManagementID: string,
showNamespaceOverride: boolean,
namespace: string,
) => {
let columns = [...allColumns];
if (_.isEmpty(activeColumns)) {
activeColumns = new Set(
columns.map((col) => {
if (col.id && !col.additional) {
return col.id;
}
}),
);
}
if (columnManagementID) {
columns = columns?.filter(
(col) =>
isColumnVisible(windowWidth, col.id, activeColumns, showNamespaceOverride, namespace) ||
col.title === '',
);
} else {
columns = columns?.filter((col) => activeColumns.has(col.id) || col.title === '');
}

const showNamespace = !namespace || showNamespaceOverride;
if (!showNamespace) {
columns = columns.filter((column) => column.id !== 'namespace');
}
return columns;
};

export type TableColumn<D> = ICell & {
title: string;
id?: string;
additional?: boolean;
sort?: (data: D[], sortDirection: SortByDirection) => D[];
};

export type RowProps<D> = {
obj: D;
index: number;
columns: TableColumn<D>[];
isScrolling: boolean;
style: object;
};

type VirtualizedTableProps<D = any> = {
data: D[];
loaded: boolean;
loadError: any;
columns: TableColumn<D>[];
Row: React.ComponentType<RowProps<D>>;
NoDataEmptyMsg?: React.ComponentType<{}>;
EmptyMsg?: React.ComponentType<{}>;
scrollNode?: () => HTMLElement;
onSelect?: OnSelect;
label?: string;
'aria-label'?: string;
gridBreakPoint?: TableGridBreakpoint;
activeColumns?: Set<string>;
columnManagementID?: string;
showNamespaceOverride?: boolean;
};

const VirtualizedTable: React.FC<VirtualizedTableProps> = ({
data,
loaded,
loadError,
columns: allColumns,
NoDataEmptyMsg,
EmptyMsg,
scrollNode,
label,
'aria-label': ariaLabel,
gridBreakPoint = TableGridBreakpoint.none,
onSelect,
Row,
activeColumns,
columnManagementID,
showNamespaceOverride,
}) => {
const [sortBy, setSortBy] = React.useState<{
index: number;
direction: SortByDirection;
}>({ index: 0, direction: SortByDirection.asc });

const columnShift = onSelect ? 1 : 0; //shift indexes by 1 if select provided

const [windowWidth, setWindowWidth] = React.useState(window.innerWidth);
const namespace = useNamespace();

const columns = React.useMemo(
() =>
getActiveColumns(
windowWidth,
allColumns,
activeColumns,
columnManagementID,
showNamespaceOverride,
namespace,
),
[windowWidth, allColumns, activeColumns, columnManagementID, showNamespaceOverride, namespace],
);

const applySort = React.useCallback(
(index, direction) => {
const url = new URL(window.location.href);
const sp = new URLSearchParams(window.location.search);

const sortColumn = columns[index - columnShift];
if (sortColumn) {
sp.set('orderBy', direction);
sp.set('sortBy', sortColumn.title);
history.replace(`${url.pathname}?${sp.toString()}${url.hash}`);
setSortBy({
index,
direction,
});
}
},
[columnShift, columns],
);

data = React.useMemo(() => {
const sortColumn = columns[sortBy.index - columnShift];
return sortColumn.sort ? sortColumn.sort(data, sortBy.direction) : data;
}, [columnShift, columns, data, sortBy.direction, sortBy.index]);

React.useEffect(() => {
const handleResize = _.debounce(() => setWindowWidth(window.innerWidth), 100);

const sp = new URLSearchParams(window.location.search);
const columnIndex = _.findIndex(columns, { title: sp.get('sortBy') });

if (columnIndex > -1) {
const sortOrder =
sp.get('orderBy') === SortByDirection.desc.valueOf()
? SortByDirection.desc
: SortByDirection.asc;
setSortBy({
index: columnIndex + columnShift,
direction: sortOrder,
});
}

// re-render after resize
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const onSort = React.useCallback(
(event, index, direction) => {
event.preventDefault();
applySort(index, direction);
},
[applySort],
);

const renderVirtualizedTable = (scrollContainer) => (
<WindowScroller scrollElement={scrollContainer}>
{({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => (
<AutoSizer disableHeight>
{({ width }) => (
<div ref={registerChild}>
<VirtualizedTableBody
Row={Row}
height={height}
isScrolling={isScrolling}
onChildScroll={onChildScroll}
data={data}
columns={columns}
scrollTop={scrollTop}
width={width}
/>
</div>
)}
</AutoSizer>
)}
</WindowScroller>
);

return (
<div className="co-m-table-grid co-m-table-grid--bordered">
<StatusBox
skeleton={<div className="loading-skeleton--table" />}
data={data}
loaded={loaded}
loadError={loadError}
// unfilteredData={propData} TODO
label={label}
NoDataEmptyMsg={NoDataEmptyMsg}
EmptyMsg={EmptyMsg}
>
<div role="grid" aria-label={ariaLabel} aria-rowcount={data?.length}>
<PfTable
cells={columns}
rows={[]}
gridBreakPoint={gridBreakPoint}
onSort={onSort}
onSelect={onSelect}
sortBy={sortBy}
className="pf-m-compact pf-m-border-rows"
role="presentation"
>
<TableHeader />
</PfTable>
{scrollNode ? (
renderVirtualizedTable(scrollNode)
) : (
<WithScrollContainer>{renderVirtualizedTable}</WithScrollContainer>
)}
</div>
</StatusBox>
</div>
);
};

export default VirtualizedTable;
80 changes: 80 additions & 0 deletions frontend/public/components/factory/Table/VirtualizedTableBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react';
import { VirtualTableBody } from '@patternfly/react-virtualized-extension';
import { CellMeasurerCache, CellMeasurer } from 'react-virtualized';
import { Scroll } from '@patternfly/react-virtualized-extension/dist/js/components/Virtualized/types';
import { TableColumn, RowProps } from './VirtualizedTable';

type VirtualizedTableBodyProps<D = any> = {
Row: React.ComponentType<RowProps<D>>;
data: D[];
height: number;
isScrolling: boolean;
onChildScroll: (params: Scroll) => void;
columns: TableColumn<D>[];
scrollTop: number;
width: number;
};

const VirtualizedTableBody: React.FC<VirtualizedTableBodyProps> = ({
Row,
height,
isScrolling,
onChildScroll,
data,
columns,
scrollTop,
width,
}) => {
const cellMeasurementCache = new CellMeasurerCache({
fixedWidth: true,
minHeight: 44,
keyMapper: (rowIndex) => data?.[rowIndex]?.metadata?.uid || rowIndex, // TODO custom keyMapper ?
});

const rowRenderer = ({ index, isScrolling: scrolling, isVisible, key, style, parent }) => {
const rowArgs: RowProps<any> = {
obj: data[index],
index,
columns,
isScrolling: scrolling,
style,
};

// do not render non visible elements (this excludes overscan)
if (!isVisible) {
return null;
}
return (
<CellMeasurer
cache={cellMeasurementCache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<Row key={key} {...rowArgs} />
</CellMeasurer>
);
};

return (
<VirtualTableBody
autoHeight
className="pf-c-table pf-m-compact pf-m-border-rows pf-c-virtualized pf-c-window-scroller"
deferredMeasurementCache={cellMeasurementCache}
rowHeight={cellMeasurementCache.rowHeight}
height={height || 0}
isScrolling={isScrolling}
onScroll={onChildScroll}
overscanRowCount={10}
columns={columns}
rows={data}
rowCount={data.length}
rowRenderer={rowRenderer}
scrollTop={scrollTop}
width={width}
/>
);
};

export default VirtualizedTableBody;
Loading

0 comments on commit d7bc64a

Please sign in to comment.