Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create VirtualizedGrid component for catalog view #5795

Merged
merged 7 commits into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 0 additions & 54 deletions frontend/__tests__/components/catalog.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { mount, ReactWrapper } from 'enzyme';
import { Provider } from 'react-redux';

import {
CatalogTile,
FilterSidePanelCategoryItem,
VerticalTabsTab,
} from '@patternfly/react-catalog-view-extension';
Expand Down Expand Up @@ -61,59 +60,6 @@ describe(CatalogTileViewPage.displayName, () => {
expect(filterItems.at(4).props().checked).toBe(false); // filter clusterServiceClasses should be false by default
});

it('renders tiles correctly', () => {
// De-activating all filters to render all tiles
const filterItems = wrapper.find<any>(FilterSidePanelCategoryItem);
filterItems.forEach((filter) => {
filter.find('input').simulate('click', { target: { checked: false } });
});

const tiles = wrapper.find<any>(CatalogTile);

expect(tiles.exists()).toBe(true);
expect(tiles.length).toEqual(25);

const cakeSqlTileProps = tiles.at(2).props();
expect(cakeSqlTileProps.title).toEqual('CakePHP + MySQL');
expect(cakeSqlTileProps.iconImg).toEqual('test-file-stub');
expect(cakeSqlTileProps.iconClass).toBe(null);
expect(cakeSqlTileProps.vendor).toEqual('provided by Red Hat, Inc.');
expect(
cakeSqlTileProps.description.startsWith(
'An example CakePHP application with a MySQL database',
),
).toBe(true);

const amqTileProps = tiles.at(22).props();
expect(amqTileProps.title).toEqual('Red Hat JBoss A-MQ 6.3 (Ephemeral, no SSL)');
expect(amqTileProps.iconImg).toEqual('test-file-stub');
expect(amqTileProps.iconClass).toBe(null);
expect(amqTileProps.vendor).toEqual('provided by Red Hat, Inc.');
expect(
amqTileProps.description.startsWith(
"Application template for JBoss A-MQ brokers. These can be deployed as standalone or in a mesh. This template doesn't feature SSL support.",
),
).toBe(true);

const wildflyTileProps = tiles.at(24).props();
expect(wildflyTileProps.title).toEqual('WildFly');
expect(wildflyTileProps.iconImg).toEqual('test-file-stub');
expect(wildflyTileProps.iconClass).toBe(null);
expect(wildflyTileProps.vendor).toEqual('provided by Red Hat, Inc.');
expect(
wildflyTileProps.description.startsWith(
'Build and run WildFly 10.1 applications on CentOS 7. For more information about using this builder image',
),
).toBe(true);

const faIconTileProps = tiles.at(5).props();
expect(faIconTileProps.title).toEqual('FA icon example');
expect(faIconTileProps.iconImg).toBe(null);
expect(faIconTileProps.iconClass).toBe('fa fa-fill-drip');
expect(faIconTileProps.vendor).toEqual('provided by Red Hat, Inc.');
expect(faIconTileProps.description).toEqual('Example to validate icon');
});

