You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2024/03/28 12:28:09 UTC
(superset) 02/03: perf(explore): virtualized datasource field sections (#27625)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 4.0
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2fa1b35c16988d436c959be66ab9f9d958873748
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Wed Mar 27 11:25:55 2024 -0700
perf(explore): virtualized datasource field sections (#27625)
(cherry picked from commit 38eecfc5d47b50f5ab24840d68e715ce2fb52709)
---
superset-frontend/package-lock.json | 19 ++
superset-frontend/package.json | 1 +
.../DatasourcePanel/DatasourcePanel.test.tsx | 12 ++
.../DatasourcePanel/DatasourcePanelItem.test.tsx | 168 +++++++++++++++
.../DatasourcePanel/DatasourcePanelItem.tsx | 234 +++++++++++++++++++++
.../explore/components/DatasourcePanel/index.tsx | 219 +++++--------------
.../components/ExploreViewContainer/index.jsx | 36 ++--
7 files changed, 504 insertions(+), 185 deletions(-)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 38d97377c3..9148048f47 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -200,6 +200,7 @@
"@types/react-table": "^7.7.19",
"@types/react-transition-group": "^4.4.10",
"@types/react-ultimate-pagination": "^1.2.0",
+ "@types/react-virtualized-auto-sizer": "^1.0.4",
"@types/react-window": "^1.8.5",
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2",
@@ -22853,6 +22854,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-virtualized-auto-sizer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz",
+ "integrity": "sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-window": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
@@ -89600,6 +89610,15 @@
"@types/react": "*"
}
},
+ "@types/react-virtualized-auto-sizer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz",
+ "integrity": "sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/react-window": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index c12e417aed..a028089f74 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -266,6 +266,7 @@
"@types/react-table": "^7.7.19",
"@types/react-transition-group": "^4.4.10",
"@types/react-ultimate-pagination": "^1.2.0",
+ "@types/react-virtualized-auto-sizer": "^1.0.4",
"@types/react-window": "^1.8.5",
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2",
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
index 95258f443e..452ee4609c 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
@@ -30,6 +30,17 @@ import {
import { DatasourceType } from '@superset-ui/core';
import DatasourceControl from 'src/explore/components/controls/DatasourceControl';
+jest.mock(
+ 'react-virtualized-auto-sizer',
+ () =>
+ ({
+ children,
+ }: {
+ children: (params: { height: number }) => React.ReactChild;
+ }) =>
+ children({ height: 500 }),
+);
+
const datasource: IDatasource = {
id: 1,
type: DatasourceType.Table,
@@ -69,6 +80,7 @@ const props: DatasourcePanelProps = {
actions: {
setControlValue: jest.fn(),
},
+ width: 300,
};
const search = (value: string, input: HTMLElement) => {
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
new file mode 100644
index 0000000000..76c4d58e2d
--- /dev/null
+++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
@@ -0,0 +1,168 @@
+/**
+ * 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 React from 'react';
+
+import {
+ columns,
+ metrics,
+} from 'src/explore/components/DatasourcePanel/fixtures';
+import { fireEvent, render, within } from 'spec/helpers/testing-library';
+import DatasourcePanelItem from './DatasourcePanelItem';
+
+const mockData = {
+ metricSlice: metrics,
+ columnSlice: columns,
+ totalMetrics: Math.max(metrics.length, 10),
+ totalColumns: Math.max(columns.length, 13),
+ width: 300,
+ showAllMetrics: false,
+ onShowAllMetricsChange: jest.fn(),
+ showAllColumns: false,
+ onShowAllColumnsChange: jest.fn(),
+ collapseMetrics: false,
+ onCollapseMetricsChange: jest.fn(),
+ collapseColumns: false,
+ onCollapseColumnsChange: jest.fn(),
+};
+
+test('renders each item accordingly', () => {
+ const { getByText, getByTestId, rerender, container } = render(
+ <DatasourcePanelItem index={0} data={mockData} style={{}} />,
+ { useDnd: true },
+ );
+
+ expect(getByText('Metrics')).toBeInTheDocument();
+ rerender(<DatasourcePanelItem index={1} data={mockData} style={{}} />);
+ expect(
+ getByText(
+ `Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`,
+ ),
+ ).toBeInTheDocument();
+ mockData.metricSlice.forEach((metric, metricIndex) => {
+ rerender(
+ <DatasourcePanelItem
+ index={metricIndex + 2}
+ data={mockData}
+ style={{}}
+ />,
+ );
+ expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument();
+ expect(
+ within(getByTestId('DatasourcePanelDragOption')).getByText(
+ metric.metric_name,
+ ),
+ ).toBeInTheDocument();
+ });
+ rerender(
+ <DatasourcePanelItem
+ index={2 + mockData.metricSlice.length}
+ data={mockData}
+ style={{}}
+ />,
+ );
+ expect(container).toHaveTextContent('');
+
+ const startIndexOfColumnSection = mockData.metricSlice.length + 3;
+ rerender(
+ <DatasourcePanelItem
+ index={startIndexOfColumnSection}
+ data={mockData}
+ style={{}}
+ />,
+ );
+ expect(getByText('Columns')).toBeInTheDocument();
+ rerender(
+ <DatasourcePanelItem
+ index={startIndexOfColumnSection + 1}
+ data={mockData}
+ style={{}}
+ />,
+ );
+ expect(
+ getByText(
+ `Showing ${mockData.columnSlice.length} of ${mockData.totalColumns}`,
+ ),
+ ).toBeInTheDocument();
+ mockData.columnSlice.forEach((column, columnIndex) => {
+ rerender(
+ <DatasourcePanelItem
+ index={startIndexOfColumnSection + columnIndex + 2}
+ data={mockData}
+ style={{}}
+ />,
+ );
+ expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument();
+ expect(
+ within(getByTestId('DatasourcePanelDragOption')).getByText(
+ column.column_name,
+ ),
+ ).toBeInTheDocument();
+ });
+});
+
+test('can collapse metrics and columns', () => {
+ mockData.onCollapseMetricsChange.mockClear();
+ mockData.onCollapseColumnsChange.mockClear();
+ const { queryByText, getByRole, rerender } = render(
+ <DatasourcePanelItem index={0} data={mockData} style={{}} />,
+ { useDnd: true },
+ );
+ fireEvent.click(getByRole('button'));
+ expect(mockData.onCollapseMetricsChange).toBeCalled();
+ expect(mockData.onCollapseColumnsChange).not.toBeCalled();
+
+ const startIndexOfColumnSection = mockData.metricSlice.length + 3;
+ rerender(
+ <DatasourcePanelItem
+ index={startIndexOfColumnSection}
+ data={mockData}
+ style={{}}
+ />,
+ );
+ fireEvent.click(getByRole('button'));
+ expect(mockData.onCollapseColumnsChange).toBeCalled();
+
+ rerender(
+ <DatasourcePanelItem
+ index={1}
+ data={{
+ ...mockData,
+ collapseMetrics: true,
+ }}
+ style={{}}
+ />,
+ );
+ expect(
+ queryByText(
+ `Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`,
+ ),
+ ).not.toBeInTheDocument();
+
+ rerender(
+ <DatasourcePanelItem
+ index={2}
+ data={{
+ ...mockData,
+ collapseMetrics: true,
+ }}
+ style={{}}
+ />,
+ );
+ expect(queryByText('Columns')).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
new file mode 100644
index 0000000000..ab89019da2
--- /dev/null
+++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
@@ -0,0 +1,234 @@
+/**
+ * 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 React, { CSSProperties } from 'react';
+import { css, Metric, styled, t, useTheme } from '@superset-ui/core';
+
+import Icons from 'src/components/Icons';
+import DatasourcePanelDragOption from './DatasourcePanelDragOption';
+import { DndItemType } from '../DndItemType';
+import { DndItemValue } from './types';
+
+export type DataSourcePanelColumn = {
+ is_dttm?: boolean | null;
+ description?: string | null;
+ expression?: string | null;
+ is_certified?: number | null;
+ column_name?: string | null;
+ name?: string | null;
+ type?: string;
+};
+
+type Props = {
+ index: number;
+ style: CSSProperties;
+ data: {
+ metricSlice: Metric[];
+ columnSlice: DataSourcePanelColumn[];
+ totalMetrics: number;
+ totalColumns: number;
+ width: number;
+ showAllMetrics: boolean;
+ onShowAllMetricsChange: (showAll: boolean) => void;
+ showAllColumns: boolean;
+ onShowAllColumnsChange: (showAll: boolean) => void;
+ collapseMetrics: boolean;
+ onCollapseMetricsChange: (collapse: boolean) => void;
+ collapseColumns: boolean;
+ onCollapseColumnsChange: (collapse: boolean) => void;
+ };
+};
+
+export const DEFAULT_MAX_COLUMNS_LENGTH = 50;
+export const DEFAULT_MAX_METRICS_LENGTH = 50;
+export const ITEM_HEIGHT = 30;
+
+const Button = styled.button`
+ background: none;
+ border: none;
+ text-decoration: underline;
+ color: ${({ theme }) => theme.colors.primary.dark1};
+`;
+
+const ButtonContainer = styled.div`
+ text-align: center;
+ padding-top: 2px;
+`;
+
+const LabelWrapper = styled.div`
+ ${({ theme }) => css`
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: ${theme.typography.sizes.s}px;
+ background-color: ${theme.colors.grayscale.light4};
+ margin: ${theme.gridUnit * 2}px 0;
+ border-radius: 4px;
+ padding: 0 ${theme.gridUnit}px;
+
+ &:first-of-type {
+ margin-top: 0;
+ }
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+
+ padding: 0;
+ cursor: pointer;
+ &:hover {
+ background-color: ${theme.colors.grayscale.light3};
+ }
+
+ & > span {
+ white-space: nowrap;
+ }
+
+ .option-label {
+ display: inline;
+ }
+
+ .metric-option {
+ & > svg {
+ min-width: ${theme.gridUnit * 4}px;
+ }
+ & > .option-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ `}
+`;
+
+const SectionHeaderButton = styled.button`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border: none;
+ background: transparent;
+ width: 100%;
+ padding-inline: 0px;
+`;
+
+const SectionHeader = styled.span`
+ ${({ theme }) => `
+ font-size: ${theme.typography.sizes.m}px;
+ line-height: 1.3;
+ `}
+`;
+
+const DatasourcePanelItem: React.FC<Props> = ({ index, style, data }) => {
+ const {
+ metricSlice: _metricSlice,
+ columnSlice,
+ totalMetrics,
+ totalColumns,
+ width,
+ showAllMetrics,
+ onShowAllMetricsChange,
+ showAllColumns,
+ onShowAllColumnsChange,
+ collapseMetrics,
+ onCollapseMetricsChange,
+ collapseColumns,
+ onCollapseColumnsChange,
+ } = data;
+ const metricSlice = collapseMetrics ? [] : _metricSlice;
+
+ const EXTRA_LINES = collapseMetrics ? 1 : 2;
+ const isColumnSection = collapseMetrics
+ ? index >= 1
+ : index > metricSlice.length + EXTRA_LINES;
+ const HEADER_LINE = isColumnSection
+ ? metricSlice.length + EXTRA_LINES + 1
+ : 0;
+ const SUBTITLE_LINE = HEADER_LINE + 1;
+ const BOTTOM_LINE =
+ (isColumnSection ? columnSlice.length : metricSlice.length) +
+ (collapseMetrics ? HEADER_LINE : SUBTITLE_LINE) +
+ 1;
+ const collapsed = isColumnSection ? collapseColumns : collapseMetrics;
+ const setCollapse = isColumnSection
+ ? onCollapseColumnsChange
+ : onCollapseMetricsChange;
+ const showAll = isColumnSection ? showAllColumns : showAllMetrics;
+ const setShowAll = isColumnSection
+ ? onShowAllColumnsChange
+ : onShowAllMetricsChange;
+ const theme = useTheme();
+
+ return (
+ <div
+ style={style}
+ css={css`
+ padding: 0 ${theme.gridUnit * 4}px;
+ `}
+ >
+ {index === HEADER_LINE && (
+ <SectionHeaderButton onClick={() => setCollapse(!collapsed)}>
+ <SectionHeader>
+ {isColumnSection ? t('Columns') : t('Metrics')}
+ </SectionHeader>
+ {collapsed ? (
+ <Icons.DownOutlined iconSize="s" />
+ ) : (
+ <Icons.UpOutlined iconSize="s" />
+ )}
+ </SectionHeaderButton>
+ )}
+ {index === SUBTITLE_LINE && !collapsed && (
+ <div className="field-length">
+ {isColumnSection
+ ? t(`Showing %s of %s`, columnSlice?.length, totalColumns)
+ : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)}
+ </div>
+ )}
+ {index > SUBTITLE_LINE && index < BOTTOM_LINE && (
+ <LabelWrapper
+ key={
+ (isColumnSection
+ ? columnSlice[index - SUBTITLE_LINE - 1].column_name
+ : metricSlice[index - SUBTITLE_LINE - 1].metric_name) +
+ String(width)
+ }
+ className="column"
+ >
+ <DatasourcePanelDragOption
+ value={
+ isColumnSection
+ ? (columnSlice[index - SUBTITLE_LINE - 1] as DndItemValue)
+ : metricSlice[index - SUBTITLE_LINE - 1]
+ }
+ type={isColumnSection ? DndItemType.Column : DndItemType.Metric}
+ />
+ </LabelWrapper>
+ )}
+ {index === BOTTOM_LINE &&
+ !collapsed &&
+ (isColumnSection
+ ? totalColumns > DEFAULT_MAX_COLUMNS_LENGTH
+ : totalMetrics > DEFAULT_MAX_METRICS_LENGTH) && (
+ <ButtonContainer>
+ <Button onClick={() => setShowAll(!showAll)}>
+ {showAll ? t('Show less...') : t('Show all...')}
+ </Button>
+ </ButtonContainer>
+ )}
+ </div>
+ );
+};
+
+export default DatasourcePanelItem;
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
index 99f6b48b89..395b70061a 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import {
css,
DatasourceType,
@@ -27,10 +27,11 @@ import {
} from '@superset-ui/core';
import { ControlConfig } from '@superset-ui/chart-controls';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import { FixedSizeList as List } from 'react-window';
import { debounce, isArray } from 'lodash';
import { matchSorter, rankings } from 'match-sorter';
-import Collapse from 'src/components/Collapse';
import Alert from 'src/components/Alert';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
@@ -38,23 +39,16 @@ import { Input } from 'src/components/Input';
import { FAST_DEBOUNCE } from 'src/constants';
import { ExploreActions } from 'src/explore/actions/exploreActions';
import Control from 'src/explore/components/Control';
-import DatasourcePanelDragOption from './DatasourcePanelDragOption';
-import { DndItemType } from '../DndItemType';
-import { DndItemValue } from './types';
+import DatasourcePanelItem, {
+ ITEM_HEIGHT,
+ DataSourcePanelColumn,
+ DEFAULT_MAX_COLUMNS_LENGTH,
+ DEFAULT_MAX_METRICS_LENGTH,
+} from './DatasourcePanelItem';
interface DatasourceControl extends ControlConfig {
datasource?: IDatasource;
}
-
-export interface DataSourcePanelColumn {
- is_dttm?: boolean | null;
- description?: string | null;
- expression?: string | null;
- is_certified?: number | null;
- column_name?: string | null;
- name?: string | null;
- type?: string;
-}
export interface IDatasource {
metrics: Metric[];
columns: DataSourcePanelColumn[];
@@ -76,22 +70,10 @@ export interface Props {
};
actions: Partial<ExploreActions> & Pick<ExploreActions, 'setControlValue'>;
// we use this props control force update when this panel resize
- shouldForceUpdate?: number;
+ width: number;
formData?: QueryFormData;
}
-const Button = styled.button`
- background: none;
- border: none;
- text-decoration: underline;
- color: ${({ theme }) => theme.colors.primary.dark1};
-`;
-
-const ButtonContainer = styled.div`
- text-align: center;
- padding-top: 2px;
-`;
-
const DatasourceContainer = styled.div`
${({ theme }) => css`
background-color: ${theme.colors.grayscale.light5};
@@ -104,8 +86,9 @@ const DatasourceContainer = styled.div`
height: auto;
}
.field-selections {
- padding: 0 0 ${4 * theme.gridUnit}px;
+ padding: 0 0 ${theme.gridUnit}px;
overflow: auto;
+ height: 100%;
}
.field-length {
margin-bottom: ${theme.gridUnit * 2}px;
@@ -127,56 +110,6 @@ const DatasourceContainer = styled.div`
`};
`;
-const LabelWrapper = styled.div`
- ${({ theme }) => css`
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: ${theme.typography.sizes.s}px;
- background-color: ${theme.colors.grayscale.light4};
- margin: ${theme.gridUnit * 2}px 0;
- border-radius: 4px;
- padding: 0 ${theme.gridUnit}px;
-
- &:first-of-type {
- margin-top: 0;
- }
- &:last-of-type {
- margin-bottom: 0;
- }
-
- padding: 0;
- cursor: pointer;
- &:hover {
- background-color: ${theme.colors.grayscale.light3};
- }
-
- & > span {
- white-space: nowrap;
- }
-
- .option-label {
- display: inline;
- }
-
- .metric-option {
- & > svg {
- min-width: ${theme.gridUnit * 4}px;
- }
- & > .option-label {
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
- `}
-`;
-
-const SectionHeader = styled.span`
- ${({ theme }) => `
- font-size: ${theme.typography.sizes.m}px;
- line-height: 1.3;
- `}
-`;
-
const StyledInfoboxWrapper = styled.div`
${({ theme }) => css`
margin: 0 ${theme.gridUnit * 2.5}px;
@@ -187,27 +120,14 @@ const StyledInfoboxWrapper = styled.div`
`}
`;
-const LabelContainer = (props: {
- children: React.ReactElement;
- className: string;
-}) => {
- const labelRef = useRef<HTMLDivElement>(null);
- const extendedProps = {
- labelRef,
- };
- return (
- <LabelWrapper className={props.className}>
- {React.cloneElement(props.children, extendedProps)}
- </LabelWrapper>
- );
-};
+const BORDER_WIDTH = 2;
export default function DataSourcePanel({
datasource,
formData,
controls: { datasource: datasourceControl },
actions,
- shouldForceUpdate,
+ width,
}: Props) {
const { columns: _columns, metrics } = datasource;
// display temporal column first
@@ -233,9 +153,8 @@ export default function DataSourcePanel({
});
const [showAllMetrics, setShowAllMetrics] = useState(false);
const [showAllColumns, setShowAllColumns] = useState(false);
-
- const DEFAULT_MAX_COLUMNS_LENGTH = 50;
- const DEFAULT_MAX_METRICS_LENGTH = 50;
+ const [collapseMetrics, setCollapseMetrics] = useState(false);
+ const [collapseColumns, setCollapseColumns] = useState(false);
const search = useMemo(
() =>
@@ -385,78 +304,40 @@ export default function DataSourcePanel({
/>
</StyledInfoboxWrapper>
)}
- <Collapse
- defaultActiveKey={['metrics', 'column']}
- expandIconPosition="right"
- ghost
- >
- {metrics?.length && (
- <Collapse.Panel
- header={<SectionHeader>{t('Metrics')}</SectionHeader>}
- key="metrics"
+ <AutoSizer>
+ {({ height }) => (
+ <List
+ width={width - BORDER_WIDTH}
+ height={height}
+ itemSize={ITEM_HEIGHT}
+ itemCount={
+ (collapseMetrics ? 0 : metricSlice?.length) +
+ (collapseColumns ? 0 : columnSlice.length) +
+ 2 + // Each section header row
+ (collapseMetrics ? 0 : 2) +
+ (collapseColumns ? 0 : 2)
+ }
+ itemData={{
+ metricSlice,
+ columnSlice,
+ width,
+ totalMetrics: lists?.metrics.length,
+ totalColumns: lists?.columns.length,
+ showAllMetrics,
+ onShowAllMetricsChange: setShowAllMetrics,
+ showAllColumns,
+ onShowAllColumnsChange: setShowAllColumns,
+ collapseMetrics,
+ onCollapseMetricsChange: setCollapseMetrics,
+ collapseColumns,
+ onCollapseColumnsChange: setCollapseColumns,
+ }}
+ overscanCount={5}
>
- <div className="field-length">
- {t(
- `Showing %s of %s`,
- metricSlice?.length,
- lists?.metrics.length,
- )}
- </div>
- {metricSlice?.map?.((m: Metric) => (
- <LabelContainer
- key={m.metric_name + String(shouldForceUpdate)}
- className="column"
- >
- <DatasourcePanelDragOption
- value={m}
- type={DndItemType.Metric}
- />
- </LabelContainer>
- ))}
- {lists?.metrics?.length > DEFAULT_MAX_METRICS_LENGTH ? (
- <ButtonContainer>
- <Button onClick={() => setShowAllMetrics(!showAllMetrics)}>
- {showAllMetrics ? t('Show less...') : t('Show all...')}
- </Button>
- </ButtonContainer>
- ) : (
- <></>
- )}
- </Collapse.Panel>
+ {DatasourcePanelItem}
+ </List>
)}
- <Collapse.Panel
- header={<SectionHeader>{t('Columns')}</SectionHeader>}
- key="column"
- >
- <div className="field-length">
- {t(
- `Showing %s of %s`,
- columnSlice.length,
- lists.columns.length,
- )}
- </div>
- {columnSlice.map(col => (
- <LabelContainer
- key={col.column_name + String(shouldForceUpdate)}
- className="column"
- >
- <DatasourcePanelDragOption
- value={col as DndItemValue}
- type={DndItemType.Column}
- />
- </LabelContainer>
- ))}
- {lists.columns.length > DEFAULT_MAX_COLUMNS_LENGTH ? (
- <ButtonContainer>
- <Button onClick={() => setShowAllColumns(!showAllColumns)}>
- {showAllColumns ? t('Show Less...') : t('Show all...')}
- </Button>
- </ButtonContainer>
- ) : (
- <></>
- )}
- </Collapse.Panel>
- </Collapse>
+ </AutoSizer>
</div>
</>
),
@@ -470,8 +351,10 @@ export default function DataSourcePanel({
search,
showAllColumns,
showAllMetrics,
+ collapseMetrics,
+ collapseColumns,
datasourceIsSaveable,
- shouldForceUpdate,
+ width,
],
);
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index 0da43ebdc0..1aeb45cb15 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -229,6 +229,20 @@ const updateHistory = debounce(
1000,
);
+const defaultSidebarsWidth = {
+ controls_width: 320,
+ datasource_width: 300,
+};
+
+function getSidebarWidths(key) {
+ return getItem(key, defaultSidebarsWidth[key]);
+}
+
+function setSidebarWidths(key, dimension) {
+ const newDimension = Number(getSidebarWidths(key)) + dimension.width;
+ setItem(key, newDimension);
+}
+
function ExploreViewContainer(props) {
const dynamicPluginContext = usePluginContext();
const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType];
@@ -243,16 +257,13 @@ function ExploreViewContainer(props) {
);
const [isCollapsed, setIsCollapsed] = useState(false);
- const [shouldForceUpdate, setShouldForceUpdate] = useState(-1);
+ const [width, setWidth] = useState(
+ getSidebarWidths(LocalStorageKeys.DatasourceWidth),
+ );
const tabId = useTabId();
const theme = useTheme();
- const defaultSidebarsWidth = {
- controls_width: 320,
- datasource_width: 300,
- };
-
const addHistory = useCallback(
async ({ isReplace = false, title } = {}) => {
const formData = props.dashboardId
@@ -534,15 +545,6 @@ function ExploreViewContainer(props) {
);
}
- function getSidebarWidths(key) {
- return getItem(key, defaultSidebarsWidth[key]);
- }
-
- function setSidebarWidths(key, dimension) {
- const newDimension = Number(getSidebarWidths(key)) + dimension.width;
- setItem(key, newDimension);
- }
-
if (props.standalone) {
return renderChartContainer();
}
@@ -593,7 +595,7 @@ function ExploreViewContainer(props) {
/>
<Resizable
onResizeStop={(evt, direction, ref, d) => {
- setShouldForceUpdate(d?.width);
+ setWidth(ref.getBoundingClientRect().width);
setSidebarWidths(LocalStorageKeys.DatasourceWidth, d);
}}
defaultSize={{
@@ -627,7 +629,7 @@ function ExploreViewContainer(props) {
datasource={props.datasource}
controls={props.controls}
actions={props.actions}
- shouldForceUpdate={shouldForceUpdate}
+ width={width}
user={props.user}
/>
</Resizable>