superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From christ...@apache.org
Subject [incubator-superset] branch lyftga updated: [Lyft-GA] Enable color consistency in a dashboard (#7135)
Date Fri, 29 Mar 2019 13:35:32 GMT
This is an automated email from the ASF dual-hosted git repository.

christine pushed a commit to branch lyftga
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/lyftga by this push:
     new 71eea53  [Lyft-GA] Enable color consistency in a dashboard (#7135)
71eea53 is described below

commit 71eea53d70235c59b7088404694bdef74b0f678b
Author: Kim Truong <47833996+khtruong@users.noreply.github.com>
AuthorDate: Fri Mar 29 06:35:22 2019 -0700

    [Lyft-GA] Enable color consistency in a dashboard (#7135)
    
    * Enable color consistency in a dashboard
    
    Moved actions, minor UI, allowed dashboard copy
    
    Fix linting errors
    
    Undo unintentional change
    
    Updated and added unit tests
    
    Fail quietly if package has not been updated
    
    Fail quietly on dashboard copy if package is old
    
    * Update packages
    
    * Remove unnecessary code
    
    * Addressed Grace's comments
    
    * Small fix for item key
    
    * Reset chart's color during exploration
    
    * Do not reset chart form data when exploring chart
---
 superset/assets/package-lock.json                  |  52 ++++++---
 superset/assets/package.json                       |   2 +-
 .../dashboard/components/DashboardBuilder_spec.jsx |  26 ++++-
 .../dashboard/components/Header_spec.jsx           |   8 +-
 .../dashboard/fixtures/mockDashboardState.js       |   3 +-
 .../dashboard/reducers/dashboardState_spec.js      |  25 ++---
 superset/assets/src/chart/ChartRenderer.jsx        |   3 +-
 .../src/dashboard/actions/dashboardLayout.js       |   3 +-
 .../assets/src/dashboard/actions/dashboardState.js |  18 +++-
 .../dashboard/components/BuilderComponentPane.jsx  | 112 ++++++-------------
 .../dashboard/components/ColorComponentPane.jsx    | 107 +++++++++++++++++++
 .../src/dashboard/components/DashboardBuilder.jsx  |  26 +++--
 .../assets/src/dashboard/components/Header.jsx     |  96 ++++++++++++-----
 .../dashboard/components/HeaderActionsDropdown.jsx |  11 +-
 .../dashboard/components/InsertComponentPane.jsx   | 118 +++++++++++++++++++++
 .../assets/src/dashboard/components/SaveModal.jsx  |  15 +++
 superset/assets/src/dashboard/containers/Chart.jsx |   3 +-
 .../src/dashboard/containers/DashboardBuilder.jsx  |  11 +-
 .../src/dashboard/containers/DashboardHeader.jsx   |   8 +-
 .../src/dashboard/reducers/dashboardState.js       |  21 +++-
 .../src/dashboard/reducers/getInitialState.js      |  20 +++-
 .../dashboard/stylesheets/builder-sidepane.less    |  14 +++
 .../src/dashboard/stylesheets/dashboard.less       |   9 +-
 .../util/charts/getFormDataWithExtraFilters.js     |   4 +
 superset/assets/src/dashboard/util/constants.js    |   7 ++
 superset/assets/src/dashboard/util/propShapes.jsx  |   5 +-
 .../components/controls/ColorSchemeControl.jsx     |  28 +++--
 superset/views/core.py                             |   6 ++
 tests/dashboard_tests.py                           |  43 ++++++++
 29 files changed, 619 insertions(+), 185 deletions(-)

diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json
index 48beb9c..c33a866 100644
--- a/superset/assets/package-lock.json
+++ b/superset/assets/package-lock.json
@@ -2145,11 +2145,10 @@
       }
     },
     "@superset-ui/color": {
-      "version": "0.9.5",
-      "resolved": "https://registry.npmjs.org/@superset-ui/color/-/color-0.9.5.tgz",
-      "integrity": "sha512-ncJr5bOb9j4VOobGkn/Szw8m2Bm3pwF0HxHgoHfcM3JgtxabtZXVlF4AAVwoYg4IXBk0eHzqETphZbbOTfInPg==",
+      "version": "0.10.8",
+      "resolved": "https://registry.npmjs.org/@superset-ui/color/-/color-0.10.8.tgz",
+      "integrity": "sha512-H1M8V9OKO3fCmOHQvW1rN9pRw2t/L1LKHvxzEj/Kccw+osckdmF8RtKEp7DaBuKMO6PF2Kq2FWNIiqNtin9whA==",
       "requires": {
-        "@superset-ui/core": "^0.9.5",
         "@types/d3-scale": "^2.0.2",
         "d3-scale": "^2.1.2"
       }
@@ -2166,7 +2165,7 @@
       "dependencies": {
         "whatwg-fetch": {
           "version": "2.0.4",
-          "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
+          "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
           "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
         }
       }
@@ -8790,7 +8789,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -8811,12 +8811,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -8831,17 +8833,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -8958,7 +8963,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -8970,6 +8976,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -8984,6 +8991,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -8991,12 +8999,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -9015,6 +9025,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -9095,7 +9106,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -9107,6 +9119,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -9192,7 +9205,8 @@
         "safe-buffer": {
           "version": "5.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -9228,6 +9242,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -9247,6 +9262,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -9290,12 +9306,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
@@ -16651,7 +16669,7 @@
         },
         "minimist": {
           "version": "0.0.8",
-          "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
           "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
         },
         "mjolnir.js": {
diff --git a/superset/assets/package.json b/superset/assets/package.json
index d657698..695cff3 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -49,7 +49,7 @@
   "dependencies": {
     "@data-ui/sparkline": "^0.0.54",
     "@superset-ui/chart": "^0.9.6",
-    "@superset-ui/color": "^0.9.5",
+    "@superset-ui/color": "^0.10.8",
     "@superset-ui/connection": "^0.9.5",
     "@superset-ui/core": "^0.9.5",
     "@superset-ui/legacy-plugin-chart-calendar": "^0.1.1",
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
index bf24644..16dc33d 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 2fff463..69e57da 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 f326a76..3763ef4 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 c3e3855..dadcf06 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 dc057a3..00c16ee 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 5b163d9..2716b00 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 135658d..ffba30e 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -220,9 +220,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) {
@@ -261,6 +261,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 4c2e92c..2d2ab08 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 (
       <div
         className="dashboard-builder-sidepane"
@@ -78,56 +66,22 @@ class BuilderComponentPane extends React.PureComponent {
                     className="viewport"
                     style={isSticky ? { ...style, top: topOffset } : null}
                   >
-                    <div
-                      className={cx(
-                        'slider-container',
-                        this.state.slideDirection,
-                      )}
-                    >
-                      <div className="component-layer slide-content">
-                        <div className="dashboard-builder-sidepane-header">
-                          <span>{t('Insert components')}</span>
-                          <i
-                            className="fa fa-times trigger"
-                            onClick={this.props.toggleBuilderPane}
-                            role="none"
-                          />
-                        </div>
-                        <div
-                          className="new-component static"
-                          role="none"
-                          onClick={this.openSlicesPane}
-                        >
-                          <div className="new-component-placeholder fa fa-area-chart" />
-                          <div className="new-component-label">
-                            {t('Your charts & filters')}
-                          </div>
-
-                          <i className="fa fa-arrow-right trigger" />
-                        </div>
-                        <NewTabs />
-                        <NewRow />
-                        <NewColumn />
-                        <NewHeader />
-                        <NewMarkdown />
-                        <NewDivider />
-                      </div>
-                      <div className="slices-layer slide-content">
-                        <div
-                          className="dashboard-builder-sidepane-header"
-                          onClick={this.closeSlicesPane}
-                          role="none"
-                        >
-                          <i className="fa fa-arrow-left trigger" />
-                          <span>{t('Your charts and filters')}</span>
-                        </div>
-                        <SliceAdder
-                          height={
-                            height + (isSticky ? SUPERSET_HEADER_HEIGHT : 0)
-                          }
-                        />
-                      </div>
-                    </div>
+                    {builderPaneType === BUILDER_PANE_TYPE.ADD_COMPONENTS && (
+                      <InsertComponentPane
+                        height={height}
+                        isSticky={isSticky}
+                        showBuilderPane={showBuilderPane}
+                      />
+                    )}
+                    {builderPaneType === BUILDER_PANE_TYPE.COLORS && (
+                      <ColorComponentPane
+                        showBuilderPane={showBuilderPane}
+                        setColorSchemeAndUnsavedChanges={
+                          setColorSchemeAndUnsavedChanges
+                        }
+                        colorScheme={colorScheme}
+                      />
+                    )}
                   </div>
                 )}
               </Sticky>
diff --git a/superset/assets/src/dashboard/components/ColorComponentPane.jsx b/superset/assets/src/dashboard/components/ColorComponentPane.jsx
new file mode 100644
index 0000000..ee6aec5
--- /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 (
+      <div className="slider-container">
+        <div className="component-layer slide-content">
+          <div className="dashboard-builder-sidepane-header">
+            <span>{'Color Settings'}</span>
+            <i
+              className="fa fa-times trigger"
+              onClick={this.onCloseButtonClick}
+              role="none"
+            />
+          </div>
+          <div
+            className="panel-body"
+            onMouseEnter={this.onMouseEnter}
+            onMouseLeave={this.onMouseLeave}
+          >
+            <ColorSchemeControl
+              description={t(
+                "Any color palette selected here will override the colors applied to this dashboard's individual charts",
+              )}
+              label={t('Color Scheme')}
+              name="color_scheme"
+              onChange={setColorSchemeAndUnsavedChanges}
+              value={colorScheme}
+              choices={this.getChoices}
+              schemes={this.getSchemes}
+              hovered={this.state.hovered}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+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 e635f90..7bd6f6f 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 {
               )}
             </ParentSize>
           </div>
-          {this.props.editMode && this.props.showBuilderPane && (
+          {editMode && builderPaneType !== BUILDER_PANE_TYPE.NONE && (
             <BuilderComponentPane
               topOffset={HEADER_HEIGHT + (topLevelTabs ? TABS_HEIGHT : 0)}
-              toggleBuilderPane={this.props.toggleBuilderPane}
+              showBuilderPane={showBuilderPane}
+              builderPaneType={builderPaneType}
+              setColorSchemeAndUnsavedChanges={setColorSchemeAndUnsavedChanges}
+              colorScheme={colorScheme}
             />
           )}
         </div>
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index b2238d0..dc34d50 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,
@@ -79,6 +83,11 @@ const propTypes = {
   maxUndoHistoryToast: PropTypes.func.isRequired,
 };
 
+const defaultProps = {
+  colorNamespace: undefined,
+  colorScheme: undefined,
+};
+
 class Header extends React.PureComponent {
   static discardChanges() {
     window.location.reload();
@@ -94,6 +103,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);
@@ -121,25 +134,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) {
@@ -170,6 +170,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,
@@ -183,14 +204,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),
     };
@@ -222,6 +253,8 @@ class Header extends React.PureComponent {
       filters,
       expandedSlices,
       css,
+      colorNamespace,
+      colorScheme,
       onUndo,
       onRedo,
       undoLength,
@@ -230,7 +263,7 @@ class Header extends React.PureComponent {
       onSave,
       updateCss,
       editMode,
-      showBuilderPane,
+      builderPaneType,
       dashboardInfo,
       hasUnsavedChanges,
       isLoading,
@@ -285,10 +318,22 @@ class Header extends React.PureComponent {
               )}
 
               {editMode && (
-                <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
-                  {showBuilderPane
-                    ? t('Hide components')
-                    : t('Insert components')}
+                <Button
+                  active={builderPaneType === BUILDER_PANE_TYPE.ADD_COMPONENTS}
+                  bsSize="small"
+                  onClick={this.onInsertComponentsButtonClick}
+                >
+                  {t('Insert components')}
+                </Button>
+              )}
+
+              {editMode && (
+                <Button
+                  active={builderPaneType === BUILDER_PANE_TYPE.COLORS}
+                  bsSize="small"
+                  onClick={this.onColorsButtonClick}
+                >
+                  {t('Colors')}
                 </Button>
               )}
 
@@ -342,6 +387,8 @@ class Header extends React.PureComponent {
             filters={filters}
             expandedSlices={expandedSlices}
             css={css}
+            colorNamespace={colorNamespace}
+            colorScheme={colorScheme}
             onSave={onSave}
             onChange={onChange}
             forceRefreshAllCharts={this.forceRefresh}
@@ -360,5 +407,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 204d5f7..bbba5a0 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,
@@ -51,7 +53,10 @@ const propTypes = {
   onSave: PropTypes.func.isRequired,
 };
 
-const defaultProps = {};
+const defaultProps = {
+  colorNamespace: undefined,
+  colorScheme: undefined,
+};
 
 class HeaderActionsDropdown extends React.PureComponent {
   static discardChanges() {
@@ -103,6 +108,8 @@ class HeaderActionsDropdown extends React.PureComponent {
       forceRefreshAllCharts,
       editMode,
       css,
+      colorNamespace,
+      colorScheme,
       hasUnsavedChanges,
       layout,
       filters,
@@ -136,6 +143,8 @@ class HeaderActionsDropdown extends React.PureComponent {
             filters={filters}
             expandedSlices={expandedSlices}
             css={css}
+            colorNamespace={colorNamespace}
+            colorScheme={colorScheme}
             onSave={onSave}
             isMenuItem
             triggerNode={<span>{t('Save as')}</span>}
diff --git a/superset/assets/src/dashboard/components/InsertComponentPane.jsx b/superset/assets/src/dashboard/components/InsertComponentPane.jsx
new file mode 100644
index 0000000..3141347
--- /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 (
+      <div className={cx('slider-container', this.state.slideDirection)}>
+        <div className="component-layer slide-content">
+          <div className="dashboard-builder-sidepane-header">
+            <span>{t('Insert components')}</span>
+            <i
+              className="fa fa-times trigger"
+              onClick={this.onCloseButtonClick}
+              role="none"
+            />
+          </div>
+          <div
+            className="new-component static"
+            role="none"
+            onClick={this.openSlicesPane}
+          >
+            <div className="new-component-placeholder fa fa-area-chart" />
+            <div className="new-component-label">
+              {t('Your charts & filters')}
+            </div>
+
+            <i className="fa fa-arrow-right trigger" />
+          </div>
+          <NewTabs />
+          <NewRow />
+          <NewColumn />
+          <NewHeader />
+          <NewMarkdown />
+          <NewDivider />
+        </div>
+        <div className="slices-layer slide-content">
+          <div
+            className="dashboard-builder-sidepane-header"
+            onClick={this.closeSlicesPane}
+            role="none"
+          >
+            <i className="fa fa-arrow-left trigger" />
+            <span>{t('Your charts and filters')}</span>
+          </div>
+          <SliceAdder
+            height={
+              this.props.height +
+              (this.props.isSticky ? SUPERSET_HEADER_HEIGHT : 0)
+            }
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+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 f9a7a6c..9370f8b 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,
@@ -46,6 +49,8 @@ const propTypes = {
 const defaultProps = {
   isMenuItem: false,
   saveType: SAVE_TYPE_OVERWRITE,
+  colorNamespace: undefined,
+  colorScheme: undefined,
 };
 
 class SaveModal extends React.PureComponent {
@@ -92,14 +97,24 @@ class SaveModal extends React.PureComponent {
       dashboardTitle,
       layout: positions,
       css,
+      colorNamespace,
+      colorScheme,
       expandedSlices,
       filters,
       dashboardId,
     } = 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 ddec2ff..eb190be 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 3ca7043..3c6514a 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 4eaca36..2878006 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,
@@ -69,6 +69,8 @@ function mapStateToProps({
     ).text,
     expandedSlices: dashboardState.expandedSlices,
     css: dashboardState.css,
+    colorNamespace: dashboardState.colorNamespace,
+    colorScheme: dashboardState.colorScheme,
     charts,
     userId: dashboardInfo.userId,
     isStarred: !!dashboardState.isStarred,
@@ -76,7 +78,7 @@ function mapStateToProps({
     hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
     maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
     editMode: !!dashboardState.editMode,
-    showBuilderPane: !!dashboardState.showBuilderPane,
+    builderPaneType: dashboardState.builderPaneType,
   };
 }
 
@@ -89,7 +91,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 72c4d36..346b5cd 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -23,14 +23,16 @@ 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,
 } from '../actions/dashboardState';
+import { BUILDER_PANE_TYPE } from '../util/constants';
 
 export default function dashboardStateReducer(state = {}, action) {
   const actionHandlers = {
@@ -72,15 +74,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 };
@@ -101,6 +112,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 9b093cd..2f71c4a 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],
+      );
     });
   }
 
@@ -186,8 +195,13 @@ export default function(bootstrapData) {
       filters,
       expandedSlices: dashboard.metadata.expanded_slices || {},
       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 3b850c8..6bf6f6f 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 3dc9672..16541db 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 f397a93..a928a12 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 5cce3ae..9b33ca8 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 c433de9..c50ffc6 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 1e1e677..34a4d3c 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 (
-      <ul className="color-scheme-container">
-        {colors.map((color, i) => (
-          <li
-            key={`${currentScheme.name}-${i}`}
-            style={{
-              backgroundColor: color,
-              border: `1px solid ${color === 'white' ? 'black' : color}`,
-            }}
-          >&nbsp;</li>
-        ))}
-      </ul>
+      <TooltipWrapper
+        label={`${currentScheme.id}-tooltip`}
+        tooltip={currentScheme.label}
+      >
+        <ul className="color-scheme-container">
+          {colors.map((color, i) => (
+            <li
+              key={`${currentScheme.id}-${i}`}
+              style={{
+                backgroundColor: color,
+                border: `1px solid ${color === 'white' ? 'black' : color}`,
+              }}
+            >&nbsp;</li>
+          ))}
+        </ul>
+      </TooltipWrapper>
     );
   }
 
diff --git a/superset/views/core.py b/superset/views/core.py
index 8d9c8f8..624e5b1 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1707,6 +1707,12 @@ class Superset(BaseSupersetView):
             {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 c436753..04dcd59 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -191,17 +191,60 @@ class DashboardTests(SupersetTestCase):
         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


Mime
View raw message