retire dashboard v1 (js and python) (#5418)
This commit is contained in:
parent
fd2d4b0e58
commit
3f2fc8f413
|
|
@ -102,7 +102,6 @@
|
|||
"react-dnd-html5-backend": "^2.5.4",
|
||||
"react-dom": "^15.6.2",
|
||||
"react-gravatar": "^2.6.1",
|
||||
"react-grid-layout": "0.16.6",
|
||||
"react-map-gl": "^3.0.4",
|
||||
"react-markdown": "^3.3.0",
|
||||
"react-redux": "^5.0.2",
|
||||
|
|
|
|||
|
|
@ -11,5 +11,4 @@ export default {
|
|||
maxUndoHistoryExceeded: false,
|
||||
isStarred: true,
|
||||
css: '',
|
||||
isV2Preview: false, // @TODO remove upon v1 deprecation
|
||||
};
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@ describe('dashboardState reducer', () => {
|
|||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
editMode: false,
|
||||
isV2Preview: false, // @TODO remove upon v1 deprecation
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -86,9 +86,6 @@ class Dashboard extends React.PureComponent {
|
|||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!nextProps.dashboardState.editMode) {
|
||||
const version = nextProps.dashboardState.isV2Preview
|
||||
? 'v2-preview'
|
||||
: 'v2';
|
||||
// log pane loads
|
||||
const loadedPaneIds = [];
|
||||
let minQueryStartTime = Infinity;
|
||||
|
|
@ -107,7 +104,7 @@ class Dashboard extends React.PureComponent {
|
|||
Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, {
|
||||
...restStats,
|
||||
duration: new Date().getTime() - paneMinQueryStart,
|
||||
version,
|
||||
version: 'v2',
|
||||
});
|
||||
|
||||
if (!this.isFirstLoad) {
|
||||
|
|
@ -128,7 +125,7 @@ class Dashboard extends React.PureComponent {
|
|||
Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
|
||||
pane_ids: loadedPaneIds,
|
||||
duration: new Date().getTime() - minQueryStartTime,
|
||||
version,
|
||||
version: 'v2',
|
||||
});
|
||||
Logger.send(this.actionLog);
|
||||
this.isFirstLoad = false;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import EditableTitle from '../../components/EditableTitle';
|
|||
import Button from '../../components/Button';
|
||||
import FaveStar from '../../components/FaveStar';
|
||||
import UndoRedoKeylisteners from './UndoRedoKeylisteners';
|
||||
import V2PreviewModal from '../deprecated/V2PreviewModal';
|
||||
|
||||
import { chartPropShape } from '../util/propShapes';
|
||||
import { t } from '../../locales';
|
||||
|
|
@ -32,7 +31,6 @@ const propTypes = {
|
|||
startPeriodicRender: PropTypes.func.isRequired,
|
||||
updateDashboardTitle: PropTypes.func.isRequired,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
isV2Preview: PropTypes.bool.isRequired,
|
||||
setEditMode: PropTypes.func.isRequired,
|
||||
showBuilderPane: PropTypes.bool.isRequired,
|
||||
toggleBuilderPane: PropTypes.func.isRequired,
|
||||
|
|
@ -60,7 +58,6 @@ class Header extends React.PureComponent {
|
|||
didNotifyMaxUndoHistoryToast: false,
|
||||
emphasizeUndo: false,
|
||||
hightlightRedo: false,
|
||||
showV2PreviewModal: props.isV2Preview,
|
||||
};
|
||||
|
||||
this.handleChangeText = this.handleChangeText.bind(this);
|
||||
|
|
@ -69,7 +66,6 @@ class Header extends React.PureComponent {
|
|||
this.toggleEditMode = this.toggleEditMode.bind(this);
|
||||
this.forceRefresh = this.forceRefresh.bind(this);
|
||||
this.overwriteDashboard = this.overwriteDashboard.bind(this);
|
||||
this.toggleShowV2PreviewModal = this.toggleShowV2PreviewModal.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
|
@ -129,10 +125,6 @@ class Header extends React.PureComponent {
|
|||
this.props.setEditMode(!this.props.editMode);
|
||||
}
|
||||
|
||||
toggleShowV2PreviewModal() {
|
||||
this.setState({ showV2PreviewModal: !this.state.showV2PreviewModal });
|
||||
}
|
||||
|
||||
overwriteDashboard() {
|
||||
const {
|
||||
dashboardTitle,
|
||||
|
|
@ -161,7 +153,6 @@ class Header extends React.PureComponent {
|
|||
filters,
|
||||
expandedSlices,
|
||||
css,
|
||||
isV2Preview,
|
||||
onUndo,
|
||||
onRedo,
|
||||
undoLength,
|
||||
|
|
@ -177,7 +168,7 @@ class Header extends React.PureComponent {
|
|||
|
||||
const userCanEdit = dashboardInfo.dash_edit_perm;
|
||||
const userCanSaveAs = dashboardInfo.dash_save_perm;
|
||||
const popButton = hasUnsavedChanges || isV2Preview;
|
||||
const popButton = hasUnsavedChanges;
|
||||
|
||||
return (
|
||||
<div className="dashboard-header">
|
||||
|
|
@ -196,20 +187,6 @@ class Header extends React.PureComponent {
|
|||
isStarred={this.props.isStarred}
|
||||
/>
|
||||
</span>
|
||||
{isV2Preview && (
|
||||
<div
|
||||
role="none"
|
||||
className="v2-preview-badge"
|
||||
onClick={this.toggleShowV2PreviewModal}
|
||||
>
|
||||
{t('v2 Preview')}
|
||||
<span className="fa fa-info-circle m-l-5" />
|
||||
</div>
|
||||
)}
|
||||
{isV2Preview &&
|
||||
this.state.showV2PreviewModal && (
|
||||
<V2PreviewModal onClose={this.toggleShowV2PreviewModal} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userCanSaveAs && (
|
||||
|
|
@ -245,32 +222,17 @@ class Header extends React.PureComponent {
|
|||
)}
|
||||
|
||||
{editMode &&
|
||||
(hasUnsavedChanges || isV2Preview) && (
|
||||
hasUnsavedChanges && (
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle={popButton ? 'primary' : undefined}
|
||||
onClick={this.overwriteDashboard}
|
||||
>
|
||||
{isV2Preview
|
||||
? t('Persist as Dashboard v2')
|
||||
: t('Save changes')}
|
||||
{t('Save changes')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!editMode &&
|
||||
isV2Preview && (
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.toggleEditMode}
|
||||
bsStyle={popButton ? 'primary' : undefined}
|
||||
disabled={!userCanEdit}
|
||||
>
|
||||
{t('Edit to persist Dashboard v2')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!editMode &&
|
||||
!isV2Preview &&
|
||||
!hasUnsavedChanges && (
|
||||
<Button
|
||||
bsSize="small"
|
||||
|
|
@ -283,7 +245,6 @@ class Header extends React.PureComponent {
|
|||
)}
|
||||
|
||||
{editMode &&
|
||||
!isV2Preview &&
|
||||
!hasUnsavedChanges && (
|
||||
<Button
|
||||
bsSize="small"
|
||||
|
|
@ -312,7 +273,6 @@ class Header extends React.PureComponent {
|
|||
editMode={editMode}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
userCanEdit={userCanEdit}
|
||||
isV2Preview={isV2Preview}
|
||||
/>
|
||||
|
||||
{editMode && (
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const propTypes = {
|
|||
filters: PropTypes.object.isRequired,
|
||||
expandedSlices: PropTypes.object.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
isV2Preview: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {};
|
||||
|
|
@ -83,7 +82,6 @@ class HeaderActionsDropdown extends React.PureComponent {
|
|||
expandedSlices,
|
||||
onSave,
|
||||
userCanEdit,
|
||||
isV2Preview,
|
||||
} = this.props;
|
||||
|
||||
const emailBody = t('Check out this dashboard: %s', window.location.href);
|
||||
|
|
@ -93,7 +91,7 @@ class HeaderActionsDropdown extends React.PureComponent {
|
|||
<DropdownButton
|
||||
title=""
|
||||
id="save-dash-split-button"
|
||||
bsStyle={hasUnsavedChanges || isV2Preview ? 'primary' : undefined}
|
||||
bsStyle={hasUnsavedChanges ? 'primary' : undefined}
|
||||
bsSize="small"
|
||||
pullRight
|
||||
>
|
||||
|
|
@ -111,9 +109,8 @@ class HeaderActionsDropdown extends React.PureComponent {
|
|||
isMenuItem
|
||||
triggerNode={<span>{t('Save as')}</span>}
|
||||
canOverwrite={userCanEdit}
|
||||
isV2Preview={isV2Preview}
|
||||
/>
|
||||
{(isV2Preview || hasUnsavedChanges) && (
|
||||
{hasUnsavedChanges && (
|
||||
<MenuItem
|
||||
eventKey="discard"
|
||||
onSelect={HeaderActionsDropdown.discardChanges}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ const propTypes = {
|
|||
onSave: PropTypes.func.isRequired,
|
||||
isMenuItem: PropTypes.bool,
|
||||
canOverwrite: PropTypes.bool.isRequired,
|
||||
isV2Preview: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
|
@ -104,16 +103,12 @@ class SaveModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { isV2Preview } = this.props;
|
||||
return (
|
||||
<ModalTrigger
|
||||
ref={this.setModalRef}
|
||||
isMenuItem={this.props.isMenuItem}
|
||||
triggerNode={this.props.triggerNode}
|
||||
modalTitle={t(
|
||||
'Save Dashboard%s',
|
||||
isV2Preview ? ' (⚠️ all saved dashboards will be V2)' : '',
|
||||
)}
|
||||
modalTitle={t('Save Dashboard')}
|
||||
modalBody={
|
||||
<FormGroup>
|
||||
<Radio
|
||||
|
|
@ -144,7 +139,7 @@ class SaveModal extends React.PureComponent {
|
|||
checked={this.state.duplicateSlices}
|
||||
onChange={this.toggleDuplicateSlices}
|
||||
/>
|
||||
<span className="m-l-5">also copy (duplicate) charts</span>
|
||||
<span className="m-l-5">{t('also copy (duplicate) charts')}</span>
|
||||
</div>
|
||||
</FormGroup>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ function mapStateToProps({
|
|||
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
|
||||
editMode: !!dashboardState.editMode,
|
||||
showBuilderPane: !!dashboardState.showBuilderPane,
|
||||
isV2Preview: dashboardState.isV2Preview,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, Button } from 'react-bootstrap';
|
||||
import { Logger, LOG_ACTIONS_READ_ABOUT_V2_CHANGES } from '../../logger';
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
v2FeedbackUrl: PropTypes.string,
|
||||
v2AutoConvertDate: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
handleConvertToV2: PropTypes.func.isRequired,
|
||||
forceV2Edit: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
v2FeedbackUrl: null,
|
||||
v2AutoConvertDate: null,
|
||||
};
|
||||
|
||||
function logReadAboutV2Changes() {
|
||||
Logger.append(LOG_ACTIONS_READ_ABOUT_V2_CHANGES, { version: 'v1' }, true);
|
||||
}
|
||||
|
||||
function PromptV2ConversionModal({
|
||||
v2FeedbackUrl,
|
||||
v2AutoConvertDate,
|
||||
onClose,
|
||||
handleConvertToV2,
|
||||
forceV2Edit,
|
||||
}) {
|
||||
const timeUntilAutoConversion = v2AutoConvertDate
|
||||
? `approximately ${moment(v2AutoConvertDate).toNow(
|
||||
true,
|
||||
)} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY)
|
||||
: 'a limited amount of time';
|
||||
|
||||
return (
|
||||
<Modal onHide={onClose} onExit={onClose} animation show>
|
||||
<Modal.Header closeButton>
|
||||
<div style={{ fontSize: 20, fontWeight: 200, margin: '0px 4px -4px' }}>
|
||||
{t('Convert to Dashboard v2 🎉')}
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<h4>{t('Who')}</h4>
|
||||
<p>
|
||||
{t(
|
||||
"As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience.",
|
||||
)}
|
||||
</p>
|
||||
<br />
|
||||
<h4>{t('What and When')}</h4>
|
||||
<p>
|
||||
{t('You have ')}
|
||||
<strong>
|
||||
{timeUntilAutoConversion}
|
||||
{t(' to convert this v1 dashboard to the new v2 format')}
|
||||
</strong>
|
||||
{t(' before it is auto-converted. ')}
|
||||
{forceV2Edit && (
|
||||
<em>
|
||||
{t(
|
||||
'Note that you may only edit dashboards using the v2 experience.',
|
||||
)}
|
||||
</em>
|
||||
)}
|
||||
{t('You may read more about these changes ')}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="http://bit.ly/superset-dash-v2"
|
||||
onClick={logReadAboutV2Changes}
|
||||
>
|
||||
here
|
||||
</a>
|
||||
{v2FeedbackUrl ? t(' or ') : ''}
|
||||
{v2FeedbackUrl ? (
|
||||
<a target="_blank" rel="noopener noreferrer" href={v2FeedbackUrl}>
|
||||
{t('provide feedback')}
|
||||
</a>
|
||||
) : (
|
||||
''
|
||||
)}.
|
||||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onClose}>
|
||||
{t(`${forceV2Edit ? 'View in' : 'Continue with'} v1`)}
|
||||
</Button>
|
||||
<Button bsStyle="primary" onClick={handleConvertToV2}>
|
||||
{t('Preview v2')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
PromptV2ConversionModal.propTypes = propTypes;
|
||||
PromptV2ConversionModal.defaultProps = defaultProps;
|
||||
|
||||
export default PromptV2ConversionModal;
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
/* eslint-env browser */
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, Button } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Logger,
|
||||
LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
|
||||
LOG_ACTIONS_FALLBACK_TO_V1,
|
||||
} from '../../logger';
|
||||
|
||||
import { t } from '../../locales';
|
||||
|
||||
const propTypes = {
|
||||
v2FeedbackUrl: PropTypes.string,
|
||||
v2AutoConvertDate: PropTypes.string,
|
||||
forceV2Edit: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
v2FeedbackUrl: null,
|
||||
v2AutoConvertDate: null,
|
||||
handleFallbackToV1: null,
|
||||
};
|
||||
|
||||
// This is a gross component but it is temporary!
|
||||
class V2PreviewModal extends React.Component {
|
||||
static logReadAboutV2Changes() {
|
||||
Logger.append(
|
||||
LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
|
||||
{ version: 'v2-preview' },
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFallbackToV1 = this.handleFallbackToV1.bind(this);
|
||||
}
|
||||
|
||||
handleFallbackToV1() {
|
||||
Logger.append(
|
||||
LOG_ACTIONS_FALLBACK_TO_V1,
|
||||
{
|
||||
force_v2_edit: this.props.forceV2Edit,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const url = new URL(window.location); // eslint-disable-line
|
||||
url.searchParams.set('version', 'v1');
|
||||
url.searchParams.delete('edit'); // remove JIC they were editing and v1 editing is not allowed
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { v2FeedbackUrl, v2AutoConvertDate, onClose } = this.props;
|
||||
|
||||
const timeUntilAutoConversion = v2AutoConvertDate
|
||||
? `approximately ${moment(v2AutoConvertDate).toNow(
|
||||
true,
|
||||
)} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY)
|
||||
: 'a limited amount of time';
|
||||
|
||||
return (
|
||||
<Modal onHide={onClose} onExit={onClose} animation show>
|
||||
<Modal.Header closeButton>
|
||||
<div
|
||||
style={{ fontSize: 20, fontWeight: 200, margin: '0px 4px -4px' }}
|
||||
>
|
||||
{t('Welcome to the new Dashboard v2 experience! 🎉')}
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<h3>{t('Who')}</h3>
|
||||
<p>
|
||||
{t(
|
||||
"As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience. You can learn more about these changes ",
|
||||
)}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="http://bit.ly/superset-dash-v2"
|
||||
onClick={V2PreviewModal.logReadAboutV2Changes}
|
||||
>
|
||||
here
|
||||
</a>
|
||||
{v2FeedbackUrl ? t(' or ') : ''}
|
||||
{v2FeedbackUrl ? (
|
||||
<a target="_blank" rel="noopener noreferrer" href={v2FeedbackUrl}>
|
||||
{t('provide feedback')}
|
||||
</a>
|
||||
) : (
|
||||
''
|
||||
)}.
|
||||
</p>
|
||||
<br />
|
||||
<h3>{t('What')}</h3>
|
||||
<p>
|
||||
{t('You are ')}
|
||||
<strong>{t('previewing')}</strong>
|
||||
{t(
|
||||
' an auto-converted v2 version of your v1 dashboard. This conversion may have introduced regressions, such as minor layout variation or incompatible custom CSS. ',
|
||||
)}
|
||||
<strong>
|
||||
{t(
|
||||
'To persist your dashboard as v2, please make any necessary changes and save the dashboard',
|
||||
)}
|
||||
</strong>
|
||||
{t(
|
||||
'. Note that non-owners/-admins will continue to see the original version until you take this action.',
|
||||
)}
|
||||
</p>
|
||||
<br />
|
||||
<h3>{t('When')}</h3>
|
||||
<p>
|
||||
{t('You have ')}
|
||||
<strong>
|
||||
{timeUntilAutoConversion}
|
||||
{t(' to edit and save this version ')}
|
||||
</strong>
|
||||
{t(
|
||||
' before it is auto-persisted to this preview. Upon save you will no longer be able to use the v1 experience.',
|
||||
)}
|
||||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.handleFallbackToV1}>
|
||||
{t('Fallback to v1')}
|
||||
</Button>
|
||||
<Button bsStyle="primary" onClick={onClose}>
|
||||
{t('Preview v2')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
V2PreviewModal.propTypes = propTypes;
|
||||
V2PreviewModal.defaultProps = defaultProps;
|
||||
|
||||
export default connect(({ dashboardInfo }) => ({
|
||||
v2FeedbackUrl: dashboardInfo.v2FeedbackUrl,
|
||||
v2AutoConvertDate: dashboardInfo.v2AutoConvertDate,
|
||||
forceV2Edit: dashboardInfo.forceV2Edit,
|
||||
}))(V2PreviewModal);
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
/* eslint camelcase: 0 */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Mustache from 'mustache';
|
||||
import { Tooltip } from 'react-bootstrap';
|
||||
|
||||
import { d3format } from '../../../modules/utils';
|
||||
import ChartBody from './ChartBody';
|
||||
import Loading from '../../../components/Loading';
|
||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger';
|
||||
import StackTraceMessage from '../../../components/StackTraceMessage';
|
||||
import RefreshChartOverlay from '../../../components/RefreshChartOverlay';
|
||||
import visPromiseLookup from '../../../visualizations';
|
||||
import sandboxedEval from '../../../modules/sandbox';
|
||||
import './chart.css';
|
||||
|
||||
const propTypes = {
|
||||
annotationData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
chartKey: PropTypes.string.isRequired,
|
||||
containerId: PropTypes.string.isRequired,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
formData: PropTypes.object.isRequired,
|
||||
headerHeight: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
setControlValue: PropTypes.func,
|
||||
timeout: PropTypes.number,
|
||||
vizType: PropTypes.string.isRequired,
|
||||
// state
|
||||
chartAlert: PropTypes.string,
|
||||
chartStatus: PropTypes.string,
|
||||
chartUpdateEndTime: PropTypes.number,
|
||||
chartUpdateStartTime: PropTypes.number,
|
||||
latestQueryFormData: PropTypes.object,
|
||||
queryRequest: PropTypes.object,
|
||||
queryResponse: PropTypes.object,
|
||||
lastRendered: PropTypes.number,
|
||||
triggerQuery: PropTypes.bool,
|
||||
refreshOverlayVisible: PropTypes.bool,
|
||||
errorMessage: PropTypes.node,
|
||||
// dashboard callbacks
|
||||
addFilter: PropTypes.func,
|
||||
getFilters: PropTypes.func,
|
||||
clearFilter: PropTypes.func,
|
||||
removeFilter: PropTypes.func,
|
||||
onQuery: PropTypes.func,
|
||||
onDismissRefreshOverlay: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
addFilter: () => ({}),
|
||||
getFilters: () => ({}),
|
||||
clearFilter: () => ({}),
|
||||
removeFilter: () => ({}),
|
||||
};
|
||||
|
||||
class Chart extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// visualizations are lazy-loaded with promises that resolve to a renderVis function
|
||||
this.state = {
|
||||
renderVis: null,
|
||||
};
|
||||
// these properties are used by visualizations
|
||||
this.annotationData = props.annotationData;
|
||||
this.containerId = props.containerId;
|
||||
this.selector = `#${this.containerId}`;
|
||||
this.formData = props.formData;
|
||||
this.datasource = props.datasource;
|
||||
this.addFilter = this.addFilter.bind(this);
|
||||
this.getFilters = this.getFilters.bind(this);
|
||||
this.clearFilter = this.clearFilter.bind(this);
|
||||
this.removeFilter = this.removeFilter.bind(this);
|
||||
this.headerHeight = this.headerHeight.bind(this);
|
||||
this.height = this.height.bind(this);
|
||||
this.width = this.width.bind(this);
|
||||
this.visPromise = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.triggerQuery) {
|
||||
this.props.actions.runQuery(this.props.formData, false,
|
||||
this.props.timeout,
|
||||
this.props.chartKey,
|
||||
);
|
||||
}
|
||||
this.loadAsyncVis(this.props.vizType);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.annotationData = nextProps.annotationData;
|
||||
this.containerId = nextProps.containerId;
|
||||
this.selector = `#${this.containerId}`;
|
||||
this.formData = nextProps.formData;
|
||||
this.datasource = nextProps.datasource;
|
||||
if (nextProps.vizType !== this.props.vizType) {
|
||||
this.setState(() => ({ renderVis: null }));
|
||||
this.loadAsyncVis(nextProps.vizType);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.queryResponse &&
|
||||
['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
|
||||
!this.props.queryResponse.error && (
|
||||
prevProps.annotationData !== this.props.annotationData ||
|
||||
prevProps.queryResponse !== this.props.queryResponse ||
|
||||
prevProps.height !== this.props.height ||
|
||||
prevProps.width !== this.props.width ||
|
||||
prevProps.lastRendered !== this.props.lastRendered)
|
||||
) {
|
||||
this.renderViz();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.visPromise = null;
|
||||
}
|
||||
|
||||
getFilters() {
|
||||
return this.props.getFilters();
|
||||
}
|
||||
|
||||
setTooltip(tooltip) {
|
||||
this.setState({ tooltip });
|
||||
}
|
||||
|
||||
loadAsyncVis(visType) {
|
||||
this.visPromise = visPromiseLookup[visType];
|
||||
|
||||
this.visPromise()
|
||||
.then((renderVis) => {
|
||||
// ensure Component is still mounted
|
||||
if (this.visPromise) {
|
||||
this.setState({ renderVis }, this.renderViz);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error); // eslint-disable-line
|
||||
this.props.actions.chartRenderingFailed(error, this.props.chartKey);
|
||||
});
|
||||
}
|
||||
|
||||
addFilter(col, vals, merge = true, refresh = true) {
|
||||
this.props.addFilter(col, vals, merge, refresh);
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.props.clearFilter();
|
||||
}
|
||||
|
||||
removeFilter(col, vals, refresh = true) {
|
||||
this.props.removeFilter(col, vals, refresh);
|
||||
}
|
||||
|
||||
clearError() {
|
||||
this.setState({ errorMsg: null });
|
||||
}
|
||||
|
||||
width() {
|
||||
return this.props.width || this.container.el.offsetWidth;
|
||||
}
|
||||
|
||||
headerHeight() {
|
||||
return this.props.headerHeight || 0;
|
||||
}
|
||||
|
||||
height() {
|
||||
return this.props.height || this.container.el.offsetHeight;
|
||||
}
|
||||
|
||||
d3format(col, number) {
|
||||
const { datasource } = this.props;
|
||||
const format = (datasource.column_formats && datasource.column_formats[col]) || '0.3s';
|
||||
|
||||
return d3format(format, number);
|
||||
}
|
||||
|
||||
error(e) {
|
||||
this.props.actions.chartRenderingFailed(e, this.props.chartKey);
|
||||
}
|
||||
|
||||
verboseMetricName(metric) {
|
||||
return this.props.datasource.verbose_map[metric] || metric;
|
||||
}
|
||||
|
||||
render_template(s) {
|
||||
const context = {
|
||||
width: this.width(),
|
||||
height: this.height(),
|
||||
};
|
||||
return Mustache.render(s, context);
|
||||
}
|
||||
|
||||
renderTooltip() {
|
||||
if (this.state.tooltip) {
|
||||
/* eslint-disable react/no-danger */
|
||||
return (
|
||||
<Tooltip
|
||||
className="chart-tooltip"
|
||||
id="chart-tooltip"
|
||||
placement="right"
|
||||
positionTop={this.state.tooltip.y - 10}
|
||||
positionLeft={this.state.tooltip.x + 30}
|
||||
arrowOffsetTop={10}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
|
||||
</Tooltip>
|
||||
);
|
||||
/* eslint-enable react/no-danger */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderViz() {
|
||||
const hasVisPromise = !!this.state.renderVis;
|
||||
|
||||
if (hasVisPromise && ['success', 'rendered'].indexOf(this.props.chartStatus) > -1) {
|
||||
const fd = this.props.formData;
|
||||
const qr = this.props.queryResponse;
|
||||
const renderStart = Logger.getTimestamp();
|
||||
try {
|
||||
// Executing user-defined data mutator function
|
||||
if (fd.js_data) {
|
||||
qr.data = sandboxedEval(fd.js_data)(qr.data);
|
||||
}
|
||||
// [re]rendering the visualization
|
||||
this.state.renderVis(this, qr, this.props.setControlValue);
|
||||
Logger.append(LOG_ACTIONS_RENDER_CHART, {
|
||||
slice_id: this.props.chartKey,
|
||||
viz_type: this.props.vizType,
|
||||
start_offset: renderStart,
|
||||
duration: Logger.getTimestamp() - renderStart,
|
||||
});
|
||||
if (this.props.chartStatus !== 'rendered') {
|
||||
this.props.actions.chartRenderingSucceeded(this.props.chartKey);
|
||||
}
|
||||
} catch (e) {
|
||||
this.props.actions.chartRenderingFailed(e, this.props.chartKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLoading = this.props.chartStatus === 'loading' || !this.state.renderVis;
|
||||
|
||||
return (
|
||||
<div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
|
||||
{this.renderTooltip()}
|
||||
{isLoading &&
|
||||
<Loading size={25} />
|
||||
}
|
||||
{this.props.chartAlert &&
|
||||
<StackTraceMessage
|
||||
message={this.props.chartAlert}
|
||||
queryResponse={this.props.queryResponse}
|
||||
/>
|
||||
}
|
||||
|
||||
{!isLoading &&
|
||||
!this.props.chartAlert &&
|
||||
this.props.refreshOverlayVisible &&
|
||||
!this.props.errorMessage &&
|
||||
this.container &&
|
||||
<RefreshChartOverlay
|
||||
height={this.height()}
|
||||
width={this.width()}
|
||||
onQuery={this.props.onQuery}
|
||||
onDismiss={this.props.onDismissRefreshOverlay}
|
||||
/>
|
||||
}
|
||||
{!isLoading && !this.props.chartAlert &&
|
||||
<ChartBody
|
||||
containerId={this.containerId}
|
||||
vizType={this.props.vizType}
|
||||
height={this.height}
|
||||
width={this.width}
|
||||
faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
|
||||
ref={(inner) => {
|
||||
this.container = inner;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chart.propTypes = propTypes;
|
||||
Chart.defaultProps = defaultProps;
|
||||
|
||||
export default Chart;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import $ from 'jquery';
|
||||
|
||||
const propTypes = {
|
||||
containerId: PropTypes.string.isRequired,
|
||||
vizType: PropTypes.string.isRequired,
|
||||
height: PropTypes.func.isRequired,
|
||||
width: PropTypes.func.isRequired,
|
||||
faded: PropTypes.bool,
|
||||
};
|
||||
|
||||
class ChartBody extends React.PureComponent {
|
||||
html(data) {
|
||||
this.el.innerHTML = data;
|
||||
}
|
||||
|
||||
css(property, value) {
|
||||
this.el.style[property] = value;
|
||||
}
|
||||
|
||||
get(n) {
|
||||
return $(this.el).get(n);
|
||||
}
|
||||
|
||||
find(classname) {
|
||||
return $(this.el).find(classname);
|
||||
}
|
||||
|
||||
show() {
|
||||
return $(this.el).show();
|
||||
}
|
||||
|
||||
height() {
|
||||
return this.props.height();
|
||||
}
|
||||
|
||||
width() {
|
||||
return this.props.width();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
id={this.props.containerId}
|
||||
className={`slice_container ${this.props.vizType}${this.props.faded ? ' faded' : ''}`}
|
||||
ref={(el) => { this.el = el; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChartBody.propTypes = propTypes;
|
||||
|
||||
export default ChartBody;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import * as Actions from './chartAction';
|
||||
import Chart from './Chart';
|
||||
|
||||
function mapStateToProps({ charts }, ownProps) {
|
||||
const chart = charts[ownProps.chartKey];
|
||||
return {
|
||||
annotationData: chart.annotationData,
|
||||
chartAlert: chart.chartAlert,
|
||||
chartStatus: chart.chartStatus,
|
||||
chartUpdateEndTime: chart.chartUpdateEndTime,
|
||||
chartUpdateStartTime: chart.chartUpdateStartTime,
|
||||
latestQueryFormData: chart.latestQueryFormData,
|
||||
lastRendered: chart.lastRendered,
|
||||
queryResponse: chart.queryResponse,
|
||||
queryRequest: chart.queryRequest,
|
||||
triggerQuery: chart.triggerQuery,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Chart);
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.chart-tooltip {
|
||||
opacity: 0.75;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../../../explore/exploreUtils';
|
||||
import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../../../modules/AnnotationTypes';
|
||||
import { Logger, LOG_ACTIONS_LOAD_CHART } from '../../../logger';
|
||||
import { COMMON_ERR_MESSAGES } from '../../../common';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
|
||||
export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
|
||||
return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key };
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
|
||||
export function chartUpdateSucceeded(queryResponse, key) {
|
||||
return { type: CHART_UPDATE_SUCCEEDED, queryResponse, key };
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
|
||||
export function chartUpdateStopped(key) {
|
||||
return { type: CHART_UPDATE_STOPPED, key };
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
|
||||
export function chartUpdateTimeout(statusText, timeout, key) {
|
||||
return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key };
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
|
||||
export function chartUpdateFailed(queryResponse, key) {
|
||||
return { type: CHART_UPDATE_FAILED, queryResponse, key };
|
||||
}
|
||||
|
||||
export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
|
||||
export function chartRenderingFailed(error, key) {
|
||||
return { type: CHART_RENDERING_FAILED, error, key };
|
||||
}
|
||||
|
||||
export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED';
|
||||
export function chartRenderingSucceeded(key) {
|
||||
return { type: CHART_RENDERING_SUCCEEDED, key };
|
||||
}
|
||||
|
||||
export const REMOVE_CHART = 'REMOVE_CHART';
|
||||
export function removeChart(key) {
|
||||
return { type: REMOVE_CHART, key };
|
||||
}
|
||||
|
||||
export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS';
|
||||
export function annotationQuerySuccess(annotation, queryResponse, key) {
|
||||
return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key };
|
||||
}
|
||||
|
||||
export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
|
||||
export function annotationQueryStarted(annotation, queryRequest, key) {
|
||||
return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
|
||||
}
|
||||
|
||||
export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
|
||||
export function annotationQueryFailed(annotation, queryResponse, key) {
|
||||
return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
|
||||
}
|
||||
|
||||
export function runAnnotationQuery(annotation, timeout = 60, formData = null, key) {
|
||||
return function (dispatch, getState) {
|
||||
const sliceKey = key || Object.keys(getState().charts)[0];
|
||||
const fd = formData || getState().charts[sliceKey].latestQueryFormData;
|
||||
|
||||
if (!requiresQuery(annotation.sourceType)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const granularity = fd.time_grain_sqla || fd.granularity;
|
||||
fd.time_grain_sqla = granularity;
|
||||
fd.granularity = granularity;
|
||||
|
||||
const sliceFormData = Object.keys(annotation.overrides)
|
||||
.reduce((d, k) => ({
|
||||
...d,
|
||||
[k]: annotation.overrides[k] || fd[k],
|
||||
}), {});
|
||||
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
|
||||
const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
|
||||
const queryRequest = $.ajax({
|
||||
url,
|
||||
dataType: 'json',
|
||||
timeout: timeout * 1000,
|
||||
});
|
||||
dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
|
||||
return queryRequest
|
||||
.then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
|
||||
.catch((err) => {
|
||||
if (err.statusText === 'timeout') {
|
||||
dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
|
||||
} else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
|
||||
dispatch(annotationQuerySuccess(annotation, err, sliceKey));
|
||||
} else if (err.statusText !== 'abort') {
|
||||
dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const TRIGGER_QUERY = 'TRIGGER_QUERY';
|
||||
export function triggerQuery(value = true, key) {
|
||||
return { type: TRIGGER_QUERY, value, key };
|
||||
}
|
||||
|
||||
// this action is used for forced re-render without fetch data
|
||||
export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
|
||||
export function renderTriggered(value, key) {
|
||||
return { type: RENDER_TRIGGERED, value, key };
|
||||
}
|
||||
|
||||
export const UPDATE_QUERY_FORM_DATA = 'UPDATE_QUERY_FORM_DATA';
|
||||
export function updateQueryFormData(value, key) {
|
||||
return { type: UPDATE_QUERY_FORM_DATA, value, key };
|
||||
}
|
||||
|
||||
export const RUN_QUERY = 'RUN_QUERY';
|
||||
export function runQuery(formData, force = false, timeout = 60, key) {
|
||||
return (dispatch) => {
|
||||
const { url, payload } = getExploreUrlAndPayload({
|
||||
formData,
|
||||
endpointType: 'json',
|
||||
force,
|
||||
});
|
||||
const logStart = Logger.getTimestamp();
|
||||
const queryRequest = $.ajax({
|
||||
type: 'POST',
|
||||
url,
|
||||
dataType: 'json',
|
||||
data: {
|
||||
form_data: JSON.stringify(payload),
|
||||
},
|
||||
timeout: timeout * 1000,
|
||||
});
|
||||
const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
|
||||
.then(() => queryRequest)
|
||||
.then((queryResponse) => {
|
||||
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
||||
slice_id: key,
|
||||
is_cached: queryResponse.is_cached,
|
||||
force_refresh: force,
|
||||
row_count: queryResponse.rowcount,
|
||||
datasource: formData.datasource,
|
||||
start_offset: logStart,
|
||||
duration: Logger.getTimestamp() - logStart,
|
||||
has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
|
||||
viz_type: formData.viz_type,
|
||||
});
|
||||
return dispatch(chartUpdateSucceeded(queryResponse, key));
|
||||
})
|
||||
.catch((err) => {
|
||||
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
||||
slice_id: key,
|
||||
has_err: true,
|
||||
datasource: formData.datasource,
|
||||
start_offset: logStart,
|
||||
duration: Logger.getTimestamp() - logStart,
|
||||
});
|
||||
if (err.statusText === 'timeout') {
|
||||
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
|
||||
} else if (err.statusText === 'abort') {
|
||||
dispatch(chartUpdateStopped(key));
|
||||
} else {
|
||||
let errObject;
|
||||
if (err.responseJSON) {
|
||||
errObject = err.responseJSON;
|
||||
} else if (err.stack) {
|
||||
errObject = {
|
||||
error: t('Unexpected error: ') + err.description,
|
||||
stacktrace: err.stack,
|
||||
};
|
||||
} else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
|
||||
errObject = {
|
||||
error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
|
||||
};
|
||||
} else {
|
||||
errObject = {
|
||||
error: t('Unexpected error.'),
|
||||
};
|
||||
}
|
||||
dispatch(chartUpdateFailed(errObject, key));
|
||||
}
|
||||
});
|
||||
const annotationLayers = formData.annotation_layers || [];
|
||||
return Promise.all([
|
||||
queryPromise,
|
||||
dispatch(triggerQuery(false, key)),
|
||||
dispatch(updateQueryFormData(payload, key)),
|
||||
...annotationLayers.map(x => dispatch(runAnnotationQuery(x, timeout, formData, key))),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
/* eslint camelcase: 0 */
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { now } from '../../../modules/dates';
|
||||
import * as actions from './chartAction';
|
||||
import { t } from '../../../locales';
|
||||
|
||||
export const chartPropType = {
|
||||
chartKey: PropTypes.string.isRequired,
|
||||
chartAlert: PropTypes.string,
|
||||
chartStatus: PropTypes.string,
|
||||
chartUpdateEndTime: PropTypes.number,
|
||||
chartUpdateStartTime: PropTypes.number,
|
||||
latestQueryFormData: PropTypes.object,
|
||||
queryRequest: PropTypes.object,
|
||||
queryResponse: PropTypes.object,
|
||||
triggerQuery: PropTypes.bool,
|
||||
lastRendered: PropTypes.number,
|
||||
};
|
||||
|
||||
export const chart = {
|
||||
chartKey: '',
|
||||
chartAlert: null,
|
||||
chartStatus: 'loading',
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: now(),
|
||||
latestQueryFormData: {},
|
||||
queryRequest: null,
|
||||
queryResponse: null,
|
||||
triggerQuery: true,
|
||||
lastRendered: 0,
|
||||
};
|
||||
|
||||
export default function chartReducer(charts = {}, action) {
|
||||
const actionHandlers = {
|
||||
[actions.CHART_UPDATE_SUCCEEDED](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'success',
|
||||
queryResponse: action.queryResponse,
|
||||
chartUpdateEndTime: now(),
|
||||
};
|
||||
},
|
||||
[actions.CHART_UPDATE_STARTED](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'loading',
|
||||
chartAlert: null,
|
||||
chartUpdateEndTime: null,
|
||||
chartUpdateStartTime: now(),
|
||||
queryRequest: action.queryRequest,
|
||||
};
|
||||
},
|
||||
[actions.CHART_UPDATE_STOPPED](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'stopped',
|
||||
chartAlert: t('Updating chart was stopped'),
|
||||
};
|
||||
},
|
||||
[actions.CHART_RENDERING_SUCCEEDED](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'rendered',
|
||||
};
|
||||
},
|
||||
[actions.CHART_RENDERING_FAILED](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'failed',
|
||||
chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
|
||||
};
|
||||
},
|
||||
[actions.CHART_UPDATE_TIMEOUT](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'failed',
|
||||
chartAlert: (
|
||||
`${t('Query timeout')} - ` +
|
||||
t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
|
||||
t('Perhaps your data has grown, your database is under unusual load, ' +
|
||||
'or you are simply querying a data source that is too large ' +
|
||||
'to be processed within the timeout range. ' +
|
||||
'If that is the case, we recommend that you summarize your data further.')),
|
||||
};
|
||||
},
|
||||
[actions.CHART_UPDATE_FAILED](state) {
|
||||
return { ...state,
|
||||
chartStatus: 'failed',
|
||||
chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
|
||||
chartUpdateEndTime: now(),
|
||||
queryResponse: action.queryResponse,
|
||||
};
|
||||
},
|
||||
[actions.TRIGGER_QUERY](state) {
|
||||
return { ...state, triggerQuery: action.value };
|
||||
},
|
||||
[actions.RENDER_TRIGGERED](state) {
|
||||
return { ...state, lastRendered: action.value };
|
||||
},
|
||||
[actions.UPDATE_QUERY_FORM_DATA](state) {
|
||||
return { ...state, latestQueryFormData: action.value };
|
||||
},
|
||||
[actions.ANNOTATION_QUERY_STARTED](state) {
|
||||
if (state.annotationQuery &&
|
||||
state.annotationQuery[action.annotation.name]) {
|
||||
state.annotationQuery[action.annotation.name].abort();
|
||||
}
|
||||
const annotationQuery = {
|
||||
...state.annotationQuery,
|
||||
[action.annotation.name]: action.queryRequest,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
annotationQuery,
|
||||
};
|
||||
},
|
||||
[actions.ANNOTATION_QUERY_SUCCESS](state) {
|
||||
const annotationData = {
|
||||
...state.annotationData,
|
||||
[action.annotation.name]: action.queryResponse.data,
|
||||
};
|
||||
const annotationError = { ...state.annotationError };
|
||||
delete annotationError[action.annotation.name];
|
||||
const annotationQuery = { ...state.annotationQuery };
|
||||
delete annotationQuery[action.annotation.name];
|
||||
return {
|
||||
...state,
|
||||
annotationData,
|
||||
annotationError,
|
||||
annotationQuery,
|
||||
};
|
||||
},
|
||||
[actions.ANNOTATION_QUERY_FAILED](state) {
|
||||
const annotationData = { ...state.annotationData };
|
||||
delete annotationData[action.annotation.name];
|
||||
const annotationError = {
|
||||
...state.annotationError,
|
||||
[action.annotation.name]: action.queryResponse ?
|
||||
action.queryResponse.error : t('Network error.'),
|
||||
};
|
||||
const annotationQuery = { ...state.annotationQuery };
|
||||
delete annotationQuery[action.annotation.name];
|
||||
return {
|
||||
...state,
|
||||
annotationData,
|
||||
annotationError,
|
||||
annotationQuery,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
if (action.type === actions.REMOVE_CHART) {
|
||||
delete charts[action.key];
|
||||
return charts;
|
||||
}
|
||||
|
||||
if (action.type in actionHandlers) {
|
||||
return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
|
||||
}
|
||||
|
||||
return charts;
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/* global window */
|
||||
import $ from 'jquery';
|
||||
import { getExploreUrlAndPayload } from '../../../explore/exploreUtils';
|
||||
import { addSuccessToast, addDangerToast } from '../../../messageToasts/actions';
|
||||
|
||||
export const ADD_FILTER = 'ADD_FILTER';
|
||||
export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
|
||||
return { type: ADD_FILTER, sliceId, col, vals, merge, refresh };
|
||||
}
|
||||
|
||||
export const CLEAR_FILTER = 'CLEAR_FILTER';
|
||||
export function clearFilter(sliceId) {
|
||||
return { type: CLEAR_FILTER, sliceId };
|
||||
}
|
||||
|
||||
export const REMOVE_FILTER = 'REMOVE_FILTER';
|
||||
export function removeFilter(sliceId, col, vals, refresh = true) {
|
||||
return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
|
||||
}
|
||||
|
||||
export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT';
|
||||
export function updateDashboardLayout(layout) {
|
||||
return { type: UPDATE_DASHBOARD_LAYOUT, layout };
|
||||
}
|
||||
|
||||
export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
|
||||
export function updateDashboardTitle(title) {
|
||||
return { type: UPDATE_DASHBOARD_TITLE, title };
|
||||
}
|
||||
|
||||
export function addSlicesToDashboard(dashboardId, sliceIds) {
|
||||
return () => (
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: `/superset/add_slices/${dashboardId}/`,
|
||||
data: {
|
||||
data: JSON.stringify({ slice_ids: sliceIds }),
|
||||
},
|
||||
})
|
||||
.done(() => {
|
||||
// Refresh page to allow for slices to re-render
|
||||
window.location.reload();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const REMOVE_SLICE = 'REMOVE_SLICE';
|
||||
export function removeSlice(slice) {
|
||||
return { type: REMOVE_SLICE, slice };
|
||||
}
|
||||
|
||||
export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
|
||||
export function updateSliceName(slice, sliceName) {
|
||||
return { type: UPDATE_SLICE_NAME, slice, sliceName };
|
||||
}
|
||||
export function saveSlice(slice, sliceName) {
|
||||
const oldName = slice.slice_name;
|
||||
return (dispatch) => {
|
||||
const sliceParams = {};
|
||||
sliceParams.slice_id = slice.slice_id;
|
||||
sliceParams.action = 'overwrite';
|
||||
sliceParams.slice_name = sliceName;
|
||||
|
||||
const { url, payload } = getExploreUrlAndPayload({
|
||||
formData: slice.form_data,
|
||||
endpointType: 'base',
|
||||
force: false,
|
||||
curUrl: null,
|
||||
requestParams: sliceParams,
|
||||
});
|
||||
return $.ajax({
|
||||
url,
|
||||
type: 'POST',
|
||||
data: {
|
||||
form_data: JSON.stringify(payload),
|
||||
},
|
||||
success: () => {
|
||||
dispatch(updateSliceName(slice, sliceName));
|
||||
dispatch(addSuccessToast('This slice name was saved successfully.'));
|
||||
},
|
||||
error: () => {
|
||||
// if server-side reject the overwrite action,
|
||||
// revert to old state
|
||||
dispatch(updateSliceName(slice, oldName));
|
||||
dispatch(addDangerToast("You don't have the rights to alter this slice"));
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
||||
export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
|
||||
export function toggleFaveStar(isStarred) {
|
||||
return { type: TOGGLE_FAVE_STAR, isStarred };
|
||||
}
|
||||
|
||||
export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
|
||||
export function fetchFaveStar(id) {
|
||||
return function (dispatch) {
|
||||
const url = `${FAVESTAR_BASE_URL}/${id}/count`;
|
||||
return $.get(url)
|
||||
.done((data) => {
|
||||
if (data.count > 0) {
|
||||
dispatch(toggleFaveStar(true));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
|
||||
export function saveFaveStar(id, isStarred) {
|
||||
return function (dispatch) {
|
||||
const urlSuffix = isStarred ? 'unselect' : 'select';
|
||||
const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
|
||||
$.get(url);
|
||||
dispatch(toggleFaveStar(!isStarred));
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
|
||||
export function toggleExpandSlice(slice, isExpanded) {
|
||||
return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded };
|
||||
}
|
||||
|
||||
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
|
||||
export function setEditMode(editMode) {
|
||||
return { type: SET_EDIT_MODE, editMode };
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ModalTrigger from '../../../../components/ModalTrigger';
|
||||
import { t } from '../../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
code: PropTypes.string,
|
||||
codeCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
codeCallback: () => {},
|
||||
};
|
||||
|
||||
export default class CodeModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { code: props.code };
|
||||
}
|
||||
beforeOpen() {
|
||||
let code = this.props.code;
|
||||
if (!code && this.props.codeCallback) {
|
||||
code = this.props.codeCallback();
|
||||
}
|
||||
this.setState({ code });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
isButton
|
||||
beforeOpen={this.beforeOpen.bind(this)}
|
||||
modalTitle={t('Active Dashboard Filters')}
|
||||
modalBody={
|
||||
<div className="CodeModal">
|
||||
<pre>
|
||||
{this.state.code}
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
CodeModal.propTypes = propTypes;
|
||||
CodeModal.defaultProps = defaultProps;
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DropdownButton, MenuItem } from 'react-bootstrap';
|
||||
|
||||
import CssEditor from './CssEditor';
|
||||
import RefreshIntervalModal from './RefreshIntervalModal';
|
||||
import SaveModal from './SaveModal';
|
||||
import SliceAdder from './SliceAdder';
|
||||
import { t } from '../../../../locales';
|
||||
import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger';
|
||||
|
||||
const $ = window.$ = require('jquery');
|
||||
|
||||
const propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
slices: PropTypes.array,
|
||||
userId: PropTypes.string.isRequired,
|
||||
addSlicesToDashboard: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
renderSlices: PropTypes.func,
|
||||
serialize: PropTypes.func,
|
||||
startPeriodicRender: PropTypes.func,
|
||||
editMode: PropTypes.bool,
|
||||
};
|
||||
|
||||
function MenuItemContent({ faIcon, text, tooltip, children }) {
|
||||
return (
|
||||
<span>
|
||||
<i className={`fa fa-${faIcon}`} /> {text} {''}
|
||||
<InfoTooltipWithTrigger
|
||||
tooltip={tooltip}
|
||||
label={`dash-${faIcon}`}
|
||||
placement="top"
|
||||
/>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
MenuItemContent.propTypes = {
|
||||
faIcon: PropTypes.string.isRequired,
|
||||
text: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
function ActionMenuItem(props) {
|
||||
return (
|
||||
<MenuItem onClick={props.onClick}>
|
||||
<MenuItemContent {...props} />
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
ActionMenuItem.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
class Controls extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
css: props.dashboard.css || '',
|
||||
cssTemplates: [],
|
||||
};
|
||||
this.refresh = this.refresh.bind(this);
|
||||
this.toggleModal = this.toggleModal.bind(this);
|
||||
this.updateDom = this.updateDom.bind(this);
|
||||
}
|
||||
componentWillMount() {
|
||||
this.updateDom(this.state.css);
|
||||
|
||||
$.get('/csstemplateasyncmodelview/api/read', (data) => {
|
||||
const cssTemplates = data.result.map(row => ({
|
||||
value: row.template_name,
|
||||
css: row.css,
|
||||
label: row.template_name,
|
||||
}));
|
||||
this.setState({ cssTemplates });
|
||||
});
|
||||
}
|
||||
refresh() {
|
||||
// Force refresh all slices
|
||||
this.props.renderSlices(true);
|
||||
}
|
||||
toggleModal(modal) {
|
||||
let currentModal;
|
||||
if (modal !== this.state.currentModal) {
|
||||
currentModal = modal;
|
||||
}
|
||||
this.setState({ currentModal });
|
||||
}
|
||||
changeCss(css) {
|
||||
this.setState({ css }, () => {
|
||||
this.updateDom(css);
|
||||
});
|
||||
this.props.onChange();
|
||||
}
|
||||
updateDom(css) {
|
||||
const className = 'CssEditor-css';
|
||||
const head = document.head || document.getElementsByTagName('head')[0];
|
||||
let style = document.querySelector('.' + className);
|
||||
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.className = className;
|
||||
style.type = 'text/css';
|
||||
head.appendChild(style);
|
||||
}
|
||||
if (style.styleSheet) {
|
||||
style.styleSheet.cssText = css;
|
||||
} else {
|
||||
style.innerHTML = css;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { dashboard, userId, filters,
|
||||
addSlicesToDashboard, startPeriodicRender,
|
||||
serialize, onSave, editMode } = this.props;
|
||||
const emailBody = t('Checkout this dashboard: %s', window.location.href);
|
||||
const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
|
||||
+ `${dashboard.dashboard_title}&Body=${emailBody}`;
|
||||
let saveText = t('Save as');
|
||||
if (editMode) {
|
||||
saveText = t('Save');
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
|
||||
<ActionMenuItem
|
||||
text={t('Force Refresh')}
|
||||
tooltip={t('Force refresh the whole dashboard')}
|
||||
faIcon="refresh"
|
||||
onClick={this.refresh}
|
||||
/>
|
||||
<RefreshIntervalModal
|
||||
onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
|
||||
triggerNode={
|
||||
<MenuItemContent
|
||||
text={t('Set autorefresh')}
|
||||
tooltip={t('Set the auto-refresh interval for this session')}
|
||||
faIcon="clock-o"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dashboard.dash_save_perm &&
|
||||
!dashboard.forceV2Edit &&
|
||||
<SaveModal
|
||||
dashboard={dashboard}
|
||||
filters={filters}
|
||||
serialize={serialize}
|
||||
onSave={onSave}
|
||||
css={this.state.css}
|
||||
triggerNode={
|
||||
<MenuItemContent
|
||||
text={saveText}
|
||||
tooltip={t('Save the dashboard')}
|
||||
faIcon="save"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{editMode &&
|
||||
<ActionMenuItem
|
||||
text={t('Edit properties')}
|
||||
tooltip={t("Edit the dashboards's properties")}
|
||||
faIcon="edit"
|
||||
onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
|
||||
/>
|
||||
}
|
||||
{editMode &&
|
||||
<ActionMenuItem
|
||||
text={t('Email')}
|
||||
tooltip={t('Email a link to this dashboard')}
|
||||
onClick={() => { window.location = emailLink; }}
|
||||
faIcon="envelope"
|
||||
/>
|
||||
}
|
||||
{editMode &&
|
||||
<SliceAdder
|
||||
dashboard={dashboard}
|
||||
addSlicesToDashboard={addSlicesToDashboard}
|
||||
userId={userId}
|
||||
triggerNode={
|
||||
<MenuItemContent
|
||||
text={t('Add Charts')}
|
||||
tooltip={t('Add some charts to this dashboard')}
|
||||
faIcon="plus"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{editMode &&
|
||||
<CssEditor
|
||||
dashboard={dashboard}
|
||||
triggerNode={
|
||||
<MenuItemContent
|
||||
text={t('Edit CSS')}
|
||||
tooltip={t('Change the style of the dashboard using CSS code')}
|
||||
faIcon="css3"
|
||||
/>
|
||||
}
|
||||
initialCss={this.state.css}
|
||||
templates={this.state.cssTemplates}
|
||||
onChange={this.changeCss.bind(this)}
|
||||
/>
|
||||
}
|
||||
</DropdownButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
Controls.propTypes = propTypes;
|
||||
|
||||
export default Controls;
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/css';
|
||||
import 'brace/theme/github';
|
||||
|
||||
import ModalTrigger from '../../../../components/ModalTrigger';
|
||||
import { t } from '../../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
initialCss: PropTypes.string,
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
templates: PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
initialCss: '',
|
||||
onChange: () => {},
|
||||
templates: [],
|
||||
};
|
||||
|
||||
class CssEditor extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
css: props.initialCss,
|
||||
cssTemplateOptions: [],
|
||||
};
|
||||
}
|
||||
changeCss(css) {
|
||||
this.setState({ css }, () => {
|
||||
this.props.onChange(css);
|
||||
});
|
||||
}
|
||||
changeCssTemplate(opt) {
|
||||
this.changeCss(opt.css);
|
||||
}
|
||||
renderTemplateSelector() {
|
||||
if (this.props.templates) {
|
||||
return (
|
||||
<div style={{ zIndex: 10 }}>
|
||||
<h5>{t('Load a template')}</h5>
|
||||
<Select
|
||||
options={this.props.templates}
|
||||
placeholder={t('Load a CSS template')}
|
||||
onChange={this.changeCssTemplate.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
modalTitle={t('CSS')}
|
||||
isMenuItem
|
||||
modalBody={
|
||||
<div>
|
||||
{this.renderTemplateSelector()}
|
||||
<div style={{ zIndex: 1 }}>
|
||||
<h5>{t('Live CSS Editor')}</h5>
|
||||
<div style={{ border: 'solid 1px grey' }}>
|
||||
<AceEditor
|
||||
mode="css"
|
||||
theme="github"
|
||||
minLines={8}
|
||||
maxLines={30}
|
||||
onChange={this.changeCss.bind(this)}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
value={this.state.css || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
CssEditor.propTypes = propTypes;
|
||||
CssEditor.defaultProps = defaultProps;
|
||||
|
||||
export default CssEditor;
|
||||
|
|
@ -1,441 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ToastsPresenter from '../../../../messageToasts/containers/ToastPresenter';
|
||||
import GridLayout from './GridLayout';
|
||||
import Header from './Header';
|
||||
import { exportChart } from '../../../../explore/exploreUtils';
|
||||
import { areObjectsEqual } from '../../../../reduxUtils';
|
||||
import {
|
||||
Logger,
|
||||
ActionLog,
|
||||
DASHBOARD_EVENT_NAMES,
|
||||
LOG_ACTIONS_PREVIEW_V2,
|
||||
LOG_ACTIONS_MOUNT_DASHBOARD,
|
||||
LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
|
||||
LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
|
||||
LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
|
||||
LOG_ACTIONS_REFRESH_CHART,
|
||||
LOG_ACTIONS_REFRESH_DASHBOARD,
|
||||
} from '../../../../logger';
|
||||
|
||||
import { t } from '../../../../locales';
|
||||
|
||||
import '../../../../../stylesheets/dashboard_deprecated.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,
|
||||
source: 'dashboard',
|
||||
sourceId: props.dashboard.id,
|
||||
eventNames: DASHBOARD_EVENT_NAMES,
|
||||
});
|
||||
Logger.start(this.loadingLog);
|
||||
|
||||
// alert for unsaved changes
|
||||
this.state = {
|
||||
unsavedChanges: false,
|
||||
};
|
||||
this.handleSetEditMode = this.handleSetEditMode.bind(this);
|
||||
this.handleConvertToV2 = this.handleConvertToV2.bind(this);
|
||||
|
||||
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);
|
||||
this.ts_mount = new Date().getTime();
|
||||
Logger.append(LOG_ACTIONS_MOUNT_DASHBOARD, { version: 'v1' });
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.firstLoad &&
|
||||
Object.values(nextProps.slices)
|
||||
.every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
|
||||
) {
|
||||
Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
|
||||
duration: new Date().getTime() - this.ts_mount,
|
||||
version: 'v1',
|
||||
});
|
||||
Logger.send(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);
|
||||
formDataExtra.extra_filters = this.effectiveExtraFilters(slice.slice_id);
|
||||
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, fetchingAllSlices = false) {
|
||||
if (force && !fetchingAllSlices) {
|
||||
const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
|
||||
Logger.append(
|
||||
LOG_ACTIONS_REFRESH_CHART,
|
||||
{
|
||||
slice_id: slice.slice_id,
|
||||
is_cached: chartQuery.is_cached,
|
||||
version: 'v1',
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
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();
|
||||
Logger.append(
|
||||
LOG_ACTIONS_REFRESH_DASHBOARD,
|
||||
{
|
||||
force,
|
||||
interval,
|
||||
chartCount: slices.length,
|
||||
version: 'v1',
|
||||
},
|
||||
true,
|
||||
);
|
||||
if (!interval) {
|
||||
slices.forEach((slice) => { this.fetchSlice(slice, force, true); });
|
||||
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, true); }, delay * i);
|
||||
});
|
||||
}
|
||||
|
||||
exploreChart(slice) {
|
||||
const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
|
||||
Logger.append(
|
||||
LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
|
||||
{
|
||||
slice_id: slice.slice_id,
|
||||
is_cached: chartQuery && chartQuery.is_cached,
|
||||
version: 'v1',
|
||||
},
|
||||
true,
|
||||
);
|
||||
const formData = this.getFormDataExtra(slice);
|
||||
exportChart(formData);
|
||||
}
|
||||
|
||||
exportCSV(slice) {
|
||||
const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
|
||||
Logger.append(
|
||||
LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
|
||||
{
|
||||
slice_id: slice.slice_id,
|
||||
is_cached: chartQuery && chartQuery.is_cached,
|
||||
version: 'v1',
|
||||
},
|
||||
true,
|
||||
);
|
||||
const formData = this.getFormDataExtra(slice);
|
||||
exportChart(formData, 'csv');
|
||||
}
|
||||
|
||||
handleConvertToV2(editMode) {
|
||||
Logger.append(
|
||||
LOG_ACTIONS_PREVIEW_V2,
|
||||
{
|
||||
force_v2_edit: this.props.dashboard.forceV2Edit,
|
||||
edit_mode: editMode === true,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const url = new URL(window.location); // eslint-disable-line
|
||||
url.searchParams.set('version', 'v2');
|
||||
if (editMode === true) url.searchParams.set('edit', true);
|
||||
window.location = url; // eslint-disable-line
|
||||
}
|
||||
|
||||
handleSetEditMode(nextEditMode) {
|
||||
if (this.props.dashboard.forceV2Edit) {
|
||||
this.handleConvertToV2(true);
|
||||
} else {
|
||||
this.props.actions.setEditMode(nextEditMode);
|
||||
}
|
||||
}
|
||||
|
||||
// re-render chart without fetch
|
||||
rerenderCharts() {
|
||||
this.getAllSlices().forEach((slice) => {
|
||||
setTimeout(() => {
|
||||
this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, editMode } = this.props;
|
||||
return (
|
||||
<div id="dashboard-container">
|
||||
<div id="dashboard-header">
|
||||
<ToastsPresenter />
|
||||
<Header
|
||||
dashboard={this.props.dashboard}
|
||||
unsavedChanges={this.state.unsavedChanges}
|
||||
filters={this.props.filters}
|
||||
userId={this.props.userId}
|
||||
isStarred={this.props.isStarred}
|
||||
updateDashboardTitle={this.updateDashboardTitle}
|
||||
onSave={this.onSave}
|
||||
onChange={this.onChange}
|
||||
serialize={this.serialize}
|
||||
fetchFaveStar={this.props.actions.fetchFaveStar}
|
||||
saveFaveStar={this.props.actions.saveFaveStar}
|
||||
renderSlices={this.fetchAllSlices}
|
||||
startPeriodicRender={this.startPeriodicRender}
|
||||
addSlicesToDashboard={this.addSlicesToDashboard}
|
||||
editMode={this.props.editMode}
|
||||
setEditMode={this.handleSetEditMode}
|
||||
handleConvertToV2={this.handleConvertToV2}
|
||||
/>
|
||||
</div>
|
||||
<div id="grid-container" className="slice-grid gridster">
|
||||
<GridLayout
|
||||
dashboard={this.props.dashboard}
|
||||
datasources={this.props.datasources}
|
||||
filters={this.props.filters}
|
||||
charts={this.props.slices}
|
||||
timeout={this.props.timeout}
|
||||
onChange={this.onChange}
|
||||
getFormDataExtra={this.getFormDataExtra}
|
||||
exploreChart={this.exploreChart}
|
||||
exportCSV={this.exportCSV}
|
||||
fetchSlice={this.fetchSlice}
|
||||
saveSlice={this.props.actions.saveSlice}
|
||||
removeSlice={this.props.actions.removeSlice}
|
||||
removeChart={this.props.actions.removeChart}
|
||||
updateDashboardLayout={this.props.actions.updateDashboardLayout}
|
||||
toggleExpandSlice={this.props.actions.toggleExpandSlice}
|
||||
addFilter={this.props.actions.addFilter}
|
||||
getFilters={this.getFilters}
|
||||
clearFilter={this.props.actions.clearFilter}
|
||||
removeFilter={this.props.actions.removeFilter}
|
||||
editMode={this.props.editMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.propTypes = propTypes;
|
||||
Dashboard.defaultProps = defaultProps;
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as dashboardActions from '../actions';
|
||||
import * as chartActions from '../../chart/chartAction';
|
||||
import Dashboard from './Dashboard';
|
||||
|
||||
function mapStateToProps({ charts, dashboard, impressionId }) {
|
||||
return {
|
||||
initMessages: dashboard.common.flash_messages,
|
||||
timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
||||
dashboard: dashboard.dashboard,
|
||||
slices: charts,
|
||||
datasources: dashboard.datasources,
|
||||
filters: dashboard.filters,
|
||||
refresh: !!dashboard.refresh,
|
||||
userId: dashboard.userId,
|
||||
isStarred: !!dashboard.isStarred,
|
||||
editMode: dashboard.editMode,
|
||||
impressionId,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
const actions = { ...chartActions, ...dashboardActions };
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
/* eslint-disable react/no-danger */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import SliceHeader from './SliceHeader';
|
||||
import ChartContainer from '../../chart/ChartContainer';
|
||||
|
||||
import '../../../../../stylesheets/dashboard_deprecated.css';
|
||||
|
||||
const propTypes = {
|
||||
timeout: PropTypes.number,
|
||||
datasource: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
isCached: PropTypes.bool,
|
||||
cachedDttm: PropTypes.string,
|
||||
isExpanded: PropTypes.bool,
|
||||
widgetHeight: PropTypes.number,
|
||||
widgetWidth: PropTypes.number,
|
||||
slice: PropTypes.object,
|
||||
chartKey: PropTypes.string,
|
||||
formData: PropTypes.object,
|
||||
filters: PropTypes.object,
|
||||
forceRefresh: PropTypes.func,
|
||||
removeSlice: PropTypes.func,
|
||||
updateSliceName: PropTypes.func,
|
||||
toggleExpandSlice: PropTypes.func,
|
||||
exploreChart: PropTypes.func,
|
||||
exportCSV: PropTypes.func,
|
||||
addFilter: PropTypes.func,
|
||||
getFilters: PropTypes.func,
|
||||
clearFilter: PropTypes.func,
|
||||
removeFilter: PropTypes.func,
|
||||
editMode: PropTypes.bool,
|
||||
annotationQuery: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
forceRefresh: () => ({}),
|
||||
removeSlice: () => ({}),
|
||||
updateSliceName: () => ({}),
|
||||
toggleExpandSlice: () => ({}),
|
||||
exploreChart: () => ({}),
|
||||
exportCSV: () => ({}),
|
||||
addFilter: () => ({}),
|
||||
getFilters: () => ({}),
|
||||
clearFilter: () => ({}),
|
||||
removeFilter: () => ({}),
|
||||
editMode: false,
|
||||
};
|
||||
|
||||
class GridCell extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const sliceId = this.props.slice.slice_id;
|
||||
this.addFilter = this.props.addFilter.bind(this, sliceId);
|
||||
this.getFilters = this.props.getFilters.bind(this, sliceId);
|
||||
this.clearFilter = this.props.clearFilter.bind(this, sliceId);
|
||||
this.removeFilter = this.props.removeFilter.bind(this, sliceId);
|
||||
}
|
||||
|
||||
getDescriptionId(slice) {
|
||||
return 'description_' + slice.slice_id;
|
||||
}
|
||||
|
||||
getHeaderId(slice) {
|
||||
return 'header_' + slice.slice_id;
|
||||
}
|
||||
|
||||
width() {
|
||||
return this.props.widgetWidth - 10;
|
||||
}
|
||||
|
||||
height(slice) {
|
||||
const widgetHeight = this.props.widgetHeight;
|
||||
const headerHeight = this.headerHeight(slice);
|
||||
const descriptionId = this.getDescriptionId(slice);
|
||||
let descriptionHeight = 0;
|
||||
if (this.props.isExpanded && this.refs[descriptionId]) {
|
||||
descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
|
||||
}
|
||||
|
||||
return widgetHeight - headerHeight - descriptionHeight;
|
||||
}
|
||||
|
||||
headerHeight(slice) {
|
||||
const headerId = this.getHeaderId(slice);
|
||||
return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isExpanded, isLoading, isCached, cachedDttm,
|
||||
removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
|
||||
chartKey, slice, datasource, formData, timeout, annotationQuery,
|
||||
exploreChart, exportCSV,
|
||||
} = this.props;
|
||||
return (
|
||||
<div
|
||||
className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
|
||||
id={`${slice.slice_id}-cell`}
|
||||
>
|
||||
<div ref={this.getHeaderId(slice)}>
|
||||
<SliceHeader
|
||||
slice={slice}
|
||||
isExpanded={isExpanded}
|
||||
isCached={isCached}
|
||||
cachedDttm={cachedDttm}
|
||||
removeSlice={removeSlice}
|
||||
updateSliceName={updateSliceName}
|
||||
toggleExpandSlice={toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
editMode={this.props.editMode}
|
||||
annotationQuery={annotationQuery}
|
||||
exploreChart={exploreChart}
|
||||
exportCSV={exportCSV}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
/* This usage of dangerouslySetInnerHTML is safe since it is being used to render
|
||||
markdown that is sanitized with bleach. See:
|
||||
https://github.com/apache/incubator-superset/pull/4390
|
||||
and
|
||||
https://github.com/apache/incubator-superset/commit/b6fcc22d5a2cb7a5e92599ed5795a0169385a825 */}
|
||||
<div
|
||||
className="slice_description bs-callout bs-callout-default"
|
||||
style={isExpanded ? {} : { display: 'none' }}
|
||||
ref={this.getDescriptionId(slice)}
|
||||
dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
|
||||
/>
|
||||
<div className="row chart-container">
|
||||
<input type="hidden" value="false" />
|
||||
<ChartContainer
|
||||
containerId={`slice-container-${slice.slice_id}`}
|
||||
chartKey={chartKey}
|
||||
datasource={datasource}
|
||||
formData={formData}
|
||||
headerHeight={this.headerHeight(slice)}
|
||||
height={this.height(slice)}
|
||||
width={this.width()}
|
||||
timeout={timeout}
|
||||
vizType={slice.formData.viz_type}
|
||||
addFilter={this.addFilter}
|
||||
getFilters={this.getFilters}
|
||||
clearFilter={this.clearFilter}
|
||||
removeFilter={this.removeFilter}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GridCell.propTypes = propTypes;
|
||||
GridCell.defaultProps = defaultProps;
|
||||
|
||||
export default GridCell;
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout';
|
||||
|
||||
import GridCell from './GridCell';
|
||||
|
||||
require('react-grid-layout/css/styles.css');
|
||||
require('react-resizable/css/styles.css');
|
||||
|
||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
|
||||
const propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
datasources: PropTypes.object,
|
||||
charts: PropTypes.object.isRequired,
|
||||
filters: PropTypes.object,
|
||||
timeout: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
getFormDataExtra: PropTypes.func,
|
||||
exploreChart: PropTypes.func,
|
||||
exportCSV: PropTypes.func,
|
||||
fetchSlice: PropTypes.func,
|
||||
saveSlice: PropTypes.func,
|
||||
removeSlice: PropTypes.func,
|
||||
removeChart: PropTypes.func,
|
||||
updateDashboardLayout: PropTypes.func,
|
||||
toggleExpandSlice: PropTypes.func,
|
||||
addFilter: PropTypes.func,
|
||||
getFilters: PropTypes.func,
|
||||
clearFilter: PropTypes.func,
|
||||
removeFilter: PropTypes.func,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => ({}),
|
||||
getFormDataExtra: () => ({}),
|
||||
exploreChart: () => ({}),
|
||||
exportCSV: () => ({}),
|
||||
fetchSlice: () => ({}),
|
||||
saveSlice: () => ({}),
|
||||
removeSlice: () => ({}),
|
||||
removeChart: () => ({}),
|
||||
updateDashboardLayout: () => ({}),
|
||||
toggleExpandSlice: () => ({}),
|
||||
addFilter: () => ({}),
|
||||
getFilters: () => ({}),
|
||||
clearFilter: () => ({}),
|
||||
removeFilter: () => ({}),
|
||||
};
|
||||
|
||||
class GridLayout extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onResizeStop = this.onResizeStop.bind(this);
|
||||
this.onDragStop = this.onDragStop.bind(this);
|
||||
this.forceRefresh = this.forceRefresh.bind(this);
|
||||
this.removeSlice = this.removeSlice.bind(this);
|
||||
this.updateSliceName = this.props.dashboard.dash_edit_perm ?
|
||||
this.updateSliceName.bind(this) : null;
|
||||
}
|
||||
|
||||
onResizeStop(layout) {
|
||||
this.props.updateDashboardLayout(layout);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
onDragStop(layout) {
|
||||
this.props.updateDashboardLayout(layout);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
getWidgetId(slice) {
|
||||
return 'widget_' + slice.slice_id;
|
||||
}
|
||||
|
||||
getWidgetHeight(slice) {
|
||||
const widgetId = this.getWidgetId(slice);
|
||||
if (!widgetId || !this.refs[widgetId]) {
|
||||
return 400;
|
||||
}
|
||||
return this.refs[widgetId].offsetHeight;
|
||||
}
|
||||
|
||||
getWidgetWidth(slice) {
|
||||
const widgetId = this.getWidgetId(slice);
|
||||
if (!widgetId || !this.refs[widgetId]) {
|
||||
return 400;
|
||||
}
|
||||
return this.refs[widgetId].offsetWidth;
|
||||
}
|
||||
|
||||
findSliceIndexById(sliceId) {
|
||||
return this.props.dashboard.slices
|
||||
.map(slice => (slice.slice_id)).indexOf(sliceId);
|
||||
}
|
||||
|
||||
forceRefresh(sliceId) {
|
||||
return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true);
|
||||
}
|
||||
|
||||
removeSlice(slice) {
|
||||
if (!slice) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove slice dashboard and charts
|
||||
this.props.removeSlice(slice);
|
||||
this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
updateSliceName(sliceId, sliceName) {
|
||||
const index = this.findSliceIndexById(sliceId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSlice = this.props.dashboard.slices[index];
|
||||
if (currentSlice.slice_name === sliceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.saveSlice(currentSlice, sliceName);
|
||||
}
|
||||
|
||||
isExpanded(slice) {
|
||||
return this.props.dashboard.metadata.expanded_slices &&
|
||||
this.props.dashboard.metadata.expanded_slices[slice.slice_id];
|
||||
}
|
||||
|
||||
render() {
|
||||
const cells = this.props.dashboard.slices.map((slice) => {
|
||||
const chartKey = `slice_${slice.slice_id}`;
|
||||
const currentChart = this.props.charts[chartKey];
|
||||
const queryResponse = currentChart.queryResponse || {};
|
||||
return (
|
||||
<div
|
||||
id={'slice_' + slice.slice_id}
|
||||
key={slice.slice_id}
|
||||
data-slice-id={slice.slice_id}
|
||||
className={`widget ${slice.form_data.viz_type}`}
|
||||
ref={this.getWidgetId(slice)}
|
||||
>
|
||||
<GridCell
|
||||
slice={slice}
|
||||
chartKey={chartKey}
|
||||
datasource={this.props.datasources[slice.form_data.datasource]}
|
||||
filters={this.props.filters}
|
||||
formData={this.props.getFormDataExtra(slice)}
|
||||
timeout={this.props.timeout}
|
||||
widgetHeight={this.getWidgetHeight(slice)}
|
||||
widgetWidth={this.getWidgetWidth(slice)}
|
||||
exploreChart={this.props.exploreChart}
|
||||
exportCSV={this.props.exportCSV}
|
||||
isExpanded={!!this.isExpanded(slice)}
|
||||
isLoading={currentChart.chartStatus === 'loading'}
|
||||
isCached={queryResponse.is_cached}
|
||||
cachedDttm={queryResponse.cached_dttm}
|
||||
toggleExpandSlice={this.props.toggleExpandSlice}
|
||||
forceRefresh={this.forceRefresh}
|
||||
removeSlice={this.removeSlice}
|
||||
updateSliceName={this.updateSliceName}
|
||||
addFilter={this.props.addFilter}
|
||||
getFilters={this.props.getFilters}
|
||||
clearFilter={this.props.clearFilter}
|
||||
removeFilter={this.props.removeFilter}
|
||||
editMode={this.props.editMode}
|
||||
annotationQuery={currentChart.annotationQuery}
|
||||
annotationError={currentChart.annotationError}
|
||||
/>
|
||||
</div>);
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveReactGridLayout
|
||||
className="layout"
|
||||
layouts={{ lg: this.props.dashboard.layout }}
|
||||
onResizeStop={this.onResizeStop}
|
||||
onDragStop={this.onDragStop}
|
||||
cols={{ lg: 48, md: 48, sm: 40, xs: 32, xxs: 24 }}
|
||||
rowHeight={10}
|
||||
autoSize
|
||||
margin={[20, 20]}
|
||||
useCSSTransforms
|
||||
draggableHandle=".drag"
|
||||
>
|
||||
{cells}
|
||||
</ResponsiveReactGridLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout.propTypes = propTypes;
|
||||
GridLayout.defaultProps = defaultProps;
|
||||
|
||||
export default GridLayout;
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Controls from './Controls';
|
||||
import EditableTitle from '../../../../components/EditableTitle';
|
||||
import Button from '../../../../components/Button';
|
||||
import FaveStar from '../../../../components/FaveStar';
|
||||
import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger';
|
||||
import PromptV2ConversionModal from '../../PromptV2ConversionModal';
|
||||
import {
|
||||
Logger,
|
||||
LOG_ACTIONS_DISMISS_V2_PROMPT,
|
||||
LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
|
||||
} from '../../../../logger';
|
||||
import { t } from '../../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
isStarred: PropTypes.bool,
|
||||
addSlicesToDashboard: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
fetchFaveStar: PropTypes.func,
|
||||
renderSlices: PropTypes.func,
|
||||
saveFaveStar: PropTypes.func,
|
||||
serialize: PropTypes.func,
|
||||
startPeriodicRender: PropTypes.func,
|
||||
updateDashboardTitle: PropTypes.func,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
setEditMode: PropTypes.func.isRequired,
|
||||
handleConvertToV2: PropTypes.func.isRequired,
|
||||
unsavedChanges: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSaveTitle = this.handleSaveTitle.bind(this);
|
||||
this.toggleEditMode = this.toggleEditMode.bind(this);
|
||||
this.state = {
|
||||
showV2PromptModal: props.dashboard.promptV2Conversion,
|
||||
};
|
||||
this.toggleShowV2PromptModal = this.toggleShowV2PromptModal.bind(this);
|
||||
}
|
||||
handleSaveTitle(title) {
|
||||
this.props.updateDashboardTitle(title);
|
||||
}
|
||||
toggleEditMode() {
|
||||
this.props.setEditMode(!this.props.editMode);
|
||||
}
|
||||
toggleShowV2PromptModal() {
|
||||
const nextShowModal = !this.state.showV2PromptModal;
|
||||
this.setState({ showV2PromptModal: nextShowModal });
|
||||
if (nextShowModal) {
|
||||
Logger.append(
|
||||
LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
|
||||
{
|
||||
force_v2_edit: this.props.dashboard.forceV2Edit,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
Logger.append(
|
||||
LOG_ACTIONS_DISMISS_V2_PROMPT,
|
||||
{
|
||||
force_v2_edit: this.props.dashboard.forceV2Edit,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
renderUnsaved() {
|
||||
if (!this.props.unsavedChanges) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<InfoTooltipWithTrigger
|
||||
label="unsaved"
|
||||
tooltip={t('Unsaved changes')}
|
||||
icon="exclamation-triangle"
|
||||
className="text-danger m-r-5"
|
||||
placement="top"
|
||||
/>
|
||||
);
|
||||
}
|
||||
renderEditButton() {
|
||||
if (!this.props.dashboard.dash_save_perm) {
|
||||
return null;
|
||||
}
|
||||
const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
|
||||
return (
|
||||
<Button
|
||||
bsStyle="default"
|
||||
className="m-r-5"
|
||||
style={{ width: '150px' }}
|
||||
onClick={this.toggleEditMode}
|
||||
>
|
||||
{btnText}
|
||||
</Button>);
|
||||
}
|
||||
render() {
|
||||
const dashboard = this.props.dashboard;
|
||||
return (
|
||||
<div className="title">
|
||||
<div className="pull-left">
|
||||
<h1 className="outer-container pull-left">
|
||||
<EditableTitle
|
||||
title={dashboard.dashboard_title}
|
||||
canEdit={dashboard.dash_save_perm && this.props.editMode}
|
||||
onSaveTitle={this.handleSaveTitle}
|
||||
showTooltip={this.props.editMode}
|
||||
/>
|
||||
<span className="favstar m-l-5">
|
||||
<FaveStar
|
||||
itemId={dashboard.id}
|
||||
fetchFaveStar={this.props.fetchFaveStar}
|
||||
saveFaveStar={this.props.saveFaveStar}
|
||||
isStarred={this.props.isStarred}
|
||||
/>
|
||||
</span>
|
||||
{dashboard.promptV2Conversion && (
|
||||
<span
|
||||
role="none"
|
||||
className="v2-preview-badge"
|
||||
onClick={this.toggleShowV2PromptModal}
|
||||
>
|
||||
{t('Convert to v2')}
|
||||
<span className="fa fa-info-circle m-l-5" />
|
||||
</span>
|
||||
)}
|
||||
{this.renderUnsaved()}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="pull-right" style={{ marginTop: '35px' }}>
|
||||
{this.renderEditButton()}
|
||||
<Controls
|
||||
dashboard={dashboard}
|
||||
filters={this.props.filters}
|
||||
userId={this.props.userId}
|
||||
addSlicesToDashboard={this.props.addSlicesToDashboard}
|
||||
onSave={this.props.onSave}
|
||||
onChange={this.props.onChange}
|
||||
renderSlices={this.props.renderSlices}
|
||||
serialize={this.props.serialize}
|
||||
startPeriodicRender={this.props.startPeriodicRender}
|
||||
editMode={this.props.editMode}
|
||||
/>
|
||||
</div>
|
||||
<div className="clearfix" />
|
||||
{this.state.showV2PromptModal &&
|
||||
dashboard.promptV2Conversion &&
|
||||
!this.props.editMode && (
|
||||
<PromptV2ConversionModal
|
||||
onClose={this.toggleShowV2PromptModal}
|
||||
handleConvertToV2={this.props.handleConvertToV2}
|
||||
forceV2Edit={dashboard.forceV2Edit}
|
||||
v2AutoConvertDate={dashboard.v2AutoConvertDate}
|
||||
v2FeedbackUrl={dashboard.v2FeedbackUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Header.propTypes = propTypes;
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'react-select';
|
||||
import ModalTrigger from '../../../../components/ModalTrigger';
|
||||
import { t } from '../../../../locales';
|
||||
|
||||
const propTypes = {
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
initialRefreshFrequency: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
initialRefreshFrequency: 0,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
const options = [
|
||||
[0, t('Don\'t refresh')],
|
||||
[10, t('10 seconds')],
|
||||
[30, t('30 seconds')],
|
||||
[60, t('1 minute')],
|
||||
[300, t('5 minutes')],
|
||||
[1800, t('30 minutes')],
|
||||
[3600, t('1 hour')],
|
||||
[21600, t('6 hours')],
|
||||
[43200, t('12 hours')],
|
||||
[86400, t('24 hours')],
|
||||
].map(o => ({ value: o[0], label: o[1] }));
|
||||
|
||||
class RefreshIntervalModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
refreshFrequency: props.initialRefreshFrequency,
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
isMenuItem
|
||||
modalTitle={t('Refresh Interval')}
|
||||
modalBody={
|
||||
<div>
|
||||
{t('Choose the refresh frequency for this dashboard')}
|
||||
<Select
|
||||
options={options}
|
||||
value={this.state.refreshFrequency}
|
||||
onChange={(opt) => {
|
||||
this.setState({ refreshFrequency: opt.value });
|
||||
this.props.onChange(opt.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
RefreshIntervalModal.propTypes = propTypes;
|
||||
RefreshIntervalModal.defaultProps = defaultProps;
|
||||
|
||||
export default RefreshIntervalModal;
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
/* global window */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
|
||||
import $ from 'jquery';
|
||||
|
||||
import { getAjaxErrorMsg } from '../../../../modules/utils';
|
||||
import ModalTrigger from '../../../../components/ModalTrigger';
|
||||
import { t } from '../../../../locales';
|
||||
import Checkbox from '../../../../components/Checkbox';
|
||||
import withToasts from '../../../../messageToasts/enhancers/withToasts';
|
||||
|
||||
const propTypes = {
|
||||
css: PropTypes.string,
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
serialize: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
addDangerToast: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class SaveModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dashboard: props.dashboard,
|
||||
css: props.css,
|
||||
saveType: 'overwrite',
|
||||
newDashName: props.dashboard.dashboard_title + ' [copy]',
|
||||
duplicateSlices: false,
|
||||
};
|
||||
this.modal = null;
|
||||
this.handleSaveTypeChange = this.handleSaveTypeChange.bind(this);
|
||||
this.handleNameChange = this.handleNameChange.bind(this);
|
||||
this.saveDashboard = this.saveDashboard.bind(this);
|
||||
}
|
||||
toggleDuplicateSlices() {
|
||||
this.setState({ duplicateSlices: !this.state.duplicateSlices });
|
||||
}
|
||||
handleSaveTypeChange(event) {
|
||||
this.setState({
|
||||
saveType: event.target.value,
|
||||
});
|
||||
}
|
||||
handleNameChange(event) {
|
||||
this.setState({
|
||||
newDashName: event.target.value,
|
||||
saveType: 'newDashboard',
|
||||
});
|
||||
}
|
||||
saveDashboardRequest(data, url, saveType) {
|
||||
const saveModal = this.modal;
|
||||
const onSaveDashboard = this.props.onSave;
|
||||
Object.assign(data, { css: this.props.css });
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url,
|
||||
data: {
|
||||
data: JSON.stringify(data),
|
||||
},
|
||||
success: (resp) => {
|
||||
saveModal.close();
|
||||
onSaveDashboard();
|
||||
if (saveType === 'newDashboard') {
|
||||
window.location = `/superset/dashboard/${resp.id}/`;
|
||||
} else {
|
||||
this.props.addSuccessToast(
|
||||
t('This dashboard was saved successfully.'),
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
saveModal.close();
|
||||
const errorMsg = getAjaxErrorMsg(error);
|
||||
this.props.addDangerToast(
|
||||
t('Sorry, there was an error saving this dashboard: ') + errorMsg,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
saveDashboard(saveType, newDashboardTitle) {
|
||||
const dashboard = this.props.dashboard;
|
||||
const positions = this.props.serialize();
|
||||
const data = {
|
||||
positions,
|
||||
css: this.state.css,
|
||||
expanded_slices: dashboard.metadata.expanded_slices || {},
|
||||
dashboard_title: dashboard.dashboard_title,
|
||||
default_filters: JSON.stringify(this.props.filters),
|
||||
duplicate_slices: this.state.duplicateSlices,
|
||||
};
|
||||
let url = null;
|
||||
if (saveType === 'overwrite') {
|
||||
url = `/superset/save_dash/${dashboard.id}/`;
|
||||
this.saveDashboardRequest(data, url, saveType);
|
||||
} else if (saveType === 'newDashboard') {
|
||||
if (!newDashboardTitle) {
|
||||
this.modal.close();
|
||||
this.props.addDangerToast(
|
||||
t('You must pick a name for the new dashboard'),
|
||||
);
|
||||
} else {
|
||||
data.dashboard_title = newDashboardTitle;
|
||||
url = `/superset/copy_dash/${dashboard.id}/`;
|
||||
this.saveDashboardRequest(data, url, saveType);
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalTrigger
|
||||
ref={(modal) => { this.modal = modal; }}
|
||||
isMenuItem
|
||||
triggerNode={this.props.triggerNode}
|
||||
modalTitle={t('Save Dashboard')}
|
||||
modalBody={
|
||||
<FormGroup>
|
||||
<Radio
|
||||
value="overwrite"
|
||||
onChange={this.handleSaveTypeChange}
|
||||
checked={this.state.saveType === 'overwrite'}
|
||||
>
|
||||
{t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
|
||||
</Radio>
|
||||
<hr />
|
||||
<Radio
|
||||
value="newDashboard"
|
||||
onChange={this.handleSaveTypeChange}
|
||||
checked={this.state.saveType === 'newDashboard'}
|
||||
>
|
||||
{t('Save as:')}
|
||||
</Radio>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder={t('[dashboard name]')}
|
||||
value={this.state.newDashName}
|
||||
onFocus={this.handleNameChange}
|
||||
onChange={this.handleNameChange}
|
||||
/>
|
||||
<div className="m-l-25 m-t-5">
|
||||
<Checkbox
|
||||
checked={this.state.duplicateSlices}
|
||||
onChange={this.toggleDuplicateSlices.bind(this)}
|
||||
/>
|
||||
<span className="m-l-5">also copy (duplicate) charts</span>
|
||||
</div>
|
||||
</FormGroup>
|
||||
}
|
||||
modalFooter={
|
||||
<div>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={() => { this.saveDashboard(this.state.saveType, this.state.newDashName); }}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SaveModal.propTypes = propTypes;
|
||||
|
||||
export default withToasts(SaveModal);
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
import React from 'react';
|
||||
import $ from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
|
||||
|
||||
import Loading from '../../../../components/Loading';
|
||||
import ModalTrigger from '../../../../components/ModalTrigger';
|
||||
import { t } from '../../../../locales';
|
||||
|
||||
require('react-bootstrap-table/css/react-bootstrap-table.css');
|
||||
|
||||
const propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
addSlicesToDashboard: PropTypes.func,
|
||||
};
|
||||
|
||||
class SliceAdder extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
slices: [],
|
||||
slicesLoaded: false,
|
||||
selectionMap: {},
|
||||
};
|
||||
|
||||
this.options = {
|
||||
defaultSortOrder: 'desc',
|
||||
defaultSortName: 'modified',
|
||||
sizePerPage: 10,
|
||||
};
|
||||
|
||||
this.addSlices = this.addSlices.bind(this);
|
||||
this.toggleSlice = this.toggleSlice.bind(this);
|
||||
|
||||
this.selectRowProp = {
|
||||
mode: 'checkbox',
|
||||
clickToSelect: true,
|
||||
onSelect: this.toggleSlice,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.slicesRequest) {
|
||||
this.slicesRequest.abort();
|
||||
}
|
||||
}
|
||||
|
||||
onEnterModal() {
|
||||
const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
|
||||
this.slicesRequest = $.ajax({
|
||||
url: uri,
|
||||
type: 'GET',
|
||||
success: (response) => {
|
||||
// Prepare slice data for table
|
||||
const slices = response.result.map(slice => ({
|
||||
id: slice.id,
|
||||
sliceName: slice.slice_name,
|
||||
vizType: slice.viz_type,
|
||||
datasourceLink: slice.datasource_link,
|
||||
modified: slice.modified,
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
slices,
|
||||
selectionMap: {},
|
||||
slicesLoaded: true,
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
this.errored = true;
|
||||
this.setState({
|
||||
errorMsg: t('Sorry, there was an error fetching charts to this dashboard: ') +
|
||||
this.getAjaxErrorMsg(error),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAjaxErrorMsg(error) {
|
||||
const respJSON = error.responseJSON;
|
||||
return (respJSON && respJSON.message) ? respJSON.message :
|
||||
error.responseText;
|
||||
}
|
||||
|
||||
addSlices() {
|
||||
const adder = this;
|
||||
this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
|
||||
// if successful, page will be reloaded.
|
||||
.fail((error) => {
|
||||
adder.errored = true;
|
||||
adder.setState({
|
||||
errorMsg: t('Sorry, there was an error adding charts to this dashboard: ') +
|
||||
this.getAjaxErrorMsg(error),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleSlice(slice) {
|
||||
const selectionMap = Object.assign({}, this.state.selectionMap);
|
||||
selectionMap[slice.id] = !selectionMap[slice.id];
|
||||
this.setState({ selectionMap });
|
||||
}
|
||||
|
||||
modifiedDateComparator(a, b, order) {
|
||||
if (order === 'desc') {
|
||||
if (a.changed_on > b.changed_on) {
|
||||
return -1;
|
||||
} else if (a.changed_on < b.changed_on) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a.changed_on < b.changed_on) {
|
||||
return -1;
|
||||
} else if (a.changed_on > b.changed_on) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
const hideLoad = this.state.slicesLoaded || this.errored;
|
||||
let enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap);
|
||||
if (enableAddSlice) {
|
||||
enableAddSlice = enableAddSlice.some(function (key) {
|
||||
return this.state.selectionMap[key];
|
||||
}, this);
|
||||
}
|
||||
const modalContent = (
|
||||
<div>
|
||||
{!hideLoad && <Loading />}
|
||||
<div className={this.errored ? '' : 'hidden'}>
|
||||
{this.state.errorMsg}
|
||||
</div>
|
||||
<div className={this.state.slicesLoaded ? '' : 'hidden'}>
|
||||
<BootstrapTable
|
||||
ref="table"
|
||||
data={this.state.slices}
|
||||
selectRow={this.selectRowProp}
|
||||
options={this.options}
|
||||
hover
|
||||
search
|
||||
pagination
|
||||
condensed
|
||||
height="auto"
|
||||
>
|
||||
<TableHeaderColumn
|
||||
dataField="id"
|
||||
isKey
|
||||
dataSort
|
||||
hidden
|
||||
/>
|
||||
<TableHeaderColumn
|
||||
dataField="sliceName"
|
||||
dataSort
|
||||
>
|
||||
{t('Name')}
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn
|
||||
dataField="vizType"
|
||||
dataSort
|
||||
>
|
||||
{t('Viz')}
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn
|
||||
dataField="datasourceLink"
|
||||
dataSort
|
||||
// Will cause react-bootstrap-table to interpret the HTML returned
|
||||
dataFormat={datasourceLink => datasourceLink}
|
||||
>
|
||||
{t('Datasource')}
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn
|
||||
dataField="modified"
|
||||
dataSort
|
||||
sortFunc={this.modifiedDateComparator}
|
||||
// Will cause react-bootstrap-table to interpret the HTML returned
|
||||
dataFormat={modified => modified}
|
||||
>
|
||||
{t('Modified')}
|
||||
</TableHeaderColumn>
|
||||
</BootstrapTable>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-default"
|
||||
data-dismiss="modal"
|
||||
onClick={this.addSlices}
|
||||
disabled={!enableAddSlice}
|
||||
>
|
||||
{t('Add Charts')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalTrigger
|
||||
triggerNode={this.props.triggerNode}
|
||||
tooltip={t('Add a new chart to the dashboard')}
|
||||
beforeOpen={this.onEnterModal.bind(this)}
|
||||
isMenuItem
|
||||
modalBody={modalContent}
|
||||
bsSize="large"
|
||||
setModalAsTriggerChildren
|
||||
modalTitle={t('Add Charts to Dashboard')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SliceAdder.propTypes = propTypes;
|
||||
|
||||
export default SliceAdder;
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { t } from '../../../../locales';
|
||||
import EditableTitle from '../../../../components/EditableTitle';
|
||||
import TooltipWrapper from '../../../../components/TooltipWrapper';
|
||||
|
||||
const propTypes = {
|
||||
slice: PropTypes.object.isRequired,
|
||||
supersetCanExplore: PropTypes.bool,
|
||||
sliceCanEdit: PropTypes.bool,
|
||||
isExpanded: PropTypes.bool,
|
||||
isCached: PropTypes.bool,
|
||||
cachedDttm: PropTypes.string,
|
||||
removeSlice: PropTypes.func,
|
||||
updateSliceName: PropTypes.func,
|
||||
toggleExpandSlice: PropTypes.func,
|
||||
forceRefresh: PropTypes.func,
|
||||
exploreChart: PropTypes.func,
|
||||
exportCSV: PropTypes.func,
|
||||
editMode: PropTypes.bool,
|
||||
annotationQuery: PropTypes.object,
|
||||
annotationError: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
forceRefresh: () => ({}),
|
||||
removeSlice: () => ({}),
|
||||
updateSliceName: () => ({}),
|
||||
toggleExpandSlice: () => ({}),
|
||||
exploreChart: () => ({}),
|
||||
exportCSV: () => ({}),
|
||||
editMode: false,
|
||||
};
|
||||
|
||||
class SliceHeader extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onSaveTitle = this.onSaveTitle.bind(this);
|
||||
this.onToggleExpandSlice = this.onToggleExpandSlice.bind(this);
|
||||
this.exportCSV = this.props.exportCSV.bind(this, this.props.slice);
|
||||
this.exploreChart = this.props.exploreChart.bind(this, this.props.slice);
|
||||
this.forceRefresh = this.props.forceRefresh.bind(this, this.props.slice.slice_id);
|
||||
this.removeSlice = this.props.removeSlice.bind(this, this.props.slice);
|
||||
}
|
||||
|
||||
onSaveTitle(newTitle) {
|
||||
if (this.props.updateSliceName) {
|
||||
this.props.updateSliceName(this.props.slice.slice_id, newTitle);
|
||||
}
|
||||
}
|
||||
|
||||
onToggleExpandSlice() {
|
||||
this.props.toggleExpandSlice(this.props.slice, !this.props.isExpanded);
|
||||
}
|
||||
|
||||
render() {
|
||||
const slice = this.props.slice;
|
||||
const isCached = this.props.isCached;
|
||||
const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
|
||||
const refreshTooltip = isCached ?
|
||||
t('Served from data cached %s . Click to force refresh.', cachedWhen) :
|
||||
t('Force refresh data');
|
||||
const annoationsLoading = t('Annotation layers are still loading.');
|
||||
const annoationsError = t('One ore more annotation layers failed loading.');
|
||||
|
||||
return (
|
||||
<div className="row chart-header">
|
||||
<div className="col-md-12">
|
||||
<div className="header">
|
||||
<EditableTitle
|
||||
title={slice.slice_name}
|
||||
canEdit={!!this.props.updateSliceName && this.props.editMode}
|
||||
onSaveTitle={this.onSaveTitle}
|
||||
showTooltip={this.props.editMode}
|
||||
noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
|
||||
/>
|
||||
{!!Object.values(this.props.annotationQuery || {}).length &&
|
||||
<TooltipWrapper
|
||||
label="annotations-loading"
|
||||
placement="top"
|
||||
tooltip={annoationsLoading}
|
||||
>
|
||||
<i className="fa fa-refresh warning" />
|
||||
</TooltipWrapper>
|
||||
}
|
||||
{!!Object.values(this.props.annotationError || {}).length &&
|
||||
<TooltipWrapper
|
||||
label="annoation-errors"
|
||||
placement="top"
|
||||
tooltip={annoationsError}
|
||||
>
|
||||
<i className="fa fa-exclamation-circle danger" />
|
||||
</TooltipWrapper>
|
||||
}
|
||||
</div>
|
||||
<div className="chart-controls">
|
||||
<div id={'controls_' + slice.slice_id} className="pull-right">
|
||||
{this.props.editMode &&
|
||||
<a>
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="move"
|
||||
tooltip={t('Move chart')}
|
||||
>
|
||||
<i className="fa fa-arrows drag" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
}
|
||||
<a className={`refresh ${isCached ? 'danger' : ''}`} onClick={this.forceRefresh}>
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="refresh"
|
||||
tooltip={refreshTooltip}
|
||||
>
|
||||
<i className="fa fa-repeat" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
{slice.description &&
|
||||
<a onClick={this.onToggleExpandSlice}>
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="description"
|
||||
tooltip={t('Toggle chart description')}
|
||||
>
|
||||
<i className="fa fa-info-circle slice_info" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
}
|
||||
{this.props.sliceCanEdit &&
|
||||
<a href={slice.edit_url} target="_blank">
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="edit"
|
||||
tooltip={t('Edit chart')}
|
||||
>
|
||||
<i className="fa fa-pencil" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
}
|
||||
<a className="exportCSV" onClick={this.exportCSV}>
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="exportCSV"
|
||||
tooltip={t('Export CSV')}
|
||||
>
|
||||
<i className="fa fa-table" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
{this.props.supersetCanExplore &&
|
||||
<a className="exploreChart" onClick={this.exploreChart}>
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="exploreChart"
|
||||
tooltip={t('Explore chart')}
|
||||
>
|
||||
<i className="fa fa-share" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
}
|
||||
{this.props.editMode &&
|
||||
<a className="remove-chart" onClick={this.removeSlice}>
|
||||
<TooltipWrapper
|
||||
placement="top"
|
||||
label="close"
|
||||
tooltip={t('Remove chart from dashboard')}
|
||||
>
|
||||
<i className="fa fa-close" />
|
||||
</TooltipWrapper>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SliceHeader.propTypes = propTypes;
|
||||
SliceHeader.defaultProps = defaultProps;
|
||||
|
||||
function mapStateToProps({ dashboard }) {
|
||||
return {
|
||||
supersetCanExplore: dashboard.dashboard.superset_can_explore,
|
||||
sliceCanEdit: dashboard.dashboard.slice_can_edit,
|
||||
};
|
||||
}
|
||||
|
||||
export { SliceHeader };
|
||||
export default connect(mapStateToProps, () => ({}))(SliceHeader);
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { initEnhancer } from '../../../reduxUtils';
|
||||
import { appSetup } from '../../../common';
|
||||
import { initJQueryAjax } from '../../../modules/utils';
|
||||
import DashboardContainer from './components/DashboardContainer';
|
||||
import rootReducer, { getInitialState } from './reducers';
|
||||
|
||||
appSetup();
|
||||
initJQueryAjax();
|
||||
|
||||
const appContainer = document.getElementById('app');
|
||||
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
|
||||
const initState = getInitialState(bootstrapData);
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initState,
|
||||
compose(
|
||||
applyMiddleware(thunk),
|
||||
initEnhancer(false),
|
||||
),
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<DashboardContainer />
|
||||
</Provider>,
|
||||
appContainer,
|
||||
);
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
/* eslint-disable camelcase */
|
||||
import { combineReducers } from 'redux';
|
||||
import d3 from 'd3';
|
||||
import shortid from 'shortid';
|
||||
|
||||
import charts, { chart } from '../chart/chartReducer';
|
||||
import * as actions from './actions';
|
||||
import { getParam } from '../../../modules/utils';
|
||||
import { alterInArr, removeFromArr } from '../../../reduxUtils';
|
||||
import { applyDefaultFormData } from '../../../explore/store';
|
||||
import { getColorFromScheme } from '../../../modules/colors';
|
||||
import messageToasts from '../../../messageToasts/reducers';
|
||||
|
||||
export function getInitialState(bootstrapData) {
|
||||
const {
|
||||
user_id,
|
||||
datasources,
|
||||
common,
|
||||
editMode,
|
||||
prompt_v2_conversion,
|
||||
force_v2_edit,
|
||||
v2_auto_convert_date,
|
||||
v2_feedback_url,
|
||||
} = bootstrapData;
|
||||
delete common.locale;
|
||||
delete common.language_pack;
|
||||
|
||||
const dashboard = {
|
||||
...bootstrapData.dashboard_data,
|
||||
promptV2Conversion: prompt_v2_conversion,
|
||||
forceV2Edit: force_v2_edit,
|
||||
v2AutoConvertDate: v2_auto_convert_date,
|
||||
v2FeedbackUrl: v2_feedback_url,
|
||||
};
|
||||
let filters = {};
|
||||
try {
|
||||
// allow request parameter overwrite dashboard metadata
|
||||
filters = JSON.parse(
|
||||
getParam('preselect_filters') || dashboard.metadata.default_filters,
|
||||
);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
// Priming the color palette with user's label-color mapping provided in
|
||||
// the dashboard's JSON metadata
|
||||
if (dashboard.metadata && dashboard.metadata.label_colors) {
|
||||
const colorMap = dashboard.metadata.label_colors;
|
||||
for (const label in colorMap) {
|
||||
getColorFromScheme(label, null, colorMap[label]);
|
||||
}
|
||||
}
|
||||
|
||||
dashboard.posDict = {};
|
||||
dashboard.layout = [];
|
||||
if (Array.isArray(dashboard.position_json)) {
|
||||
dashboard.position_json.forEach(position => {
|
||||
dashboard.posDict[position.slice_id] = position;
|
||||
});
|
||||
} else {
|
||||
dashboard.position_json = [];
|
||||
}
|
||||
|
||||
const lastRowId = Math.max(
|
||||
0,
|
||||
Math.max.apply(
|
||||
null,
|
||||
dashboard.position_json.map(pos => pos.row + pos.size_y),
|
||||
),
|
||||
);
|
||||
let newSliceCounter = 0;
|
||||
dashboard.slices.forEach(slice => {
|
||||
const sliceId = slice.slice_id;
|
||||
let pos = dashboard.posDict[sliceId];
|
||||
if (!pos) {
|
||||
// append new slices to dashboard bottom, 3 slices per row
|
||||
pos = {
|
||||
col: (newSliceCounter % 3) * 16 + 1,
|
||||
row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
|
||||
size_x: 16,
|
||||
size_y: 16,
|
||||
};
|
||||
newSliceCounter++;
|
||||
}
|
||||
|
||||
dashboard.layout.push({
|
||||
i: String(sliceId),
|
||||
x: pos.col - 1,
|
||||
y: pos.row,
|
||||
w: pos.size_x,
|
||||
minW: 2,
|
||||
h: pos.size_y,
|
||||
});
|
||||
});
|
||||
|
||||
// will use charts action/reducers to handle chart render
|
||||
const initCharts = {};
|
||||
dashboard.slices.forEach(slice => {
|
||||
const chartKey = `slice_${slice.slice_id}`;
|
||||
initCharts[chartKey] = {
|
||||
...chart,
|
||||
chartKey,
|
||||
slice_id: slice.slice_id,
|
||||
form_data: slice.form_data,
|
||||
formData: applyDefaultFormData(slice.form_data),
|
||||
};
|
||||
});
|
||||
|
||||
// also need to add formData for dashboard.slices
|
||||
dashboard.slices = dashboard.slices.map(slice => ({
|
||||
...slice,
|
||||
formData: applyDefaultFormData(slice.form_data),
|
||||
}));
|
||||
|
||||
return {
|
||||
charts: initCharts,
|
||||
dashboard: {
|
||||
filters,
|
||||
dashboard,
|
||||
userId: user_id,
|
||||
datasources,
|
||||
common,
|
||||
editMode,
|
||||
},
|
||||
messageToasts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export const dashboard = function(state = {}, action) {
|
||||
const actionHandlers = {
|
||||
[actions.UPDATE_DASHBOARD_TITLE]() {
|
||||
const newDashboard = {
|
||||
...state.dashboard,
|
||||
dashboard_title: action.title,
|
||||
};
|
||||
return { ...state, dashboard: newDashboard };
|
||||
},
|
||||
[actions.UPDATE_DASHBOARD_LAYOUT]() {
|
||||
const newDashboard = { ...state.dashboard, layout: action.layout };
|
||||
return { ...state, dashboard: newDashboard };
|
||||
},
|
||||
[actions.REMOVE_SLICE]() {
|
||||
const key = String(action.slice.slice_id);
|
||||
const newLayout = state.dashboard.layout.filter(
|
||||
reactPos => reactPos.i !== key,
|
||||
);
|
||||
const newDashboard = removeFromArr(
|
||||
state.dashboard,
|
||||
'slices',
|
||||
action.slice,
|
||||
'slice_id',
|
||||
);
|
||||
// if this slice is a filter
|
||||
const newFilter = { ...state.filters };
|
||||
let refresh = false;
|
||||
if (state.filters[key]) {
|
||||
delete newFilter[key];
|
||||
refresh = true;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
dashboard: { ...newDashboard, layout: newLayout },
|
||||
filters: newFilter,
|
||||
refresh,
|
||||
};
|
||||
},
|
||||
[actions.TOGGLE_FAVE_STAR]() {
|
||||
return { ...state, isStarred: action.isStarred };
|
||||
},
|
||||
[actions.SET_EDIT_MODE]() {
|
||||
return { ...state, editMode: action.editMode };
|
||||
},
|
||||
[actions.TOGGLE_EXPAND_SLICE]() {
|
||||
const updatedExpandedSlices = {
|
||||
...state.dashboard.metadata.expanded_slices,
|
||||
};
|
||||
const sliceId = action.slice.slice_id;
|
||||
if (action.isExpanded) {
|
||||
updatedExpandedSlices[sliceId] = true;
|
||||
} else {
|
||||
delete updatedExpandedSlices[sliceId];
|
||||
}
|
||||
const metadata = {
|
||||
...state.dashboard.metadata,
|
||||
expanded_slices: updatedExpandedSlices,
|
||||
};
|
||||
const newDashboard = { ...state.dashboard, metadata };
|
||||
return { ...state, dashboard: newDashboard };
|
||||
},
|
||||
|
||||
// filters
|
||||
[actions.ADD_FILTER]() {
|
||||
const selectedSlice = state.dashboard.slices.find(
|
||||
slice => slice.slice_id === action.sliceId,
|
||||
);
|
||||
if (!selectedSlice) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let filters = state.filters;
|
||||
const { sliceId, col, vals, merge, refresh } = action;
|
||||
const filterKeys = [
|
||||
'__time_range',
|
||||
'__time_col',
|
||||
'__time_grain',
|
||||
'__time_origin',
|
||||
'__granularity',
|
||||
];
|
||||
if (
|
||||
filterKeys.indexOf(col) >= 0 ||
|
||||
selectedSlice.formData.groupby.indexOf(col) !== -1
|
||||
) {
|
||||
let newFilter = {};
|
||||
if (!(sliceId in filters)) {
|
||||
// Straight up set the filters if none existed for the slice
|
||||
newFilter = { [col]: vals };
|
||||
} else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
|
||||
newFilter = { ...filters[sliceId], [col]: vals };
|
||||
// d3.merge pass in array of arrays while some value form filter components
|
||||
// from and to filter box require string to be process and return
|
||||
} else if (filters[sliceId][col] instanceof Array) {
|
||||
newFilter[col] = d3.merge([filters[sliceId][col], vals]);
|
||||
} else {
|
||||
newFilter[col] = d3.merge([[filters[sliceId][col]], vals])[0] || '';
|
||||
}
|
||||
filters = { ...filters, [sliceId]: newFilter };
|
||||
}
|
||||
return { ...state, filters, refresh };
|
||||
},
|
||||
[actions.CLEAR_FILTER]() {
|
||||
const newFilters = { ...state.filters };
|
||||
delete newFilters[action.sliceId];
|
||||
return { ...state, filter: newFilters, refresh: true };
|
||||
},
|
||||
[actions.REMOVE_FILTER]() {
|
||||
const { sliceId, col, vals, refresh } = action;
|
||||
const excluded = new Set(vals);
|
||||
const valFilter = val => !excluded.has(val);
|
||||
|
||||
let filters = state.filters;
|
||||
// Have to be careful not to modify the dashboard state so that
|
||||
// the render actually triggers
|
||||
if (sliceId in state.filters && col in state.filters[sliceId]) {
|
||||
const newFilter = filters[sliceId][col].filter(valFilter);
|
||||
filters = { ...filters, [sliceId]: newFilter };
|
||||
}
|
||||
return { ...state, filters, refresh };
|
||||
},
|
||||
|
||||
// slice reducer
|
||||
[actions.UPDATE_SLICE_NAME]() {
|
||||
const newDashboard = alterInArr(
|
||||
state.dashboard,
|
||||
'slices',
|
||||
action.slice,
|
||||
{ slice_name: action.sliceName },
|
||||
'slice_id',
|
||||
);
|
||||
return { ...state, dashboard: newDashboard };
|
||||
},
|
||||
};
|
||||
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
charts,
|
||||
dashboard,
|
||||
impressionId: () => shortid.generate(),
|
||||
messageToasts,
|
||||
});
|
||||
|
|
@ -83,7 +83,6 @@ export default function dashboardStateReducer(state = {}, action) {
|
|||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
editMode: false,
|
||||
isV2Preview: false, // @TODO remove upon v1 deprecation
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import { getParam } from '../../modules/utils';
|
|||
import { applyDefaultFormData } from '../../explore/store';
|
||||
import { getColorFromScheme } from '../../modules/colors';
|
||||
import findFirstParentContainerId from '../util/findFirstParentContainer';
|
||||
import layoutConverter from '../util/dashboardLayoutConverter';
|
||||
import getEmptyLayout from '../util/getEmptyLayout';
|
||||
import newComponentFactory from '../util/newComponentFactory';
|
||||
import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
|
||||
import { DASHBOARD_HEADER_ID } from '../util/constants';
|
||||
import {
|
||||
DASHBOARD_HEADER_TYPE,
|
||||
CHART_TYPE,
|
||||
|
|
@ -18,15 +17,7 @@ import {
|
|||
} from '../util/componentTypes';
|
||||
|
||||
export default function(bootstrapData) {
|
||||
const {
|
||||
user_id,
|
||||
datasources,
|
||||
common,
|
||||
editMode,
|
||||
force_v2_edit: forceV2Edit,
|
||||
v2_auto_convert_date: v2AutoConvertDate,
|
||||
v2_feedback_url: v2FeedbackUrl,
|
||||
} = bootstrapData;
|
||||
const { user_id, datasources, common, editMode } = bootstrapData;
|
||||
delete common.locale;
|
||||
delete common.language_pack;
|
||||
|
||||
|
|
@ -52,12 +43,7 @@ export default function(bootstrapData) {
|
|||
|
||||
// dashboard layout
|
||||
const { position_json: positionJson } = dashboard;
|
||||
const shouldConvertToV2 =
|
||||
positionJson && positionJson[DASHBOARD_VERSION_KEY] !== 'v2';
|
||||
|
||||
const layout = shouldConvertToV2
|
||||
? layoutConverter(dashboard)
|
||||
: positionJson || getEmptyLayout();
|
||||
const layout = positionJson || getEmptyLayout();
|
||||
|
||||
// create a lookup to sync layout names with slice names
|
||||
const chartIdToLayoutId = {};
|
||||
|
|
@ -160,9 +146,6 @@ export default function(bootstrapData) {
|
|||
superset_can_explore: dashboard.superset_can_explore,
|
||||
slice_can_edit: dashboard.slice_can_edit,
|
||||
common,
|
||||
v2AutoConvertDate,
|
||||
v2FeedbackUrl,
|
||||
forceV2Edit,
|
||||
},
|
||||
dashboardState: {
|
||||
sliceIds: Array.from(sliceIds),
|
||||
|
|
@ -174,7 +157,6 @@ export default function(bootstrapData) {
|
|||
showBuilderPane: dashboard.dash_edit_perm && editMode,
|
||||
hasUnsavedChanges: false,
|
||||
maxUndoHistoryExceeded: false,
|
||||
isV2Preview: shouldConvertToV2,
|
||||
},
|
||||
dashboardLayout,
|
||||
messageToasts: [],
|
||||
|
|
|
|||
|
|
@ -167,28 +167,6 @@ body {
|
|||
position: relative;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* @TODO remove upon v1 deprecation */
|
||||
.v2-preview-badge {
|
||||
margin-left: 8px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
background: linear-gradient(to bottom right, @pink, @purple);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
line-height: 1em;
|
||||
cursor: pointer;
|
||||
opacity: 0.9;
|
||||
flex-wrap: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ace_gutter {
|
||||
|
|
|
|||
|
|
@ -1,513 +0,0 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable no-loop-func */
|
||||
import shortid from 'shortid';
|
||||
|
||||
import getEmptyLayout from './getEmptyLayout';
|
||||
|
||||
import {
|
||||
ROW_TYPE,
|
||||
COLUMN_TYPE,
|
||||
CHART_TYPE,
|
||||
MARKDOWN_TYPE,
|
||||
DASHBOARD_GRID_TYPE,
|
||||
} from './componentTypes';
|
||||
|
||||
import {
|
||||
DASHBOARD_GRID_ID,
|
||||
GRID_MIN_COLUMN_COUNT,
|
||||
GRID_MIN_ROW_UNITS,
|
||||
GRID_COLUMN_COUNT,
|
||||
} from './constants';
|
||||
|
||||
const MAX_RECURSIVE_LEVEL = 6;
|
||||
const GRID_RATIO = 4;
|
||||
const ROW_HEIGHT = 8;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param positions: single array of slices
|
||||
* @returns boundary object {top: number, bottom: number, left: number, right: number}
|
||||
*/
|
||||
function getBoundary(positions) {
|
||||
let top = Number.MAX_VALUE;
|
||||
let bottom = 0;
|
||||
let left = Number.MAX_VALUE;
|
||||
let right = 1;
|
||||
positions.forEach(item => {
|
||||
const { row, col, size_x, size_y } = item;
|
||||
if (row <= top) top = row;
|
||||
if (col <= left) left = col;
|
||||
if (bottom <= row + size_y) bottom = row + size_y;
|
||||
if (right <= col + size_x) right = col + size_x;
|
||||
});
|
||||
|
||||
return {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return shortid.generate();
|
||||
}
|
||||
|
||||
function getRowContainer() {
|
||||
return {
|
||||
type: ROW_TYPE,
|
||||
id: `DASHBOARD_ROW_TYPE-${generateId()}`,
|
||||
children: [],
|
||||
meta: {
|
||||
background: 'BACKGROUND_TRANSPARENT',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getColContainer() {
|
||||
return {
|
||||
type: COLUMN_TYPE,
|
||||
id: `DASHBOARD_COLUMN_TYPE-${generateId()}`,
|
||||
children: [],
|
||||
meta: {
|
||||
background: 'BACKGROUND_TRANSPARENT',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getChartHolder(item) {
|
||||
const { size_x, size_y, slice_id, code, slice_name } = item;
|
||||
|
||||
const width = Math.max(
|
||||
GRID_MIN_COLUMN_COUNT,
|
||||
Math.round(size_x / GRID_RATIO),
|
||||
);
|
||||
const height = Math.max(
|
||||
GRID_MIN_ROW_UNITS,
|
||||
Math.round(((size_y / GRID_RATIO) * 100) / ROW_HEIGHT),
|
||||
);
|
||||
if (code !== undefined) {
|
||||
let markdownContent = ' '; // white-space markdown
|
||||
if (code) {
|
||||
markdownContent = code;
|
||||
} else if (slice_name.trim()) {
|
||||
markdownContent = `##### ${slice_name}`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: MARKDOWN_TYPE,
|
||||
id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
|
||||
children: [],
|
||||
meta: {
|
||||
width,
|
||||
height,
|
||||
code: markdownContent,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: CHART_TYPE,
|
||||
id: `DASHBOARD_CHART_TYPE-${generateId()}`,
|
||||
children: [],
|
||||
meta: {
|
||||
width,
|
||||
height,
|
||||
chartId: parseInt(slice_id, 10),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getChildrenMax(items, attr, layout) {
|
||||
return Math.max.apply(null, items.map(child => layout[child].meta[attr]));
|
||||
}
|
||||
|
||||
function getChildrenSum(items, attr, layout) {
|
||||
return items.reduce(
|
||||
(preValue, child) => preValue + layout[child].meta[attr],
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function sortByRowId(item1, item2) {
|
||||
return item1.row - item2.row;
|
||||
}
|
||||
|
||||
function sortByColId(item1, item2) {
|
||||
return item1.col - item2.col;
|
||||
}
|
||||
|
||||
function hasOverlap(positions, xAxis = true) {
|
||||
return positions
|
||||
.slice()
|
||||
.sort(xAxis ? sortByColId : sortByRowId)
|
||||
.some((item, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xAxis) {
|
||||
return item.col + item.size_x > arr[index + 1].col;
|
||||
}
|
||||
return item.row + item.size_y > arr[index + 1].row;
|
||||
});
|
||||
}
|
||||
|
||||
function isWideLeafComponent(component) {
|
||||
return (
|
||||
[CHART_TYPE, MARKDOWN_TYPE].indexOf(component.type) > -1 &&
|
||||
component.meta.width > GRID_MIN_COLUMN_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
function canReduceColumnWidth(columnComponent, root) {
|
||||
return (
|
||||
columnComponent.type === COLUMN_TYPE &&
|
||||
columnComponent.meta.width > GRID_MIN_COLUMN_COUNT &&
|
||||
columnComponent.children.every(
|
||||
childId =>
|
||||
isWideLeafComponent(root[childId]) ||
|
||||
(root[childId].type === ROW_TYPE &&
|
||||
root[childId].children.every(id => isWideLeafComponent(root[id]))),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function reduceRowWidth(rowComponent, root) {
|
||||
// find widest free chart and reduce width
|
||||
const widestChartId = rowComponent.children
|
||||
.filter(childId => isWideLeafComponent(root[childId]))
|
||||
.reduce((prev, current) => {
|
||||
if (root[prev].meta.width >= root[current].meta.width) {
|
||||
return prev;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
|
||||
if (widestChartId) {
|
||||
root[widestChartId].meta.width -= 1;
|
||||
}
|
||||
return getChildrenSum(rowComponent.children, 'width', root);
|
||||
}
|
||||
|
||||
function reduceComponentWidth(component) {
|
||||
if (isWideLeafComponent(component)) {
|
||||
component.meta.width -= 1;
|
||||
}
|
||||
return component.meta.width;
|
||||
}
|
||||
|
||||
function doConvert(positions, level, parent, root) {
|
||||
if (positions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (positions.length === 1 || level >= MAX_RECURSIVE_LEVEL) {
|
||||
// special treatment for single chart dash, always wrap chart inside a row
|
||||
if (parent.type === DASHBOARD_GRID_TYPE) {
|
||||
const rowContainer = getRowContainer();
|
||||
root[rowContainer.id] = rowContainer;
|
||||
parent.children.push(rowContainer.id);
|
||||
parent = rowContainer;
|
||||
}
|
||||
|
||||
const chartHolder = getChartHolder(positions[0]);
|
||||
root[chartHolder.id] = chartHolder;
|
||||
parent.children.push(chartHolder.id);
|
||||
return;
|
||||
}
|
||||
|
||||
let currentItems = positions.slice();
|
||||
const { top, bottom, left, right } = getBoundary(positions);
|
||||
// find row dividers
|
||||
const layers = [];
|
||||
let currentRow = top + 1;
|
||||
while (currentItems.length && currentRow <= bottom) {
|
||||
const upper = [];
|
||||
const lower = [];
|
||||
|
||||
const isRowDivider = currentItems.every(item => {
|
||||
const { row, size_y } = item;
|
||||
if (row + size_y <= currentRow) {
|
||||
lower.push(item);
|
||||
return true;
|
||||
} else if (row >= currentRow) {
|
||||
upper.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isRowDivider) {
|
||||
currentItems = upper.slice();
|
||||
layers.push(lower);
|
||||
}
|
||||
currentRow += 1;
|
||||
}
|
||||
|
||||
layers.forEach(layer => {
|
||||
if (layer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layer.length === 1 && parent.type === COLUMN_TYPE) {
|
||||
const chartHolder = getChartHolder(layer[0]);
|
||||
root[chartHolder.id] = chartHolder;
|
||||
parent.children.push(chartHolder.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// create a new row
|
||||
const rowContainer = getRowContainer();
|
||||
root[rowContainer.id] = rowContainer;
|
||||
parent.children.push(rowContainer.id);
|
||||
|
||||
currentItems = layer.slice();
|
||||
if (!hasOverlap(currentItems)) {
|
||||
currentItems.sort(sortByColId).forEach(item => {
|
||||
const chartHolder = getChartHolder(item);
|
||||
root[chartHolder.id] = chartHolder;
|
||||
rowContainer.children.push(chartHolder.id);
|
||||
});
|
||||
} else {
|
||||
// find col dividers for each layer
|
||||
let currentCol = left + 1;
|
||||
while (currentItems.length && currentCol <= right) {
|
||||
const upper = [];
|
||||
const lower = [];
|
||||
|
||||
const isColDivider = currentItems.every(item => {
|
||||
const { col, size_x } = item;
|
||||
if (col + size_x <= currentCol) {
|
||||
lower.push(item);
|
||||
return true;
|
||||
} else if (col >= currentCol) {
|
||||
upper.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isColDivider) {
|
||||
if (lower.length === 1) {
|
||||
const chartHolder = getChartHolder(lower[0]);
|
||||
root[chartHolder.id] = chartHolder;
|
||||
rowContainer.children.push(chartHolder.id);
|
||||
} else {
|
||||
// create a new column
|
||||
const colContainer = getColContainer();
|
||||
root[colContainer.id] = colContainer;
|
||||
|
||||
if (!hasOverlap(lower, false)) {
|
||||
lower.sort(sortByRowId).forEach(item => {
|
||||
const chartHolder = getChartHolder(item);
|
||||
root[chartHolder.id] = chartHolder;
|
||||
colContainer.children.push(chartHolder.id);
|
||||
});
|
||||
} else {
|
||||
doConvert(lower, level + 2, colContainer, root);
|
||||
}
|
||||
|
||||
// add col meta
|
||||
if (colContainer.children.length) {
|
||||
rowContainer.children.push(colContainer.id);
|
||||
// add col meta
|
||||
colContainer.meta.width = getChildrenMax(
|
||||
colContainer.children,
|
||||
'width',
|
||||
root,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
currentItems = upper.slice();
|
||||
}
|
||||
currentCol += 1;
|
||||
}
|
||||
}
|
||||
|
||||
rowContainer.meta.width = getChildrenSum(
|
||||
rowContainer.children,
|
||||
'width',
|
||||
root,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function convertToLayout(positions) {
|
||||
const root = getEmptyLayout();
|
||||
|
||||
doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
|
||||
|
||||
// remove row's width/height and col's height
|
||||
Object.values(root).forEach(item => {
|
||||
if (ROW_TYPE === item.type) {
|
||||
const meta = item.meta;
|
||||
if (meta.width > GRID_COLUMN_COUNT) {
|
||||
let currentWidth = meta.width;
|
||||
while (
|
||||
currentWidth > GRID_COLUMN_COUNT &&
|
||||
item.children.filter(id => isWideLeafComponent(root[id])).length
|
||||
) {
|
||||
currentWidth = reduceRowWidth(item, root);
|
||||
}
|
||||
|
||||
// reduce column width
|
||||
if (currentWidth > GRID_COLUMN_COUNT) {
|
||||
// find column that: width > 2 and each row has at least 1 chart can reduce
|
||||
// 2 loops: same column may reduce multiple times
|
||||
let colIds;
|
||||
do {
|
||||
colIds = item.children.filter(colId =>
|
||||
canReduceColumnWidth(root[colId], root),
|
||||
);
|
||||
let idx = 0;
|
||||
while (idx < colIds.length && currentWidth > GRID_COLUMN_COUNT) {
|
||||
const currentColumn = colIds[idx];
|
||||
root[currentColumn].children.forEach(childId => {
|
||||
if (root[childId].type === ROW_TYPE) {
|
||||
root[childId].meta.width = reduceRowWidth(
|
||||
root[childId],
|
||||
root,
|
||||
);
|
||||
} else {
|
||||
root[childId].meta.width = reduceComponentWidth(
|
||||
root[childId],
|
||||
);
|
||||
}
|
||||
});
|
||||
root[currentColumn].meta.width = getChildrenMax(
|
||||
root[currentColumn].children,
|
||||
'width',
|
||||
root,
|
||||
);
|
||||
currentWidth = getChildrenSum(item.children, 'width', root);
|
||||
idx += 1;
|
||||
}
|
||||
} while (colIds.length && currentWidth > GRID_COLUMN_COUNT);
|
||||
}
|
||||
}
|
||||
delete meta.width;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log(JSON.stringify(root));
|
||||
return root;
|
||||
}
|
||||
|
||||
function mergePosition(position, bottomLine, maxColumn) {
|
||||
const { col, size_x, size_y } = position;
|
||||
const endColumn = col + size_x > maxColumn ? bottomLine.length : col + size_x;
|
||||
const sectionLength =
|
||||
bottomLine.slice(col).findIndex(value => value > bottomLine[col]) + 1;
|
||||
|
||||
const currentBottom =
|
||||
sectionLength > 0 && sectionLength < size_x
|
||||
? Math.max.apply(null, bottomLine.slice(col, col + size_x))
|
||||
: bottomLine[col];
|
||||
bottomLine.fill(currentBottom + size_y, col, endColumn);
|
||||
}
|
||||
|
||||
// In original position data, a lot of position's row attribute are not correct, and same positions are
|
||||
// assigned to more than 1 chart. The convert function depends on row id, col id to split
|
||||
// the whole dashboard into nested rows and columns. Bad row id will lead to many empty spaces, or a few
|
||||
// charts are overlapped in the same row.
|
||||
// This function read positions by row first. Then based on previous col id, width and height attribute,
|
||||
// re-calculate next position's row id.
|
||||
function scanDashboardPositionsData(positions) {
|
||||
const bottomLine = new Array(49).fill(0);
|
||||
bottomLine[0] = Number.MAX_VALUE;
|
||||
const maxColumn = Math.max.apply(
|
||||
null,
|
||||
positions.slice().map(position => position.col),
|
||||
);
|
||||
|
||||
const positionsByRowId = {};
|
||||
positions
|
||||
.slice()
|
||||
.sort(sortByRowId)
|
||||
.forEach(position => {
|
||||
const { row } = position;
|
||||
if (positionsByRowId[row] === undefined) {
|
||||
positionsByRowId[row] = [];
|
||||
}
|
||||
positionsByRowId[row].push(position);
|
||||
});
|
||||
const rawPositions = Object.values(positionsByRowId);
|
||||
const updatedPositions = [];
|
||||
|
||||
while (rawPositions.length) {
|
||||
const nextRow = rawPositions.shift();
|
||||
let nextCol = 1;
|
||||
while (nextRow.length) {
|
||||
// special treatment for duplicated positions: display wider one first
|
||||
const availableIndexByColumn = nextRow
|
||||
.filter(position => position.col === nextCol)
|
||||
.map((position, index) => index);
|
||||
if (availableIndexByColumn.length) {
|
||||
const idx =
|
||||
availableIndexByColumn.length > 1
|
||||
? availableIndexByColumn.sort(
|
||||
(idx1, idx2) => nextRow[idx2].size_x - nextRow[idx1].size_x,
|
||||
)[0]
|
||||
: availableIndexByColumn[0];
|
||||
|
||||
const nextPosition = nextRow.splice(idx, 1)[0];
|
||||
mergePosition(nextPosition, bottomLine, maxColumn + 1);
|
||||
nextPosition.row = bottomLine[nextPosition.col] - nextPosition.size_y;
|
||||
updatedPositions.push(nextPosition);
|
||||
nextCol += nextPosition.size_x;
|
||||
} else {
|
||||
nextCol = nextRow[0].col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedPositions;
|
||||
}
|
||||
|
||||
export default function(dashboard) {
|
||||
const positions = [];
|
||||
let { position_json } = dashboard;
|
||||
const positionDict = {};
|
||||
if (Array.isArray(position_json)) {
|
||||
// scan and fix positions data: extra spaces, dup rows, .etc
|
||||
position_json = scanDashboardPositionsData(position_json);
|
||||
position_json.forEach(position => {
|
||||
positionDict[position.slice_id] = position;
|
||||
});
|
||||
} else {
|
||||
position_json = [];
|
||||
}
|
||||
|
||||
// position data clean up. some dashboard didn't have position_json
|
||||
const lastRowId = Math.max(
|
||||
0,
|
||||
Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
|
||||
);
|
||||
let newSliceCounter = 0;
|
||||
dashboard.slices.forEach(({ slice_id, form_data, slice_name }) => {
|
||||
let position = positionDict[slice_id];
|
||||
if (!position) {
|
||||
// append new slices to dashboard bottom, 3 slices per row
|
||||
position = {
|
||||
col: (newSliceCounter % 3) * 16 + 1,
|
||||
row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
|
||||
size_x: 16,
|
||||
size_y: 16,
|
||||
slice_id,
|
||||
};
|
||||
newSliceCounter += 1;
|
||||
}
|
||||
if (form_data && ['markup', 'separator'].indexOf(form_data.viz_type) > -1) {
|
||||
position = {
|
||||
...position,
|
||||
code: form_data.code,
|
||||
slice_name,
|
||||
};
|
||||
}
|
||||
positions.push(position);
|
||||
});
|
||||
|
||||
return convertToLayout(positions);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { Modal, Alert, Button, Radio } from 'react-bootstrap';
|
|||
import Select from 'react-select';
|
||||
import { t } from '../../locales';
|
||||
import { supersetURL } from '../../utils/common';
|
||||
import { EXPLORE_ONLY_VIZ_TYPE } from '../constants';
|
||||
|
||||
const propTypes = {
|
||||
can_overwrite: PropTypes.bool,
|
||||
|
|
@ -31,6 +32,7 @@ class SaveModal extends React.Component {
|
|||
alert: null,
|
||||
action: props.can_overwrite ? 'overwrite' : 'saveas',
|
||||
addToDash: 'noSave',
|
||||
vizType: props.form_data.viz_type,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
|
|
@ -122,6 +124,7 @@ class SaveModal extends React.Component {
|
|||
this.setState({ alert: null });
|
||||
}
|
||||
render() {
|
||||
const canNotSaveToDash = EXPLORE_ONLY_VIZ_TYPE.indexOf(this.state.vizType) > -1;
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
|
|
@ -182,6 +185,7 @@ class SaveModal extends React.Component {
|
|||
|
||||
<Radio
|
||||
inline
|
||||
disabled={canNotSaveToDash}
|
||||
checked={this.state.addToDash === 'existing'}
|
||||
onChange={this.changeDash.bind(this, 'existing')}
|
||||
>
|
||||
|
|
@ -189,6 +193,7 @@ class SaveModal extends React.Component {
|
|||
</Radio>
|
||||
<Select
|
||||
className="save-modal-selector"
|
||||
disabled={canNotSaveToDash}
|
||||
options={this.props.dashboards}
|
||||
onChange={this.onChange.bind(this, 'saveToDashboardId')}
|
||||
autoSize={false}
|
||||
|
|
@ -200,11 +205,13 @@ class SaveModal extends React.Component {
|
|||
inline
|
||||
checked={this.state.addToDash === 'new'}
|
||||
onChange={this.changeDash.bind(this, 'new')}
|
||||
disabled={canNotSaveToDash}
|
||||
>
|
||||
{t('Add to new dashboard')}
|
||||
</Radio>
|
||||
<input
|
||||
onChange={this.onChange.bind(this, 'newDashboardName')}
|
||||
disabled={canNotSaveToDash}
|
||||
onFocus={this.changeDash.bind(this, 'new')}
|
||||
placeholder={t('[dashboard name]')}
|
||||
/>
|
||||
|
|
@ -224,7 +231,7 @@ class SaveModal extends React.Component {
|
|||
type="button"
|
||||
id="btn_modal_save_goto_dash"
|
||||
className="btn btn-primary pull-left gotodash"
|
||||
disabled={this.state.addToDash === 'noSave'}
|
||||
disabled={this.state.addToDash === 'noSave' || canNotSaveToDash}
|
||||
onClick={this.saveOrOverwrite.bind(this, true)}
|
||||
>
|
||||
{t('Save & go to dashboard')}
|
||||
|
|
|
|||
|
|
@ -37,3 +37,5 @@ export const MULTI_OPERATORS = [OPERATORS.in, OPERATORS['not in']];
|
|||
export const sqlaAutoGeneratedMetricNameRegex = /^(sum|min|max|avg|count|count_distinct)__.*$/i;
|
||||
export const sqlaAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|AVG|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i;
|
||||
export const druidAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i;
|
||||
|
||||
export const EXPLORE_ONLY_VIZ_TYPE = ['separator', 'markup'];
|
||||
|
|
|
|||
|
|
@ -145,13 +145,6 @@ export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart';
|
|||
export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART = 'export_csv_dashboard_chart';
|
||||
export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter';
|
||||
|
||||
// @TODO remove upon v1 deprecation
|
||||
export const LOG_ACTIONS_PREVIEW_V2 = 'preview_dashboard_v2';
|
||||
export const LOG_ACTIONS_FALLBACK_TO_V1 = 'fallback_to_dashboard_v1';
|
||||
export const LOG_ACTIONS_READ_ABOUT_V2_CHANGES = 'read_about_v2_changes';
|
||||
export const LOG_ACTIONS_DISMISS_V2_PROMPT = 'dismiss_v2_conversion_prompt';
|
||||
export const LOG_ACTIONS_SHOW_V2_INFO_PROMPT = 'show_v2_conversion_prompt';
|
||||
|
||||
export const DASHBOARD_EVENT_NAMES = [
|
||||
LOG_ACTIONS_MOUNT_DASHBOARD,
|
||||
LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
|
||||
|
|
@ -163,12 +156,6 @@ export const DASHBOARD_EVENT_NAMES = [
|
|||
LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
|
||||
LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
|
||||
LOG_ACTIONS_REFRESH_DASHBOARD,
|
||||
|
||||
LOG_ACTIONS_PREVIEW_V2,
|
||||
LOG_ACTIONS_FALLBACK_TO_V1,
|
||||
LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
|
||||
LOG_ACTIONS_DISMISS_V2_PROMPT,
|
||||
LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
|
||||
];
|
||||
|
||||
export const EXPLORE_EVENT_NAMES = [
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ const config = {
|
|||
addSlice: ['babel-polyfill', APP_DIR + '/src/addSlice/index.jsx'],
|
||||
explore: ['babel-polyfill', APP_DIR + '/src/explore/index.jsx'],
|
||||
dashboard: ['babel-polyfill', APP_DIR + '/src/dashboard/index.jsx'],
|
||||
dashboard_deprecated: ['babel-polyfill', APP_DIR + '/src/dashboard/deprecated/v1/index.jsx'],
|
||||
sqllab: ['babel-polyfill', APP_DIR + '/src/SqlLab/index.jsx'],
|
||||
welcome: ['babel-polyfill', APP_DIR + '/src/welcome/index.jsx'],
|
||||
profile: ['babel-polyfill', APP_DIR + '/src/profile/index.jsx'],
|
||||
|
|
|
|||
|
|
@ -401,10 +401,10 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
|
|||
self.json_metadata = value
|
||||
|
||||
@property
|
||||
def position_array(self):
|
||||
def position(self):
|
||||
if self.position_json:
|
||||
return json.loads(self.position_json)
|
||||
return []
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def import_obj(cls, dashboard_to_import, import_time=None):
|
||||
|
|
@ -420,16 +420,7 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
|
|||
def alter_positions(dashboard, old_to_new_slc_id_dict):
|
||||
""" Updates slice_ids in the position json.
|
||||
|
||||
Sample position json v1:
|
||||
[{
|
||||
"col": 5,
|
||||
"row": 10,
|
||||
"size_x": 4,
|
||||
"size_y": 2,
|
||||
"slice_id": "3610"
|
||||
}]
|
||||
|
||||
Sample position json v2:
|
||||
Sample position_json data:
|
||||
{
|
||||
"DASHBOARD_VERSION_KEY": "v2",
|
||||
"DASHBOARD_ROOT_ID": {
|
||||
|
|
@ -455,32 +446,17 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
|
|||
}
|
||||
"""
|
||||
position_data = json.loads(dashboard.position_json)
|
||||
is_v2_dash = (
|
||||
isinstance(position_data, dict) and
|
||||
position_data.get('DASHBOARD_VERSION_KEY') == 'v2'
|
||||
)
|
||||
if is_v2_dash:
|
||||
position_json = position_data.values()
|
||||
for value in position_json:
|
||||
if (isinstance(value, dict) and value.get('meta') and
|
||||
value.get('meta').get('chartId')):
|
||||
old_slice_id = value.get('meta').get('chartId')
|
||||
position_json = position_data.values()
|
||||
for value in position_json:
|
||||
if (isinstance(value, dict) and value.get('meta') and
|
||||
value.get('meta').get('chartId')):
|
||||
old_slice_id = value.get('meta').get('chartId')
|
||||
|
||||
if old_slice_id in old_to_new_slc_id_dict:
|
||||
value['meta']['chartId'] = (
|
||||
old_to_new_slc_id_dict[old_slice_id]
|
||||
)
|
||||
dashboard.position_json = json.dumps(position_data)
|
||||
else:
|
||||
position_array = dashboard.position_array
|
||||
for position in position_array:
|
||||
if 'slice_id' not in position:
|
||||
continue
|
||||
old_slice_id = int(position['slice_id'])
|
||||
if old_slice_id in old_to_new_slc_id_dict:
|
||||
position['slice_id'] = '{}'.format(
|
||||
old_to_new_slc_id_dict[old_slice_id])
|
||||
dashboard.position_json = json.dumps(position_array)
|
||||
value['meta']['chartId'] = (
|
||||
old_to_new_slc_id_dict[old_slice_id]
|
||||
)
|
||||
dashboard.position_json = json.dumps(position_data)
|
||||
|
||||
logging.info('Started import of the dashboard: {}'
|
||||
.format(dashboard_to_import.to_json()))
|
||||
|
|
|
|||
|
|
@ -1561,7 +1561,6 @@ class Superset(BaseSupersetView):
|
|||
dash.owners = [g.user] if g.user else []
|
||||
dash.dashboard_title = data['dashboard_title']
|
||||
|
||||
is_v2_dash = Superset._is_v2_dash(data['positions'])
|
||||
if data['duplicate_slices']:
|
||||
# Duplicating slices as well, mapping old ids to new ones
|
||||
old_to_new_sliceids = {}
|
||||
|
|
@ -1577,18 +1576,14 @@ class Superset(BaseSupersetView):
|
|||
# update chartId of layout entities
|
||||
# in v2_dash positions json data, chartId should be integer,
|
||||
# while in older version slice_id is string type
|
||||
if is_v2_dash:
|
||||
for value in data['positions'].values():
|
||||
if (
|
||||
isinstance(value, dict) and value.get('meta') and
|
||||
value.get('meta').get('chartId')
|
||||
):
|
||||
old_id = '{}'.format(value.get('meta').get('chartId'))
|
||||
new_id = int(old_to_new_sliceids[old_id])
|
||||
value['meta']['chartId'] = new_id
|
||||
else:
|
||||
for d in data['positions']:
|
||||
d['slice_id'] = old_to_new_sliceids[d['slice_id']]
|
||||
for value in data['positions'].values():
|
||||
if (
|
||||
isinstance(value, dict) and value.get('meta') and
|
||||
value.get('meta').get('chartId')
|
||||
):
|
||||
old_id = '{}'.format(value.get('meta').get('chartId'))
|
||||
new_id = int(old_to_new_sliceids[old_id])
|
||||
value['meta']['chartId'] = new_id
|
||||
else:
|
||||
dash.slices = original_dash.slices
|
||||
dash.params = original_dash.params
|
||||
|
|
@ -1617,43 +1612,9 @@ class Superset(BaseSupersetView):
|
|||
session.close()
|
||||
return 'SUCCESS'
|
||||
|
||||
@staticmethod
|
||||
def _is_v2_dash(positions):
|
||||
return (
|
||||
isinstance(positions, dict) and
|
||||
positions.get('DASHBOARD_VERSION_KEY') == 'v2'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _set_dash_metadata(dashboard, data):
|
||||
positions = data['positions']
|
||||
is_v2_dash = Superset._is_v2_dash(positions)
|
||||
|
||||
# @TODO remove upon v1 deprecation
|
||||
if not is_v2_dash:
|
||||
positions = data['positions']
|
||||
slice_ids = [int(d['slice_id']) for d in positions]
|
||||
dashboard.slices = [o for o in dashboard.slices if o.id in slice_ids]
|
||||
positions = sorted(data['positions'], key=lambda x: int(x['slice_id']))
|
||||
dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
|
||||
md = dashboard.params_dict
|
||||
dashboard.css = data['css']
|
||||
dashboard.dashboard_title = data['dashboard_title']
|
||||
|
||||
if 'filter_immune_slices' not in md:
|
||||
md['filter_immune_slices'] = []
|
||||
if 'timed_refresh_immune_slices' not in md:
|
||||
md['timed_refresh_immune_slices'] = []
|
||||
if 'filter_immune_slice_fields' not in md:
|
||||
md['filter_immune_slice_fields'] = {}
|
||||
md['expanded_slices'] = data['expanded_slices']
|
||||
default_filters_data = json.loads(data.get('default_filters', '{}'))
|
||||
applicable_filters =\
|
||||
{key: v for key, v in default_filters_data.items()
|
||||
if int(key) in slice_ids}
|
||||
md['default_filters'] = json.dumps(applicable_filters)
|
||||
dashboard.json_metadata = json.dumps(md, indent=4)
|
||||
return
|
||||
|
||||
# find slices in the position data
|
||||
slice_ids = []
|
||||
|
|
@ -2140,57 +2101,14 @@ class Superset(BaseSupersetView):
|
|||
standalone_mode = request.args.get('standalone') == 'true'
|
||||
edit_mode = request.args.get('edit') == 'true'
|
||||
|
||||
# TODO remove switch upon v1 deprecation 🎉
|
||||
# during v2 rollout, multiple factors determine whether we show v1 or v2
|
||||
# if layout == v1
|
||||
# view = v1 for non-editors
|
||||
# view = v1 or v2 for editors depending on config + request (force)
|
||||
# edit = v1 or v2 for editors depending on config + request (force)
|
||||
#
|
||||
# if layout == v2 (not backwards compatible)
|
||||
# view = v2
|
||||
# edit = v2
|
||||
dashboard_layout = dash.data.get('position_json', {})
|
||||
is_v2_dash = (
|
||||
isinstance(dashboard_layout, dict) and
|
||||
dashboard_layout.get('DASHBOARD_VERSION_KEY') == 'v2'
|
||||
)
|
||||
|
||||
force_v1 = request.args.get('version') == 'v1' and not is_v2_dash
|
||||
force_v2 = request.args.get('version') == 'v2'
|
||||
force_v2_edit = (
|
||||
is_v2_dash or
|
||||
not app.config.get('CAN_FALLBACK_TO_DASH_V1_EDIT_MODE')
|
||||
)
|
||||
v2_is_default_view = app.config.get('DASH_V2_IS_DEFAULT_VIEW_FOR_EDITORS')
|
||||
prompt_v2_conversion = False
|
||||
if is_v2_dash:
|
||||
dashboard_view = 'v2'
|
||||
elif not dash_edit_perm:
|
||||
dashboard_view = 'v1'
|
||||
else:
|
||||
if force_v2 or (v2_is_default_view and not force_v1):
|
||||
dashboard_view = 'v2'
|
||||
else:
|
||||
dashboard_view = 'v1'
|
||||
prompt_v2_conversion = not force_v1
|
||||
if force_v2_edit:
|
||||
dash_edit_perm = False
|
||||
|
||||
# Hack to log the dashboard_id properly, even when getting a slug
|
||||
@log_this
|
||||
def dashboard(**kwargs): # noqa
|
||||
pass
|
||||
|
||||
# TODO remove extra logging upon v1 deprecation 🎉
|
||||
dashboard(
|
||||
dashboard_id=dash.id,
|
||||
dashboard_version='v2' if is_v2_dash else 'v1',
|
||||
dashboard_view=dashboard_view,
|
||||
dashboard_version='v2',
|
||||
dash_edit_perm=dash_edit_perm,
|
||||
force_v1=force_v1,
|
||||
force_v2=force_v2,
|
||||
force_v2_edit=force_v2_edit,
|
||||
edit_mode=edit_mode)
|
||||
|
||||
dashboard_data = dash.data
|
||||
|
|
@ -2208,26 +2126,14 @@ class Superset(BaseSupersetView):
|
|||
'datasources': {ds.uid: ds.data for ds in datasources},
|
||||
'common': self.common_bootsrap_payload(),
|
||||
'editMode': edit_mode,
|
||||
# TODO remove the following upon v1 deprecation 🎉
|
||||
'force_v2_edit': force_v2_edit,
|
||||
'prompt_v2_conversion': prompt_v2_conversion,
|
||||
'v2_auto_convert_date': app.config.get('PLANNED_V2_AUTO_CONVERT_DATE'),
|
||||
'v2_feedback_url': app.config.get('V2_FEEDBACK_URL'),
|
||||
}
|
||||
|
||||
if request.args.get('json') == 'true':
|
||||
return json_success(json.dumps(bootstrap_data))
|
||||
|
||||
if dashboard_view == 'v2':
|
||||
entry = 'dashboard'
|
||||
template = 'superset/dashboard.html'
|
||||
else:
|
||||
entry = 'dashboard_deprecated'
|
||||
template = 'superset/dashboard_v1_deprecated.html'
|
||||
|
||||
return self.render_template(
|
||||
template,
|
||||
entry=entry,
|
||||
'superset/dashboard.html',
|
||||
entry='dashboard',
|
||||
standalone_mode=standalone_mode,
|
||||
title=dash.dashboard_title,
|
||||
bootstrap_data=json.dumps(bootstrap_data),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,25 @@ class DashboardTests(SupersetTestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def get_mock_positions(self, dash):
|
||||
positions = {
|
||||
'DASHBOARD_VERSION_KEY': 'v2',
|
||||
}
|
||||
for i, slc in enumerate(dash.slices):
|
||||
id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
|
||||
d = {
|
||||
'type': 'DASHBOARD_CHART_TYPE',
|
||||
'id': id,
|
||||
'children': [],
|
||||
'meta': {
|
||||
'width': 4,
|
||||
'height': 50,
|
||||
'chartId': slc.id,
|
||||
},
|
||||
}
|
||||
positions[id] = d
|
||||
return positions
|
||||
|
||||
def test_dashboard(self):
|
||||
self.login(username='admin')
|
||||
urls = {}
|
||||
|
|
@ -61,10 +80,11 @@ class DashboardTests(SupersetTestCase):
|
|||
self.login(username=username)
|
||||
dash = db.session.query(models.Dashboard).filter_by(
|
||||
slug='births').first()
|
||||
positions = self.get_mock_positions(dash)
|
||||
data = {
|
||||
'css': '',
|
||||
'expanded_slices': {},
|
||||
'positions': dash.position_array,
|
||||
'positions': positions,
|
||||
'dashboard_title': dash.dashboard_title,
|
||||
}
|
||||
url = '/superset/save_dash/{}/'.format(dash.id)
|
||||
|
|
@ -76,12 +96,13 @@ class DashboardTests(SupersetTestCase):
|
|||
dash = db.session.query(models.Dashboard).filter_by(
|
||||
slug='world_health').first()
|
||||
|
||||
positions = self.get_mock_positions(dash)
|
||||
filters = {str(dash.slices[0].id): {'region': ['North America']}}
|
||||
default_filters = json.dumps(filters)
|
||||
data = {
|
||||
'css': '',
|
||||
'expanded_slices': {},
|
||||
'positions': dash.position_array,
|
||||
'positions': positions,
|
||||
'dashboard_title': dash.dashboard_title,
|
||||
'default_filters': default_filters,
|
||||
}
|
||||
|
|
@ -104,12 +125,13 @@ class DashboardTests(SupersetTestCase):
|
|||
slug='world_health').first()
|
||||
|
||||
# add an invalid filter slice
|
||||
positions = self.get_mock_positions(dash)
|
||||
filters = {str(99999): {'region': ['North America']}}
|
||||
default_filters = json.dumps(filters)
|
||||
data = {
|
||||
'css': '',
|
||||
'expanded_slices': {},
|
||||
'positions': dash.position_array,
|
||||
'positions': positions,
|
||||
'dashboard_title': dash.dashboard_title,
|
||||
'default_filters': default_filters,
|
||||
}
|
||||
|
|
@ -131,10 +153,11 @@ class DashboardTests(SupersetTestCase):
|
|||
.first()
|
||||
)
|
||||
origin_title = dash.dashboard_title
|
||||
positions = self.get_mock_positions(dash)
|
||||
data = {
|
||||
'css': '',
|
||||
'expanded_slices': {},
|
||||
'positions': dash.position_array,
|
||||
'positions': positions,
|
||||
'dashboard_title': 'new title',
|
||||
}
|
||||
url = '/superset/save_dash/{}/'.format(dash.id)
|
||||
|
|
@ -153,11 +176,12 @@ class DashboardTests(SupersetTestCase):
|
|||
self.login(username=username)
|
||||
dash = db.session.query(models.Dashboard).filter_by(
|
||||
slug='births').first()
|
||||
positions = self.get_mock_positions(dash)
|
||||
data = {
|
||||
'css': '',
|
||||
'duplicate_slices': False,
|
||||
'expanded_slices': {},
|
||||
'positions': dash.position_array,
|
||||
'positions': positions,
|
||||
'dashboard_title': 'Copy Of Births',
|
||||
}
|
||||
|
||||
|
|
@ -216,9 +240,16 @@ class DashboardTests(SupersetTestCase):
|
|||
self.login(username=username)
|
||||
dash = db.session.query(models.Dashboard).filter_by(
|
||||
slug='births').first()
|
||||
positions = dash.position_array[:-1]
|
||||
origin_slices_length = len(dash.slices)
|
||||
|
||||
positions = self.get_mock_positions(dash)
|
||||
# remove one chart
|
||||
chart_keys = []
|
||||
for key in positions.keys():
|
||||
if key.startswith('DASHBOARD_CHART_TYPE'):
|
||||
chart_keys.append(key)
|
||||
positions.pop(chart_keys[0])
|
||||
|
||||
data = {
|
||||
'css': '',
|
||||
'expanded_slices': {},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ from .base_tests import SupersetTestCase
|
|||
class ImportExportTests(SupersetTestCase):
|
||||
"""Testing export import functionality for dashboards"""
|
||||
|
||||
requires_examples = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ImportExportTests, self).__init__(*args, **kwargs)
|
||||
|
||||
|
|
@ -155,9 +157,9 @@ class ImportExportTests(SupersetTestCase):
|
|||
self.assertEquals(
|
||||
len(expected_dash.slices), len(actual_dash.slices))
|
||||
expected_slices = sorted(
|
||||
expected_dash.slices, key=lambda s: s.slice_name)
|
||||
expected_dash.slices, key=lambda s: s.slice_name or '')
|
||||
actual_slices = sorted(
|
||||
actual_dash.slices, key=lambda s: s.slice_name)
|
||||
actual_dash.slices, key=lambda s: s.slice_name or '')
|
||||
for e_slc, a_slc in zip(expected_slices, actual_slices):
|
||||
self.assert_slice_equals(e_slc, a_slc)
|
||||
if check_position:
|
||||
|
|
@ -191,7 +193,10 @@ class ImportExportTests(SupersetTestCase):
|
|||
set([m.metric_name for m in actual_ds.metrics]))
|
||||
|
||||
def assert_slice_equals(self, expected_slc, actual_slc):
|
||||
self.assertEquals(expected_slc.slice_name, actual_slc.slice_name)
|
||||
# to avoid bad slice data (no slice_name)
|
||||
expected_slc_name = expected_slc.slice_name or ''
|
||||
actual_slc_name = actual_slc.slice_name or ''
|
||||
self.assertEquals(expected_slc_name, actual_slc_name)
|
||||
self.assertEquals(
|
||||
expected_slc.datasource_type, actual_slc.datasource_type)
|
||||
self.assertEquals(expected_slc.viz_type, actual_slc.viz_type)
|
||||
|
|
@ -209,6 +214,7 @@ class ImportExportTests(SupersetTestCase):
|
|||
resp.data.decode('utf-8'),
|
||||
object_hook=utils.decode_dashboards,
|
||||
)['dashboards']
|
||||
|
||||
self.assert_dash_equals(birth_dash, exported_dashboards[0])
|
||||
self.assertEquals(
|
||||
birth_dash.id,
|
||||
|
|
@ -320,13 +326,18 @@ class ImportExportTests(SupersetTestCase):
|
|||
dash_with_1_slice = self.create_dashboard(
|
||||
'dash_with_1_slice', slcs=[slc], id=10002)
|
||||
dash_with_1_slice.position_json = """
|
||||
[{{
|
||||
"col": 5,
|
||||
"row": 10,
|
||||
"size_x": 4,
|
||||
"size_y": 2,
|
||||
"slice_id": "{}"
|
||||
}}]
|
||||
{{"DASHBOARD_VERSION_KEY": "v2",
|
||||
"DASHBOARD_CHART_TYPE-{0}": {{
|
||||
"type": "DASHBOARD_CHART_TYPE",
|
||||
"id": {0},
|
||||
"children": [],
|
||||
"meta": {{
|
||||
"width": 4,
|
||||
"height": 50,
|
||||
"chartId": {0}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
""".format(slc.id)
|
||||
imported_dash_id = models.Dashboard.import_obj(
|
||||
dash_with_1_slice, import_time=1990)
|
||||
|
|
@ -340,10 +351,8 @@ class ImportExportTests(SupersetTestCase):
|
|||
self.assertEquals({'remote_id': 10002, 'import_time': 1990},
|
||||
json.loads(imported_dash.json_metadata))
|
||||
|
||||
expected_position = dash_with_1_slice.position_array
|
||||
expected_position[0]['slice_id'] = '{}'.format(
|
||||
imported_dash.slices[0].id)
|
||||
self.assertEquals(expected_position, imported_dash.position_array)
|
||||
expected_position = dash_with_1_slice.position
|
||||
self.assertEquals(expected_position, imported_dash.position)
|
||||
|
||||
def test_import_dashboard_2_slices(self):
|
||||
e_slc = self.create_slice('e_slc', id=10007, table_name='energy_usage')
|
||||
|
|
|
|||
Loading…
Reference in New Issue