diff --git a/superset/assets/package.json b/superset/assets/package.json index da5405624f074..c81a902e610aa 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -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", diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js index fd640d1f8b2b8..d405ccf327218 100644 --- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js +++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js @@ -11,5 +11,4 @@ export default { maxUndoHistoryExceeded: false, isStarred: true, css: '', - isV2Preview: false, // @TODO remove upon v1 deprecation }; diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js index f8095cd875972..7772f71015515 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js @@ -135,7 +135,6 @@ describe('dashboardState reducer', () => { hasUnsavedChanges: false, maxUndoHistoryExceeded: false, editMode: false, - isV2Preview: false, // @TODO remove upon v1 deprecation }); }); diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx index 80d4bdf1e1f06..2bb9b9ca20ed0 100644 --- a/superset/assets/src/dashboard/components/Dashboard.jsx +++ b/superset/assets/src/dashboard/components/Dashboard.jsx @@ -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; diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 3b1b6b1f3690c..0c1951b8d7838 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -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 (
@@ -196,20 +187,6 @@ class Header extends React.PureComponent { isStarred={this.props.isStarred} /> - {isV2Preview && ( -
- {t('v2 Preview')} - -
- )} - {isV2Preview && - this.state.showV2PreviewModal && ( - - )}
{userCanSaveAs && ( @@ -245,32 +222,17 @@ class Header extends React.PureComponent { )} {editMode && - (hasUnsavedChanges || isV2Preview) && ( + hasUnsavedChanges && ( - )} - - {!editMode && - isV2Preview && ( - )} {!editMode && - !isV2Preview && !hasUnsavedChanges && ( - - - - ); -} - -PromptV2ConversionModal.propTypes = propTypes; -PromptV2ConversionModal.defaultProps = defaultProps; - -export default PromptV2ConversionModal; diff --git a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx deleted file mode 100644 index 828651fbde004..0000000000000 --- a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx +++ /dev/null @@ -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 ( - - -
- {t('Welcome to the new Dashboard v2 experience! 🎉')} -
-
- -

{t('Who')}

-

- {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 ", - )} - - here - - {v2FeedbackUrl ? t(' or ') : ''} - {v2FeedbackUrl ? ( - - {t('provide feedback')} - - ) : ( - '' - )}. -

-
-

{t('What')}

-

- {t('You are ')} - {t('previewing')} - {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. ', - )} - - {t( - 'To persist your dashboard as v2, please make any necessary changes and save the dashboard', - )} - - {t( - '. Note that non-owners/-admins will continue to see the original version until you take this action.', - )} -

-
-

{t('When')}

-

- {t('You have ')} - - {timeUntilAutoConversion} - {t(' to edit and save this version ')} - - {t( - ' before it is auto-persisted to this preview. Upon save you will no longer be able to use the v1 experience.', - )} -

-
- - - - -
- ); - } -} - -V2PreviewModal.propTypes = propTypes; -V2PreviewModal.defaultProps = defaultProps; - -export default connect(({ dashboardInfo }) => ({ - v2FeedbackUrl: dashboardInfo.v2FeedbackUrl, - v2AutoConvertDate: dashboardInfo.v2AutoConvertDate, - forceV2Edit: dashboardInfo.forceV2Edit, -}))(V2PreviewModal); diff --git a/superset/assets/src/dashboard/deprecated/chart/Chart.jsx b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx deleted file mode 100644 index 870688413adf3..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/Chart.jsx +++ /dev/null @@ -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 ( - -
- - ); - /* 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 ( -
- {this.renderTooltip()} - {isLoading && - - } - {this.props.chartAlert && - - } - - {!isLoading && - !this.props.chartAlert && - this.props.refreshOverlayVisible && - !this.props.errorMessage && - this.container && - - } - {!isLoading && !this.props.chartAlert && - { - this.container = inner; - }} - /> - } -
- ); - } -} - -Chart.propTypes = propTypes; -Chart.defaultProps = defaultProps; - -export default Chart; diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx deleted file mode 100644 index b459f4418207d..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx +++ /dev/null @@ -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 ( -
{ this.el = el; }} - /> - ); - } -} - -ChartBody.propTypes = propTypes; - -export default ChartBody; diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx deleted file mode 100644 index b731412fc5ff7..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx +++ /dev/null @@ -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); diff --git a/superset/assets/src/dashboard/deprecated/chart/chart.css b/superset/assets/src/dashboard/deprecated/chart/chart.css deleted file mode 100644 index eda2054f92af9..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/chart.css +++ /dev/null @@ -1,4 +0,0 @@ -.chart-tooltip { - opacity: 0.75; - font-size: 12px; -} diff --git a/superset/assets/src/dashboard/deprecated/chart/chartAction.js b/superset/assets/src/dashboard/deprecated/chart/chartAction.js deleted file mode 100644 index 254f1d60e454b..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/chartAction.js +++ /dev/null @@ -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))), - ]); - }; -} diff --git a/superset/assets/src/dashboard/deprecated/chart/chartReducer.js b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js deleted file mode 100644 index 8d11249598470..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/chartReducer.js +++ /dev/null @@ -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; -} diff --git a/superset/assets/src/dashboard/deprecated/v1/actions.js b/superset/assets/src/dashboard/deprecated/v1/actions.js deleted file mode 100644 index a8701207cbef3..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/actions.js +++ /dev/null @@ -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 }; -} diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx deleted file mode 100644 index 3f802c3471a95..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx +++ /dev/null @@ -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 ( - -
-              {this.state.code}
-            
-
- } - /> - ); - } -} -CodeModal.propTypes = propTypes; -CodeModal.defaultProps = defaultProps; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx deleted file mode 100644 index 01d5dcf2e4204..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx +++ /dev/null @@ -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 ( - - {text} {''} - - {children} - - ); -} -MenuItemContent.propTypes = { - faIcon: PropTypes.string.isRequired, - text: PropTypes.string, - tooltip: PropTypes.string, - children: PropTypes.node, -}; - -function ActionMenuItem(props) { - return ( - - - - ); -} -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 ( - - - - startPeriodicRender(refreshInterval * 1000)} - triggerNode={ - - } - /> - {dashboard.dash_save_perm && - !dashboard.forceV2Edit && - - } - /> - } - {editMode && - { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }} - /> - } - {editMode && - { window.location = emailLink; }} - faIcon="envelope" - /> - } - {editMode && - - } - /> - } - {editMode && - - } - initialCss={this.state.css} - templates={this.state.cssTemplates} - onChange={this.changeCss.bind(this)} - /> - } - - - ); - } -} -Controls.propTypes = propTypes; - -export default Controls; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx deleted file mode 100644 index ee11ff26d626b..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx +++ /dev/null @@ -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 ( -
-
{t('Load a template')}
- - -
-
- ); - } -} - -GridCell.propTypes = propTypes; -GridCell.defaultProps = defaultProps; - -export default GridCell; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx deleted file mode 100644 index ef0ec24796de4..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx +++ /dev/null @@ -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 ( -
- -
); - }); - - return ( - - {cells} - - ); - } -} - -GridLayout.propTypes = propTypes; -GridLayout.defaultProps = defaultProps; - -export default GridLayout; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx deleted file mode 100644 index c801c0aa0d19c..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx +++ /dev/null @@ -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 ( - - ); - } - renderEditButton() { - if (!this.props.dashboard.dash_save_perm) { - return null; - } - const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard'; - return ( - ); - } - render() { - const dashboard = this.props.dashboard; - return ( -
-
-

- - - - - {dashboard.promptV2Conversion && ( - - {t('Convert to v2')} - - - )} - {this.renderUnsaved()} -

-
-
- {this.renderEditButton()} - -
-
- {this.state.showV2PromptModal && - dashboard.promptV2Conversion && - !this.props.editMode && ( - - )} -
- ); - } -} -Header.propTypes = propTypes; - -export default Header; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx deleted file mode 100644 index 3e43f9365ecc9..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx +++ /dev/null @@ -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 ( - - {t('Choose the refresh frequency for this dashboard')} - {t('Add to new dashboard')}   @@ -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')} diff --git a/superset/assets/src/explore/constants.js b/superset/assets/src/explore/constants.js index 30e1565306cb0..711ef086ee64a 100644 --- a/superset/assets/src/explore/constants.js +++ b/superset/assets/src/explore/constants.js @@ -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']; diff --git a/superset/assets/src/logger.js b/superset/assets/src/logger.js index 42f67f8101424..afd33d8ea0d6d 100644 --- a/superset/assets/src/logger.js +++ b/superset/assets/src/logger.js @@ -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 = [ diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js index 56aca88ee96f1..0291f1d84436f 100644 --- a/superset/assets/webpack.config.js +++ b/superset/assets/webpack.config.js @@ -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'], diff --git a/superset/models/core.py b/superset/models/core.py index 8c77814a2ca22..65670772bf444 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -399,10 +399,10 @@ def params(self, value): 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): @@ -418,16 +418,7 @@ def import_obj(cls, dashboard_to_import, import_time=None): 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": { @@ -453,32 +444,17 @@ def alter_positions(dashboard, old_to_new_slc_id_dict): } """ 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') - - 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']) + 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: - 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())) diff --git a/superset/views/core.py b/superset/views/core.py index 9dc903083f400..dc3070a615993 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1561,7 +1561,6 @@ def copy_dash(self, dashboard_id): 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 @@ def copy_dash(self, dashboard_id): # 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 @@ def save_dash(self, dashboard_id): 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 @@ def dashboard(self, dashboard_id): 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 @@ def dashboard(**kwargs): # noqa '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), diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index 3dda07bf17385..1ab3727b1edb5 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -33,6 +33,25 @@ def setUp(self): 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 @@ def test_save_dash(self, username='admin'): 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 @@ def test_save_dash_with_filter(self, username='admin'): 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 @@ def test_save_dash_with_invalid_filters(self, username='admin'): 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 @@ def test_save_dash_with_dashboard_title(self, username='admin'): .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 @@ def test_copy_dash(self, username='admin'): 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 @@ def test_remove_slices(self, username='admin'): 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': {}, diff --git a/tests/import_export_tests.py b/tests/import_export_tests.py index dc9c4ade5b023..4b2ba74d73568 100644 --- a/tests/import_export_tests.py +++ b/tests/import_export_tests.py @@ -22,6 +22,8 @@ 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 @@ def assert_dash_equals(self, expected_dash, actual_dash, 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 @@ def assert_datasource_equals(self, expected_ds, actual_ds): 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 @@ def test_export_1_dashboard(self): 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 @@ def test_import_dashboard_1_slice(self): 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 @@ def test_import_dashboard_1_slice(self): 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')