mirror of https://github.com/apache/superset.git
perf(dashboard): Virtualization POC (#21438)
This commit is contained in:
parent
070b865e32
commit
406e44bba1
|
@ -31,6 +31,7 @@ export enum FeatureFlag {
|
||||||
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
|
DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL',
|
||||||
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
|
DASHBOARD_NATIVE_FILTERS = 'DASHBOARD_NATIVE_FILTERS',
|
||||||
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
|
DASHBOARD_NATIVE_FILTERS_SET = 'DASHBOARD_NATIVE_FILTERS_SET',
|
||||||
|
DASHBOARD_VIRTUALIZATION = 'DASHBOARD_VIRTUALIZATION',
|
||||||
DASHBOARD_RBAC = 'DASHBOARD_RBAC',
|
DASHBOARD_RBAC = 'DASHBOARD_RBAC',
|
||||||
DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
DATAPANEL_CLOSED_BY_DEFAULT = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
||||||
DISABLE_DATASET_SOURCE_EDIT = 'DISABLE_DATASET_SOURCE_EDIT',
|
DISABLE_DATASET_SOURCE_EDIT = 'DISABLE_DATASET_SOURCE_EDIT',
|
||||||
|
|
|
@ -28,6 +28,7 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
|
||||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
|
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
|
import { isCurrentUserBot } from 'src/utils/isBot';
|
||||||
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
||||||
import ChartRenderer from './ChartRenderer';
|
import ChartRenderer from './ChartRenderer';
|
||||||
import { ChartErrorMessage } from './ChartErrorMessage';
|
import { ChartErrorMessage } from './ChartErrorMessage';
|
||||||
|
@ -74,6 +75,7 @@ const propTypes = {
|
||||||
ownState: PropTypes.object,
|
ownState: PropTypes.object,
|
||||||
postTransformProps: PropTypes.func,
|
postTransformProps: PropTypes.func,
|
||||||
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
||||||
|
isInView: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BLANK = {};
|
const BLANK = {};
|
||||||
|
@ -92,6 +94,7 @@ const defaultProps = {
|
||||||
chartStackTrace: null,
|
chartStackTrace: null,
|
||||||
isDeactivatedViz: false,
|
isDeactivatedViz: false,
|
||||||
force: false,
|
force: false,
|
||||||
|
isInView: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Styles = styled.div`
|
const Styles = styled.div`
|
||||||
|
@ -307,11 +310,17 @@ class Chart extends React.PureComponent {
|
||||||
width={width}
|
width={width}
|
||||||
>
|
>
|
||||||
<div className="slice_container" data-test="slice-container">
|
<div className="slice_container" data-test="slice-container">
|
||||||
<ChartRenderer
|
{this.props.isInView ||
|
||||||
{...this.props}
|
!isFeatureEnabled(FeatureFlag.DASHBOARD_VIRTUALIZATION) ||
|
||||||
source={this.props.dashboardId ? 'dashboard' : 'explore'}
|
isCurrentUserBot() ? (
|
||||||
data-test={this.props.vizType}
|
<ChartRenderer
|
||||||
/>
|
{...this.props}
|
||||||
|
source={this.props.dashboardId ? 'dashboard' : 'explore'}
|
||||||
|
data-test={this.props.vizType}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loading />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isLoading && !isDeactivatedViz && <Loading />}
|
{isLoading && !isDeactivatedViz && <Loading />}
|
||||||
</Styles>
|
</Styles>
|
||||||
|
|
|
@ -92,6 +92,7 @@ const propTypes = {
|
||||||
filterState: PropTypes.object,
|
filterState: PropTypes.object,
|
||||||
postTransformProps: PropTypes.func,
|
postTransformProps: PropTypes.func,
|
||||||
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
||||||
|
isInView: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -382,6 +383,7 @@ class Chart extends React.Component {
|
||||||
filterboxMigrationState,
|
filterboxMigrationState,
|
||||||
postTransformProps,
|
postTransformProps,
|
||||||
datasetsStatus,
|
datasetsStatus,
|
||||||
|
isInView,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { width } = this.state;
|
const { width } = this.state;
|
||||||
|
@ -511,6 +513,7 @@ class Chart extends React.Component {
|
||||||
filterboxMigrationState={filterboxMigrationState}
|
filterboxMigrationState={filterboxMigrationState}
|
||||||
postTransformProps={postTransformProps}
|
postTransformProps={postTransformProps}
|
||||||
datasetsStatus={datasetsStatus}
|
datasetsStatus={datasetsStatus}
|
||||||
|
isInView={isInView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SliceContainer>
|
</SliceContainer>
|
||||||
|
|
|
@ -137,7 +137,7 @@ describe('ChartHolder', () => {
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ChartHolder {...defaultProps} editMode />
|
<ChartHolder {...defaultProps} editMode isInView />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -414,6 +414,7 @@ describe('ChartHolder', () => {
|
||||||
deleteComponent={deleteComponent}
|
deleteComponent={deleteComponent}
|
||||||
fullSizeChartId={null}
|
fullSizeChartId={null}
|
||||||
editMode
|
editMode
|
||||||
|
isInView
|
||||||
/>
|
/>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -67,6 +67,8 @@ interface ChartHolderProps {
|
||||||
handleComponentDrop: (...args: unknown[]) => unknown;
|
handleComponentDrop: (...args: unknown[]) => unknown;
|
||||||
setFullSizeChartId: (chartId: number | null) => void;
|
setFullSizeChartId: (chartId: number | null) => void;
|
||||||
postAddSliceFromDashboard?: () => void;
|
postAddSliceFromDashboard?: () => void;
|
||||||
|
|
||||||
|
isInView: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartHolder: React.FC<ChartHolderProps> = ({
|
const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||||
|
@ -91,6 +93,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||||
handleComponentDrop,
|
handleComponentDrop,
|
||||||
setFullSizeChartId,
|
setFullSizeChartId,
|
||||||
postAddSliceFromDashboard,
|
postAddSliceFromDashboard,
|
||||||
|
isInView,
|
||||||
}) => {
|
}) => {
|
||||||
const { chartId } = component.meta;
|
const { chartId } = component.meta;
|
||||||
const isFullSize = fullSizeChartId === chartId;
|
const isFullSize = fullSizeChartId === chartId;
|
||||||
|
@ -314,6 +317,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||||
setControlValue={handleExtraControl}
|
setControlValue={handleExtraControl}
|
||||||
extraControls={extraControls}
|
extraControls={extraControls}
|
||||||
postTransformProps={handlePostTransformProps}
|
postTransformProps={handlePostTransformProps}
|
||||||
|
isInView={isInView}
|
||||||
/>
|
/>
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<HoverMenu position="top">
|
<HoverMenu position="top">
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||||
|
|
||||||
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
||||||
import DragHandle from 'src/dashboard/components/dnd/DragHandle';
|
import DragHandle from 'src/dashboard/components/dnd/DragHandle';
|
||||||
|
@ -32,6 +33,7 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
|
||||||
import { componentShape } from 'src/dashboard/util/propShapes';
|
import { componentShape } from 'src/dashboard/util/propShapes';
|
||||||
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
|
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
|
||||||
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
|
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
|
||||||
|
import { isCurrentUserBot } from 'src/utils/isBot';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
@ -61,6 +63,7 @@ class Row extends React.PureComponent {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
|
isInView: false,
|
||||||
};
|
};
|
||||||
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
|
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
|
||||||
this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
|
this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
|
||||||
|
@ -69,6 +72,50 @@ class Row extends React.PureComponent {
|
||||||
'background',
|
'background',
|
||||||
);
|
);
|
||||||
this.handleChangeFocus = this.handleChangeFocus.bind(this);
|
this.handleChangeFocus = this.handleChangeFocus.bind(this);
|
||||||
|
|
||||||
|
this.containerRef = React.createRef();
|
||||||
|
this.observerEnabler = null;
|
||||||
|
this.observerDisabler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if chart not rendered - render it if it's less than 1 view height away from current viewport
|
||||||
|
// if chart rendered - remove it if it's more than 4 view heights away from current viewport
|
||||||
|
componentDidMount() {
|
||||||
|
if (
|
||||||
|
isFeatureEnabled(FeatureFlag.DASHBOARD_VIRTUALIZATION) &&
|
||||||
|
!isCurrentUserBot()
|
||||||
|
) {
|
||||||
|
this.observerEnabler = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && !this.state.isInView) {
|
||||||
|
this.setState({ isInView: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: '100% 0px',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.observerDisabler = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (!entry.isIntersecting && this.state.isInView) {
|
||||||
|
this.setState({ isInView: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: '400% 0px',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const element = this.containerRef.current;
|
||||||
|
if (element) {
|
||||||
|
this.observerEnabler.observe(element);
|
||||||
|
this.observerDisabler.observe(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.observerEnabler?.disconnect();
|
||||||
|
this.observerDisabler?.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeFocus(nextFocus) {
|
handleChangeFocus(nextFocus) {
|
||||||
|
@ -161,6 +208,7 @@ class Row extends React.PureComponent {
|
||||||
backgroundStyle.className,
|
backgroundStyle.className,
|
||||||
)}
|
)}
|
||||||
data-test={`grid-row-${backgroundStyle.className}`}
|
data-test={`grid-row-${backgroundStyle.className}`}
|
||||||
|
ref={this.containerRef}
|
||||||
>
|
>
|
||||||
{rowItems.map((componentId, itemIndex) => (
|
{rowItems.map((componentId, itemIndex) => (
|
||||||
<DashboardComponent
|
<DashboardComponent
|
||||||
|
@ -178,6 +226,7 @@ class Row extends React.PureComponent {
|
||||||
onResizeStop={onResizeStop}
|
onResizeStop={onResizeStop}
|
||||||
isComponentVisible={isComponentVisible}
|
isComponentVisible={isComponentVisible}
|
||||||
onChangeTab={onChangeTab}
|
onChangeTab={onChangeTab}
|
||||||
|
isInView={this.state.isInView}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// navigator.webdriver is true when browser is controlled by a bot
|
||||||
|
export const isCurrentUserBot = () => window?.navigator?.webdriver;
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum DASHBOARD_VIRTUALIZATION_MODE {
|
||||||
|
NONE = 'NONE',
|
||||||
|
VIEWPORT = 'VIEWPORT',
|
||||||
|
PAGINATED = 'PAGINATED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDashboardVirtualizationEnabled = (
|
||||||
|
virtualizationMode: DASHBOARD_VIRTUALIZATION_MODE,
|
||||||
|
) =>
|
||||||
|
virtualizationMode === DASHBOARD_VIRTUALIZATION_MODE.VIEWPORT ||
|
||||||
|
virtualizationMode === DASHBOARD_VIRTUALIZATION_MODE.PAGINATED;
|
|
@ -425,6 +425,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
|
||||||
# Feature is under active development and breaking changes are expected
|
# Feature is under active development and breaking changes are expected
|
||||||
"DASHBOARD_NATIVE_FILTERS_SET": False,
|
"DASHBOARD_NATIVE_FILTERS_SET": False,
|
||||||
"DASHBOARD_FILTERS_EXPERIMENTAL": False,
|
"DASHBOARD_FILTERS_EXPERIMENTAL": False,
|
||||||
|
"DASHBOARD_VIRTUALIZATION": False,
|
||||||
"GLOBAL_ASYNC_QUERIES": False,
|
"GLOBAL_ASYNC_QUERIES": False,
|
||||||
"VERSIONED_EXPORT": True,
|
"VERSIONED_EXPORT": True,
|
||||||
"EMBEDDED_SUPERSET": False,
|
"EMBEDDED_SUPERSET": False,
|
||||||
|
@ -761,7 +762,6 @@ SQLLAB_SCHEDULE_WARNING_MESSAGE = None
|
||||||
# Force refresh while auto-refresh in dashboard
|
# Force refresh while auto-refresh in dashboard
|
||||||
DASHBOARD_AUTO_REFRESH_MODE: Literal["fetch", "force"] = "force"
|
DASHBOARD_AUTO_REFRESH_MODE: Literal["fetch", "force"] = "force"
|
||||||
|
|
||||||
|
|
||||||
# Default celery config is to use SQLA as a broker, in a production setting
|
# Default celery config is to use SQLA as a broker, in a production setting
|
||||||
# you'll want to use a proper broker as specified here:
|
# you'll want to use a proper broker as specified here:
|
||||||
# http://docs.celeryproject.org/en/latest/getting-started/brokers/index.html
|
# http://docs.celeryproject.org/en/latest/getting-started/brokers/index.html
|
||||||
|
|
|
@ -103,6 +103,7 @@ FRONTEND_CONF_KEYS = (
|
||||||
"SQLALCHEMY_DISPLAY_TEXT",
|
"SQLALCHEMY_DISPLAY_TEXT",
|
||||||
"GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL",
|
"GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL",
|
||||||
"DASHBOARD_AUTO_REFRESH_MODE",
|
"DASHBOARD_AUTO_REFRESH_MODE",
|
||||||
|
"DASHBOARD_VIRTUALIZATION",
|
||||||
"SCHEDULED_QUERIES",
|
"SCHEDULED_QUERIES",
|
||||||
"EXCEL_EXTENSIONS",
|
"EXCEL_EXTENSIONS",
|
||||||
"CSV_EXTENSIONS",
|
"CSV_EXTENSIONS",
|
||||||
|
|
Loading…
Reference in New Issue