mirror of https://github.com/apache/superset.git
perf(explore): virtualized datasource field sections (#27625)
(cherry picked from commit 38eecfc5d4
)
This commit is contained in:
parent
a024b4ac1b
commit
2fa1b35c16
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
|
@ -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;
|
|
@ -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}
|
||||||
|
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}
|
||||||
>
|
>
|
||||||
{metrics?.length && (
|
{DatasourcePanelItem}
|
||||||
<Collapse.Panel
|
</List>
|
||||||
header={<SectionHeader>{t('Metrics')}</SectionHeader>}
|
|
||||||
key="metrics"
|
|
||||||
>
|
|
||||||
<div className="field-length">
|
|
||||||
{t(
|
|
||||||
`Showing %s of %s`,
|
|
||||||
metricSlice?.length,
|
|
||||||
lists?.metrics.length,
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</AutoSizer>
|
||||||
{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
|
|
||||||
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,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue