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>