mirror of
https://github.com/apache/superset.git
synced 2026-09-01 04:51:23 +00:00
chore(deps): bump react-window and @types/react-window in /superset-frontend (#42528)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Amin Ghadersohi
Claude Sonnet 5
parent
2a9c5acb01
commit
38727906a2
Generated
+29
-40
@@ -139,7 +139,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.10",
|
||||
"react-window": "^2.3.0",
|
||||
"redux": "^4.2.1",
|
||||
"redux-localstorage": "^0.4.1",
|
||||
"redux-thunk": "^2.1.0",
|
||||
@@ -207,7 +207,6 @@
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
"@types/redux-mock-store": "^1.0.6",
|
||||
"@types/rison": "0.1.0",
|
||||
@@ -12856,15 +12855,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-window": {
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz",
|
||||
"integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/redux-localstorage": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux-localstorage/-/redux-localstorage-1.0.12.tgz",
|
||||
@@ -34473,6 +34463,12 @@
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-arborist/node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-arborist/node_modules/react-dnd": {
|
||||
"version": "14.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz",
|
||||
@@ -34512,6 +34508,23 @@
|
||||
"dnd-core": "14.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-arborist/node_modules/react-window": {
|
||||
"version": "1.8.11",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
|
||||
"integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-arborist/node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
@@ -35200,28 +35213,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.11",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
|
||||
"integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.3.0.tgz",
|
||||
"integrity": "sha512-FW6TIpaOH646k51X7yE+LSCWGkt5Pfsnc1fVyq/sCI9h0pTqmMiBXM04pzFKg3Bt7NGkeV6kqbU8d/QjmFS7Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window/node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/read": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read/-/read-4.1.0.tgz",
|
||||
@@ -42790,7 +42790,6 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
@@ -42916,7 +42915,6 @@
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^6.0.0",
|
||||
"nanoid": "*",
|
||||
@@ -42972,15 +42970,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/react-ace": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz",
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.10",
|
||||
"react-window": "^2.3.0",
|
||||
"redux": "^4.2.1",
|
||||
"redux-localstorage": "^0.4.1",
|
||||
"redux-thunk": "^2.1.0",
|
||||
@@ -284,7 +284,6 @@
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
"@types/redux-mock-store": "^1.0.6",
|
||||
"@types/rison": "0.1.0",
|
||||
|
||||
@@ -109,7 +109,6 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^6.0.0",
|
||||
"nanoid": "*",
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, waitFor } from '@superset-ui/core/spec';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Table } from './index';
|
||||
|
||||
// These tests exercise VirtualTable's react-window v2 `Grid` wiring
|
||||
// (`cellComponent`/`cellProps`/`gridRef`), which previously had no direct
|
||||
// coverage - `Table.test.tsx` only exercises the non-virtualized code path.
|
||||
|
||||
interface BasicData {
|
||||
columnName: string;
|
||||
columnType: string;
|
||||
}
|
||||
|
||||
const testData: BasicData[] = [
|
||||
{ columnName: 'Number', columnType: 'Numerical' },
|
||||
{ columnName: 'String', columnType: 'Physical' },
|
||||
{ columnName: 'Date', columnType: 'Virtual' },
|
||||
];
|
||||
|
||||
const testColumns: ColumnsType<BasicData> = [
|
||||
{
|
||||
title: 'Column Name',
|
||||
dataIndex: 'columnName',
|
||||
key: 'columnName',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Column Type',
|
||||
dataIndex: 'columnType',
|
||||
key: 'columnType',
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
|
||||
test('virtualized table renders headers and row content through the react-window Grid', async () => {
|
||||
render(
|
||||
<Table
|
||||
columns={testColumns}
|
||||
data={testData}
|
||||
virtualize
|
||||
height={200}
|
||||
usePagination={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
testColumns.forEach(column =>
|
||||
expect(
|
||||
screen
|
||||
.getAllByText(column.title as string)
|
||||
.find(el => el.closest('th')),
|
||||
).toBeInTheDocument(),
|
||||
),
|
||||
);
|
||||
|
||||
testData.forEach(row => {
|
||||
expect(screen.getByText(row.columnName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('virtualized table cells keep the DOM hooks other code (cypress, downloadAsImage) relies on', async () => {
|
||||
const { container } = render(
|
||||
<Table
|
||||
columns={testColumns}
|
||||
data={testData}
|
||||
virtualize
|
||||
height={200}
|
||||
usePagination={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.virtual-grid')).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelectorAll('.virtual-table-cell').length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('cell render functions receive their row data via cellProps rather than a stale closure', async () => {
|
||||
const columnsWithRender: ColumnsType<BasicData> = [
|
||||
{
|
||||
title: 'Column Name',
|
||||
dataIndex: 'columnName',
|
||||
key: 'columnName',
|
||||
width: 150,
|
||||
render: (value: string) => `rendered:${value}`,
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<Table
|
||||
columns={columnsWithRender}
|
||||
data={testData}
|
||||
virtualize
|
||||
height={200}
|
||||
usePagination={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('rendered:Number')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -24,10 +24,14 @@ import {
|
||||
} from 'antd/es/table';
|
||||
import classNames from 'classnames';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { useEffect, useRef, useState, useCallback, CSSProperties } from 'react';
|
||||
import { VariableSizeGrid as Grid } from 'react-window';
|
||||
import { useRef, useState, useCallback, type UIEvent } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
type CellComponentProps,
|
||||
type GridImperativeAPI,
|
||||
} from 'react-window';
|
||||
import { safeHtmlSpan } from '@superset-ui/core';
|
||||
import { useTheme, styled } from '@apache-superset/core/theme';
|
||||
import { useTheme, styled, SupersetTheme } from '@apache-superset/core/theme';
|
||||
|
||||
import { TableSize, ETableAction } from './index';
|
||||
|
||||
@@ -71,6 +75,59 @@ const StyledTable = styled(AntTable)(
|
||||
const SMALL = 39;
|
||||
const MIDDLE = 47;
|
||||
|
||||
interface VirtualGridCellProps {
|
||||
mergedColumns: AntTableProps<any>['columns'];
|
||||
rawData: readonly object[];
|
||||
cellSize: number;
|
||||
allowHTML: boolean;
|
||||
theme: SupersetTheme;
|
||||
}
|
||||
|
||||
// Rendered via `cellComponent`, so it must be a stable reference (module scope)
|
||||
// rather than defined inline on every render of the enclosing table -
|
||||
// otherwise react-window would treat it as a new component type each render
|
||||
// and remount every cell. All the data it needs is threaded through
|
||||
// `cellProps` instead of being closed over.
|
||||
const VirtualGridCell = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
style,
|
||||
mergedColumns,
|
||||
rawData,
|
||||
cellSize,
|
||||
allowHTML,
|
||||
theme,
|
||||
}: CellComponentProps<VirtualGridCellProps>) => {
|
||||
const data: any = rawData?.[rowIndex];
|
||||
// Set default content
|
||||
let content = data?.[(mergedColumns as any)?.[columnIndex]?.dataIndex];
|
||||
// Check if the column has a render function
|
||||
const render = mergedColumns?.[columnIndex]?.render;
|
||||
if (typeof render === 'function') {
|
||||
// Use render function to generate formatted content using column's render function
|
||||
content = render(content, data, rowIndex);
|
||||
}
|
||||
|
||||
if (allowHTML && typeof content === 'string') {
|
||||
content = safeHtmlSpan(content);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCell
|
||||
className={classNames('virtual-table-cell', {
|
||||
'virtual-table-cell-last':
|
||||
columnIndex === (mergedColumns?.length ?? 0) - 1,
|
||||
})}
|
||||
style={style}
|
||||
title={typeof content === 'string' ? content : undefined}
|
||||
theme={theme}
|
||||
height={cellSize}
|
||||
>
|
||||
{content}
|
||||
</StyledCell>
|
||||
);
|
||||
};
|
||||
|
||||
const VirtualTable = <RecordType extends object>(
|
||||
props: VirtualTableProps<RecordType>,
|
||||
) => {
|
||||
@@ -126,19 +183,15 @@ const VirtualTable = <RecordType extends object>(
|
||||
(lastColumn.width as number) + Math.floor(tableWidth - totalWidth);
|
||||
}
|
||||
|
||||
const gridRef = useRef<any>();
|
||||
const gridRef = useRef<GridImperativeAPI>(null);
|
||||
const [connectObject] = useState<any>(() => {
|
||||
const obj = {};
|
||||
Object.defineProperty(obj, 'scrollLeft', {
|
||||
get: () => {
|
||||
if (gridRef.current) {
|
||||
return gridRef.current?.state?.scrollLeft;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
get: () => gridRef.current?.element?.scrollLeft ?? 0,
|
||||
set: (scrollLeft: number) => {
|
||||
if (gridRef.current) {
|
||||
gridRef.current.scrollTo({ scrollLeft });
|
||||
const element = gridRef.current?.element;
|
||||
if (element) {
|
||||
element.scrollLeft = scrollLeft;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -146,14 +199,11 @@ const VirtualTable = <RecordType extends object>(
|
||||
return obj;
|
||||
});
|
||||
|
||||
const resetVirtualGrid = () => {
|
||||
gridRef.current?.resetAfterIndices({
|
||||
columnIndex: 0,
|
||||
shouldForceUpdate: true,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => resetVirtualGrid, [tableWidth, columns, size]);
|
||||
// No manual cache-reset is needed here (react-window v2 has no
|
||||
// `resetAfterIndices`-style API): `columnWidth` below is a fresh inline
|
||||
// closure over `mergedColumns` on every render, so react-window's internal
|
||||
// size cache - which is invalidated whenever the `columnWidth`/`rowHeight`
|
||||
// function reference changes - recomputes automatically.
|
||||
|
||||
/*
|
||||
* antd Table has a runtime error when it tries to fire the onChange event triggered from a pageChange
|
||||
@@ -166,7 +216,10 @@ const VirtualTable = <RecordType extends object>(
|
||||
* We intentionally leave horizontal scroll where it was so user can focus on
|
||||
* specific range of columns as they page through data
|
||||
*/
|
||||
gridRef.current?.scrollTo?.({ scrollTop: 0 });
|
||||
const element = gridRef.current?.element;
|
||||
if (element) {
|
||||
element.scrollTop = 0;
|
||||
}
|
||||
|
||||
onChange?.(
|
||||
{
|
||||
@@ -192,61 +245,31 @@ const VirtualTable = <RecordType extends object>(
|
||||
const cellSize = size === TableSize.Middle ? MIDDLE : SMALL;
|
||||
return (
|
||||
<Grid
|
||||
ref={gridRef}
|
||||
gridRef={gridRef}
|
||||
className="virtual-grid"
|
||||
columnCount={mergedColumns.length}
|
||||
columnWidth={(index: number) => {
|
||||
const { width = DEFAULT_COL_WIDTH } = mergedColumns[index];
|
||||
return width as number;
|
||||
}}
|
||||
height={height || (scroll!.y as number)}
|
||||
rowCount={rawData.length}
|
||||
rowHeight={() => cellSize}
|
||||
width={tableWidth}
|
||||
onScroll={({ scrollLeft }: { scrollLeft: number }) => {
|
||||
onScroll({ scrollLeft });
|
||||
style={{
|
||||
height: height || (scroll!.y as number),
|
||||
width: tableWidth,
|
||||
}}
|
||||
>
|
||||
{({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
style,
|
||||
}: {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
style: CSSProperties;
|
||||
}) => {
|
||||
const data: any = rawData?.[rowIndex];
|
||||
// Set default content
|
||||
let content =
|
||||
data?.[(mergedColumns as any)?.[columnIndex]?.dataIndex];
|
||||
// Check if the column has a render function
|
||||
const render = mergedColumns[columnIndex]?.render;
|
||||
if (typeof render === 'function') {
|
||||
// Use render function to generate formatted content using column's render function
|
||||
content = render(content, data, rowIndex);
|
||||
}
|
||||
|
||||
if (allowHTML && typeof content === 'string') {
|
||||
content = safeHtmlSpan(content);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCell
|
||||
className={classNames('virtual-table-cell', {
|
||||
'virtual-table-cell-last':
|
||||
columnIndex === mergedColumns.length - 1,
|
||||
})}
|
||||
style={style}
|
||||
title={typeof content === 'string' ? content : undefined}
|
||||
theme={theme}
|
||||
height={cellSize}
|
||||
>
|
||||
{content}
|
||||
</StyledCell>
|
||||
);
|
||||
cellComponent={VirtualGridCell}
|
||||
cellProps={{
|
||||
mergedColumns,
|
||||
rawData,
|
||||
cellSize,
|
||||
allowHTML,
|
||||
theme,
|
||||
}}
|
||||
</Grid>
|
||||
onScroll={(event: UIEvent<HTMLDivElement>) => {
|
||||
onScroll({ scrollLeft: event.currentTarget.scrollLeft });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -44,7 +43,7 @@ import {
|
||||
Icons,
|
||||
} from '@superset-ui/core/components';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { List, type RowComponentProps } from 'react-window';
|
||||
import { InputRef } from 'antd';
|
||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||
import { VirtualizedMenuItem } from '../MenuItemWithTruncation';
|
||||
@@ -53,6 +52,34 @@ import { Dataset } from '../types';
|
||||
const SUBMENU_HEIGHT = 200;
|
||||
const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
|
||||
|
||||
interface DrillByColumnRowProps {
|
||||
columns: Column[];
|
||||
onSelectColumn: (event: React.MouseEvent, column: Column) => void;
|
||||
}
|
||||
|
||||
// Rendered via `rowComponent`, so it must be a stable reference (module
|
||||
// scope) rather than defined inline on every render of the submenu -
|
||||
// otherwise react-window would treat it as a new component type each
|
||||
// render and remount every row. All the data it needs is threaded
|
||||
// through `rowProps` instead of being closed over.
|
||||
function DrillByColumnRow({
|
||||
index,
|
||||
style,
|
||||
columns,
|
||||
onSelectColumn,
|
||||
}: RowComponentProps<DrillByColumnRowProps>) {
|
||||
const column = columns[index];
|
||||
return (
|
||||
<VirtualizedMenuItem
|
||||
tooltipText={column.verbose_name || column.column_name}
|
||||
onClick={e => onSelectColumn(e, column)}
|
||||
style={style}
|
||||
>
|
||||
{column.verbose_name || column.column_name}
|
||||
</VirtualizedMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export interface DrillBySubmenuProps {
|
||||
drillByConfig?: ContextMenuFilters['drillBy'];
|
||||
formData: BaseFormData & { [key: string]: any };
|
||||
@@ -190,26 +217,9 @@ export const DrillBySubmenu = ({
|
||||
|
||||
const isDisabled = !handlesDimensionContextMenu || !hasDrillBy;
|
||||
|
||||
const Row = ({
|
||||
index,
|
||||
data,
|
||||
style,
|
||||
}: {
|
||||
index: number;
|
||||
data: { columns: Column[] };
|
||||
style: CSSProperties;
|
||||
}) => {
|
||||
const { columns } = data;
|
||||
const column = columns[index];
|
||||
return (
|
||||
<VirtualizedMenuItem
|
||||
tooltipText={column.verbose_name || column.column_name}
|
||||
onClick={e => handleSelection(e, column)}
|
||||
style={style}
|
||||
>
|
||||
{column.verbose_name || column.column_name}
|
||||
</VirtualizedMenuItem>
|
||||
);
|
||||
const listRowProps: DrillByColumnRowProps = {
|
||||
columns: filteredColumns,
|
||||
onSelectColumn: handleSelection,
|
||||
};
|
||||
|
||||
const popoverContent = (
|
||||
@@ -260,15 +270,13 @@ export const DrillBySubmenu = ({
|
||||
</div>
|
||||
) : filteredColumns.length ? (
|
||||
<List
|
||||
width="100%"
|
||||
height={SUBMENU_HEIGHT}
|
||||
itemSize={35}
|
||||
itemCount={filteredColumns.length}
|
||||
itemData={{ columns: filteredColumns }}
|
||||
style={{ width: '100%', height: SUBMENU_HEIGHT }}
|
||||
rowHeight={35}
|
||||
rowCount={filteredColumns.length}
|
||||
rowProps={listRowProps}
|
||||
rowComponent={DrillByColumnRow}
|
||||
overscanCount={20}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
css={css`
|
||||
|
||||
@@ -45,28 +45,26 @@ jest.mock(
|
||||
children({ height: 500, width: 400 }),
|
||||
);
|
||||
|
||||
// Mock react-window VariableSizeList to render all items for testing
|
||||
// Mock react-window List to render all items for testing
|
||||
jest.mock('react-window', () => ({
|
||||
VariableSizeList: ({
|
||||
children: Row,
|
||||
itemCount,
|
||||
itemData,
|
||||
List: ({
|
||||
rowComponent: Row,
|
||||
rowCount,
|
||||
rowProps,
|
||||
}: {
|
||||
children: React.ComponentType<{
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
data: unknown;
|
||||
}>;
|
||||
itemCount: number;
|
||||
itemData: unknown;
|
||||
rowComponent: React.ComponentType<
|
||||
{ index: number; style: React.CSSProperties } & Record<string, unknown>
|
||||
>;
|
||||
rowCount: number;
|
||||
rowProps: Record<string, unknown>;
|
||||
}) => (
|
||||
<div data-testid="virtualized-list">
|
||||
{Array.from({ length: itemCount }, (_, index) => (
|
||||
{Array.from({ length: rowCount }, (_, index) => (
|
||||
<Row
|
||||
key={index}
|
||||
index={index}
|
||||
style={{ height: 'auto', position: 'relative' }}
|
||||
data={itemData}
|
||||
{...rowProps}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { CSSProperties, memo } from 'react';
|
||||
import type { ListChildComponentProps } from 'react-window';
|
||||
import type { RowComponentProps } from 'react-window';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import type { UniqueIdentifier } from '@dnd-kit/core';
|
||||
import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
|
||||
@@ -135,32 +135,34 @@ const TreeItemWrapper = memo(function TreeItemWrapper({
|
||||
);
|
||||
});
|
||||
|
||||
function VirtualizedTreeItemComponent({
|
||||
// Not wrapped in `memo()` here: react-window v2 already wraps `rowComponent`
|
||||
// in its own `memo()` internally (comparing `ariaAttributes`/`style`/the
|
||||
// rest of the row props), so an outer `memo()` would be redundant - and
|
||||
// React 19's `memo()` typings widen the wrapped component's return type to
|
||||
// `ReactNode`, which react-window v2's `rowComponent` type (expecting
|
||||
// `ReactElement | null`) rejects.
|
||||
export function VirtualizedTreeItem({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}: ListChildComponentProps<VirtualizedTreeItemData>) {
|
||||
const {
|
||||
flattenedItems,
|
||||
collapsedIds,
|
||||
selectedItemIds,
|
||||
editingFolderId,
|
||||
folderChildCounts,
|
||||
itemSeparatorInfo,
|
||||
visibleItemIds,
|
||||
searchTerm,
|
||||
metricsMap,
|
||||
columnsMap,
|
||||
activeId,
|
||||
draggedFolderChildIds,
|
||||
forbiddenDropFolderIds,
|
||||
currentDropTargetId,
|
||||
onToggleCollapse,
|
||||
onSelect,
|
||||
onStartEdit,
|
||||
onFinishEdit,
|
||||
} = data;
|
||||
|
||||
flattenedItems,
|
||||
collapsedIds,
|
||||
selectedItemIds,
|
||||
editingFolderId,
|
||||
folderChildCounts,
|
||||
itemSeparatorInfo,
|
||||
visibleItemIds,
|
||||
searchTerm,
|
||||
metricsMap,
|
||||
columnsMap,
|
||||
activeId,
|
||||
draggedFolderChildIds,
|
||||
forbiddenDropFolderIds,
|
||||
currentDropTargetId,
|
||||
onToggleCollapse,
|
||||
onSelect,
|
||||
onStartEdit,
|
||||
onFinishEdit,
|
||||
}: RowComponentProps<VirtualizedTreeItemData>) {
|
||||
const item = flattenedItems[index];
|
||||
|
||||
if (!item) {
|
||||
@@ -224,5 +226,3 @@ function VirtualizedTreeItemComponent({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const VirtualizedTreeItem = memo(VirtualizedTreeItemComponent);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { List, type ListImperativeAPI } from 'react-window';
|
||||
import type { UniqueIdentifier } from '@dnd-kit/core';
|
||||
import type { Metric, ColumnMeta } from '@superset-ui/chart-controls';
|
||||
import { FoldersEditorItemType } from '../types';
|
||||
@@ -82,7 +82,7 @@ export function VirtualizedTreeList({
|
||||
onStartEdit,
|
||||
onFinishEdit,
|
||||
}: VirtualizedTreeListProps) {
|
||||
const listRef = useRef<List>(null);
|
||||
const listRef = useRef<ListImperativeAPI>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Custom auto-scroll during drag (replaces dnd-kit's auto-scroll which conflicts with virtualization)
|
||||
@@ -93,20 +93,15 @@ export function VirtualizedTreeList({
|
||||
listHeight: height,
|
||||
});
|
||||
|
||||
// Reset list cache when items structure changes, but not during drag
|
||||
// Resetting during drag causes jumping/flickering
|
||||
useEffect(() => {
|
||||
if (!isDragging) {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}
|
||||
}, [
|
||||
flattenedItems,
|
||||
collapsedIds,
|
||||
folderChildCounts,
|
||||
itemSeparatorInfo,
|
||||
visibleItemIds,
|
||||
isDragging,
|
||||
]);
|
||||
// No manual cache-reset is needed here (react-window v2 has no
|
||||
// `resetAfterIndex`-style API): `getItemSize` below is a `useCallback`
|
||||
// whose dependency list matches everything that can change row heights,
|
||||
// so react-window's internal size cache - which is invalidated whenever
|
||||
// the `rowHeight` function reference changes - recomputes automatically.
|
||||
// Because `flattenedItems` is held stable during drag (see
|
||||
// useDragHandlers), `getItemSize`'s reference - and therefore the cache -
|
||||
// naturally stays stable during drag too, preserving the old "don't
|
||||
// reset while dragging" behavior without needing an explicit gate.
|
||||
|
||||
// Calculate item size for react-window
|
||||
const getItemSize = useCallback(
|
||||
@@ -169,7 +164,7 @@ export function VirtualizedTreeList({
|
||||
);
|
||||
|
||||
// Prepare item data for the row renderer
|
||||
const itemData: VirtualizedTreeItemData = useMemo(
|
||||
const rowProps: VirtualizedTreeItemData = useMemo(
|
||||
() => ({
|
||||
flattenedItems,
|
||||
collapsedIds,
|
||||
@@ -218,16 +213,14 @@ export function VirtualizedTreeList({
|
||||
return (
|
||||
<div ref={containerRef} style={{ width, height, position: 'relative' }}>
|
||||
<List
|
||||
ref={listRef}
|
||||
width={width}
|
||||
height={height}
|
||||
itemCount={flattenedItems.length}
|
||||
itemSize={getItemSize}
|
||||
itemData={itemData}
|
||||
listRef={listRef}
|
||||
style={{ width, height }}
|
||||
rowCount={flattenedItems.length}
|
||||
rowHeight={getItemSize}
|
||||
rowProps={rowProps}
|
||||
rowComponent={VirtualizedTreeItem}
|
||||
overscanCount={overscanCount}
|
||||
>
|
||||
{VirtualizedTreeItem}
|
||||
</List>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { VariableSizeList as List } from 'react-window';
|
||||
import type { ListImperativeAPI } from 'react-window';
|
||||
|
||||
// Distance from edge where auto-scroll activates (in pixels)
|
||||
const SCROLL_THRESHOLD = 80;
|
||||
@@ -28,7 +28,7 @@ const BASE_SCROLL_SPEED = 8;
|
||||
const MAX_SPEED_MULTIPLIER = 3;
|
||||
|
||||
interface UseAutoScrollOptions {
|
||||
listRef: React.RefObject<List>;
|
||||
listRef: React.RefObject<ListImperativeAPI | null>;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
isDragging: boolean;
|
||||
listHeight: number;
|
||||
@@ -110,7 +110,7 @@ export function useAutoScroll({
|
||||
// Animation frame callback - uses time-based scrolling for consistent speed
|
||||
const scrollFrame = (currentTime: number) => {
|
||||
const list = listRef.current;
|
||||
const outerElement = (list as any)?._outerRef;
|
||||
const outerElement = list?.element;
|
||||
|
||||
if (!list || !outerElement || !state.direction) {
|
||||
// Restore pointer events when scrolling stops
|
||||
@@ -149,7 +149,7 @@ export function useAutoScroll({
|
||||
|
||||
if (Math.abs(newScroll - currentScroll) > 0.5) {
|
||||
// Use direct DOM manipulation for smoother scrolling
|
||||
// react-window's scrollTo triggers re-renders which can cause stutter
|
||||
// react-window's scrollToRow triggers re-renders which can cause stutter
|
||||
outerElement.scrollTop = newScroll;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
import { DatasourceType } from '@superset-ui/core';
|
||||
import { sliceEntitiesForDashboard as mockSliceEntities } from 'spec/fixtures/mockSliceEntities';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import SliceAdder, { SliceAdderProps, sortByComparator } from './SliceAdder';
|
||||
import SliceAdder, {
|
||||
SliceAdderProps,
|
||||
sortByComparator,
|
||||
SliceListRow,
|
||||
} from './SliceAdder';
|
||||
|
||||
// Mock the Select component to avoid debounce issues
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
@@ -236,4 +240,54 @@ describe('SliceAdder', () => {
|
||||
// Verify the internal state was updated by checking if new charts are available
|
||||
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Covers the react-window v2 `rowComponent`/`rowProps` wiring: SliceAdder
|
||||
// hoists its row renderer to module scope (see SliceAdder.tsx) so
|
||||
// react-window doesn't remount every row on each render. This verifies
|
||||
// that hoisted row renders correctly from the flattened rowProps react-window
|
||||
// passes it (index/style plus the custom row props), rather than through
|
||||
// closures over component state.
|
||||
test('SliceListRow renders a chart card for the slice at the given index', () => {
|
||||
const filteredSlices = Object.values(mockSliceEntities.slices);
|
||||
render(
|
||||
<SliceListRow
|
||||
index={0}
|
||||
style={{}}
|
||||
ariaAttributes={{
|
||||
role: 'listitem',
|
||||
'aria-posinset': 1,
|
||||
'aria-setsize': filteredSlices.length,
|
||||
}}
|
||||
filteredSlices={filteredSlices}
|
||||
selectedSliceIdsSet={new Set()}
|
||||
editMode={false}
|
||||
/>,
|
||||
{ useDnd: true, store: mockStore },
|
||||
);
|
||||
expect(screen.getByTestId('chart-card')).toBeInTheDocument();
|
||||
expect(screen.getByText(filteredSlices[0].slice_name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('SliceListRow renders the slice at the row index passed via rowProps, not index 0', () => {
|
||||
const filteredSlices = Object.values(mockSliceEntities.slices);
|
||||
render(
|
||||
<SliceListRow
|
||||
index={1}
|
||||
style={{}}
|
||||
ariaAttributes={{
|
||||
role: 'listitem',
|
||||
'aria-posinset': 2,
|
||||
'aria-setsize': filteredSlices.length,
|
||||
}}
|
||||
filteredSlices={filteredSlices}
|
||||
selectedSliceIdsSet={new Set()}
|
||||
editMode={false}
|
||||
/>,
|
||||
{ useDnd: true, store: mockStore },
|
||||
);
|
||||
expect(screen.getByText(filteredSlices[1].slice_name)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(filteredSlices[0].slice_name),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
/* eslint-env browser */
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { List, type RowComponentProps } from 'react-window';
|
||||
// @ts-expect-error
|
||||
import { createFilter } from 'react-search-input';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
@@ -167,6 +167,71 @@ function getFilteredSortedSlices(
|
||||
.sort(sortByComparator(sortBy));
|
||||
}
|
||||
|
||||
export interface SliceListRowProps {
|
||||
filteredSlices: Slice[];
|
||||
selectedSliceIdsSet: Set<number>;
|
||||
editMode: boolean;
|
||||
}
|
||||
|
||||
// Rendered via `rowComponent`, so it must be a stable reference (module
|
||||
// scope) rather than defined inline on every render of SliceAdder -
|
||||
// otherwise react-window would treat it as a new component type each
|
||||
// render and remount every row (losing in-flight drag state). All the
|
||||
// data it needs is threaded through `rowProps` instead of being closed
|
||||
// over.
|
||||
export function SliceListRow({
|
||||
index,
|
||||
style,
|
||||
filteredSlices,
|
||||
selectedSliceIdsSet,
|
||||
editMode,
|
||||
}: RowComponentProps<SliceListRowProps>) {
|
||||
const cellData = filteredSlices[index];
|
||||
|
||||
const isSelected = selectedSliceIdsSet.has(cellData.slice_id);
|
||||
const type = CHART_TYPE;
|
||||
const id = NEW_CHART_ID;
|
||||
|
||||
const meta = {
|
||||
chartId: cellData.slice_id,
|
||||
sliceName: cellData.slice_name,
|
||||
};
|
||||
return (
|
||||
<DragDroppable
|
||||
key={cellData.slice_id}
|
||||
component={{ type, id, meta }}
|
||||
parentComponent={{
|
||||
id: NEW_COMPONENTS_SOURCE_ID,
|
||||
type: NEW_COMPONENT_SOURCE_TYPE,
|
||||
}}
|
||||
index={index}
|
||||
depth={0}
|
||||
disableDragDrop={isSelected}
|
||||
editMode={editMode}
|
||||
// we must use a custom drag preview within the List because
|
||||
// it does not seem to work within a fixed-position container
|
||||
useEmptyDragPreview
|
||||
// List library expect style props here
|
||||
// actual style should be applied to nested AddSliceCard component
|
||||
style={{}}
|
||||
>
|
||||
{({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
|
||||
<AddSliceCard
|
||||
innerRef={dragSourceRef}
|
||||
style={style}
|
||||
sliceName={cellData.slice_name}
|
||||
lastModified={cellData.changed_on_humanized}
|
||||
visType={cellData.viz_type}
|
||||
datasourceUrl={cellData.datasource_url}
|
||||
datasourceName={cellData.datasource_name}
|
||||
thumbnailUrl={cellData.thumbnail_url}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
)}
|
||||
</DragDroppable>
|
||||
);
|
||||
}
|
||||
|
||||
function SliceAdder({
|
||||
fetchSlices,
|
||||
updateSlices,
|
||||
@@ -321,53 +386,8 @@ function SliceAdder({
|
||||
[fetchSlices, searchTerm, sortBy],
|
||||
);
|
||||
|
||||
const rowRenderer = useCallback(
|
||||
({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||
const cellData = filteredSlices[index];
|
||||
|
||||
const isSelected = selectedSliceIdsSet.has(cellData.slice_id);
|
||||
const type = CHART_TYPE;
|
||||
const id = NEW_CHART_ID;
|
||||
|
||||
const meta = {
|
||||
chartId: cellData.slice_id,
|
||||
sliceName: cellData.slice_name,
|
||||
};
|
||||
return (
|
||||
<DragDroppable
|
||||
key={cellData.slice_id}
|
||||
component={{ type, id, meta }}
|
||||
parentComponent={{
|
||||
id: NEW_COMPONENTS_SOURCE_ID,
|
||||
type: NEW_COMPONENT_SOURCE_TYPE,
|
||||
}}
|
||||
index={index}
|
||||
depth={0}
|
||||
disableDragDrop={isSelected}
|
||||
editMode={editMode}
|
||||
// we must use a custom drag preview within the List because
|
||||
// it does not seem to work within a fixed-position container
|
||||
useEmptyDragPreview
|
||||
// List library expect style props here
|
||||
// actual style should be applied to nested AddSliceCard component
|
||||
style={{}}
|
||||
>
|
||||
{({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
|
||||
<AddSliceCard
|
||||
innerRef={dragSourceRef}
|
||||
style={style}
|
||||
sliceName={cellData.slice_name}
|
||||
lastModified={cellData.changed_on_humanized}
|
||||
visType={cellData.viz_type}
|
||||
datasourceUrl={cellData.datasource_url}
|
||||
datasourceName={cellData.datasource_name}
|
||||
thumbnailUrl={cellData.thumbnail_url}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
)}
|
||||
</DragDroppable>
|
||||
);
|
||||
},
|
||||
const listRowProps = useMemo<SliceListRowProps>(
|
||||
() => ({ filteredSlices, selectedSliceIdsSet, editMode }),
|
||||
[filteredSlices, selectedSliceIdsSet, editMode],
|
||||
);
|
||||
|
||||
@@ -448,14 +468,13 @@ function SliceAdder({
|
||||
<AutoSizer>
|
||||
{({ height, width }: { height: number; width: number }) => (
|
||||
<List
|
||||
width={width}
|
||||
height={height}
|
||||
itemCount={filteredSlices.length}
|
||||
itemSize={DEFAULT_CELL_HEIGHT}
|
||||
itemKey={index => filteredSlices[index].slice_id}
|
||||
>
|
||||
{rowRenderer}
|
||||
</List>
|
||||
style={{ width, height }}
|
||||
rowCount={filteredSlices.length}
|
||||
rowHeight={DEFAULT_CELL_HEIGHT}
|
||||
rowProps={listRowProps}
|
||||
rowComponent={SliceListRow}
|
||||
rowKey={(index, data) => data.filteredSlices[index].slice_id}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</ChartList>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { List } from 'react-window';
|
||||
import { FlattenedItem, Folder } from './types';
|
||||
import DatasourcePanelItem from './DatasourcePanelItem';
|
||||
|
||||
@@ -98,7 +98,6 @@ export const DatasourceItems = ({
|
||||
height,
|
||||
folders,
|
||||
}: DatasourceItemsProps) => {
|
||||
const listRef = useRef<List>(null);
|
||||
const [collapsedFolderIds, setCollapsedFolderIds] = useState<Set<string>>(
|
||||
new Set(
|
||||
folders.filter(folder => folder.isCollapsed).map(folder => folder.id),
|
||||
@@ -122,17 +121,15 @@ export const DatasourceItems = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// reset the list cache when flattenedItems length changes to recalculate the heights
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, [flattenedItems]);
|
||||
|
||||
const getItemSize = useCallback(
|
||||
// No manual cache-reset is needed: `rowHeight` is a useCallback tied to
|
||||
// `flattenedItems`, so react-window's internal size cache recomputes
|
||||
// automatically whenever its reference changes.
|
||||
const rowHeight = useCallback(
|
||||
(index: number) => flattenedItems[index].height,
|
||||
[flattenedItems],
|
||||
);
|
||||
|
||||
const itemData = useMemo(
|
||||
const rowProps = useMemo(
|
||||
() => ({
|
||||
flattenedItems,
|
||||
folderMap,
|
||||
@@ -151,15 +148,12 @@ export const DatasourceItems = ({
|
||||
|
||||
return (
|
||||
<List
|
||||
ref={listRef}
|
||||
width={width - BORDER_WIDTH}
|
||||
height={height}
|
||||
itemSize={getItemSize}
|
||||
itemCount={flattenedItems.length}
|
||||
itemData={itemData}
|
||||
style={{ width: width - BORDER_WIDTH, height }}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={flattenedItems.length}
|
||||
rowProps={rowProps}
|
||||
rowComponent={DatasourcePanelItem}
|
||||
overscanCount={5}
|
||||
>
|
||||
{DatasourcePanelItem}
|
||||
</List>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+15
-4
@@ -22,12 +22,12 @@ import {
|
||||
} from 'src/explore/components/DatasourcePanel/fixtures';
|
||||
import { screen, userEvent, render } from 'spec/helpers/testing-library';
|
||||
import DatasourcePanelItem, {
|
||||
DatasourcePanelItemProps,
|
||||
DatasourcePanelItemRowProps,
|
||||
} from './DatasourcePanelItem';
|
||||
import { FoldersEditorItemType } from 'src/components/Datasource/types';
|
||||
import { MetricItem, ColumnItem } from './types';
|
||||
|
||||
const mockData: DatasourcePanelItemProps['data'] = {
|
||||
const mockData: DatasourcePanelItemRowProps = {
|
||||
flattenedItems: [
|
||||
{ type: 'header', depth: 0, folderId: '1', height: 50 },
|
||||
...metrics.map((m, idx) => ({
|
||||
@@ -82,11 +82,22 @@ const mockData: DatasourcePanelItemProps['data'] = {
|
||||
collapsedFolderIds: new Set(),
|
||||
};
|
||||
|
||||
const setup = (data: DatasourcePanelItemProps['data'] = mockData) =>
|
||||
const setup = (data: DatasourcePanelItemRowProps = mockData) =>
|
||||
render(
|
||||
<>
|
||||
{data.flattenedItems.map((_, index) => (
|
||||
<DatasourcePanelItem index={index} data={data} style={{}} />
|
||||
<DatasourcePanelItem
|
||||
// eslint-disable-next-line react/no-array-index-key -- test fixture has no stable id
|
||||
key={index}
|
||||
index={index}
|
||||
style={{}}
|
||||
ariaAttributes={{
|
||||
role: 'listitem',
|
||||
'aria-posinset': index + 1,
|
||||
'aria-setsize': data.flattenedItems.length,
|
||||
}}
|
||||
{...data}
|
||||
/>
|
||||
))}
|
||||
</>,
|
||||
{ useDnd: true, useRedux: true, initialState: { explore: {} } },
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { CSSProperties, ReactNode, useCallback } from 'react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import type { RowComponentProps } from 'react-window';
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { useCSSTextTruncation } from '@superset-ui/core';
|
||||
@@ -112,30 +113,26 @@ const Divider = styled.div`
|
||||
`}
|
||||
`;
|
||||
|
||||
export interface DatasourcePanelItemProps {
|
||||
index: number;
|
||||
style: CSSProperties;
|
||||
data: {
|
||||
flattenedItems: FlattenedItem[];
|
||||
folderMap: Map<string, Folder>;
|
||||
width: number;
|
||||
onToggleCollapse: (folderId: string) => void;
|
||||
collapsedFolderIds: Set<string>;
|
||||
};
|
||||
export interface DatasourcePanelItemRowProps {
|
||||
flattenedItems: FlattenedItem[];
|
||||
folderMap: Map<string, Folder>;
|
||||
width: number;
|
||||
onToggleCollapse: (folderId: string) => void;
|
||||
collapsedFolderIds: Set<string>;
|
||||
}
|
||||
|
||||
export type DatasourcePanelItemProps =
|
||||
RowComponentProps<DatasourcePanelItemRowProps>;
|
||||
|
||||
const DatasourcePanelItem = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
flattenedItems,
|
||||
folderMap,
|
||||
width,
|
||||
onToggleCollapse,
|
||||
collapsedFolderIds,
|
||||
}: DatasourcePanelItemProps) => {
|
||||
const {
|
||||
flattenedItems,
|
||||
folderMap,
|
||||
width,
|
||||
onToggleCollapse,
|
||||
collapsedFolderIds,
|
||||
} = data;
|
||||
const item = flattenedItems[index];
|
||||
const theme = useTheme();
|
||||
const [labelRef, labelIsTruncated] = useCSSTextTruncation<HTMLSpanElement>({
|
||||
|
||||
Reference in New Issue
Block a user