perf(explore): virtualized datasource field sections (#27625)

(cherry picked from commit 38eecfc5d4)
This commit is contained in:
JUST.in DO IT 2024-03-27 11:25:55 -07:00 committed by Michael S. Molina
parent a024b4ac1b
commit 2fa1b35c16
7 changed files with 504 additions and 185 deletions

View File

@ -200,6 +200,7 @@
"@types/react-table": "^7.7.19", "@types/react-table": "^7.7.19",
"@types/react-transition-group": "^4.4.10", "@types/react-transition-group": "^4.4.10",
"@types/react-ultimate-pagination": "^1.2.0", "@types/react-ultimate-pagination": "^1.2.0",
"@types/react-virtualized-auto-sizer": "^1.0.4",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/redux-localstorage": "^1.0.8", "@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2", "@types/redux-mock-store": "^1.0.2",
@ -22853,6 +22854,15 @@
"@types/react": "*" "@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": { "node_modules/@types/react-window": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
@ -89600,6 +89610,15 @@
"@types/react": "*" "@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": { "@types/react-window": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",

View File

@ -266,6 +266,7 @@
"@types/react-table": "^7.7.19", "@types/react-table": "^7.7.19",
"@types/react-transition-group": "^4.4.10", "@types/react-transition-group": "^4.4.10",
"@types/react-ultimate-pagination": "^1.2.0", "@types/react-ultimate-pagination": "^1.2.0",
"@types/react-virtualized-auto-sizer": "^1.0.4",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@types/redux-localstorage": "^1.0.8", "@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2", "@types/redux-mock-store": "^1.0.2",

View File

@ -30,6 +30,17 @@ import {
import { DatasourceType } from '@superset-ui/core'; import { DatasourceType } from '@superset-ui/core';
import DatasourceControl from 'src/explore/components/controls/DatasourceControl'; 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 = { const datasource: IDatasource = {
id: 1, id: 1,
type: DatasourceType.Table, type: DatasourceType.Table,
@ -69,6 +80,7 @@ const props: DatasourcePanelProps = {
actions: { actions: {
setControlValue: jest.fn(), setControlValue: jest.fn(),
}, },
width: 300,
}; };
const search = (value: string, input: HTMLElement) => { const search = (value: string, input: HTMLElement) => {

View File

@ -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();
});

View File

@ -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;

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
css, css,
DatasourceType, DatasourceType,
@ -27,10 +27,11 @@ import {
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ControlConfig } from '@superset-ui/chart-controls'; 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 { debounce, isArray } from 'lodash';
import { matchSorter, rankings } from 'match-sorter'; import { matchSorter, rankings } from 'match-sorter';
import Collapse from 'src/components/Collapse';
import Alert from 'src/components/Alert'; import Alert from 'src/components/Alert';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
@ -38,23 +39,16 @@ import { Input } from 'src/components/Input';
import { FAST_DEBOUNCE } from 'src/constants'; import { FAST_DEBOUNCE } from 'src/constants';
import { ExploreActions } from 'src/explore/actions/exploreActions'; import { ExploreActions } from 'src/explore/actions/exploreActions';
import Control from 'src/explore/components/Control'; import Control from 'src/explore/components/Control';
import DatasourcePanelDragOption from './DatasourcePanelDragOption'; import DatasourcePanelItem, {
import { DndItemType } from '../DndItemType'; ITEM_HEIGHT,
import { DndItemValue } from './types'; DataSourcePanelColumn,
DEFAULT_MAX_COLUMNS_LENGTH,
DEFAULT_MAX_METRICS_LENGTH,
} from './DatasourcePanelItem';
interface DatasourceControl extends ControlConfig { interface DatasourceControl extends ControlConfig {
datasource?: IDatasource; 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 { export interface IDatasource {
metrics: Metric[]; metrics: Metric[];
columns: DataSourcePanelColumn[]; columns: DataSourcePanelColumn[];
@ -76,22 +70,10 @@ export interface Props {
}; };
actions: Partial<ExploreActions> & Pick<ExploreActions, 'setControlValue'>; actions: Partial<ExploreActions> & Pick<ExploreActions, 'setControlValue'>;
// we use this props control force update when this panel resize // we use this props control force update when this panel resize
shouldForceUpdate?: number; width: number;
formData?: QueryFormData; 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` const DatasourceContainer = styled.div`
${({ theme }) => css` ${({ theme }) => css`
background-color: ${theme.colors.grayscale.light5}; background-color: ${theme.colors.grayscale.light5};
@ -104,8 +86,9 @@ const DatasourceContainer = styled.div`
height: auto; height: auto;
} }
.field-selections { .field-selections {
padding: 0 0 ${4 * theme.gridUnit}px; padding: 0 0 ${theme.gridUnit}px;
overflow: auto; overflow: auto;
height: 100%;
} }
.field-length { .field-length {
margin-bottom: ${theme.gridUnit * 2}px; 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` const StyledInfoboxWrapper = styled.div`
${({ theme }) => css` ${({ theme }) => css`
margin: 0 ${theme.gridUnit * 2.5}px; margin: 0 ${theme.gridUnit * 2.5}px;
@ -187,27 +120,14 @@ const StyledInfoboxWrapper = styled.div`
`} `}
`; `;
const LabelContainer = (props: { const BORDER_WIDTH = 2;
children: React.ReactElement;
className: string;
}) => {
const labelRef = useRef<HTMLDivElement>(null);
const extendedProps = {
labelRef,
};
return (
<LabelWrapper className={props.className}>
{React.cloneElement(props.children, extendedProps)}
</LabelWrapper>
);
};
export default function DataSourcePanel({ export default function DataSourcePanel({
datasource, datasource,
formData, formData,
controls: { datasource: datasourceControl }, controls: { datasource: datasourceControl },
actions, actions,
shouldForceUpdate, width,
}: Props) { }: Props) {
const { columns: _columns, metrics } = datasource; const { columns: _columns, metrics } = datasource;
// display temporal column first // display temporal column first
@ -233,9 +153,8 @@ export default function DataSourcePanel({
}); });
const [showAllMetrics, setShowAllMetrics] = useState(false); const [showAllMetrics, setShowAllMetrics] = useState(false);
const [showAllColumns, setShowAllColumns] = useState(false); const [showAllColumns, setShowAllColumns] = useState(false);
const [collapseMetrics, setCollapseMetrics] = useState(false);
const DEFAULT_MAX_COLUMNS_LENGTH = 50; const [collapseColumns, setCollapseColumns] = useState(false);
const DEFAULT_MAX_METRICS_LENGTH = 50;
const search = useMemo( const search = useMemo(
() => () =>
@ -385,78 +304,40 @@ export default function DataSourcePanel({
/> />
</StyledInfoboxWrapper> </StyledInfoboxWrapper>
)} )}
<Collapse <AutoSizer>
defaultActiveKey={['metrics', 'column']} {({ height }) => (
expandIconPosition="right" <List
ghost width={width - BORDER_WIDTH}
> height={height}
{metrics?.length && ( itemSize={ITEM_HEIGHT}
<Collapse.Panel itemCount={
header={<SectionHeader>{t('Metrics')}</SectionHeader>} (collapseMetrics ? 0 : metricSlice?.length) +
key="metrics" (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"> {DatasourcePanelItem}
{t( </List>
`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>
)} )}
<Collapse.Panel </AutoSizer>
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>
</div> </div>
</> </>
), ),
@ -470,8 +351,10 @@ export default function DataSourcePanel({
search, search,
showAllColumns, showAllColumns,
showAllMetrics, showAllMetrics,
collapseMetrics,
collapseColumns,
datasourceIsSaveable, datasourceIsSaveable,
shouldForceUpdate, width,
], ],
); );

View File

@ -229,6 +229,20 @@ const updateHistory = debounce(
1000, 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) { function ExploreViewContainer(props) {
const dynamicPluginContext = usePluginContext(); const dynamicPluginContext = usePluginContext();
const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType]; const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType];
@ -243,16 +257,13 @@ function ExploreViewContainer(props) {
); );
const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false);
const [shouldForceUpdate, setShouldForceUpdate] = useState(-1); const [width, setWidth] = useState(
getSidebarWidths(LocalStorageKeys.DatasourceWidth),
);
const tabId = useTabId(); const tabId = useTabId();
const theme = useTheme(); const theme = useTheme();
const defaultSidebarsWidth = {
controls_width: 320,
datasource_width: 300,
};
const addHistory = useCallback( const addHistory = useCallback(
async ({ isReplace = false, title } = {}) => { async ({ isReplace = false, title } = {}) => {
const formData = props.dashboardId 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) { if (props.standalone) {
return renderChartContainer(); return renderChartContainer();
} }
@ -593,7 +595,7 @@ function ExploreViewContainer(props) {
/> />
<Resizable <Resizable
onResizeStop={(evt, direction, ref, d) => { onResizeStop={(evt, direction, ref, d) => {
setShouldForceUpdate(d?.width); setWidth(ref.getBoundingClientRect().width);
setSidebarWidths(LocalStorageKeys.DatasourceWidth, d); setSidebarWidths(LocalStorageKeys.DatasourceWidth, d);
}} }}
defaultSize={{ defaultSize={{
@ -627,7 +629,7 @@ function ExploreViewContainer(props) {
datasource={props.datasource} datasource={props.datasource}
controls={props.controls} controls={props.controls}
actions={props.actions} actions={props.actions}
shouldForceUpdate={shouldForceUpdate} width={width}
user={props.user} user={props.user}
/> />
</Resizable> </Resizable>