| /** |
| * 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 PropTypes from 'prop-types'; |
| import React from 'react'; |
| import { Alert } from 'react-bootstrap'; |
| import { styled, logging } from '@superset-ui/core'; |
| |
| import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; |
| import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger/LogUtils'; |
| import Loading from '../components/Loading'; |
| import RefreshChartOverlay from '../components/RefreshChartOverlay'; |
| import ErrorMessageWithStackTrace from '../components/ErrorMessage/ErrorMessageWithStackTrace'; |
| import ErrorBoundary from '../components/ErrorBoundary'; |
| import ChartRenderer from './ChartRenderer'; |
| |
| const propTypes = { |
| annotationData: PropTypes.object, |
| actions: PropTypes.object, |
| chartId: PropTypes.number.isRequired, |
| datasource: PropTypes.object.isRequired, |
| // current chart is included by dashboard |
| dashboardId: PropTypes.number, |
| // original selected values for FilterBox viz |
| // so that FilterBox can pre-populate selected values |
| // only affect UI control |
| initialValues: PropTypes.object, |
| // formData contains chart's own filter parameter |
| // and merged with extra filter that current dashboard applying |
| formData: PropTypes.object.isRequired, |
| height: PropTypes.number, |
| width: PropTypes.number, |
| setControlValue: PropTypes.func, |
| timeout: PropTypes.number, |
| vizType: PropTypes.string.isRequired, |
| triggerRender: PropTypes.bool, |
| owners: PropTypes.arrayOf( |
| PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
| ), |
| // state |
| chartAlert: PropTypes.string, |
| chartStatus: PropTypes.string, |
| chartStackTrace: PropTypes.string, |
| queriesResponse: PropTypes.arrayOf(PropTypes.object), |
| triggerQuery: PropTypes.bool, |
| refreshOverlayVisible: PropTypes.bool, |
| errorMessage: PropTypes.node, |
| // dashboard callbacks |
| addFilter: PropTypes.func, |
| onQuery: PropTypes.func, |
| onFilterMenuOpen: PropTypes.func, |
| onFilterMenuClose: PropTypes.func, |
| }; |
| |
| const BLANK = {}; |
| |
| const defaultProps = { |
| addFilter: () => BLANK, |
| onFilterMenuOpen: () => BLANK, |
| onFilterMenuClose: () => BLANK, |
| initialValues: BLANK, |
| setControlValue() {}, |
| triggerRender: false, |
| dashboardId: null, |
| chartStackTrace: null, |
| }; |
| |
| const Styles = styled.div` |
| min-height: ${p => p.height}px; |
| position: relative; |
| |
| .chart-tooltip { |
| opacity: 0.75; |
| font-size: ${({ theme }) => theme.typography.sizes.s}px; |
| } |
| `; |
| |
| class Chart extends React.PureComponent { |
| constructor(props) { |
| super(props); |
| this.handleRenderContainerFailure = this.handleRenderContainerFailure.bind( |
| this, |
| ); |
| } |
| |
| componentDidMount() { |
| if (this.props.triggerQuery) { |
| this.runQuery(); |
| } |
| } |
| |
| componentDidUpdate() { |
| if (this.props.triggerQuery) { |
| this.runQuery(); |
| } |
| } |
| |
| runQuery() { |
| if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) { |
| // Load saved chart with a GET request |
| this.props.actions.getSavedChart( |
| this.props.formData, |
| false, |
| this.props.timeout, |
| this.props.chartId, |
| this.props.dashboardId, |
| ); |
| } else { |
| // Create chart with POST request |
| this.props.actions.postChartFormData( |
| this.props.formData, |
| false, |
| this.props.timeout, |
| this.props.chartId, |
| this.props.dashboardId, |
| ); |
| } |
| } |
| |
| handleRenderContainerFailure(error, info) { |
| const { actions, chartId } = this.props; |
| logging.warn(error); |
| actions.chartRenderingFailed( |
| error.toString(), |
| chartId, |
| info ? info.componentStack : null, |
| ); |
| |
| actions.logEvent(LOG_ACTIONS_RENDER_CHART, { |
| slice_id: chartId, |
| has_err: true, |
| error_details: error.toString(), |
| start_offset: this.renderStartTime, |
| ts: new Date().getTime(), |
| duration: Logger.getTimestamp() - this.renderStartTime, |
| }); |
| } |
| |
| renderErrorMessage(queryResponse) { |
| const { chartAlert, chartStackTrace, dashboardId, owners } = this.props; |
| |
| const error = queryResponse?.errors?.[0]; |
| if (error) { |
| const extra = error.extra || {}; |
| extra.owners = owners; |
| error.extra = extra; |
| } |
| const message = chartAlert || queryResponse?.message; |
| return ( |
| <ErrorMessageWithStackTrace |
| error={error} |
| subtitle={message} |
| copyText={message} |
| link={queryResponse ? queryResponse.link : null} |
| source={dashboardId ? 'dashboard' : 'explore'} |
| stackTrace={chartStackTrace} |
| /> |
| ); |
| } |
| |
| render() { |
| const { |
| width, |
| height, |
| chartAlert, |
| chartStatus, |
| errorMessage, |
| onQuery, |
| refreshOverlayVisible, |
| queriesResponse = [], |
| } = this.props; |
| |
| const isLoading = chartStatus === 'loading'; |
| const isFaded = refreshOverlayVisible && !errorMessage; |
| this.renderContainerStartTime = Logger.getTimestamp(); |
| if (chartStatus === 'failed') { |
| return queriesResponse.map(item => this.renderErrorMessage(item)); |
| } |
| if (errorMessage) { |
| return ( |
| <Alert data-test="alert-warning" bsStyle="warning"> |
| {errorMessage} |
| </Alert> |
| ); |
| } |
| |
| return ( |
| <ErrorBoundary |
| onError={this.handleRenderContainerFailure} |
| showMessage={false} |
| > |
| <Styles |
| className="chart-container" |
| data-test="chart-container" |
| height={height} |
| > |
| <div |
| className={`slice_container ${isFaded ? ' faded' : ''}`} |
| data-test="slice-container" |
| > |
| <ChartRenderer {...this.props} data-test={this.props.vizType} /> |
| </div> |
| |
| {!isLoading && !chartAlert && isFaded && ( |
| <RefreshChartOverlay |
| width={width} |
| height={height} |
| onQuery={onQuery} |
| /> |
| )} |
| |
| {isLoading && <Loading />} |
| </Styles> |
| </ErrorBoundary> |
| ); |
| } |
| } |
| |
| Chart.propTypes = propTypes; |
| Chart.defaultProps = defaultProps; |
| |
| export default Chart; |