it('categorizes catalog items', () => {
const categories = categorizeItems(
catalogItems,
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/console-shared/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './popper';
export * from './shortcuts';
export * from './drawer';
export * from './health-checks';
export * from './virtualized-grid';
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { CellMeasurer } from 'react-virtualized';
import { RenderCell, RenderHeader, GridChildrenProps, Item, CellItem } from './types';
import { CellMeasurementContext } from './utils';

type CellProps = {
renderCell: RenderCell;
renderHeader?: RenderHeader;
style?: React.CSSProperties;
} & GridChildrenProps;

const Cell: React.FC<CellProps> = ({
data,
columnCount,
items,
rowCount,
style: { width, ...style },
renderCell,
renderHeader,
}) => {
const { cache, cellMargin } = React.useContext(CellMeasurementContext);
const { key, columnIndex, rowIndex, parent } = data;
const index = rowIndex * columnCount + columnIndex;
const item: CellItem = items[index];
const isItemString = typeof item === 'string';

const cellStyle = {
...style,
width: isItemString ? '100%' : width,
padding: `${cellMargin}px ${cellMargin}px ${
rowIndex === rowCount - 1 ? `${cellMargin}px` : 0
} ${cellMargin}px`,
};

return item ? (
<CellMeasurer
cache={cache}
columnIndex={columnIndex}
key={key}
parent={parent}
rowIndex={rowIndex}
>
<div style={cellStyle}>
{isItemString ? renderHeader(item as string) : renderCell(item as Item)}
</div>
</CellMeasurer>
) : null;
};

export default Cell;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react';
import { Grid as GridComponent, GridCellProps } from 'react-virtualized';
import { Item, GridChildrenProps } from './types';
import { CellMeasurementContext } from './utils';

type GridProps = {
height: number;
width: number;
scrollTop: number;
items: Item[];
children: (props: GridChildrenProps) => React.ReactNode;
};

const Grid: React.FC<GridProps> = ({ height, width, scrollTop, items, children }) => {
const { cache, cellWidth, cellMargin, overscanRowCount, estimatedCellHeight } = React.useContext(
CellMeasurementContext,
);
const itemCount = items.length;
const idealItemWidth = cellWidth + cellMargin;
const columnCount = Math.max(1, Math.floor(width / idealItemWidth));
const rowCount = Math.ceil(itemCount / columnCount);
const cellRenderer = (data: GridCellProps) => children({ data, columnCount, items, rowCount });
return (
<GridComponent
sahil143 marked this conversation as resolved.
Show resolved Hide resolved
autoHeight
tabIndex={null}
height={height ?? 0}
width={width}
scrollTop={scrollTop}
rowHeight={cache.rowHeight}
deferredMeasurementCache={cache}
columnWidth={idealItemWidth}
rowCount={rowCount}
columnCount={columnCount}
cellRenderer={cellRenderer}
overscanRowCount={overscanRowCount}
estimatedRowSize={estimatedCellHeight}
/>
);
};

export default Grid;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import { Grid as GridComponent, GridCellProps } from 'react-virtualized';
import { getItemsAndRowCount, CellMeasurementContext } from './utils';
import { Params, GroupedItems, GridChildrenProps } from './types';

type GroupByFilterGridProps = {
height: number;
width: number;
scrollTop: number;
items: GroupedItems;
children: (props: GridChildrenProps) => React.ReactNode;
};

const GroupByFilterGrid: React.FC<GroupByFilterGridProps> = ({
height,
width,
scrollTop,
items: groupedItems,
children,
}) => {
const {
cache,
cellWidth,
cellMargin,
overscanRowCount,
headerHeight,
estimatedCellHeight,
} = React.useContext(CellMeasurementContext);
const idealItemWidth = cellWidth + cellMargin;
const columnCountEstimate = Math.max(1, Math.floor(width / idealItemWidth));
const { items, rowCount, columnCount, headerRows } = getItemsAndRowCount(
groupedItems,
columnCountEstimate,
);
const cellRenderer = (data: GridCellProps) => children({ data, columnCount, items, rowCount });
const getRowHeight = React.useCallback(
({ index }: Params): number => {
if (headerRows.includes(index)) {
return headerHeight;
}
return cache.rowHeight({ index });
},
[cache, headerHeight, headerRows],
);
return (
<GridComponent
autoHeight
tabIndex={null}
height={height ?? 0}
width={width}
scrollTop={scrollTop}
rowHeight={getRowHeight}
deferredMeasurementCache={cache}
columnWidth={idealItemWidth}
rowCount={rowCount}
columnCount={columnCount}
cellRenderer={cellRenderer}
overscanRowCount={overscanRowCount}
estimatedRowSize={estimatedCellHeight}
/>
);
};

export default GroupByFilterGrid;
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from 'react';
import { WindowScroller, AutoSizer, Size, CellMeasurerCache } from 'react-virtualized';
import { Item, GroupedItems, GridChildrenProps, RenderHeader, RenderCell } from './types';
import GroupByFilterGrid from './GroupByFilterGrid';
import Grid from './Grid';
import Cell from './Cell';
import { WithScrollContainer } from '@console/internal/components/utils';
import {
IDEAL_SPACE_BW_TILES,
IDEAL_CELL_WIDTH,
DEFAULT_CELL_HEIGHT,
OVERSCAN_ROW_COUNT,
HEADER_FIXED_HEIGHT,
ESTIMATED_ROW_SIZE,
} from './const';
import { CellMeasurementContext } from './utils';

type VirtualizedGridProps = {
items: Item[] | GroupedItems;
renderCell: RenderCell;
/**
* should be set when items are grouped/ `isItemGrouped` is set to true and each group has a heading
*/
renderHeader?: RenderHeader;
/**
* Default value: false
* should be set true when items are grouped
*/
isItemsGrouped?: boolean;
cellWidth?: number;
cellMargin?: number;
celldefaultHeight?: number;
overscanRowCount?: number;
headerHeight?: number;
estimatedCellHeight?: number;
};

const VirtualizedGrid: React.FC<VirtualizedGridProps> = ({
items,
renderCell,
isItemsGrouped = false,
renderHeader,
cellMargin = IDEAL_SPACE_BW_TILES,
cellWidth = IDEAL_CELL_WIDTH,
celldefaultHeight = DEFAULT_CELL_HEIGHT,
overscanRowCount = OVERSCAN_ROW_COUNT,
headerHeight = HEADER_FIXED_HEIGHT,
estimatedCellHeight = ESTIMATED_ROW_SIZE,
}) => {
const cache: CellMeasurerCache = new CellMeasurerCache({
defaultHeight: celldefaultHeight,
minHeight: 200,
fixedWidth: true,
});
return (
<CellMeasurementContext.Provider
value={{ cache, cellMargin, cellWidth, overscanRowCount, headerHeight, estimatedCellHeight }}
>
<WithScrollContainer>
{(scrollElement) => (
<WindowScroller scrollElement={scrollElement ?? window}>
{({ registerChild, ...props }) => (
<AutoSizer disableHeight>
{({ width }: Size) => (
<div ref={registerChild}>
{isItemsGrouped ? (
<GroupByFilterGrid {...props} width={width} items={items as GroupedItems}>
{(gridProps: GridChildrenProps) => {
const {
data: { key, style },
} = gridProps;
return (
<Cell
{...gridProps}
key={key}
style={style}
renderHeader={renderHeader}
renderCell={renderCell}
/>
);
}}
</GroupByFilterGrid>
) : (
<Grid {...props} width={width} items={items as Item[]}>
{(gridProps: GridChildrenProps) => {
const {
data: { key, style },
} = gridProps;
return (
<Cell {...gridProps} key={key} style={style} renderCell={renderCell} />
);
}}
</Grid>
)}
</div>
)}
</AutoSizer>
)}
</WindowScroller>
)}
</WithScrollContainer>
</CellMeasurementContext.Provider>
);
};

export default VirtualizedGrid;