diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json index 81fcbaa0fd304..6901767288724 100644 --- a/superset/assets/package-lock.json +++ b/superset/assets/package-lock.json @@ -2232,9 +2232,9 @@ } }, "@superset-ui/color": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@superset-ui/color/-/color-0.10.1.tgz", - "integrity": "sha512-GblA9+h947un4K6s6v3uRTYCDEBi8GAp3wyEHVXfhSv/YXwyzpyhvhXoF8APSz+8cDVkKYY2svZVOALE0QDI1Q==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@superset-ui/color/-/color-0.10.8.tgz", + "integrity": "sha512-H1M8V9OKO3fCmOHQvW1rN9pRw2t/L1LKHvxzEj/Kccw+osckdmF8RtKEp7DaBuKMO6PF2Kq2FWNIiqNtin9whA==", "requires": { "@types/d3-scale": "^2.0.2", "d3-scale": "^2.1.2" diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index bf24644e140a9..16dc33dea052d 100644 --- a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -31,6 +31,7 @@ import DashboardComponent from '../../../../src/dashboard/containers/DashboardCo import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader'; import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid'; import * as dashboardStateActions from '../../../../src/dashboard/actions/dashboardState'; +import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants'; import WithDragDropContext from '../helpers/WithDragDropContext'; import { @@ -61,7 +62,10 @@ describe('DashboardBuilder', () => { dashboardLayout, deleteTopLevelTabs() {}, editMode: false, - showBuilderPane: false, + showBuilderPane() {}, + builderPaneType: BUILDER_PANE_TYPE.NONE, + setColorSchemeAndUnsavedChanges() {}, + colorScheme: undefined, handleComponentDrop() {}, toggleBuilderPane() {}, }; @@ -143,11 +147,27 @@ describe('DashboardBuilder', () => { expect(parentSize.find(DashboardGrid)).toHaveLength(expectedCount); }); - it('should render a BuilderComponentPane if editMode=showBuilderPane=true', () => { + it('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => { const wrapper = setup(); expect(wrapper.find(BuilderComponentPane)).toHaveLength(0); - wrapper.setProps({ ...props, editMode: true, showBuilderPane: true }); + wrapper.setProps({ + ...props, + editMode: true, + builderPaneType: BUILDER_PANE_TYPE.ADD_COMPONENTS, + }); + expect(wrapper.find(BuilderComponentPane)).toHaveLength(1); + }); + + it('should render a BuilderComponentPane if editMode=true and user selects "Colors" pane', () => { + const wrapper = setup(); + expect(wrapper.find(BuilderComponentPane)).toHaveLength(0); + + wrapper.setProps({ + ...props, + editMode: true, + builderPaneType: BUILDER_PANE_TYPE.COLORS, + }); expect(wrapper.find(BuilderComponentPane)).toHaveLength(1); }); diff --git a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx index 2fff4638cabb5..69e57da8df2aa 100644 --- a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx @@ -24,6 +24,7 @@ import FaveStar from '../../../../src/components/FaveStar'; import HeaderActionsDropdown from '../../../../src/dashboard/components/HeaderActionsDropdown'; import Button from '../../../../src/components/Button'; import UndoRedoKeylisteners from '../../../../src/dashboard/components/UndoRedoKeylisteners'; +import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants'; describe('Header', () => { const props = { @@ -46,7 +47,8 @@ describe('Header', () => { updateDashboardTitle: () => {}, editMode: false, setEditMode: () => {}, - showBuilderPane: false, + showBuilderPane: () => {}, + builderPaneType: BUILDER_PANE_TYPE.NONE, toggleBuilderPane: () => {}, updateCss: () => {}, hasUnsavedChanges: false, @@ -150,9 +152,9 @@ describe('Header', () => { expect(wrapper.find(HeaderActionsDropdown)).toHaveLength(1); }); - it('should render four Buttons', () => { + it('should render five Buttons', () => { const wrapper = setup(overrideProps); - expect(wrapper.find(Button)).toHaveLength(4); + expect(wrapper.find(Button)).toHaveLength(5); }); it('should set up undo/redo', () => { diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js index f326a76ee6cf2..3763ef41a4743 100644 --- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js +++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js @@ -17,6 +17,7 @@ * under the License. */ import { id as sliceId } from './mockChartQueries'; +import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants'; export default { sliceIds: [sliceId], @@ -24,7 +25,7 @@ export default { filters: {}, expandedSlices: {}, editMode: false, - showBuilderPane: false, + builderPaneType: BUILDER_PANE_TYPE.NONE, hasUnsavedChanges: false, maxUndoHistoryExceeded: false, isStarred: true, diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js index c3e385580ab44..dadcf06c8bf19 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js @@ -25,12 +25,12 @@ import { SET_EDIT_MODE, SET_MAX_UNDO_HISTORY_EXCEEDED, SET_UNSAVED_CHANGES, - TOGGLE_BUILDER_PANE, TOGGLE_EXPAND_SLICE, TOGGLE_FAVE_STAR, } from '../../../../src/dashboard/actions/dashboardState'; import dashboardStateReducer from '../../../../src/dashboard/reducers/dashboardState'; +import { BUILDER_PANE_TYPE } from '../../../../src/dashboard/util/constants'; describe('dashboardState reducer', () => { it('should return initial state', () => { @@ -79,23 +79,10 @@ describe('dashboardState reducer', () => { { editMode: false }, { type: SET_EDIT_MODE, editMode: true }, ), - ).toEqual({ editMode: true, showBuilderPane: true }); - }); - - it('should toggle builder pane', () => { - expect( - dashboardStateReducer( - { showBuilderPane: false }, - { type: TOGGLE_BUILDER_PANE }, - ), - ).toEqual({ showBuilderPane: true }); - - expect( - dashboardStateReducer( - { showBuilderPane: true }, - { type: TOGGLE_BUILDER_PANE }, - ), - ).toEqual({ showBuilderPane: false }); + ).toEqual({ + editMode: true, + builderPaneType: BUILDER_PANE_TYPE.ADD_COMPONENTS, + }); }); it('should toggle expanded slices', () => { @@ -150,6 +137,8 @@ describe('dashboardState reducer', () => { hasUnsavedChanges: false, maxUndoHistoryExceeded: false, editMode: false, + builderPaneType: BUILDER_PANE_TYPE.NONE, + updatedColorScheme: false, }); }); diff --git a/superset/assets/src/chart/ChartRenderer.jsx b/superset/assets/src/chart/ChartRenderer.jsx index dc057a339e010..00c16ee0b90fb 100644 --- a/superset/assets/src/chart/ChartRenderer.jsx +++ b/superset/assets/src/chart/ChartRenderer.jsx @@ -87,7 +87,8 @@ class ChartRenderer extends React.Component { nextProps.height !== this.props.height || nextProps.width !== this.props.width || nextState.tooltip !== this.state.tooltip || - nextProps.triggerRender) { + nextProps.triggerRender || + nextProps.formData.color_scheme !== this.props.formData.color_scheme) { return true; } } diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js index 5b163d9619f8e..2716b006eb0c8 100644 --- a/superset/assets/src/dashboard/actions/dashboardLayout.js +++ b/superset/assets/src/dashboard/actions/dashboardLayout.js @@ -209,7 +209,8 @@ export function undoLayoutAction() { if ( dashboardLayout.past.length === 0 && - !dashboardState.maxUndoHistoryExceeded + !dashboardState.maxUndoHistoryExceeded && + !dashboardState.updatedColorScheme ) { dispatch(setUnsavedChanges(false)); } diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js index 086481919615c..5a036449bb9cc 100644 --- a/superset/assets/src/dashboard/actions/dashboardState.js +++ b/superset/assets/src/dashboard/actions/dashboardState.js @@ -225,9 +225,9 @@ export function startPeriodicRender(interval) { }; } -export const TOGGLE_BUILDER_PANE = 'TOGGLE_BUILDER_PANE'; -export function toggleBuilderPane() { - return { type: TOGGLE_BUILDER_PANE }; +export const SHOW_BUILDER_PANE = 'SHOW_BUILDER_PANE'; +export function showBuilderPane(builderPaneType) { + return { type: SHOW_BUILDER_PANE, builderPaneType }; } export function addSliceToDashboard(id) { @@ -266,6 +266,18 @@ export function removeSliceFromDashboard(id) { }; } +export const SET_COLOR_SCHEME = 'SET_COLOR_SCHEME'; +export function setColorScheme(colorScheme) { + return { type: SET_COLOR_SCHEME, colorScheme }; +} + +export function setColorSchemeAndUnsavedChanges(colorScheme) { + return dispatch => { + dispatch(setColorScheme(colorScheme)); + dispatch(setUnsavedChanges(true)); + }; +} + // Undo history --------------------------------------------------------------- export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED'; export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) { diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx index 4c2e92ce34116..2d2ab08993aef 100644 --- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx +++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx @@ -19,49 +19,37 @@ /* eslint-env browser */ import PropTypes from 'prop-types'; import React from 'react'; -import cx from 'classnames'; import { StickyContainer, Sticky } from 'react-sticky'; import { ParentSize } from '@vx/responsive'; -import { t } from '@superset-ui/translation'; -import NewColumn from './gridComponents/new/NewColumn'; -import NewDivider from './gridComponents/new/NewDivider'; -import NewHeader from './gridComponents/new/NewHeader'; -import NewRow from './gridComponents/new/NewRow'; -import NewTabs from './gridComponents/new/NewTabs'; -import NewMarkdown from './gridComponents/new/NewMarkdown'; -import SliceAdder from '../containers/SliceAdder'; - -const SUPERSET_HEADER_HEIGHT = 59; +import InsertComponentPane, { + SUPERSET_HEADER_HEIGHT, +} from './InsertComponentPane'; +import ColorComponentPane from './ColorComponentPane'; +import { BUILDER_PANE_TYPE } from '../util/constants'; const propTypes = { topOffset: PropTypes.number, - toggleBuilderPane: PropTypes.func.isRequired, + showBuilderPane: PropTypes.func.isRequired, + builderPaneType: PropTypes.string.isRequired, + setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired, + colorScheme: PropTypes.string, }; const defaultProps = { topOffset: 0, + colorScheme: undefined, }; class BuilderComponentPane extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - slideDirection: 'slide-out', - }; - - this.openSlicesPane = this.slide.bind(this, 'slide-in'); - this.closeSlicesPane = this.slide.bind(this, 'slide-out'); - } - - slide(direction) { - this.setState({ - slideDirection: direction, - }); - } - render() { - const { topOffset } = this.props; + const { + topOffset, + builderPaneType, + showBuilderPane, + setColorSchemeAndUnsavedChanges, + colorScheme, + } = this.props; return (
-
-
-
- {t('Insert components')} - -
-
-
-
- {t('Your charts & filters')} -
- - -
- - - - - - -
-
-
- - {t('Your charts and filters')} -
- -
-
+ {builderPaneType === BUILDER_PANE_TYPE.ADD_COMPONENTS && ( + + )} + {builderPaneType === BUILDER_PANE_TYPE.COLORS && ( + + )}
)} diff --git a/superset/assets/src/dashboard/components/ColorComponentPane.jsx b/superset/assets/src/dashboard/components/ColorComponentPane.jsx new file mode 100644 index 0000000000000..ee6aec5852871 --- /dev/null +++ b/superset/assets/src/dashboard/components/ColorComponentPane.jsx @@ -0,0 +1,107 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +/* eslint-env browser */ +import PropTypes from 'prop-types'; +import React from 'react'; +import { getCategoricalSchemeRegistry } from '@superset-ui/color'; +import { t } from '@superset-ui/translation'; + +import ColorSchemeControl from '../../explore/components/controls/ColorSchemeControl'; +import { BUILDER_PANE_TYPE } from '../util/constants'; + +const propTypes = { + showBuilderPane: PropTypes.func.isRequired, + setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired, + colorScheme: PropTypes.string, +}; + +const defaultProps = { + colorScheme: undefined, +}; + +class ColorComponentPane extends React.PureComponent { + constructor(props) { + super(props); + this.state = { hovered: false }; + this.categoricalSchemeRegistry = getCategoricalSchemeRegistry(); + this.getChoices = this.getChoices.bind(this); + this.getSchemes = this.getSchemes.bind(this); + this.onCloseButtonClick = this.onCloseButtonClick.bind(this); + this.onMouseEnter = this.setHover.bind(this, true); + this.onMouseLeave = this.setHover.bind(this, false); + } + + onCloseButtonClick() { + this.props.showBuilderPane(BUILDER_PANE_TYPE.NONE); + } + + getChoices() { + return this.categoricalSchemeRegistry.keys().map(s => [s, s]); + } + + getSchemes() { + return this.categoricalSchemeRegistry.getMap(); + } + + setHover(hovered) { + this.setState({ hovered }); + } + + render() { + const { setColorSchemeAndUnsavedChanges, colorScheme } = this.props; + + return ( +
+
+
+ {'Color Settings'} + +
+
+ +
+
+
+ ); + } +} + +ColorComponentPane.propTypes = propTypes; +ColorComponentPane.defaultProps = defaultProps; + +export default ColorComponentPane; diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx index e635f902ddd8a..7bd6f6f39b373 100644 --- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx +++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx @@ -38,6 +38,7 @@ import WithPopoverMenu from './menu/WithPopoverMenu'; import getDragDropManager from '../util/getDragDropManager'; import { + BUILDER_PANE_TYPE, DASHBOARD_GRID_ID, DASHBOARD_ROOT_ID, DASHBOARD_ROOT_DEPTH, @@ -51,13 +52,15 @@ const propTypes = { dashboardLayout: PropTypes.object.isRequired, deleteTopLevelTabs: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, - showBuilderPane: PropTypes.bool, + showBuilderPane: PropTypes.func.isRequired, + builderPaneType: PropTypes.string.isRequired, + setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired, + colorScheme: PropTypes.string, handleComponentDrop: PropTypes.func.isRequired, - toggleBuilderPane: PropTypes.func.isRequired, }; const defaultProps = { - showBuilderPane: false, + colorScheme: undefined, }; class DashboardBuilder extends React.Component { @@ -102,7 +105,15 @@ class DashboardBuilder extends React.Component { } render() { - const { handleComponentDrop, dashboardLayout, editMode } = this.props; + const { + handleComponentDrop, + dashboardLayout, + editMode, + showBuilderPane, + builderPaneType, + setColorSchemeAndUnsavedChanges, + colorScheme, + } = this.props; const { tabIndex } = this.state; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; const rootChildId = dashboardRoot.children[0]; @@ -202,10 +213,13 @@ class DashboardBuilder extends React.Component { )}
- {this.props.editMode && this.props.showBuilderPane && ( + {editMode && builderPaneType !== BUILDER_PANE_TYPE.NONE && ( )} diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 796a2df09ab3c..366efbe3d8f7b 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -19,6 +19,7 @@ /* eslint-env browser */ import React from 'react'; import PropTypes from 'prop-types'; +import { CategoricalColorNamespace } from '@superset-ui/color'; import { t } from '@superset-ui/translation'; import HeaderActionsDropdown from './HeaderActionsDropdown'; @@ -29,6 +30,7 @@ import UndoRedoKeylisteners from './UndoRedoKeylisteners'; import { chartPropShape } from '../util/propShapes'; import { + BUILDER_PANE_TYPE, UNDO_LIMIT, SAVE_TYPE_OVERWRITE, DASHBOARD_POSITION_DATA_LIMIT, @@ -52,6 +54,8 @@ const propTypes = { filters: PropTypes.object.isRequired, expandedSlices: PropTypes.object.isRequired, css: PropTypes.string.isRequired, + colorNamespace: PropTypes.string, + colorScheme: PropTypes.string, isStarred: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, onSave: PropTypes.func.isRequired, @@ -63,8 +67,8 @@ const propTypes = { updateDashboardTitle: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, setEditMode: PropTypes.func.isRequired, - showBuilderPane: PropTypes.bool.isRequired, - toggleBuilderPane: PropTypes.func.isRequired, + showBuilderPane: PropTypes.func.isRequired, + builderPaneType: PropTypes.string.isRequired, updateCss: PropTypes.func.isRequired, logEvent: PropTypes.func.isRequired, hasUnsavedChanges: PropTypes.bool.isRequired, @@ -81,6 +85,11 @@ const propTypes = { setRefreshFrequency: PropTypes.func.isRequired, }; +const defaultProps = { + colorNamespace: undefined, + colorScheme: undefined, +}; + class Header extends React.PureComponent { static discardChanges() { window.location.reload(); @@ -96,6 +105,10 @@ class Header extends React.PureComponent { this.handleChangeText = this.handleChangeText.bind(this); this.handleCtrlZ = this.handleCtrlZ.bind(this); this.handleCtrlY = this.handleCtrlY.bind(this); + this.onInsertComponentsButtonClick = this.onInsertComponentsButtonClick.bind( + this, + ); + this.onColorsButtonClick = this.onColorsButtonClick.bind(this); this.toggleEditMode = this.toggleEditMode.bind(this); this.forceRefresh = this.forceRefresh.bind(this); this.startPeriodicRender = this.startPeriodicRender.bind(this); @@ -128,25 +141,12 @@ class Header extends React.PureComponent { clearTimeout(this.ctrlZTimeout); } - forceRefresh() { - if (!this.props.isLoading) { - const chartList = Object.values(this.props.charts); - this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, { - force: true, - interval: 0, - chartCount: chartList.length, - }); - return this.props.fetchCharts(chartList, true); - } - return false; + onInsertComponentsButtonClick() { + this.props.showBuilderPane(BUILDER_PANE_TYPE.ADD_COMPONENTS); } - startPeriodicRender(interval) { - this.props.logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, { - force: true, - interval, - }); - return this.props.startPeriodicRender(interval); + onColorsButtonClick() { + this.props.showBuilderPane(BUILDER_PANE_TYPE.COLORS); } handleChangeText(nextText) { @@ -177,6 +177,27 @@ class Header extends React.PureComponent { }); } + forceRefresh() { + if (!this.props.isLoading) { + const chartList = Object.values(this.props.charts); + this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, { + force: true, + interval: 0, + chartCount: chartList.length, + }); + return this.props.fetchCharts(chartList, true); + } + return false; + } + + startPeriodicRender(interval) { + this.props.logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, { + force: true, + interval, + }); + return this.props.startPeriodicRender(interval); + } + toggleEditMode() { this.props.logEvent(LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, { edit_mode: !this.props.editMode, @@ -190,14 +211,24 @@ class Header extends React.PureComponent { layout: positions, expandedSlices, css, + colorNamespace, + colorScheme, filters, dashboardInfo, } = this.props; + const scale = CategoricalColorNamespace.getScale( + colorScheme, + colorNamespace, + ); + const labelColors = scale.getColorMap(); const data = { positions, expanded_slices: expandedSlices, css, + color_namespace: colorNamespace, + color_scheme: colorScheme, + label_colors: labelColors, dashboard_title: dashboardTitle, default_filters: safeStringify(filters), }; @@ -229,6 +260,8 @@ class Header extends React.PureComponent { filters, expandedSlices, css, + colorNamespace, + colorScheme, onUndo, onRedo, undoLength, @@ -237,7 +270,7 @@ class Header extends React.PureComponent { onSave, updateCss, editMode, - showBuilderPane, + builderPaneType, dashboardInfo, hasUnsavedChanges, isLoading, @@ -294,10 +327,22 @@ class Header extends React.PureComponent { )} {editMode && ( - + )} + + {editMode && ( + )} @@ -351,6 +396,8 @@ class Header extends React.PureComponent { filters={filters} expandedSlices={expandedSlices} css={css} + colorNamespace={colorNamespace} + colorScheme={colorScheme} onSave={onSave} onChange={onChange} forceRefreshAllCharts={this.forceRefresh} @@ -371,5 +418,6 @@ class Header extends React.PureComponent { } Header.propTypes = propTypes; +Header.defaultProps = defaultProps; export default Header; diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx index 4c36d9a3c6120..5fc9d0ede851c 100644 --- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx @@ -37,6 +37,8 @@ const propTypes = { dashboardTitle: PropTypes.string.isRequired, hasUnsavedChanges: PropTypes.bool.isRequired, css: PropTypes.string.isRequired, + colorNamespace: PropTypes.string, + colorScheme: PropTypes.string, onChange: PropTypes.func.isRequired, updateCss: PropTypes.func.isRequired, forceRefreshAllCharts: PropTypes.func.isRequired, @@ -53,7 +55,10 @@ const propTypes = { onSave: PropTypes.func.isRequired, }; -const defaultProps = {}; +const defaultProps = { + colorNamespace: undefined, + colorScheme: undefined, +}; class HeaderActionsDropdown extends React.PureComponent { static discardChanges() { @@ -111,6 +116,8 @@ class HeaderActionsDropdown extends React.PureComponent { refreshFrequency, editMode, css, + colorNamespace, + colorScheme, hasUnsavedChanges, layout, filters, @@ -145,6 +152,8 @@ class HeaderActionsDropdown extends React.PureComponent { expandedSlices={expandedSlices} refreshFrequency={refreshFrequency} css={css} + colorNamespace={colorNamespace} + colorScheme={colorScheme} onSave={onSave} isMenuItem triggerNode={{t('Save as')}} diff --git a/superset/assets/src/dashboard/components/InsertComponentPane.jsx b/superset/assets/src/dashboard/components/InsertComponentPane.jsx new file mode 100644 index 0000000000000..31413471f7520 --- /dev/null +++ b/superset/assets/src/dashboard/components/InsertComponentPane.jsx @@ -0,0 +1,118 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +/* eslint-env browser */ +import PropTypes from 'prop-types'; +import React from 'react'; +import cx from 'classnames'; +import { t } from '@superset-ui/translation'; + +import NewColumn from './gridComponents/new/NewColumn'; +import NewDivider from './gridComponents/new/NewDivider'; +import NewHeader from './gridComponents/new/NewHeader'; +import NewRow from './gridComponents/new/NewRow'; +import NewTabs from './gridComponents/new/NewTabs'; +import NewMarkdown from './gridComponents/new/NewMarkdown'; +import SliceAdder from '../containers/SliceAdder'; +import { BUILDER_PANE_TYPE } from '../util/constants'; + +export const SUPERSET_HEADER_HEIGHT = 59; + +const propTypes = { + height: PropTypes.number.isRequired, + isSticky: PropTypes.bool.isRequired, + showBuilderPane: PropTypes.func.isRequired, +}; + +class InsertComponentPane extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + slideDirection: 'slide-out', + }; + + this.onCloseButtonClick = this.onCloseButtonClick.bind(this); + this.openSlicesPane = this.slide.bind(this, 'slide-in'); + this.closeSlicesPane = this.slide.bind(this, 'slide-out'); + } + + onCloseButtonClick() { + this.props.showBuilderPane(BUILDER_PANE_TYPE.NONE); + } + + slide(direction) { + this.setState({ + slideDirection: direction, + }); + } + + render() { + return ( +
+
+
+ {t('Insert components')} + +
+
+
+
+ {t('Your charts & filters')} +
+ + +
+ + + + + + +
+
+
+ + {t('Your charts and filters')} +
+ +
+
+ ); + } +} + +InsertComponentPane.propTypes = propTypes; + +export default InsertComponentPane; diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx index aa5436979c65e..1873f0c3133a3 100644 --- a/superset/assets/src/dashboard/components/SaveModal.jsx +++ b/superset/assets/src/dashboard/components/SaveModal.jsx @@ -20,6 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap'; +import { CategoricalColorNamespace } from '@superset-ui/color'; import { t } from '@superset-ui/translation'; import ModalTrigger from '../../components/ModalTrigger'; @@ -38,6 +39,8 @@ const propTypes = { triggerNode: PropTypes.node.isRequired, filters: PropTypes.object.isRequired, css: PropTypes.string.isRequired, + colorNamespace: PropTypes.string, + colorScheme: PropTypes.string, onSave: PropTypes.func.isRequired, isMenuItem: PropTypes.bool, canOverwrite: PropTypes.bool.isRequired, @@ -47,6 +50,8 @@ const propTypes = { const defaultProps = { isMenuItem: false, saveType: SAVE_TYPE_OVERWRITE, + colorNamespace: undefined, + colorScheme: undefined, }; class SaveModal extends React.PureComponent { @@ -93,15 +98,25 @@ class SaveModal extends React.PureComponent { dashboardTitle, layout: positions, css, + colorNamespace, + colorScheme, expandedSlices, filters, dashboardId, refreshFrequency, } = this.props; + const scale = CategoricalColorNamespace.getScale( + colorScheme, + colorNamespace, + ); + const labelColors = scale.getColorMap(); const data = { positions, css, + color_namespace: colorNamespace, + color_scheme: colorScheme, + label_colors: labelColors, expanded_slices: expandedSlices, dashboard_title: saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle, diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx index 1e0e64c602479..c0926db0a9839 100644 --- a/superset/assets/src/dashboard/containers/Chart.jsx +++ b/superset/assets/src/dashboard/containers/Chart.jsx @@ -43,7 +43,7 @@ function mapStateToProps( ) { const { id } = ownProps; const chart = chartQueries[id] || {}; - const { filters } = dashboardState; + const { filters, colorScheme } = dashboardState; return { chart, @@ -58,6 +58,7 @@ function mapStateToProps( chart, dashboardMetadata: dashboardInfo.metadata, filters, + colorScheme, sliceId: id, }), editMode: dashboardState.editMode, diff --git a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx index 3ca70431a436d..3c6514a2f736c 100644 --- a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx +++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx @@ -20,7 +20,10 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import DashboardBuilder from '../components/DashboardBuilder'; -import { toggleBuilderPane } from '../actions/dashboardState'; +import { + setColorSchemeAndUnsavedChanges, + showBuilderPane, +} from '../actions/dashboardState'; import { deleteTopLevelTabs, handleComponentDrop, @@ -30,7 +33,8 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) { return { dashboardLayout: undoableLayout.present, editMode: dashboardState.editMode, - showBuilderPane: dashboardState.showBuilderPane, + builderPaneType: dashboardState.builderPaneType, + colorScheme: dashboardState.colorScheme, }; } @@ -39,7 +43,8 @@ function mapDispatchToProps(dispatch) { { deleteTopLevelTabs, handleComponentDrop, - toggleBuilderPane, + showBuilderPane, + setColorSchemeAndUnsavedChanges, }, dispatch, ); diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx index 570d790ae72bd..05e90fb924437 100644 --- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx +++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx @@ -24,7 +24,7 @@ import isDashboardLoading from '../util/isDashboardLoading'; import { setEditMode, - toggleBuilderPane, + showBuilderPane, fetchFaveStar, saveFaveStar, fetchCharts, @@ -71,6 +71,8 @@ function mapStateToProps({ expandedSlices: dashboardState.expandedSlices, refreshFrequency: dashboardState.refreshFrequency, css: dashboardState.css, + colorNamespace: dashboardState.colorNamespace, + colorScheme: dashboardState.colorScheme, charts, userId: dashboardInfo.userId, isStarred: !!dashboardState.isStarred, @@ -78,7 +80,7 @@ function mapStateToProps({ hasUnsavedChanges: !!dashboardState.hasUnsavedChanges, maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded, editMode: !!dashboardState.editMode, - showBuilderPane: !!dashboardState.showBuilderPane, + builderPaneType: dashboardState.builderPaneType, }; } @@ -91,7 +93,7 @@ function mapDispatchToProps(dispatch) { onUndo: undoLayoutAction, onRedo: redoLayoutAction, setEditMode, - toggleBuilderPane, + showBuilderPane, fetchFaveStar, saveFaveStar, fetchCharts, diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js index 24066ebca6f18..007f63a286553 100644 --- a/superset/assets/src/dashboard/reducers/dashboardState.js +++ b/superset/assets/src/dashboard/reducers/dashboardState.js @@ -23,15 +23,17 @@ import { ON_CHANGE, ON_SAVE, REMOVE_SLICE, + SET_COLOR_SCHEME, SET_EDIT_MODE, SET_MAX_UNDO_HISTORY_EXCEEDED, SET_UNSAVED_CHANGES, - TOGGLE_BUILDER_PANE, + SHOW_BUILDER_PANE, TOGGLE_EXPAND_SLICE, TOGGLE_FAVE_STAR, UPDATE_CSS, SET_REFRESH_FREQUENCY, } from '../actions/dashboardState'; +import { BUILDER_PANE_TYPE } from '../util/constants'; export default function dashboardStateReducer(state = {}, action) { const actionHandlers = { @@ -73,15 +75,24 @@ export default function dashboardStateReducer(state = {}, action) { return { ...state, editMode: action.editMode, - showBuilderPane: !!action.editMode, + builderPaneType: action.editMode + ? BUILDER_PANE_TYPE.ADD_COMPONENTS + : BUILDER_PANE_TYPE.NONE, }; }, [SET_MAX_UNDO_HISTORY_EXCEEDED]() { const { maxUndoHistoryExceeded = true } = action.payload; return { ...state, maxUndoHistoryExceeded }; }, - [TOGGLE_BUILDER_PANE]() { - return { ...state, showBuilderPane: !state.showBuilderPane }; + [SHOW_BUILDER_PANE]() { + return { ...state, builderPaneType: action.builderPaneType }; + }, + [SET_COLOR_SCHEME]() { + return { + ...state, + colorScheme: action.colorScheme, + updatedColorScheme: true, + }; }, [TOGGLE_EXPAND_SLICE]() { const updatedExpandedSlices = { ...state.expandedSlices }; @@ -102,6 +113,8 @@ export default function dashboardStateReducer(state = {}, action) { hasUnsavedChanges: false, maxUndoHistoryExceeded: false, editMode: false, + builderPaneType: BUILDER_PANE_TYPE.NONE, + updatedColorScheme: false, }; }, diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js index a9c4d8f95f514..69396de786fb2 100644 --- a/superset/assets/src/dashboard/reducers/getInitialState.js +++ b/superset/assets/src/dashboard/reducers/getInitialState.js @@ -17,6 +17,7 @@ * under the License. */ /* eslint-disable camelcase */ +import { isString } from 'lodash'; import shortid from 'shortid'; import { CategoricalColorNamespace } from '@superset-ui/color'; @@ -28,6 +29,7 @@ import findFirstParentContainerId from '../util/findFirstParentContainer'; import getEmptyLayout from '../util/getEmptyLayout'; import newComponentFactory from '../util/newComponentFactory'; import { + BUILDER_PANE_TYPE, DASHBOARD_HEADER_ID, GRID_DEFAULT_CHART_WIDTH, GRID_COLUMN_COUNT, @@ -55,9 +57,16 @@ export default function(bootstrapData) { // Priming the color palette with user's label-color mapping provided in // the dashboard's JSON metadata if (dashboard.metadata && dashboard.metadata.label_colors) { - const colorMap = dashboard.metadata.label_colors; + const scheme = dashboard.metadata.color_scheme; + const namespace = dashboard.metadata.color_namespace; + const colorMap = isString(dashboard.metadata.label_colors) + ? JSON.parse(dashboard.metadata.label_colors) + : dashboard.metadata.label_colors; Object.keys(colorMap).forEach(label => { - CategoricalColorNamespace.getScale().setColor(label, colorMap[label]); + CategoricalColorNamespace.getScale(scheme, namespace).setColor( + label, + colorMap[label], + ); }); } @@ -188,8 +197,13 @@ export default function(bootstrapData) { expandedSlices: dashboard.metadata.expanded_slices || {}, refreshFrequency: dashboard.metadata.refresh_frequency || 0, css: dashboard.css || '', + colorNamespace: dashboard.metadata.color_namespace, + colorScheme: dashboard.metadata.color_scheme, editMode: dashboard.dash_edit_perm && editMode, - showBuilderPane: dashboard.dash_edit_perm && editMode, + builderPaneType: + dashboard.dash_edit_perm && editMode + ? BUILDER_PANE_TYPE.ADD_COMPONENTS + : BUILDER_PANE_TYPE.NONE, hasUnsavedChanges: false, maxUndoHistoryExceeded: false, }, diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less index 3b850c84fa7e2..6bf6f6fafdba7 100644 --- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less +++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less @@ -185,4 +185,18 @@ outline: none; } } + + .color-scheme-container { + list-style: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + } + + .color-scheme-container li { + flex-basis: 9px; + height: 10px; + margin: 9px 1px; + } } diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less index 3dc9672b1997b..16541db1eefc4 100644 --- a/superset/assets/src/dashboard/stylesheets/dashboard.less +++ b/superset/assets/src/dashboard/stylesheets/dashboard.less @@ -120,7 +120,14 @@ body { display: flex; flex-direction: row; flex-wrap: nowrap; - & > :not(:last-child) { + & > :nth-child(3) { + border-radius: 2px 0px 0px 2px; + border-right: none; + } + & > :nth-child(4) { + border-radius: 0px 2px 2px 0px; + } + & > :not(:nth-child(3)):not(:last-child) { margin-right: 8px; } } diff --git a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js index f397a937f8de7..a928a12b944d8 100644 --- a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js +++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js @@ -28,12 +28,15 @@ export default function getFormDataWithExtraFilters({ chart = {}, dashboardMetadata, filters, + colorScheme, sliceId, }) { // if dashboard metadata + filters have not changed, use cache if possible if ( (cachedDashboardMetadataByChart[sliceId] || {}) === dashboardMetadata && (cachedFiltersByChart[sliceId] || {}) === filters && + (colorScheme == null || + cachedFormdataByChart[sliceId].color_scheme === colorScheme) && !!cachedFormdataByChart[sliceId] ) { return cachedFormdataByChart[sliceId]; @@ -41,6 +44,7 @@ export default function getFormDataWithExtraFilters({ const formData = { ...chart.formData, + ...(colorScheme && { color_scheme: colorScheme }), extra_filters: getEffectiveExtraFilters({ dashboardMetadata, filters, diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js index 5cce3ae37929a..9b33ca8991e6c 100644 --- a/superset/assets/src/dashboard/util/constants.js +++ b/superset/assets/src/dashboard/util/constants.js @@ -62,3 +62,10 @@ export const SAVE_TYPE_NEWDASHBOARD = 'newDashboard'; // default dashboard layout data size limit // could be overwritten by server-side config export const DASHBOARD_POSITION_DATA_LIMIT = 65535; + +// Dashboard pane types +export const BUILDER_PANE_TYPE = { + NONE: 'NONE', + ADD_COMPONENTS: 'ADD_COMPONENTS', + COLORS: 'COLORS', +}; diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx index c433de924e15a..c50ffc6a09496 100644 --- a/superset/assets/src/dashboard/util/propShapes.jsx +++ b/superset/assets/src/dashboard/util/propShapes.jsx @@ -72,7 +72,10 @@ export const dashboardStatePropShape = PropTypes.shape({ filters: PropTypes.object.isRequired, expandedSlices: PropTypes.object, editMode: PropTypes.bool, - showBuilderPane: PropTypes.bool, + builderPaneType: PropTypes.string.isRequired, + colorNamespace: PropTypes.string, + colorScheme: PropTypes.string, + updatedColorScheme: PropTypes.bool, hasUnsavedChanges: PropTypes.bool, }); diff --git a/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx b/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx index 1e1e67718e6ec..34a4d3cf1f905 100644 --- a/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx +++ b/superset/assets/src/explore/components/controls/ColorSchemeControl.jsx @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import { isFunction } from 'lodash'; import { Creatable } from 'react-select'; import ControlHeader from '../ControlHeader'; +import TooltipWrapper from '../../../components/TooltipWrapper'; const propTypes = { description: PropTypes.string, @@ -77,17 +78,22 @@ export default class ColorSchemeControl extends React.PureComponent { } return ( -
    - {colors.map((color, i) => ( -
  •  
  • - ))} -
+ +
    + {colors.map((color, i) => ( +
  •  
  • + ))} +
+
); } diff --git a/superset/views/core.py b/superset/views/core.py index 39343d67c7756..794bcb36c79ca 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1741,6 +1741,12 @@ def _set_dash_metadata(dashboard, data): {key: v for key, v in default_filters_data.items() if int(key) in slice_ids} md['default_filters'] = json.dumps(applicable_filters) + if data.get('color_namespace'): + md['color_namespace'] = data.get('color_namespace') + if data.get('color_scheme'): + md['color_scheme'] = data.get('color_scheme') + if data.get('label_colors'): + md['label_colors'] = data.get('label_colors') dashboard.json_metadata = json.dumps(md) @api diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index c436753dd0971..04dcd5907a032 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -191,17 +191,60 @@ def test_save_dash_with_dashboard_title(self, username='admin'): data['dashboard_title'] = origin_title self.get_resp(url, data=dict(data=json.dumps(data))) + def test_save_dash_with_colors(self, username='admin'): + self.login(username=username) + dash = ( + db.session.query(models.Dashboard) + .filter_by(slug='births') + .first() + ) + positions = self.get_mock_positions(dash) + new_label_colors = { + 'data value': 'random color', + } + data = { + 'css': '', + 'expanded_slices': {}, + 'positions': positions, + 'dashboard_title': dash.dashboard_title, + 'color_namespace': 'Color Namespace Test', + 'color_scheme': 'Color Scheme Test', + 'label_colors': new_label_colors, + + } + url = '/superset/save_dash/{}/'.format(dash.id) + self.get_resp(url, data=dict(data=json.dumps(data))) + updatedDash = ( + db.session.query(models.Dashboard) + .filter_by(slug='births') + .first() + ) + self.assertIn('color_namespace', updatedDash.json_metadata) + self.assertIn('color_scheme', updatedDash.json_metadata) + self.assertIn('label_colors', updatedDash.json_metadata) + # bring back original dashboard + del data['color_namespace'] + del data['color_scheme'] + del data['label_colors'] + self.get_resp(url, data=dict(data=json.dumps(data))) + 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) + new_label_colors = { + 'data value': 'random color', + } data = { 'css': '', 'duplicate_slices': False, 'expanded_slices': {}, 'positions': positions, 'dashboard_title': 'Copy Of Births', + 'color_namespace': 'Color Namespace Test', + 'color_scheme': 'Color Scheme Test', + 'label_colors': new_label_colors, } # Save changes to Births dashboard and retrieve updated dash