From commits-return-1140-archive-asf-public=cust-asf.ponee.io@superset.incubator.apache.org Fri Jun 22 02:54:23 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id 863AB1807C2 for ; Fri, 22 Jun 2018 02:54:19 +0200 (CEST) Received: (qmail 41837 invoked by uid 500); 22 Jun 2018 00:54:18 -0000 Mailing-List: contact commits-help@superset.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@superset.incubator.apache.org Delivered-To: mailing list commits@superset.incubator.apache.org Received: (qmail 41590 invoked by uid 99); 22 Jun 2018 00:54:18 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 22 Jun 2018 00:54:18 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id B39DC850D1; Fri, 22 Jun 2018 00:54:16 +0000 (UTC) Date: Fri, 22 Jun 2018 00:54:32 +0000 To: "commits@superset.apache.org" Subject: [incubator-superset] 17/26: [dashboard v2] add v1 switch (#5126) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: ccwilliams@apache.org In-Reply-To: <152962885522.16472.1136442268545544298@gitbox.apache.org> References: <152962885522.16472.1136442268545544298@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: incubator-superset X-Git-Refname: refs/heads/dashboard-builder X-Git-Reftype: branch X-Git-Rev: 0e0c76881dcc7ce6cadd23dfabaef01970e32379 X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20180622005416.B39DC850D1@gitbox.apache.org> This is an automated email from the ASF dual-hosted git repository. ccwilliams pushed a commit to branch dashboard-builder in repository https://gitbox.apache.org/repos/asf/incubator-superset.git commit 0e0c76881dcc7ce6cadd23dfabaef01970e32379 Author: Chris Williams AuthorDate: Wed Jun 6 14:10:37 2018 -0700 [dashboard v2] add v1 switch (#5126) * [dashboard] copy all dashboard v1 into working v1 switch * [dashboard] add functional v1 <> v2 switch with messaging * [dashboard] add v2 logging to v1 dashboard, add read-v2-changes link, add client logging to track v1 <> v2 switches * [dashboard] Remove default values for feedback url + v2 auto convert date * [dashboard v2] fix misc UI/UX issues * [dashboard v2] fix Markdown persistance issues and css, fix copy dash title, don't enforce shallow hovering with drop indicator * [dashboard v2] improve non-shallow drop target UX, fix Markdown drop indicator, clarify slice adder filter/sort * [dashboard v2] delete empty rows on drag or delete events that leave them without children, add test * [dashboard v2] improve v1<>v2 switch modals, add convert to v2 badge in v1, fix unsaved changes issue in preview mode, don't auto convert column child widths for now * [dashboard v2][dnd] add drop position cache to fix non-shallow drops * [dashboard] fix test script with glob instead of recurse, fix tests, add temp fix for tab nesting, ignore v1 lint errors * [dashboard] v2 badge style tweaks, add back v1 _set_dash_metadata for v1 editing * [dashboard] fix python linting and tests * [dashboard] lint tests --- superset/assets/.eslintignore | 1 + superset/assets/package.json | 4 +- .../dashboard/actions/dashboardLayout_spec.js | 5 +- .../dashboard/components/DashboardBuilder_spec.jsx | 26 +- .../dashboard/components/DashboardGrid_spec.jsx | 11 +- .../dashboard/fixtures/mockDashboardState.js | 2 + .../dashboard/reducers/dashboardLayout_spec.js | 74 ++-- .../dashboard/reducers/dashboardState_spec.js | 4 +- .../dashboard/util/isValidChild_spec.js | 2 +- superset/assets/src/chart/Chart.jsx | 4 +- .../src/dashboard/actions/dashboardLayout.js | 15 +- .../assets/src/dashboard/actions/messageToasts.js | 12 +- .../src/dashboard/components/AddSliceCard.jsx | 2 + .../assets/src/dashboard/components/Controls.jsx | 138 ------- .../assets/src/dashboard/components/Dashboard.jsx | 22 +- .../src/dashboard/components/DashboardBuilder.jsx | 40 +- .../src/dashboard/components/DashboardGrid.jsx | 73 +--- .../assets/src/dashboard/components/Header.jsx | 177 +++++---- .../dashboard/components/HeaderActionsDropdown.jsx | 163 ++++++++ .../assets/src/dashboard/components/SaveModal.jsx | 10 +- .../assets/src/dashboard/components/SliceAdder.jsx | 19 +- superset/assets/src/dashboard/components/Toast.jsx | 15 +- .../src/dashboard/components/dnd/handleDrop.js | 2 + .../src/dashboard/components/dnd/handleHover.js | 2 +- .../components/gridComponents/ChartHolder.jsx | 2 +- .../components/gridComponents/Markdown.jsx | 49 ++- .../dashboard/components/gridComponents/Tab.jsx | 23 +- superset/assets/src/dashboard/containers/Chart.jsx | 2 +- .../src/dashboard/containers/DashboardHeader.jsx | 19 +- .../deprecated/PromptV2ConversionModal.jsx | 102 +++++ .../src/dashboard/deprecated/V2PreviewModal.jsx | 148 +++++++ .../src/{ => dashboard/deprecated}/chart/Chart.jsx | 150 ++++---- .../src/dashboard/deprecated/chart/ChartBody.jsx | 55 +++ .../dashboard/deprecated/chart/ChartContainer.jsx | 29 ++ .../src/dashboard/deprecated/chart/chart.css | 4 + .../src/dashboard/deprecated/chart/chartAction.js | 195 ++++++++++ .../src/dashboard/deprecated/chart/chartReducer.js | 158 ++++++++ .../assets/src/dashboard/deprecated/v1/actions.js | 127 +++++++ .../deprecated/v1/components/CodeModal.jsx | 48 +++ .../deprecated/v1/components/Controls.jsx | 214 +++++++++++ .../deprecated/v1/components/CssEditor.jsx | 91 +++++ .../deprecated/v1/components/Dashboard.jsx | 423 +++++++++++++++++++++ .../v1/components/DashboardContainer.jsx | 31 ++ .../deprecated/v1/components/GridCell.jsx | 157 ++++++++ .../deprecated/v1/components/GridLayout.jsx | 198 ++++++++++ .../dashboard/deprecated/v1/components/Header.jsx | 184 +++++++++ .../v1/components/RefreshIntervalModal.jsx | 64 ++++ .../deprecated/v1/components/SaveModal.jsx | 161 ++++++++ .../deprecated/v1/components/SliceAdder.jsx | 219 +++++++++++ .../deprecated/v1/components/SliceHeader.jsx | 194 ++++++++++ .../assets/src/dashboard/deprecated/v1/index.jsx | 28 ++ .../assets/src/dashboard/deprecated/v1/reducers.js | 272 +++++++++++++ .../src/dashboard/reducers/dashboardLayout.js | 58 +-- .../src/dashboard/reducers/dashboardState.js | 2 + .../src/dashboard/reducers/getInitialState.js | 27 +- .../dashboard/stylesheets/builder-sidepane.less | 41 +- .../assets/src/dashboard/stylesheets/builder.less | 1 - .../dashboard/stylesheets/components/markdown.less | 7 +- .../stylesheets/components/new-component.less | 6 +- .../src/dashboard/stylesheets/components/tabs.less | 4 + .../src/dashboard/stylesheets/dashboard.less | 74 +++- .../src/dashboard/stylesheets/popover-menu.less | 1 + .../assets/src/dashboard/util/getDropPosition.js | 58 ++- .../assets/src/dashboard/util/injectCustomCss.js | 17 + superset/assets/src/dashboard/util/isValidChild.js | 2 +- superset/assets/src/dashboard/util/propShapes.jsx | 1 + superset/assets/src/logger.js | 13 + .../assets/stylesheets/dashboard_deprecated.css | 181 +++++++++ superset/assets/webpack.config.js | 1 + superset/config.py | 8 + superset/connectors/sqla/models.py | 4 +- .../superset/dashboard_v1_deprecated.html | 10 + superset/views/core.py | 103 ++++- tests/dashboard_tests.py | 20 +- 74 files changed, 4213 insertions(+), 596 deletions(-) diff --git a/superset/assets/.eslintignore b/superset/assets/.eslintignore index 7479173..61262fc 100644 --- a/superset/assets/.eslintignore +++ b/superset/assets/.eslintignore @@ -8,3 +8,4 @@ node_modules*/* stylesheets/* vendor/* docs/* +src/dashboard/deprecated/* diff --git a/superset/assets/package.json b/superset/assets/package.json index 21abd17..c68e490 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -8,8 +8,8 @@ "test": "spec" }, "scripts": { - "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/**/*_spec.*", - "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js --recursive spec/**/*_spec.*", + "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js 'spec/**/*_spec.*'", + "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js 'spec/**/*_spec.*'", "dev": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool eval-cheap-source-map", "dev-slow": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool inline-source-map", "dev-fast": "echo 'dev-fast in now replaced by dev'", diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js index 0c4fe12..84f0856 100644 --- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js +++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js @@ -138,7 +138,6 @@ describe('dashboardLayout actions', () => { }); }); - // describe('createComponent', () => {}); describe('createTopLevelTabs', () => { it('should dispatch a createTopLevelTabs action', () => { const { getState, dispatch } = setup({ @@ -282,7 +281,7 @@ describe('dashboardLayout actions', () => { it('should move a component if the component is not new', () => { const { getState, dispatch } = setup({ - dashboardLayout: { present: { id: { type: ROW_TYPE } } }, + dashboardLayout: { present: { id: { type: ROW_TYPE, children: [] } } }, }); const dropResult = { source: { id: 'id', index: 0, type: ROW_TYPE }, @@ -324,7 +323,7 @@ describe('dashboardLayout actions', () => { ); }); - it('should delete the parent Tabs if the moved Tab was the only child', () => { + it('should delete a parent Row or Tabs if the moved child was the only child', () => { const { getState, dispatch } = setup({ dashboardLayout: { present: { diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index 6b5d051..4c3185f 100644 --- a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -13,8 +13,7 @@ import DashboardBuilder from '../../../../src/dashboard/components/DashboardBuil import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent'; import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader'; import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid'; -import DragDroppable from '../../../../src/dashboard/components/dnd/DragDroppable'; - +import WithDragDropContext from '../helpers/WithDragDropContext'; import { dashboardLayout as undoableDashboardLayout, dashboardLayoutWithTabs as undoableDashboardLayoutWithTabs, @@ -32,12 +31,17 @@ describe('DashboardBuilder', () => { editMode: false, showBuilderPane: false, handleComponentDrop() {}, + toggleBuilderPane() {}, }; function setup(overrideProps, useProvider = false, store = mockStore) { const builder = ; return useProvider - ? mount({builder}) + ? mount( + + {builder} + , + ) : shallow(builder); } @@ -56,23 +60,11 @@ describe('DashboardBuilder', () => { ); }); - it('should render a DashboardHeader', () => { - const wrapper = setup(); + it('should render a DragDroppable DashboardHeader', () => { + const wrapper = setup(null, true); expect(wrapper.find(DashboardHeader)).to.have.length(1); }); - it('should render a DragDroppable DashboardHeader if editMode=true and no top-level Tabs exist', () => { - const withoutTabs = setup(); - const withoutTabsEditMode = setup({ editMode: true }); - const withTabs = setup({ - dashboardLayout: layoutWithTabs, - }); - - expect(withoutTabs.find(DragDroppable)).to.have.length(0); - expect(withoutTabsEditMode.find(DragDroppable)).to.have.length(1); - expect(withTabs.find(DragDroppable)).to.have.length(0); - }); - it('should render a Sticky top-level Tabs if the dashboard has tabs', () => { const wrapper = setup( { dashboardLayout: layoutWithTabs }, diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx index 7e9de51..3121e7e 100644 --- a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx @@ -42,9 +42,14 @@ describe('DashboardGrid', () => { expect(wrapper.find(DashboardComponent)).to.have.length(2); }); - it('should render two empty DragDroppables targets when editMode=true', () => { - const wrapper = setup({ editMode: true }); - expect(wrapper.find(DragDroppable)).to.have.length(2); + it('should render an empty DragDroppables target when the gridComponent has no children', () => { + const withChildren = setup({ editMode: true }); + const withoutChildren = setup({ + editMode: true, + gridComponent: { ...props.gridComponent, children: [] }, + }); + expect(withChildren.find(DragDroppable)).to.have.length(0); + expect(withoutChildren.find(DragDroppable)).to.have.length(1); }); it('should render grid column guides when resizing', () => { diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js index 9d05344..fd640d1 100644 --- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js +++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js @@ -10,4 +10,6 @@ export default { hasUnsavedChanges: false, maxUndoHistoryExceeded: false, isStarred: true, + css: '', + isV2Preview: false, // @TODO remove upon v1 deprecation }; diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js index cbe1729..dd933ac 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js @@ -14,7 +14,6 @@ import { import { CHART_TYPE, - COLUMN_TYPE, DASHBOARD_GRID_TYPE, DASHBOARD_ROOT_TYPE, ROW_TYPE, @@ -25,7 +24,6 @@ import { import { DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, - GRID_MIN_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID, NEW_TABS_ID, NEW_ROW_ID, @@ -54,6 +52,7 @@ describe('dashboardLayout reducer', () => { }, parentId: { id: 'parentId', + type: ROW_TYPE, children: ['toDelete', 'anotherId'], }, }, @@ -66,6 +65,42 @@ describe('dashboardLayout reducer', () => { parentId: { id: 'parentId', children: ['anotherId'], + type: ROW_TYPE, + }, + }); + }); + + it('should delete a parent if the parent was a row and no longer has children', () => { + expect( + layoutReducer( + { + grandparentId: { + id: 'grandparentId', + children: ['parentId'], + }, + parentId: { + id: 'parentId', + type: ROW_TYPE, + children: ['toDelete'], + }, + toDelete: { + id: 'toDelete', + children: ['child1'], + }, + child1: { + id: 'child1', + children: [], + }, + }, + { + type: DELETE_COMPONENT, + payload: { id: 'toDelete', parentId: 'parentId' }, + }, + ), + ).to.deep.equal({ + grandparentId: { + id: 'grandparentId', + children: [], }, }); }); @@ -170,41 +205,6 @@ describe('dashboardLayout reducer', () => { }); }); - it('should set the width of a moved component with column type parent to the minimum width', () => { - const layout = { - source: { - id: 'source', - type: ROW_TYPE, - children: ['dontMove', 'toMove'], - }, - destination: { - id: 'destination', - type: COLUMN_TYPE, - children: [], - meta: { width: 100 }, - }, - toMove: { - id: 'toMove', - type: CHART_TYPE, - children: [], - meta: { width: 1001 }, - }, - }; - - const dropResult = { - source: { id: 'source', type: ROW_TYPE, index: 1 }, - destination: { id: 'destination', type: COLUMN_TYPE, index: 0 }, - dragging: { id: 'toMove', type: CHART_TYPE }, - }; - - const result = layoutReducer(layout, { - type: MOVE_COMPONENT, - payload: { dropResult }, - }); - - expect(result.toMove.meta.width).to.equal(GRID_MIN_COLUMN_COUNT); - }); - it('should wrap a moved component in a row if need be', () => { const layout = { source: { diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js index 89c4ffe..f8095cd 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js @@ -128,12 +128,14 @@ describe('dashboardState reducer', () => { }); }); - it('should set unsaved changes and max undo history to false on save', () => { + it('should set unsaved changes, max undo history, and editMode to false on save', () => { expect( dashboardStateReducer({ hasUnsavedChanges: true }, { type: ON_SAVE }), ).to.deep.equal({ hasUnsavedChanges: false, maxUndoHistoryExceeded: false, + editMode: false, + isV2Preview: false, // @TODO remove upon v1 deprecation }); }); diff --git a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js index ec57494..3563059 100644 --- a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js +++ b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js @@ -108,7 +108,7 @@ describe('isValidChild', () => { [ROOT, [MARKDOWN]], [ROOT, GRID, [TAB]], [ROOT, GRID, TABS, [ROW]], - [ROOT, GRID, TABS, TAB, [TABS]], + // [ROOT, GRID, TABS, TAB, [TABS]], // @TODO this needs to be fixed [ROOT, GRID, ROW, [TABS]], [ROOT, GRID, ROW, [TAB]], [ROOT, GRID, ROW, [DIVIDER]], diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx index 060249f..1718fc7 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/chart/Chart.jsx @@ -190,8 +190,8 @@ class Chart extends React.PureComponent { this.props.actions.chartRenderingSucceeded(chartId); } Logger.append(LOG_ACTIONS_RENDER_CHART, { - label: 'slice_' + chartId, - vis_type: vizType, + slice_id: 'slice_' + chartId, + viz_type: vizType, start_offset: renderStart, duration: Logger.getTimestamp() - renderStart, }); diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js index d210ee6..c4908b0 100644 --- a/superset/assets/src/dashboard/actions/dashboardLayout.js +++ b/superset/assets/src/dashboard/actions/dashboardLayout.js @@ -2,7 +2,12 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo'; import { addInfoToast } from './messageToasts'; import { setUnsavedChanges } from './dashboardState'; -import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes'; +import { + CHART_TYPE, + MARKDOWN_TYPE, + TABS_TYPE, + ROW_TYPE, +} from '../util/componentTypes'; import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID, @@ -155,8 +160,7 @@ export function handleComponentDrop(dropResult) { if (overflowsParent) { return dispatch( addInfoToast( - `Parent does not have enough space for this component. - Try decreasing its width or add it to a new row.`, + `Parent does not have enough space for this component. Try decreasing its width or add it to a new row.`, ), ); } @@ -180,12 +184,13 @@ export function handleComponentDrop(dropResult) { const { dashboardLayout: undoableLayout } = getState(); - // if we moved a Tab and the parent Tabs no longer has children, delete it. + // if we moved a child from a Tab or Row parent and it was the only child, delete the parent. if (!isNewComponent) { const { present: layout } = undoableLayout; const sourceComponent = layout[source.id]; if ( - sourceComponent.type === TABS_TYPE && + (sourceComponent.type === TABS_TYPE || + sourceComponent.type === ROW_TYPE) && sourceComponent.children.length === 0 ) { const parentId = findParentId({ diff --git a/superset/assets/src/dashboard/actions/messageToasts.js b/superset/assets/src/dashboard/actions/messageToasts.js index fde02c4..e5c04e6 100644 --- a/superset/assets/src/dashboard/actions/messageToasts.js +++ b/superset/assets/src/dashboard/actions/messageToasts.js @@ -12,13 +12,14 @@ function getToastUuid(type) { } export const ADD_TOAST = 'ADD_TOAST'; -export function addToast({ toastType, text }) { +export function addToast({ toastType, text, duration }) { return { type: ADD_TOAST, payload: { id: getToastUuid(toastType), toastType, text, + duration, }, }; } @@ -36,17 +37,20 @@ export function removeToast(id) { // Different types of toasts export const ADD_INFO_TOAST = 'ADD_INFO_TOAST'; export function addInfoToast(text) { - return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST })); + return dispatch => + dispatch(addToast({ text, toastType: INFO_TOAST, duration: 4000 })); } export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST'; export function addSuccessToast(text) { - return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST })); + return dispatch => + dispatch(addToast({ text, toastType: SUCCESS_TOAST, duration: 4000 })); } export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST'; export function addWarningToast(text) { - return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST })); + return dispatch => + dispatch(addToast({ text, toastType: WARNING_TOAST, duration: 4000 })); } export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST'; diff --git a/superset/assets/src/dashboard/components/AddSliceCard.jsx b/superset/assets/src/dashboard/components/AddSliceCard.jsx index 7fd9ba4..c8266ad 100644 --- a/superset/assets/src/dashboard/components/AddSliceCard.jsx +++ b/superset/assets/src/dashboard/components/AddSliceCard.jsx @@ -1,6 +1,7 @@ import cx from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; +import { t } from '../../locales'; const propTypes = { datasourceLink: PropTypes.string, @@ -49,6 +50,7 @@ function AddSliceCard({ + {isSelected &&
{t('Added')}
} ); } diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx deleted file mode 100644 index 9d54b09..0000000 --- a/superset/assets/src/dashboard/components/Controls.jsx +++ /dev/null @@ -1,138 +0,0 @@ -/* global window */ -import React from 'react'; -import PropTypes from 'prop-types'; -import $ from 'jquery'; -import { DropdownButton, MenuItem } from 'react-bootstrap'; - -import CssEditor from './CssEditor'; -import RefreshIntervalModal from './RefreshIntervalModal'; -import { t } from '../../locales'; - -function 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; - } -} - -const propTypes = { - addSuccessToast: PropTypes.func.isRequired, - addDangerToast: PropTypes.func.isRequired, - dashboardInfo: PropTypes.object.isRequired, - dashboardTitle: PropTypes.string.isRequired, - css: PropTypes.string.isRequired, - slices: PropTypes.array, - onChange: PropTypes.func.isRequired, - updateCss: PropTypes.func.isRequired, - forceRefreshAllCharts: PropTypes.func.isRequired, - startPeriodicRender: PropTypes.func.isRequired, - editMode: PropTypes.bool, -}; - -const defaultProps = { - editMode: false, - slices: [], -}; - -class Controls extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - css: props.css, - cssTemplates: [], - }; - - this.changeCss = this.changeCss.bind(this); - } - - componentWillMount() { - 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 }); - }); - } - - changeCss(css) { - this.setState({ css }, () => { - updateDom(css); - }); - this.props.onChange(); - this.props.updateCss(css); - } - - render() { - const { - dashboardTitle, - startPeriodicRender, - forceRefreshAllCharts, - editMode, - } = this.props; - - const emailBody = t('Checkout this dashboard: %s', window.location.href); - const emailLink = - 'mailto:?Subject=Superset%20Dashboard%20' + - `${dashboardTitle}&Body=${emailBody}`; - - return ( - - - - {t('Force refresh dashboard')} - - - startPeriodicRender(refreshInterval * 1000) - } - triggerNode={{t('Set auto-refresh interval')}} - /> - {editMode && ( - - {t('Edit dashboard metadata')} - - )} - {editMode && ( - {t('Email dashboard link')} - )} - {editMode && ( - {t('Edit CSS')}} - initialCss={this.state.css} - templates={this.state.cssTemplates} - onChange={this.changeCss} - /> - )} - - - ); - } -} - -Controls.propTypes = propTypes; -Controls.defaultProps = defaultProps; - -export default Controls; diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx index 62bcbb5..99e93aa 100644 --- a/superset/assets/src/dashboard/components/Dashboard.jsx +++ b/superset/assets/src/dashboard/components/Dashboard.jsx @@ -86,6 +86,9 @@ class Dashboard extends React.PureComponent { componentWillReceiveProps(nextProps) { if (!nextProps.dashboardState.editMode) { + const version = nextProps.dashboardState.isV2Preview + ? 'v2-preview' + : 'v2'; // log pane loads const loadedPaneIds = []; const allPanesDidLoad = Object.entries(nextProps.loadStats).every( @@ -101,6 +104,7 @@ class Dashboard extends React.PureComponent { Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, { ...restStats, duration, + version, }); if (!this.isFirstLoad) { @@ -118,6 +122,7 @@ class Dashboard extends React.PureComponent { Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, { pane_ids: loadedPaneIds, duration: new Date().getTime() - this.ts_mount, + version, }); Logger.send(this.actionLog); this.isFirstLoad = false; @@ -128,25 +133,20 @@ class Dashboard extends React.PureComponent { const nextChartIds = getChartIdsFromLayout(nextProps.layout); if (currentChartIds.length < nextChartIds.length) { - // adding new chart const newChartIds = nextChartIds.filter( key => currentChartIds.indexOf(key) === -1, ); - if (newChartIds.length) { - newChartIds.forEach(newChartId => - this.props.actions.addSliceToDashboard(newChartId), - ); - } + newChartIds.forEach(newChartId => + this.props.actions.addSliceToDashboard(newChartId), + ); } else if (currentChartIds.length > nextChartIds.length) { // remove chart const removedChartIds = currentChartIds.filter( key => nextChartIds.indexOf(key) === -1, ); - if (removedChartIds.length) { - removedChartIds.forEach(removedChartId => - this.props.actions.removeSliceFromDashboard(removedChartId), - ); - } + removedChartIds.forEach(removedChartId => + this.props.actions.removeSliceFromDashboard(removedChartId), + ); } } diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx index 30e2e78..2156ed3 100644 --- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx +++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx @@ -96,26 +96,24 @@ class DashboardBuilder extends React.Component { - {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist - - ) : ( - - {({ dropIndicatorProps }) => ( -
- - {dropIndicatorProps &&
} -
- )} - - )} + + {({ dropIndicatorProps }) => ( +
+ + {dropIndicatorProps &&
} +
+ )} + {topLevelTabs && ( @@ -175,7 +173,7 @@ class DashboardBuilder extends React.Component { diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx index 77503bb..4689051 100644 --- a/superset/assets/src/dashboard/components/DashboardGrid.jsx +++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx @@ -26,7 +26,6 @@ class DashboardGrid extends React.PureComponent { rowGuideTop: null, }; - this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this); this.handleResizeStart = this.handleResizeStart.bind(this); this.handleResize = this.handleResize.bind(this); this.handleResizeStop = this.handleResizeStop.bind(this); @@ -76,19 +75,6 @@ class DashboardGrid extends React.PureComponent { })); } - handleTopDropTargetDrop(dropResult) { - if (dropResult) { - this.props.handleComponentDrop({ - ...dropResult, - destination: { - ...dropResult.destination, - // force appending as the first child if top drop target - index: 0, - }, - }); - } - } - render() { const { gridComponent, @@ -107,26 +93,6 @@ class DashboardGrid extends React.PureComponent { return width < 100 ? null : (
- {/* empty drop target makes top droppable */} - {editMode && ( - - {({ dropIndicatorProps }) => - dropIndicatorProps && ( -
- ) - } - - )} - {gridComponent.children.map((id, index) => ( ))} - {/* empty drop target makes bottom droppable */} - {editMode && ( - - {({ dropIndicatorProps }) => - dropIndicatorProps && ( -
- ) - } - - )} + {/* make the grid droppable in the case that there are no children */} + {editMode && + gridComponent.children.length === 0 && ( + + {({ dropIndicatorProps }) => + dropIndicatorProps && ( +
+ ) + } + + )} {isResizing && Array(GRID_COLUMN_COUNT) diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 31bd08c..5fa4afe 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -1,25 +1,17 @@ /* eslint-env browser */ import React from 'react'; import PropTypes from 'prop-types'; -import { - DropdownButton, - MenuItem, - ButtonGroup, - ButtonToolbar, -} from 'react-bootstrap'; +import { ButtonGroup, ButtonToolbar } from 'react-bootstrap'; -import Controls from './Controls'; +import HeaderActionsDropdown from './HeaderActionsDropdown'; import EditableTitle from '../../components/EditableTitle'; import Button from '../../components/Button'; import FaveStar from '../../components/FaveStar'; -import SaveModal from './SaveModal'; +import V2PreviewModal from '../deprecated/V2PreviewModal'; + import { chartPropShape } from '../util/propShapes'; import { t } from '../../locales'; -import { - UNDO_LIMIT, - SAVE_TYPE_NEWDASHBOARD, - SAVE_TYPE_OVERWRITE, -} from '../util/constants'; +import { UNDO_LIMIT, SAVE_TYPE_OVERWRITE } from '../util/constants'; const propTypes = { addSuccessToast: PropTypes.func.isRequired, @@ -40,6 +32,7 @@ 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, @@ -65,12 +58,14 @@ class Header extends React.PureComponent { super(props); this.state = { didNotifyMaxUndoHistoryToast: false, + showV2PreviewModal: props.isV2Preview, }; this.handleChangeText = this.handleChangeText.bind(this); 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) { @@ -105,6 +100,10 @@ class Header extends React.PureComponent { this.props.setEditMode(!this.props.editMode); } + toggleShowV2PreviewModal() { + this.setState({ showV2PreviewModal: !this.state.showV2PreviewModal }); + } + overwriteDashboard() { const { dashboardTitle, @@ -133,6 +132,7 @@ class Header extends React.PureComponent { filters, expandedSlices, css, + isV2Preview, onUndo, onRedo, undoLength, @@ -148,6 +148,7 @@ class Header extends React.PureComponent { const userCanEdit = dashboardInfo.dash_edit_perm; const userCanSaveAs = dashboardInfo.dash_save_perm; + const popButton = hasUnsavedChanges || isV2Preview; return (
@@ -158,7 +159,7 @@ class Header extends React.PureComponent { onSaveTitle={this.handleChangeText} showTooltip={false} /> - + + {isV2Preview && ( +
+ {t('v2 Preview')} + +
+ )} + {isV2Preview && + this.state.showV2PreviewModal && ( + + )}
+ {userCanSaveAs && ( @@ -193,76 +209,83 @@ class Header extends React.PureComponent { {editMode && ( )} - {!hasUnsavedChanges ? ( - - ) : ( - - )} - - {t('Save as')}} - canOverwrite={userCanEdit} - /> - {hasUnsavedChanges && ( - - {t('Discard changes')} - + {editMode && + (hasUnsavedChanges || isV2Preview) && ( + + )} + + {!editMode && + isV2Preview && ( + + )} + + {!editMode && + !isV2Preview && + !hasUnsavedChanges && ( + )} - + + {editMode && + !isV2Preview && + !hasUnsavedChanges && ( + + )} + + )} - -
); diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx new file mode 100644 index 0000000..7b8a245 --- /dev/null +++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx @@ -0,0 +1,163 @@ +/* global window */ +import React from 'react'; +import PropTypes from 'prop-types'; +import $ from 'jquery'; +import { DropdownButton, MenuItem } from 'react-bootstrap'; + +import CssEditor from './CssEditor'; +import RefreshIntervalModal from './RefreshIntervalModal'; +import SaveModal from './SaveModal'; +import injectCustomCss from '../util/injectCustomCss'; +import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants'; +import { t } from '../../locales'; + +const propTypes = { + addSuccessToast: PropTypes.func.isRequired, + addDangerToast: PropTypes.func.isRequired, + dashboardId: PropTypes.number.isRequired, + dashboardTitle: PropTypes.string.isRequired, + hasUnsavedChanges: PropTypes.bool.isRequired, + css: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + updateCss: PropTypes.func.isRequired, + forceRefreshAllCharts: PropTypes.func.isRequired, + startPeriodicRender: PropTypes.func.isRequired, + editMode: PropTypes.bool.isRequired, + userCanEdit: PropTypes.bool.isRequired, + layout: PropTypes.object.isRequired, + filters: PropTypes.object.isRequired, + expandedSlices: PropTypes.object.isRequired, + onSave: PropTypes.func.isRequired, + isV2Preview: PropTypes.bool.isRequired, +}; + +const defaultProps = {}; + +class HeaderActionsDropdown extends React.PureComponent { + static discardChanges() { + window.location.reload(); + } + + constructor(props) { + super(props); + this.state = { + css: props.css, + cssTemplates: [], + }; + + this.changeCss = this.changeCss.bind(this); + } + + componentWillMount() { + injectCustomCss(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 }); + }); + } + + changeCss(css) { + this.setState({ css }, () => { + injectCustomCss(css); + }); + this.props.onChange(); + this.props.updateCss(css); + } + + render() { + const { + dashboardTitle, + dashboardId, + startPeriodicRender, + forceRefreshAllCharts, + editMode, + css, + hasUnsavedChanges, + layout, + filters, + expandedSlices, + onSave, + userCanEdit, + isV2Preview, + } = this.props; + + const emailBody = t('Check out this dashboard: %s', window.location.href); + const emailLink = `mailto:?Subject=Superset%20Dashboard%20${dashboardTitle}&Body=${emailBody}`; + + return ( + + {t('Save as')}} + canOverwrite={userCanEdit} + isV2Preview={isV2Preview} + /> + {(isV2Preview || hasUnsavedChanges) && ( + + {t('Discard changes')} + + )} + + + + + {t('Force refresh dashboard')} + + + startPeriodicRender(refreshInterval * 1000) + } + triggerNode={{t('Set auto-refresh interval')}} + /> + {editMode && ( + + {t('Edit dashboard metadata')} + + )} + {editMode && ( + {t('Email dashboard link')} + )} + {editMode && ( + {t('Edit CSS')}} + initialCss={this.state.css} + templates={this.state.cssTemplates} + onChange={this.changeCss} + /> + )} + + ); + } +} + +HeaderActionsDropdown.propTypes = propTypes; +HeaderActionsDropdown.defaultProps = defaultProps; + +export default HeaderActionsDropdown; diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx index 9d63331..f5ad9d0 100644 --- a/superset/assets/src/dashboard/components/SaveModal.jsx +++ b/superset/assets/src/dashboard/components/SaveModal.jsx @@ -22,6 +22,7 @@ const propTypes = { onSave: PropTypes.func.isRequired, isMenuItem: PropTypes.bool, canOverwrite: PropTypes.bool.isRequired, + isV2Preview: PropTypes.bool.isRequired, }; const defaultProps = { @@ -82,7 +83,8 @@ class SaveModal extends React.PureComponent { positions, css, expanded_slices: expandedSlices, - dashboard_title: dashboardTitle, + dashboard_title: + saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle, default_filters: JSON.stringify(filters), duplicate_slices: this.state.duplicateSlices, }; @@ -102,12 +104,16 @@ class SaveModal extends React.PureComponent { } render() { + const { isV2Preview } = this.props; return (
+ + {KEYS_TO_SORT.map((item, index) => ( - {item.label} + Sort by {item.label} ))} - -
{this.props.isLoading && ( diff --git a/superset/assets/src/dashboard/components/Toast.jsx b/superset/assets/src/dashboard/components/Toast.jsx index 3c5a3ca..a2b5f0a 100644 --- a/superset/assets/src/dashboard/components/Toast.jsx +++ b/superset/assets/src/dashboard/components/Toast.jsx @@ -14,14 +14,9 @@ import { const propTypes = { toast: toastShape.isRequired, onCloseToast: PropTypes.func.isRequired, - delay: PropTypes.number, - duration: PropTypes.number, // if duration is >0, the toast will close on its own }; -const defaultProps = { - delay: 0, - duration: 0, -}; +const defaultProps = {}; class Toast extends React.Component { constructor(props) { @@ -35,12 +30,12 @@ class Toast extends React.Component { } componentDidMount() { - const { delay, duration } = this.props; + const { toast } = this.props; - setTimeout(this.showToast, delay); + setTimeout(this.showToast); - if (duration > 0) { - this.hideTimer = setTimeout(this.handleClosePress, delay + duration); + if (toast.duration > 0) { + this.hideTimer = setTimeout(this.handleClosePress, toast.duration); } } diff --git a/superset/assets/src/dashboard/components/dnd/handleDrop.js b/superset/assets/src/dashboard/components/dnd/handleDrop.js index 3739b18..faeeffa 100644 --- a/superset/assets/src/dashboard/components/dnd/handleDrop.js +++ b/superset/assets/src/dashboard/components/dnd/handleDrop.js @@ -1,4 +1,5 @@ import getDropPosition, { + clearDropCache, DROP_TOP, DROP_RIGHT, DROP_BOTTOM, @@ -75,6 +76,7 @@ export default function handleDrop(props, monitor, Component) { } onDrop(dropResult); + clearDropCache(); return dropResult; } diff --git a/superset/assets/src/dashboard/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js index a303e13..cb98a6f 100644 --- a/superset/assets/src/dashboard/components/dnd/handleHover.js +++ b/superset/assets/src/dashboard/components/dnd/handleHover.js @@ -1,7 +1,7 @@ import throttle from 'lodash.throttle'; import getDropPosition from '../../util/getDropPosition'; -const HOVER_THROTTLE_MS = 200; +const HOVER_THROTTLE_MS = 150; function handleHover(props, monitor, Component) { // this may happen due to throttling diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx index ab030f4..9ad9522 100644 --- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -101,7 +101,7 @@ class ChartHolder extends React.Component { + +Click here to edit [markdown](https://bit.ly/1dQOfRK)`; class Markdown extends React.PureComponent { constructor(props) { @@ -51,7 +57,7 @@ class Markdown extends React.PureComponent { isFocused: false, markdownSource: props.component.meta.code, editor: null, - editorMode: props.component.meta.code ? 'preview' : 'edit', // show edit mode when code is empty + editorMode: 'preview', }; this.handleChangeFocus = this.handleChangeFocus.bind(this); @@ -61,6 +67,13 @@ class Markdown extends React.PureComponent { this.setEditor = this.setEditor.bind(this); } + componentWillReceiveProps(nextProps) { + const nextSource = nextProps.component.meta.code; + if (this.state.markdownSource !== nextSource) { + this.setState({ markdownSource: nextSource }); + } + } + componentDidUpdate(prevProps) { if ( this.state.editor && @@ -79,7 +92,10 @@ class Markdown extends React.PureComponent { } handleChangeFocus(nextFocus) { - this.setState(() => ({ isFocused: Boolean(nextFocus) })); + const nextFocused = !!nextFocus; + const nextEditMode = nextFocused ? 'edit' : 'preview'; + this.setState(() => ({ isFocused: nextFocused })); + this.handleChangeEditorMode(nextEditMode); } handleChangeEditorMode(mode) { @@ -120,10 +136,15 @@ class Markdown extends React.PureComponent { mode="markdown" theme="textmate" onChange={this.handleMarkdownChange} - width={'100%'} - height={'100%'} + width="100%" + height="100%" editorProps={{ $blockScrolling: true }} - value={this.state.markdownSource || markdownPlaceHolder} + value={ + // thisl allows "select all => delete" to give an empty editor + typeof this.state.markdownSource === 'string' + ? this.state.markdownSource + : markdownPlaceHolder + } readOnly={false} onLoad={this.setEditor} /> @@ -132,7 +153,10 @@ class Markdown extends React.PureComponent { renderPreviewMode() { return ( - + ); } @@ -163,7 +187,7 @@ class Markdown extends React.PureComponent {
- - {dropIndicatorProps &&
}
+ {dropIndicatorProps &&
} )} diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx index 63619c1..4cba2e6 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx @@ -92,6 +92,8 @@ export default class Tab extends React.PureComponent { renderTabContent() { const { component: tabComponent, + parentComponent: tabParentComponent, + index, depth, availableColumnCount, columnWidth, @@ -117,6 +119,25 @@ export default class Tab extends React.PureComponent { onResizeStop={onResizeStop} /> ))} + {/* Make the content of the tab component droppable in the case that there are no children */} + {tabComponent.children.length === 0 && ( + + {({ dropIndicatorProps }) => + dropIndicatorProps && ( +
+ ) + } + + )}
); } @@ -136,7 +157,7 @@ export default class Tab extends React.PureComponent { // disable drag drop of top-level Tab's to prevent invalid nesting of a child in // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would // reusult in circular children - disableDragDrop={depth === DASHBOARD_ROOT_DEPTH + 1} + disableDragDrop={depth <= DASHBOARD_ROOT_DEPTH + 1} editMode={editMode} > {({ dropIndicatorProps, dragSourceRef }) => ( diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx index 5631a25..c046c02 100644 --- a/superset/assets/src/dashboard/containers/Chart.jsx +++ b/superset/assets/src/dashboard/containers/Chart.jsx @@ -28,7 +28,7 @@ function mapStateToProps( return { chart, - datasource: chart && datasources[chart.form_data.datasource], + datasource: (chart && datasources[chart.form_data.datasource]) || {}, slice: sliceEntities.slices[id], timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, filters: filters[id] || EMPTY_FILTERS, diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx index 19be06c..32eda1a 100644 --- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx +++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx @@ -29,7 +29,7 @@ import { DASHBOARD_HEADER_ID } from '../util/constants'; function mapStateToProps({ dashboardLayout: undoableLayout, - dashboardState: dashboard, + dashboardState, dashboardInfo, charts, }) { @@ -38,19 +38,20 @@ function mapStateToProps({ undoLength: undoableLayout.past.length, redoLength: undoableLayout.future.length, layout: undoableLayout.present, - filters: dashboard.filters, + filters: dashboardState.filters, dashboardTitle: ( (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {} ).text, - expandedSlices: dashboard.expandedSlices, - css: dashboard.css, + expandedSlices: dashboardState.expandedSlices, + css: dashboardState.css, charts, userId: dashboardInfo.userId, - isStarred: !!dashboard.isStarred, - hasUnsavedChanges: !!dashboard.hasUnsavedChanges, - maxUndoHistoryExceeded: !!dashboard.maxUndoHistoryExceeded, - editMode: !!dashboard.editMode, - showBuilderPane: !!dashboard.showBuilderPane, + isStarred: !!dashboardState.isStarred, + hasUnsavedChanges: !!dashboardState.hasUnsavedChanges, + maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded, + editMode: !!dashboardState.editMode, + showBuilderPane: !!dashboardState.showBuilderPane, + isV2Preview: dashboardState.isV2Preview, }; } diff --git a/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx new file mode 100644 index 0000000..876fa78 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx @@ -0,0 +1,102 @@ +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 ( + + +
+ {t('Convert to Dashboard v2 🎉')} +
+
+ +

{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.", + )} +

+
+

{t('What and When')}

+

+ {t('You have ')} + + {timeUntilAutoConversion} + {t(' to convert this v1 dashboard to the new v2 format')} + + {t(' before it is auto-converted. ')} + {forceV2Edit && ( + + {t( + 'Note that you may only edit dashboards using the v2 experience.', + )} + + )} + {t('You may read more about these changes ')} + + here + + {v2FeedbackUrl ? t(' or ') : ''} + {v2FeedbackUrl ? ( + + {t('provide feedback')} + + ) : ( + '' + )}. +

+
+ + + + +
+ ); +} + +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 new file mode 100644 index 0000000..a0b7eed --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx @@ -0,0 +1,148 @@ +/* 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/chart/Chart.jsx b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx similarity index 58% copy from superset/assets/src/chart/Chart.jsx copy to superset/assets/src/dashboard/deprecated/chart/Chart.jsx index 060249f..bade493 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx @@ -4,20 +4,20 @@ import PropTypes from 'prop-types'; import Mustache from 'mustache'; import { Tooltip } from 'react-bootstrap'; -import { d3format } from '../modules/utils'; +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 visMap from '../visualizations'; -import sandboxedEval from '../modules/sandbox'; +import Loading from '../../../components/Loading'; +import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger'; +import StackTraceMessage from '../../../components/StackTraceMessage'; +import RefreshChartOverlay from '../../../components/RefreshChartOverlay'; +import visMap from '../../../visualizations'; +import sandboxedEval from '../../../modules/sandbox'; import './chart.css'; const propTypes = { annotationData: PropTypes.object, actions: PropTypes.object, - chartId: PropTypes.number.isRequired, + chartKey: PropTypes.string.isRequired, containerId: PropTypes.string.isRequired, datasource: PropTypes.object.isRequired, formData: PropTypes.object.isRequired, @@ -42,6 +42,8 @@ const propTypes = { // dashboard callbacks addFilter: PropTypes.func, getFilters: PropTypes.func, + clearFilter: PropTypes.func, + removeFilter: PropTypes.func, onQuery: PropTypes.func, onDismissRefreshOverlay: PropTypes.func, }; @@ -49,6 +51,8 @@ const propTypes = { const defaultProps = { addFilter: () => ({}), getFilters: () => ({}), + clearFilter: () => ({}), + removeFilter: () => ({}), }; class Chart extends React.PureComponent { @@ -63,6 +67,8 @@ class Chart extends React.PureComponent { 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); @@ -70,11 +76,10 @@ class Chart extends React.PureComponent { componentDidMount() { if (this.props.triggerQuery) { - const { formData } = this.props; - this.props.actions.runQuery(formData, false, this.props.timeout, this.props.chartId); - } else { - // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data - this.renderViz(); + this.props.actions.runQuery(this.props.formData, false, + this.props.timeout, + this.props.chartKey, + ); } } @@ -88,10 +93,10 @@ class Chart extends React.PureComponent { componentDidUpdate(prevProps) { if ( - this.props.queryResponse && - ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 && - !this.props.queryResponse.error && - (prevProps.annotationData !== this.props.annotationData || + 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 || @@ -113,14 +118,20 @@ class Chart extends React.PureComponent { 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 && this.container.el && this.container.el.offsetWidth) - ); + return this.props.width || this.container.el.offsetWidth; } headerHeight() { @@ -128,9 +139,7 @@ class Chart extends React.PureComponent { } height() { - return ( - this.props.height || (this.container && this.container.el && this.container.el.offsetHeight) - ); + return this.props.height || this.container.el.offsetHeight; } d3format(col, number) { @@ -141,7 +150,7 @@ class Chart extends React.PureComponent { } error(e) { - this.props.actions.chartRenderingFailed(e, this.props.chartId); + this.props.actions.chartRenderingFailed(e, this.props.chartKey); } verboseMetricName(metric) { @@ -158,6 +167,7 @@ class Chart extends React.PureComponent { renderTooltip() { if (this.state.tooltip) { + /* eslint-disable react/no-danger */ return ( -
+
); + /* eslint-enable react/no-danger */ } return null; } renderViz() { - const { vizType, formData, queryResponse, setControlValue, chartId, chartStatus } = this.props; - const visRenderer = visMap[vizType]; + const viz = visMap[this.props.vizType]; + const fd = this.props.formData; + const qr = this.props.queryResponse; const renderStart = Logger.getTimestamp(); try { // Executing user-defined data mutator function - if (formData.js_data) { - queryResponse.data = sandboxedEval(formData.js_data)(queryResponse.data); - } - visRenderer(this, queryResponse, setControlValue); - if (chartStatus !== 'rendered') { - this.props.actions.chartRenderingSucceeded(chartId); + if (fd.js_data) { + qr.data = sandboxedEval(fd.js_data)(qr.data); } + // [re]rendering the visualization + viz(this, qr, this.props.setControlValue); Logger.append(LOG_ACTIONS_RENDER_CHART, { - label: 'slice_' + chartId, - vis_type: vizType, + slice_id: this.props.chartKey, + viz_type: this.props.vizType, start_offset: renderStart, duration: Logger.getTimestamp() - renderStart, }); - this.props.actions.chartRenderingSucceeded(chartId); + this.props.actions.chartRenderingSucceeded(this.props.chartKey); } catch (e) { - console.error(e); // eslint-disable-line no-console - this.props.actions.chartRenderingFailed(e, chartId); + this.props.actions.chartRenderingFailed(e, this.props.chartKey); } } render() { const isLoading = this.props.chartStatus === 'loading'; - - // this allows to be positioned in the middle of the chart - const containerStyles = isLoading ? { height: this.height(), width: this.width() } : null; return ( -
+
{this.renderTooltip()} - {isLoading && } - {this.props.chartAlert && ( - - )} + {isLoading && + + } + {this.props.chartAlert && + + } {!isLoading && !this.props.chartAlert && this.props.refreshOverlayVisible && !this.props.errorMessage && - this.container && ( - - )} - - {!isLoading && - !this.props.chartAlert && ( - { - this.container = inner; - }} - /> - )} + this.container && + + } + {!isLoading && !this.props.chartAlert && + { + this.container = inner; + }} + /> + }
); } diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx new file mode 100644 index 0000000..b459f44 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000..b731412 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..eda2054 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/chart/chart.css @@ -0,0 +1,4 @@ +.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 new file mode 100644 index 0000000..52f9c47 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/chart/chartAction.js @@ -0,0 +1,195 @@ +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: 'slice_' + 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: 'slice_' + 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 new file mode 100644 index 0000000..8d11249 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js @@ -0,0 +1,158 @@ +/* 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 new file mode 100644 index 0000000..7381486 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/actions.js @@ -0,0 +1,127 @@ +/* global notify */ +import $ from 'jquery'; +import { getExploreUrlAndPayload } from '../../../explore/exploreUtils'; + +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)); + notify.success('This slice name was saved successfully.'); + }, + error: () => { + // if server-side reject the overwrite action, + // revert to old state + dispatch(updateSliceName(slice, oldName)); + notify.error("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 new file mode 100644 index 0000000..3f802c3 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..6a6fa47 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx @@ -0,0 +1,214 @@ +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 && + + } + /> + } + {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 new file mode 100644 index 0000000..ee11ff2 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx @@ -0,0 +1,91 @@ +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 new file mode 100644 index 0000000..ef0ec24 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx @@ -0,0 +1,198 @@ +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 new file mode 100644 index 0000000..a84ee89 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx @@ -0,0 +1,184 @@ +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_PREVIEW_V2, + 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, + 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); + this.handleConvertToV2 = this.handleConvertToV2.bind(this); + } + handleSaveTitle(title) { + this.props.updateDashboardTitle(title); + } + 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 + } + 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 new file mode 100644 index 0000000..3e43f93 --- /dev/null +++ b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx @@ -0,0 +1,64 @@ +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')} +