blob: 859b5607529fffca6f17e16d8f0f6b19a88ba046 [file] [log] [blame]
/**
* 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 PropTypes from 'prop-types';
import { t } from '@superset-ui/core';
import { PluginContext } from 'src/components/DynamicPlugins';
import Loading from 'src/components/Loading';
import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId';
import DashboardBuilder from '../containers/DashboardBuilder';
import {
chartPropShape,
slicePropShape,
dashboardInfoPropShape,
dashboardStatePropShape,
} from '../util/propShapes';
import {
LOG_ACTIONS_HIDE_BROWSER_TAB,
LOG_ACTIONS_MOUNT_DASHBOARD,
Logger,
} from '../../logger/LogUtils';
import OmniContainer from '../../components/OmniContainer';
import { areObjectsEqual } from '../../reduxUtils';
import '../stylesheets/index.less';
import getLocationHash from '../util/getLocationHash';
import isDashboardEmpty from '../util/isDashboardEmpty';
import { getAffectedOwnDataCharts } from '../util/charts/getOwnDataCharts';
const propTypes = {
actions: PropTypes.shape({
addSliceToDashboard: PropTypes.func.isRequired,
removeSliceFromDashboard: PropTypes.func.isRequired,
triggerQuery: PropTypes.func.isRequired,
logEvent: PropTypes.func.isRequired,
}).isRequired,
dashboardInfo: dashboardInfoPropShape.isRequired,
dashboardState: dashboardStatePropShape.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
slices: PropTypes.objectOf(slicePropShape).isRequired,
activeFilters: PropTypes.object.isRequired,
datasources: PropTypes.object.isRequired,
ownDataCharts: PropTypes.object.isRequired,
layout: PropTypes.object.isRequired,
impressionId: PropTypes.string.isRequired,
initMessages: PropTypes.array,
timeout: PropTypes.number,
userId: PropTypes.string,
};
const defaultProps = {
initMessages: [],
timeout: 60,
userId: '',
};
class Dashboard extends React.PureComponent {
static contextType = PluginContext;
static onBeforeUnload(hasChanged) {
if (hasChanged) {
window.addEventListener('beforeunload', Dashboard.unload);
} else {
window.removeEventListener('beforeunload', Dashboard.unload);
}
}
static unload() {
const message = t('You have unsaved changes.');
window.event.returnValue = message; // Gecko + IE
return message; // Gecko + Webkit, Safari, Chrome etc.
}
constructor(props) {
super(props);
this.appliedFilters = props.activeFilters ?? {};
this.appliedOwnDataCharts = props.ownDataCharts ?? {};
this.onVisibilityChange = this.onVisibilityChange.bind(this);
}
componentDidMount() {
const appContainer = document.getElementById('app');
const bootstrapData = appContainer?.getAttribute('data-bootstrap') || '';
const { dashboardState, layout } = this.props;
const eventData = {
is_edit_mode: dashboardState.editMode,
mount_duration: Logger.getTimestamp(),
is_empty: isDashboardEmpty(layout),
is_published: dashboardState.isPublished,
bootstrap_data_length: bootstrapData.length,
};
const directLinkComponentId = getLocationHash();
if (directLinkComponentId) {
eventData.target_id = directLinkComponentId;
}
this.props.actions.logEvent(LOG_ACTIONS_MOUNT_DASHBOARD, eventData);
// Handle browser tab visibility change
if (document.visibilityState === 'hidden') {
this.visibilityEventData = {
start_offset: Logger.getTimestamp(),
ts: new Date().getTime(),
};
}
window.addEventListener('visibilitychange', this.onVisibilityChange);
}
UNSAFE_componentWillReceiveProps(nextProps) {
const currentChartIds = getChartIdsFromLayout(this.props.layout);
const nextChartIds = getChartIdsFromLayout(nextProps.layout);
if (currentChartIds.length < nextChartIds.length) {
const newChartIds = nextChartIds.filter(
key => currentChartIds.indexOf(key) === -1,
);
newChartIds.forEach(newChartId =>
this.props.actions.addSliceToDashboard(
newChartId,
getLayoutComponentFromChartId(nextProps.layout, newChartId),
),
);
} else if (currentChartIds.length > nextChartIds.length) {
// remove chart
const removedChartIds = currentChartIds.filter(
key => nextChartIds.indexOf(key) === -1,
);
removedChartIds.forEach(removedChartId =>
this.props.actions.removeSliceFromDashboard(removedChartId),
);
}
}
componentDidUpdate() {
const { hasUnsavedChanges, editMode } = this.props.dashboardState;
const { appliedFilters, appliedOwnDataCharts } = this;
const { activeFilters, ownDataCharts } = this.props;
if (
!editMode &&
(!areObjectsEqual(appliedOwnDataCharts, ownDataCharts) ||
!areObjectsEqual(appliedFilters, activeFilters))
) {
this.applyFilters();
}
if (hasUnsavedChanges) {
Dashboard.onBeforeUnload(true);
} else {
Dashboard.onBeforeUnload(false);
}
}
componentWillUnmount() {
window.removeEventListener('visibilitychange', this.onVisibilityChange);
}
onVisibilityChange() {
if (document.visibilityState === 'hidden') {
// from visible to hidden
this.visibilityEventData = {
start_offset: Logger.getTimestamp(),
ts: new Date().getTime(),
};
} else if (document.visibilityState === 'visible') {
// from hidden to visible
const logStart = this.visibilityEventData.start_offset;
this.props.actions.logEvent(LOG_ACTIONS_HIDE_BROWSER_TAB, {
...this.visibilityEventData,
duration: Logger.getTimestamp() - logStart,
});
}
}
// return charts in array
getAllCharts() {
return Object.values(this.props.charts);
}
applyFilters() {
const { appliedFilters } = this;
const { activeFilters, ownDataCharts } = this.props;
// refresh charts if a filter was removed, added, or changed
const currFilterKeys = Object.keys(activeFilters);
const appliedFilterKeys = Object.keys(appliedFilters);
const allKeys = new Set(currFilterKeys.concat(appliedFilterKeys));
const affectedChartIds = getAffectedOwnDataCharts(
ownDataCharts,
this.appliedOwnDataCharts,
);
[...allKeys].forEach(filterKey => {
if (!currFilterKeys.includes(filterKey)) {
// filterKey is removed?
affectedChartIds.push(...appliedFilters[filterKey].scope);
} else if (!appliedFilterKeys.includes(filterKey)) {
// filterKey is newly added?
affectedChartIds.push(...activeFilters[filterKey].scope);
} else {
// if filterKey changes value,
// update charts in its scope
if (
!areObjectsEqual(
appliedFilters[filterKey].values,
activeFilters[filterKey].values,
)
) {
affectedChartIds.push(...activeFilters[filterKey].scope);
}
// if filterKey changes scope,
// update all charts in its scope
if (
!areObjectsEqual(
appliedFilters[filterKey].scope,
activeFilters[filterKey].scope,
)
) {
const chartsInScope = (activeFilters[filterKey].scope || []).concat(
appliedFilters[filterKey].scope || [],
);
affectedChartIds.push(...chartsInScope);
}
}
});
// remove dup in affectedChartIds
this.refreshCharts([...new Set(affectedChartIds)]);
this.appliedFilters = activeFilters;
this.appliedOwnDataCharts = ownDataCharts;
}
refreshCharts(ids) {
ids.forEach(id => {
this.props.actions.triggerQuery(true, id);
});
}
render() {
if (this.context.loading) {
return <Loading />;
}
return (
<>
<OmniContainer logEvent={this.props.actions.logEvent} />
<DashboardBuilder />
</>
);
}
}
Dashboard.propTypes = propTypes;
Dashboard.defaultProps = defaultProps;
export default Dashboard;