retire dashboard v1 (js and python) (#5418)

This commit is contained in:
Grace Guo 2018-07-24 15:23:30 -07:00 committed by GitHub
parent fd2d4b0e58
commit 3f2fc8f413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 105 additions and 4210 deletions

View File

@ -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",

View File

@ -11,5 +11,4 @@ export default {
maxUndoHistoryExceeded: false,
isStarred: true,
css: '',
isV2Preview: false, // @TODO remove upon v1 deprecation
};

View File

@ -135,7 +135,6 @@ describe('dashboardState reducer', () => {
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
editMode: false,
isV2Preview: false, // @TODO remove upon v1 deprecation
});
});

View File

@ -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;

View File

@ -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 && (

View File

@ -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}

View File

@ -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>
}

View File

@ -51,7 +51,6 @@ function mapStateToProps({
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
editMode: !!dashboardState.editMode,
showBuilderPane: !!dashboardState.showBuilderPane,
isV2Preview: dashboardState.isV2Preview,
};
}

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -1,4 +0,0 @@
.chart-tooltip {
opacity: 0.75;
font-size: 12px;
}

View File

@ -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))),
]);
};
}

View File

@ -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;
}

View File

@ -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 };
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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,
);

View File

@ -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,
});

View File

@ -83,7 +83,6 @@ export default function dashboardStateReducer(state = {}, action) {
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
editMode: false,
isV2Preview: false, // @TODO remove upon v1 deprecation
};
},

View File

@ -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: [],

View File

@ -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 {

View File

@ -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);
}

View File

@ -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')} &nbsp;
</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')}

View File

@ -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'];

View File

@ -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 = [

View File

@ -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'],

View File

@ -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()))

View File

@ -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),

View File

@ -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': {},

View File

@ -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')