Skip to content
Snippets Groups Projects
Unverified Commit 14854fe3 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Virtualize table and card lists in data picker (#27582)

* Convert `VirtualizedList` to TypeScript

* Add `VirtualizedSelectList` component

* Virtualize raw table list

* Virtualize model and question list

* Fix rendering virtualized lists in tests
parent 74de573d
No related branches found
No related tags found
No related merge requests found
Showing
with 95 additions and 33 deletions
/* eslint-disable react/prop-types */
import React from "react";
import { List, WindowScroller, AutoSizer } from "react-virtualized";
function VirtualizedList({ items, rowHeight, renderItem, scrollElement }) {
export interface VirtualizedListProps<Item = unknown>
extends React.HTMLProps<HTMLUListElement> {
items: Item[];
rowHeight: number;
renderItem: (props: { item: Item; index: number }) => React.ReactNode;
scrollElement?: HTMLElement | null;
}
function VirtualizedList<Item>({
items,
rowHeight,
renderItem,
scrollElement,
}: VirtualizedListProps<Item>) {
const rowRenderer = React.useCallback(
({ index, key, style }) => (
<div key={key} style={style}>
......@@ -15,7 +27,7 @@ function VirtualizedList({ items, rowHeight, renderItem, scrollElement }) {
const renderScrollComponent = React.useCallback(
({ width }) => {
return (
<WindowScroller scrollElement={scrollElement}>
<WindowScroller scrollElement={scrollElement || undefined}>
{({ height, isScrolling, scrollTop }) => (
<List
autoHeight
......
import styled from "@emotion/styled";
import SelectList from "metabase/components/SelectList";
export const StyledSelectList = styled(SelectList)`
export const ListContainer = styled.div`
width: 100%;
padding-left: 1rem;
`;
import React, { useCallback, useMemo } from "react";
import _ from "underscore";
import SelectList from "metabase/components/SelectList";
import { canonicalCollectionId } from "metabase/collections/utils";
import type { ITreeNodeItem } from "metabase/components/tree/types";
......@@ -13,9 +11,10 @@ import type { DataPickerSelectedItem, VirtualTable } from "../types";
import EmptyState from "../EmptyState";
import LoadingState from "../LoadingState";
import VirtualizedSelectList from "../VirtualizedSelectList";
import PanePicker from "../PanePicker";
import { StyledSelectList } from "./CardPicker.styled";
import { ListContainer } from "./CardPicker.styled";
type TargetModel = "model" | "question";
......@@ -55,7 +54,7 @@ function TableSelectListItem({
onSelect: (id: Table["id"]) => void;
}) {
return (
<SelectList.Item
<VirtualizedSelectList.Item
id={table.id}
name={table.display_name}
isSelected={isSelected}
......@@ -63,7 +62,7 @@ function TableSelectListItem({
onSelect={onSelect}
>
{table.display_name}
</SelectList.Item>
</VirtualizedSelectList.Item>
);
}
......@@ -104,7 +103,7 @@ function CardPickerView({
);
const renderVirtualTable = useCallback(
(table: VirtualTable) => (
({ item: table }: { item: VirtualTable }) => (
<TableSelectListItem
key={table.id}
table={table}
......@@ -130,9 +129,14 @@ function CardPickerView({
) : isEmpty ? (
<EmptyState />
) : (
<StyledSelectList>
{virtualTables?.map?.(renderVirtualTable)}
</StyledSelectList>
<ListContainer>
{Array.isArray(virtualTables) && (
<VirtualizedSelectList<VirtualTable>
items={virtualTables}
renderItem={renderVirtualTable}
/>
)}
</ListContainer>
)}
</PanePicker>
);
......
import styled from "@emotion/styled";
import SelectList from "metabase/components/SelectList";
export const StyledSelectList = styled(SelectList)`
export const ListContainer = styled.div`
width: 100%;
padding: 0 1rem;
`;
import React, { useCallback, useMemo } from "react";
import _ from "underscore";
import SelectList from "metabase/components/SelectList";
import type { ITreeNodeItem } from "metabase/components/tree/types";
import type Database from "metabase-lib/metadata/Database";
......@@ -13,8 +11,9 @@ import type { DataPickerSelectedItem } from "../types";
import EmptyState from "../EmptyState";
import LoadingState from "../LoadingState";
import VirtualizedSelectList from "../VirtualizedSelectList";
import PanePicker from "../PanePicker";
import { StyledSelectList } from "./RawDataPicker.styled";
import { ListContainer } from "./RawDataPicker.styled";
interface RawDataPickerViewProps {
databases: Database[];
......@@ -61,7 +60,7 @@ function TableSelectListItem({
}) {
const name = table.displayName();
return (
<SelectList.Item
<VirtualizedSelectList.Item
id={table.id}
name={name}
isSelected={isSelected}
......@@ -69,7 +68,7 @@ function TableSelectListItem({
onSelect={onSelect}
>
{name}
</SelectList.Item>
</VirtualizedSelectList.Item>
);
}
......@@ -135,9 +134,8 @@ function RawDataPickerView({
);
const renderTable = useCallback(
(table: Table) => (
({ item: table }: { item: Table }) => (
<TableSelectListItem
key={table.id}
table={table}
isSelected={selectedTableIds.includes(table.id)}
onSelect={onSelectedTable}
......@@ -162,7 +160,14 @@ function RawDataPickerView({
) : isEmpty ? (
<EmptyState />
) : (
<StyledSelectList>{tables?.map?.(renderTable)}</StyledSelectList>
<ListContainer>
{Array.isArray(tables) && (
<VirtualizedSelectList<Table>
items={tables}
renderItem={renderTable}
/>
)}
</ListContainer>
)}
</PanePicker>
);
......
import React from "react";
import VirtualizedList, {
VirtualizedListProps,
} from "metabase/components/VirtualizedList";
import SelectList from "metabase/components/SelectList";
const SELECT_LIST_ITEM_HEIGHT = 40;
type VirtualizedSelectListProps<Item> = Omit<
VirtualizedListProps<Item>,
"rowHeight" | "role"
>;
function VirtualizedSelectList<Item>(props: VirtualizedSelectListProps<Item>) {
return (
<VirtualizedList<Item>
data-testid="select-list"
{...props}
role="menu"
rowHeight={SELECT_LIST_ITEM_HEIGHT}
/>
);
}
export default Object.assign(VirtualizedSelectList, {
Item: SelectList.Item,
});
import nock from "nock";
import { screen, waitFor } from "__support__/ui";
import { screen } from "__support__/ui";
import { SAMPLE_DATABASE } from "__support__/sample_database_fixture";
import { setup } from "./common";
import { setup, setupVirtualizedLists } from "./common";
describe("DataPicker", () => {
beforeAll(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
setupVirtualizedLists();
});
afterEach(() => {
......
......@@ -13,6 +13,7 @@ import {
import {
setup,
setupVirtualizedLists,
EMPTY_COLLECTION,
SAMPLE_COLLECTION,
SAMPLE_MODEL,
......@@ -27,7 +28,7 @@ const ROOT_COLLECTION_MODEL_VIRTUAL_SCHEMA_ID = getCollectionVirtualSchemaId(
describe("DataPicker — picking models", () => {
beforeAll(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
setupVirtualizedLists();
});
afterEach(() => {
......
......@@ -13,6 +13,7 @@ import {
import {
setup,
setupVirtualizedLists,
EMPTY_COLLECTION,
SAMPLE_COLLECTION,
SAMPLE_QUESTION,
......@@ -25,7 +26,7 @@ const ROOT_COLLECTION_QUESTIONS_VIRTUAL_SCHEMA_ID =
describe("DataPicker — picking questions", () => {
beforeAll(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
setupVirtualizedLists();
});
afterEach(() => {
......
......@@ -10,11 +10,11 @@ import {
import { generateSchemaId } from "metabase-lib/metadata/utils/schema";
import { setup } from "./common";
import { setup, setupVirtualizedLists } from "./common";
describe("DataPicker — picking raw data", () => {
beforeAll(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
setupVirtualizedLists();
});
afterEach(() => {
......
......@@ -111,6 +111,21 @@ interface SetupOpts {
hasNestedQueriesEnabled?: boolean;
}
// react-virtualized's AutoSizer uses offsetWidth and offsetHeight.
// Jest runs in JSDom which doesn't support measurements APIs.
export function setupVirtualizedLists() {
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
configurable: true,
value: 100,
});
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
configurable: true,
value: 100,
});
window.HTMLElement.prototype.scrollIntoView = jest.fn();
}
export async function setup({
initialValue = { tableIds: [] },
filters,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment