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-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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
* 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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue