import React from 'react'; import PropTypes from 'prop-types'; import AlertsWrapper from '../../components/AlertsWrapper'; import GridLayout from './GridLayout'; import Header from './Header'; import { exportChart } from '../../explore/exploreUtils'; import { areObjectsEqual } from '../../reduxUtils'; import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD, LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger'; import { t } from '../../locales'; import '../../../stylesheets/dashboard.css'; const propTypes = { actions: PropTypes.object, initMessages: PropTypes.array, dashboard: PropTypes.object.isRequired, slices: PropTypes.object, datasources: PropTypes.object, filters: PropTypes.object, refresh: PropTypes.bool, timeout: PropTypes.number, userId: PropTypes.string, isStarred: PropTypes.bool, editMode: PropTypes.bool, impressionId: PropTypes.string, }; const defaultProps = { initMessages: [], dashboard: {}, slices: {}, datasources: {}, filters: {}, refresh: false, timeout: 60, userId: '', isStarred: false, editMode: false, }; class Dashboard extends React.PureComponent { constructor(props) { super(props); this.refreshTimer = null; this.firstLoad = true; this.loadingLog = new ActionLog({ impressionId: props.impressionId, actionType: LOG_ACTIONS_PAGE_LOAD, source: 'dashboard', sourceId: props.dashboard.id, eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT], }); Logger.start(this.loadingLog); // alert for unsaved changes this.state = { unsavedChanges: false }; this.rerenderCharts = this.rerenderCharts.bind(this); this.updateDashboardTitle = this.updateDashboardTitle.bind(this); this.onSave = this.onSave.bind(this); this.onChange = this.onChange.bind(this); this.serialize = this.serialize.bind(this); this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices()); this.startPeriodicRender = this.startPeriodicRender.bind(this); this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this); this.fetchSlice = this.fetchSlice.bind(this); this.getFormDataExtra = this.getFormDataExtra.bind(this); this.exploreChart = this.exploreChart.bind(this); this.exportCSV = this.exportCSV.bind(this); this.props.actions.fetchFaveStar = this.props.actions.fetchFaveStar.bind(this); this.props.actions.saveFaveStar = this.props.actions.saveFaveStar.bind(this); this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this); this.props.actions.removeSlice = this.props.actions.removeSlice.bind(this); this.props.actions.removeChart = this.props.actions.removeChart.bind(this); this.props.actions.updateDashboardLayout = this.props.actions.updateDashboardLayout.bind(this); this.props.actions.toggleExpandSlice = this.props.actions.toggleExpandSlice.bind(this); this.props.actions.addFilter = this.props.actions.addFilter.bind(this); this.props.actions.clearFilter = this.props.actions.clearFilter.bind(this); this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this); } componentDidMount() { window.addEventListener('resize', this.rerenderCharts); } componentWillReceiveProps(nextProps) { if (this.firstLoad && Object.values(nextProps.slices) .every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1)) ) { Logger.end(this.loadingLog); this.firstLoad = false; } } componentDidUpdate(prevProps) { if (this.props.refresh) { let changedFilterKey; const prevFiltersKeySet = new Set(Object.keys(prevProps.filters)); Object.keys(this.props.filters).some((key) => { prevFiltersKeySet.delete(key); if (prevProps.filters[key] === undefined || !areObjectsEqual(prevProps.filters[key], this.props.filters[key])) { changedFilterKey = key; return true; } return false; }); // has changed filter or removed a filter? if (!!changedFilterKey || prevFiltersKeySet.size) { this.refreshExcept(changedFilterKey); } } } componentWillUnmount() { window.removeEventListener('resize', this.rerenderCharts); } onBeforeUnload(hasChanged) { if (hasChanged) { window.addEventListener('beforeunload', this.unload); } else { window.removeEventListener('beforeunload', this.unload); } } onChange() { this.onBeforeUnload(true); this.setState({ unsavedChanges: true }); } onSave() { this.onBeforeUnload(false); this.setState({ unsavedChanges: false }); } // return charts in array getAllSlices() { return Object.values(this.props.slices); } getFormDataExtra(slice) { const formDataExtra = Object.assign({}, slice.formData); const extraFilters = this.effectiveExtraFilters(slice.slice_id); formDataExtra.extra_filters = formDataExtra.filters.concat(extraFilters); return formDataExtra; } getFilters(sliceId) { return this.props.filters[sliceId]; } unload() { const message = t('You have unsaved changes.'); window.event.returnValue = message; // Gecko + IE return message; // Gecko + Webkit, Safari, Chrome etc. } effectiveExtraFilters(sliceId) { const metadata = this.props.dashboard.metadata; const filters = this.props.filters; const f = []; const immuneSlices = metadata.filter_immune_slices || []; if (sliceId && immuneSlices.includes(sliceId)) { // The slice is immune to dashboard filters return f; } // Building a list of fields the slice is immune to filters on let immuneToFields = []; if ( sliceId && metadata.filter_immune_slice_fields && metadata.filter_immune_slice_fields[sliceId]) { immuneToFields = metadata.filter_immune_slice_fields[sliceId]; } for (const filteringSliceId in filters) { if (filteringSliceId === sliceId.toString()) { // Filters applied by the slice don't apply to itself continue; } for (const field in filters[filteringSliceId]) { if (!immuneToFields.includes(field)) { f.push({ col: field, op: 'in', val: filters[filteringSliceId][field], }); } } } return f; } refreshExcept(filterKey) { const immune = this.props.dashboard.metadata.filter_immune_slices || []; let slices = this.getAllSlices(); if (filterKey) { slices = slices.filter(slice => ( String(slice.slice_id) !== filterKey && immune.indexOf(slice.slice_id) === -1 )); } this.fetchSlices(slices); } stopPeriodicRender() { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } } startPeriodicRender(interval) { this.stopPeriodicRender(); const immune = this.props.dashboard.metadata.timed_refresh_immune_slices || []; const refreshAll = () => { const affectedSlices = this.getAllSlices() .filter(slice => immune.indexOf(slice.slice_id) === -1); this.fetchSlices(affectedSlices, true, interval * 0.2); }; const fetchAndRender = () => { refreshAll(); if (interval > 0) { this.refreshTimer = setTimeout(fetchAndRender, interval); } }; fetchAndRender(); } updateDashboardTitle(title) { this.props.actions.updateDashboardTitle(title); this.onChange(); } serialize() { return this.props.dashboard.layout.map(reactPos => ({ slice_id: reactPos.i, col: reactPos.x + 1, row: reactPos.y, size_x: reactPos.w, size_y: reactPos.h, })); } addSlicesToDashboard(sliceIds) { return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds); } fetchSlice(slice, force = false) { return this.props.actions.runQuery( this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey, ); } // fetch and render an list of slices fetchSlices(slc, force = false, interval = 0) { const slices = slc || this.getAllSlices(); if (!interval) { slices.forEach((slice) => { this.fetchSlice(slice, force); }); return; } const meta = this.props.dashboard.metadata; const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds if (typeof meta.stagger_refresh !== 'boolean') { meta.stagger_refresh = meta.stagger_refresh === undefined ? true : meta.stagger_refresh === 'true'; } const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0; slices.forEach((slice, i) => { setTimeout(() => { this.fetchSlice(slice, force); }, delay * i); }); } exploreChart(slice) { const formData = this.getFormDataExtra(slice); exportChart(formData); } exportCSV(slice) { const formData = this.getFormDataExtra(slice); exportChart(formData, 'csv'); } // re-render chart without fetch rerenderCharts() { this.getAllSlices().forEach((slice) => { setTimeout(() => { this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey); }, 50); }); } render() { return (
); } } Dashboard.propTypes = propTypes; Dashboard.defaultProps = defaultProps; export default Dashboard;