You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/04/04 18:40:20 UTC

[incubator-superset] branch chris--grid-root-and-spacer updated (bf52ea1 -> 3ffe470)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a change to branch chris--grid-root-and-spacer
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git.


    from bf52ea1  [dashboard-builder] ResizableContainer min size can't be smaller than size, fix row style, role=none on WithPopoverMenu container
     new 1a4bc31  [edit mode] add edit mode to redux and propogate to all <DashboardComponent />s
     new 3ffe470  [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 superset/assets/javascripts/dashboard/index.jsx    |  8 ++
 .../javascripts/dashboard/v2/actions/editMode.js   |  9 +++
 .../dashboard/v2/actions/messageToasts.js          | 47 ++++++++++++
 .../dashboard/v2/components/DashboardBuilder.jsx   | 18 +++--
 .../dashboard/v2/components/DashboardGrid.jsx      | 10 ++-
 .../dashboard/v2/components/DashboardHeader.jsx    | 10 +--
 .../javascripts/dashboard/v2/components/Toast.jsx  | 87 ++++++++++++++++++++++
 .../dashboard/v2/components/ToastPresenter.jsx     | 39 ++++++++++
 .../dashboard/v2/components/dnd/DragDroppable.jsx  |  4 +
 .../v2/components/gridComponents/Chart.jsx         | 17 +++--
 .../v2/components/gridComponents/Column.jsx        | 22 ++++--
 .../v2/components/gridComponents/Divider.jsx       | 10 ++-
 .../v2/components/gridComponents/Header.jsx        | 13 +++-
 .../dashboard/v2/components/gridComponents/Row.jsx | 21 ++++--
 .../dashboard/v2/components/gridComponents/Tab.jsx |  6 +-
 .../v2/components/gridComponents/Tabs.jsx          | 25 ++++---
 .../gridComponents/new/DraggableNewComponent.jsx   |  1 +
 .../v2/components/menu/WithPopoverMenu.jsx         | 37 +++++----
 .../v2/components/resizable/ResizableContainer.jsx | 14 +++-
 .../dashboard/v2/containers/DashboardBuilder.jsx   |  3 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  3 +-
 .../dashboard/v2/containers/DashboardGrid.jsx      |  2 +-
 .../dashboard/v2/containers/DashboardHeader.jsx    |  6 +-
 .../dashboard/v2/containers/ToastPresenter.jsx     | 10 +++
 .../javascripts/dashboard/v2/reducers/editMode.js  | 11 +++
 .../javascripts/dashboard/v2/reducers/index.js     |  4 +
 .../dashboard/v2/reducers/messageToasts.js         | 18 +++++
 .../dashboard/v2/stylesheets/builder.less          |  6 +-
 .../dashboard/v2/stylesheets/components/chart.less |  4 +-
 .../v2/stylesheets/components/column.less          | 17 +++++
 .../v2/stylesheets/components/divider.less         |  2 +-
 .../v2/stylesheets/components/header.less          |  4 +-
 .../dashboard/v2/stylesheets/components/row.less   | 18 +++++
 .../javascripts/dashboard/v2/stylesheets/grid.less | 24 ++----
 .../dashboard/v2/stylesheets/index.less            |  1 +
 .../dashboard/v2/stylesheets/toast.less            | 61 +++++++++++++++
 .../dashboard/v2/stylesheets/variables.less        |  8 ++
 .../javascripts/dashboard/v2/util/constants.js     |  6 ++
 .../javascripts/dashboard/v2/util/propShapes.jsx   |  7 ++
 39 files changed, 509 insertions(+), 104 deletions(-)
 create mode 100644 superset/assets/javascripts/dashboard/v2/actions/editMode.js
 create mode 100644 superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
 create mode 100644 superset/assets/javascripts/dashboard/v2/components/Toast.jsx
 create mode 100644 superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
 create mode 100644 superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
 create mode 100644 superset/assets/javascripts/dashboard/v2/reducers/editMode.js
 create mode 100644 superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
 create mode 100644 superset/assets/javascripts/dashboard/v2/stylesheets/toast.less

-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.

[incubator-superset] 01/02: [edit mode] add edit mode to redux and propogate to all s

Posted by cc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch chris--grid-root-and-spacer
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git

commit 1a4bc3172d0b992c91b6ef9b05af83fcbd1f7e8d
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Tue Apr 3 15:56:07 2018 -0700

    [edit mode] add edit mode to redux and propogate to all <DashboardComponent />s
---
 superset/assets/javascripts/dashboard/index.jsx    |  1 +
 .../javascripts/dashboard/v2/actions/editMode.js   |  9 ++++++
 .../dashboard/v2/components/DashboardBuilder.jsx   | 16 ++++++----
 .../dashboard/v2/components/DashboardGrid.jsx      | 10 +++---
 .../dashboard/v2/components/DashboardHeader.jsx    | 10 +++---
 .../dashboard/v2/components/dnd/DragDroppable.jsx  |  4 +++
 .../v2/components/gridComponents/Chart.jsx         | 17 ++++++----
 .../v2/components/gridComponents/Column.jsx        | 22 ++++++++-----
 .../v2/components/gridComponents/Divider.jsx       | 10 ++++--
 .../v2/components/gridComponents/Header.jsx        | 13 +++++---
 .../dashboard/v2/components/gridComponents/Row.jsx | 21 +++++++-----
 .../dashboard/v2/components/gridComponents/Tab.jsx |  6 +++-
 .../v2/components/gridComponents/Tabs.jsx          | 25 +++++++++------
 .../gridComponents/new/DraggableNewComponent.jsx   |  1 +
 .../v2/components/menu/WithPopoverMenu.jsx         | 37 ++++++++++++++--------
 .../v2/components/resizable/ResizableContainer.jsx | 14 ++++++--
 .../dashboard/v2/containers/DashboardBuilder.jsx   |  3 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  3 +-
 .../dashboard/v2/containers/DashboardGrid.jsx      |  2 +-
 .../dashboard/v2/containers/DashboardHeader.jsx    |  6 +++-
 .../javascripts/dashboard/v2/reducers/editMode.js  | 11 +++++++
 .../javascripts/dashboard/v2/reducers/index.js     |  2 ++
 .../dashboard/v2/stylesheets/builder.less          |  6 ++--
 .../dashboard/v2/stylesheets/components/chart.less |  4 +--
 .../v2/stylesheets/components/column.less          | 17 ++++++++++
 .../v2/stylesheets/components/divider.less         |  2 +-
 .../dashboard/v2/stylesheets/components/row.less   | 18 +++++++++++
 .../javascripts/dashboard/v2/stylesheets/grid.less | 24 +++-----------
 28 files changed, 211 insertions(+), 103 deletions(-)

diff --git a/superset/assets/javascripts/dashboard/index.jsx b/superset/assets/javascripts/dashboard/index.jsx
index 926f6b2..f7471f5 100644
--- a/superset/assets/javascripts/dashboard/index.jsx
+++ b/superset/assets/javascripts/dashboard/index.jsx
@@ -25,6 +25,7 @@ const initState = {
     present: emptyDashboardLayout,
     future: [],
   },
+  editMode: true,
 };
 
 const store = createStore(
diff --git a/superset/assets/javascripts/dashboard/v2/actions/editMode.js b/superset/assets/javascripts/dashboard/v2/actions/editMode.js
new file mode 100644
index 0000000..0a849ea
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/actions/editMode.js
@@ -0,0 +1,9 @@
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export function setEditMode(editMode) {
+  return {
+    type: SET_EDIT_MODE,
+    payload: {
+      editMode,
+    },
+  };
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index 2a0bfe7..fc938b1 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -1,3 +1,4 @@
+import cx from 'classnames';
 import React from 'react';
 import PropTypes from 'prop-types';
 import HTML5Backend from 'react-dnd-html5-backend';
@@ -18,11 +19,10 @@ import {
 } from '../util/constants';
 
 const propTypes = {
-  editMode: PropTypes.bool,
-
   // redux
   dashboardLayout: PropTypes.object.isRequired,
   deleteTopLevelTabs: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
@@ -52,7 +52,7 @@ class DashboardBuilder extends React.Component {
 
   render() {
     const { tabIndex } = this.state;
-    const { handleComponentDrop, dashboardLayout, deleteTopLevelTabs } = this.props;
+    const { handleComponentDrop, dashboardLayout, deleteTopLevelTabs, editMode } = this.props;
     const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
     const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
@@ -64,8 +64,8 @@ class DashboardBuilder extends React.Component {
     const gridComponent = dashboardLayout[gridComponentId];
 
     return (
-      <div className="dashboard-v2">
-        {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
+      <div className={cx('dashboard-v2', editMode && 'dashboard-v2--editing')}>
+        {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
           <DashboardHeader />
         ) : (
           <DragDroppable
@@ -75,6 +75,7 @@ class DashboardBuilder extends React.Component {
             index={0}
             orientation="column"
             onDrop={handleComponentDrop}
+            editMode
           >
             {({ dropIndicatorProps }) => (
               <div>
@@ -94,6 +95,7 @@ class DashboardBuilder extends React.Component {
                 onClick={deleteTopLevelTabs}
               />,
             ]}
+            editMode={editMode}
           >
             <DashboardComponent
               id={topLevelTabs.id}
@@ -105,12 +107,12 @@ class DashboardBuilder extends React.Component {
             />
           </WithPopoverMenu>}
 
-        <div className="dashboard-builder">
+        <div className="dashboard-content">
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
           />
-          <BuilderComponentPane />
+          {editMode && <BuilderComponentPane />}
         </div>
       </div>
     );
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
index cfe99c7..9f4cb93 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -13,6 +13,7 @@ import {
 
 const propTypes = {
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
   gridComponent: componentShape.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
   resizeComponent: PropTypes.func.isRequired,
@@ -70,7 +71,7 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { gridComponent, handleComponentDrop, depth } = this.props;
+    const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
     const { isResizing, rowGuideTop } = this.state;
 
     return (
@@ -99,18 +100,19 @@ class DashboardGrid extends React.PureComponent {
                 ))}
 
                 {/* render an empty drop target */}
-                {gridComponent.children.length === 0 &&
+                {editMode &&
                   <DragDroppable
                     component={gridComponent}
                     depth={depth}
                     parentComponent={null}
-                    index={0}
+                    index={gridComponent.children.length}
                     orientation="column"
                     onDrop={handleComponentDrop}
                     className="empty-grid-droptarget"
+                    editMode
                   >
                     {({ dropIndicatorProps }) => dropIndicatorProps &&
-                      <div {...dropIndicatorProps} />}
+                      <div className="drop-indicator drop-indicator--top" />}
                   </DragDroppable>}
 
                 {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
index 2c89f33..ca204e5 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
@@ -7,8 +7,7 @@ import { componentShape } from '../util/propShapes';
 import EditableTitle from '../../../components/EditableTitle';
 
 const propTypes = {
-  // editMode: PropTypes.bool.isRequired,
-  // setEditMode: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
   component: componentShape.isRequired,
 
   // redux
@@ -17,6 +16,7 @@ const propTypes = {
   onRedo: PropTypes.func.isRequired,
   canUndo: PropTypes.bool.isRequired,
   canRedo: PropTypes.bool.isRequired,
+  setEditMode: PropTypes.func.isRequired,
 };
 
 class DashboardHeader extends React.Component {
@@ -27,8 +27,7 @@ class DashboardHeader extends React.Component {
   }
 
   toggleEditMode() {
-    console.log('@TODO toggleEditMode');
-    // this.props.setEditMode(!this.props.editMode);
+    this.props.setEditMode(!this.props.editMode);
   }
 
   handleChangeText(nextText) {
@@ -47,8 +46,7 @@ class DashboardHeader extends React.Component {
   }
 
   render() {
-    const { component, onUndo, onRedo, canUndo, canRedo } = this.props;
-    const editMode = true;
+    const { component, onUndo, onRedo, canUndo, canRedo, editMode } = this.props;
 
     return (
       <div className="dashboard-header">
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
index 89664e5..775e092 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
@@ -18,6 +18,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   style: PropTypes.object,
   onDrop: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 
   // from react-dnd
   isDragging: PropTypes.bool.isRequired,
@@ -70,8 +71,11 @@ class DragDroppable extends React.Component {
       isDragging,
       isDraggingOver,
       style,
+      editMode,
     } = this.props;
 
+    if (!editMode) return children({});
+
     const { dropIndicator } = this.state;
 
     return (
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
index 7ca506d..668d268 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
@@ -9,10 +9,7 @@ import ResizableContainer from '../resizable/ResizableContainer';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
 import { ROW_TYPE } from '../../util/componentTypes';
-import {
-  GRID_MIN_COLUMN_COUNT,
-  GRID_MIN_ROW_UNITS,
-} from '../../util/constants';
+import { GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS } from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -21,6 +18,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -71,6 +69,7 @@ class Chart extends React.Component {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     return (
@@ -82,6 +81,7 @@ class Chart extends React.Component {
         depth={depth}
         onDrop={handleComponentDrop}
         disableDragDrop={isFocused}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <ResizableContainer
@@ -97,16 +97,19 @@ class Chart extends React.Component {
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
+            editMode={editMode}
           >
-            <HoverMenu innerRef={dragSourceRef} position="top">
-              <DragHandle position="top" />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu innerRef={dragSourceRef} position="top">
+                <DragHandle position="top" />
+              </HoverMenu>}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
               menuItems={[
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
+              editMode={editMode}
             >
               <div className="dashboard-component dashboard-component-chart">
                 <div className="fa fa-area-chart" />
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
index bf33710..fe5a721 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
@@ -24,6 +24,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -90,6 +91,7 @@ class Column extends React.PureComponent {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     const columnItems = columnComponent.children || [];
@@ -105,6 +107,7 @@ class Column extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <ResizableContainer
@@ -118,6 +121,7 @@ class Column extends React.PureComponent {
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
+            editMode={editMode}
           >
             <WithPopoverMenu
               isFocused={this.state.isFocused}
@@ -130,6 +134,7 @@ class Column extends React.PureComponent {
                   onChange={this.handleChangeBackground}
                 />,
               ]}
+              editMode={editMode}
             >
               <div
                 className={cx(
@@ -138,14 +143,15 @@ class Column extends React.PureComponent {
                   backgroundStyle.className,
                 )}
               >
-                <HoverMenu innerRef={dragSourceRef} position="top">
-                  <DragHandle position="top" />
-                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-                  <IconButton
-                    onClick={this.handleChangeFocus}
-                    className="fa fa-cog"
-                  />
-                </HoverMenu>
+                {editMode &&
+                  <HoverMenu innerRef={dragSourceRef} position="top">
+                    <DragHandle position="top" />
+                    <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                    <IconButton
+                      onClick={this.handleChangeFocus}
+                      className="fa fa-cog"
+                    />
+                  </HoverMenu>}
 
                 {columnItems.map((componentId, itemIndex) => (
                   <DashboardComponent
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
index ff29c3f..b3010e9 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
@@ -13,6 +13,7 @@ const propTypes = {
   depth: PropTypes.number.isRequired,
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
   deleteComponent: PropTypes.func.isRequired,
 };
@@ -35,6 +36,7 @@ class Divider extends React.PureComponent {
       parentComponent,
       index,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     return (
@@ -45,12 +47,14 @@ class Divider extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            <HoverMenu position="left">
-              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu position="left">
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+              </HoverMenu>}
 
             <div className="dashboard-component dashboard-component-divider" />
 
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
index d8744d6..594cf6b 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   depth: PropTypes.number.isRequired,
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // redux
   handleComponentDrop: PropTypes.func.isRequired,
@@ -79,6 +80,7 @@ class Header extends React.PureComponent {
       parentComponent,
       index,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     const headerStyle = headerStyleOptions.find(
@@ -98,12 +100,14 @@ class Header extends React.PureComponent {
         depth={depth}
         onDrop={handleComponentDrop}
         disableDragDrop={isFocused}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            <HoverMenu position="left">
-              <DragHandle position="left" />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu position="left">
+                <DragHandle position="left" />
+              </HoverMenu>}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
@@ -122,6 +126,7 @@ class Header extends React.PureComponent {
                 />,
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
+              editMode={editMode}
             >
               <div
                 className={cx(
@@ -133,7 +138,7 @@ class Header extends React.PureComponent {
               >
                 <EditableTitle
                   title={component.meta.text}
-                  canEdit={isFocused}
+                  canEdit={editMode && isFocused}
                   onSaveTitle={this.handleChangeText}
                   showTooltip={false}
                 />
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
index 99296dd..9866bc8 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -90,6 +91,7 @@ class Row extends React.PureComponent {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     const rowItems = rowComponent.children || [];
@@ -106,6 +108,7 @@ class Row extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <WithPopoverMenu
@@ -119,6 +122,7 @@ class Row extends React.PureComponent {
                 onChange={this.handleChangeBackground}
               />,
             ]}
+            editMode={editMode}
           >
             <div
               className={cx(
@@ -127,14 +131,15 @@ class Row extends React.PureComponent {
                 backgroundStyle.className,
               )}
             >
-              <HoverMenu innerRef={dragSourceRef} position="left">
-                <DragHandle position="left" />
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-                <IconButton
-                  onClick={this.handleChangeFocus}
-                  className="fa fa-cog"
-                />
-              </HoverMenu>
+              {editMode &&
+                <HoverMenu innerRef={dragSourceRef} position="left">
+                  <DragHandle position="left" />
+                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                  <IconButton
+                    onClick={this.handleChangeFocus}
+                    className="fa fa-cog"
+                  />
+                </HoverMenu>}
 
               {rowItems.map((componentId, itemIndex) => (
                 <DashboardComponent
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
index 9b41949..218c4e7 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
   onDropOnTab: PropTypes.func,
   onDeleteTab: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -127,6 +128,7 @@ export default class Tab extends React.PureComponent {
       parentComponent,
       index,
       depth,
+      editMode,
     } = this.props;
 
     return (
@@ -141,6 +143,7 @@ export default class Tab extends React.PureComponent {
         // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
         // reusult in circular children
         disableDragDrop={isFocused || depth === DASHBOARD_ROOT_DEPTH + 1}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div className="dragdroppable-tab" ref={dragSourceRef}>
@@ -149,10 +152,11 @@ export default class Tab extends React.PureComponent {
               menuItems={parentComponent.children.length <= 1 ? [] : [
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
+              editMode={editMode}
             >
               <EditableTitle
                 title={component.meta.text}
-                canEdit={isFocused}
+                canEdit={editMode && isFocused}
                 onSaveTitle={this.handleChangeText}
                 showTooltip={false}
               />
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
index f76fa19..1f5f0c6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
@@ -22,7 +22,8 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
-  renderTabContent: PropTypes.bool,
+  renderTabContent: PropTypes.bool, // whether to render tabs + content or just tabs
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -130,6 +131,7 @@ class Tabs extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       renderTabContent,
+      editMode,
     } = this.props;
 
     const { tabIndex: selectedTabIndex } = this.state;
@@ -143,13 +145,15 @@ class Tabs extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
           <div className="dashboard-component dashboard-component-tabs">
-            <HoverMenu innerRef={tabsDragSourceRef} position="left">
-              <DragHandle position="left" />
-              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu innerRef={tabsDragSourceRef} position="left">
+                <DragHandle position="left" />
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+              </HoverMenu>}
 
             <BootstrapTabs
               id={tabsComponent.id}
@@ -200,11 +204,12 @@ class Tabs extends React.PureComponent {
                 </BootstrapTab>
               ))}
 
-              {tabIds.length < MAX_TAB_COUNT &&
-                <BootstrapTab
-                  eventKey={NEW_TAB_INDEX}
-                  title={<div className="fa fa-plus" />}
-                />}
+              {editMode &&
+                tabIds.length < MAX_TAB_COUNT &&
+                  <BootstrapTab
+                    eventKey={NEW_TAB_INDEX}
+                    title={<div className="fa fa-plus" />}
+                  />}
 
             </BootstrapTabs>
 
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
index 778f58e..eebd6e0 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
@@ -26,6 +26,7 @@ export default class DraggableNewComponent extends React.PureComponent {
         parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
         index={0}
         depth={0}
+        editMode
       >
         {({ dragSourceRef }) => (
           <div ref={dragSourceRef} className="new-component">
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
index a1d4c0e..f213442 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
@@ -9,6 +9,7 @@ const propTypes = {
   onChangeFocus: PropTypes.func,
   isFocused: PropTypes.bool,
   shouldFocus: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -32,10 +33,14 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   componentWillReceiveProps(nextProps) {
-    if (nextProps.isFocused && !this.state.isFocused) {
+    if (nextProps.editMode && nextProps.isFocused && !this.state.isFocused) {
       document.addEventListener('click', this.handleClick, true);
       document.addEventListener('drag', this.handleClick, true);
       this.setState({ isFocused: true });
+    } else if (this.state.isFocused && !nextProps.editMode) {
+      document.removeEventListener('click', this.handleClick, true);
+      document.removeEventListener('drag', this.handleClick, true);
+      this.setState({ isFocused: false });
     }
   }
 
@@ -49,10 +54,14 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   handleClick(event) {
-    const { onChangeFocus, shouldFocus: shouldFocusThunk } = this.props;
-    const shouldFocus = shouldFocusThunk(event, this.container);
+    const { onChangeFocus, shouldFocus: shouldFocusFunc, disableClick, editMode } = this.props;
+    const shouldFocus = shouldFocusFunc(event, this.container);
+
+    if (!editMode) {
+      return;
+    }
 
-    if (shouldFocus && !this.state.isFocused) {
+    if (!disableClick && shouldFocus && !this.state.isFocused) {
       // if not focused, set focus and add a window event listener to capture outside clicks
       // this enables us to not set a click listener for ever item on a dashboard
       document.addEventListener('click', this.handleClick, true);
@@ -72,26 +81,28 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   render() {
-    const { children, menuItems, disableClick } = this.props;
+    const { children, menuItems, editMode } = this.props;
     const { isFocused } = this.state;
 
     return (
       <div
         ref={this.setRef}
-        onClick={!disableClick && this.handleClick}
+        onClick={this.handleClick}
         role="none"
         className={cx(
           'with-popover-menu',
-          isFocused && 'with-popover-menu--focused',
+          editMode && isFocused && 'with-popover-menu--focused',
         )}
       >
         {children}
-        {isFocused && menuItems.length ?
-          <div className="popover-menu" >
-            {menuItems.map((node, i) => (
-              <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
-            ))}
-          </div> : null}
+        {editMode &&
+          isFocused &&
+          menuItems.length > 0 &&
+            <div className="popover-menu" >
+              {menuItems.map((node, i) => (
+                <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
+              ))}
+            </div>}
       </div>
     );
   }
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
index 0b1e975..a532ff0 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -28,6 +28,7 @@ const propTypes = {
   onResizeStop: PropTypes.func,
   onResize: PropTypes.func,
   onResizeStart: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -101,9 +102,9 @@ class ResizableContainer extends React.PureComponent {
 
     if (onResizeStop) {
       const nextWidthMultiple =
-        widthMultiple + Math.floor(delta.width / (widthStep + gutterWidth));
+        widthMultiple + Math.round(delta.width / (widthStep + gutterWidth));
       const nextHeightMultiple =
-        heightMultiple + Math.ceil(delta.height / heightStep);
+        heightMultiple + Math.round(delta.height / heightStep);
 
       onResizeStop({
         id,
@@ -133,6 +134,7 @@ class ResizableContainer extends React.PureComponent {
       minHeightMultiple,
       maxHeightMultiple,
       gutterWidth,
+      editMode,
     } = this.props;
 
     const size = {
@@ -148,6 +150,14 @@ class ResizableContainer extends React.PureComponent {
           || undefined,
     };
 
+    if (!editMode) {
+      return (
+        <div style={{ ...size }}>
+          {children}
+        </div>
+      );
+    }
+
     let enableConfig = resizableConfig.notAdjustable;
     if (adjustableWidth && adjustableHeight) enableConfig = resizableConfig.widthAndHeight;
     else if (adjustableWidth) enableConfig = resizableConfig.widthOnly;
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
index 7ed5bfc..b8d717e 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
@@ -7,9 +7,10 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
   return {
     dashboardLayout: undoableLayout.present,
+    editMode,
   };
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
index 7af3f5f..add5a6d 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
@@ -25,13 +25,14 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboardLayout: undoableLayout, selectedTabs }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps) {
   const dashboardLayout = undoableLayout.present;
   const { id, parentId } = ownProps;
   const component = dashboardLayout[id];
   const props = {
     component,
     parentComponent: dashboardLayout[parentId],
+    editMode,
   };
 
   // rows and columns need more data about their child dimensions
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
index ddb2fc0..67b2396 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
@@ -14,4 +14,4 @@ function mapDispatchToProps(dispatch) {
   }, dispatch);
 }
 
-export default connect(null, mapDispatchToProps)(DashboardGrid);
+export default connect(({ editMode }) => ({ editMode }), mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
index 985f9ee..8855d2c 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
@@ -10,11 +10,14 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout }) {
+import { setEditMode } from '../actions/editMode';
+
+function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
   return {
     component: undoableLayout.present[DASHBOARD_HEADER_ID],
     canUndo: undoableLayout.past.length > 0,
     canRedo: undoableLayout.future.length > 0,
+    editMode,
   };
 }
 
@@ -24,6 +27,7 @@ function mapDispatchToProps(dispatch) {
     handleComponentDrop,
     onUndo: UndoActionCreators.undo,
     onRedo: UndoActionCreators.redo,
+    setEditMode,
   }, dispatch);
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/editMode.js b/superset/assets/javascripts/dashboard/v2/reducers/editMode.js
new file mode 100644
index 0000000..b1a1630
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/editMode.js
@@ -0,0 +1,11 @@
+import { SET_EDIT_MODE } from '../actions/editMode';
+
+export default function editModeReducer(editMode = false, action) {
+  switch (action.type) {
+    case SET_EDIT_MODE:
+      return action.payload.editMode;
+
+    default:
+      return editMode;
+  }
+}
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index 994a1df..b824e9a 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -2,6 +2,7 @@ import { combineReducers } from 'redux';
 import undoable, { distinctState } from 'redux-undo';
 
 import dashboardLayout from './dashboardLayout';
+import editMode from './editMode';
 
 const undoableLayout = undoable(dashboardLayout, {
   limit: 15,
@@ -10,4 +11,5 @@ const undoableLayout = undoable(dashboardLayout, {
 
 export default combineReducers({
   dashboardLayout: undoableLayout,
+  editMode,
 });
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
index 206d63b..3651c57 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
@@ -14,7 +14,7 @@
   box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1); /* @TODO color */
 }
 
-.dashboard-builder {
+.dashboard-content {
   display: flex;
   flex-direction: row;
   flex-wrap: nowrap;
@@ -32,12 +32,12 @@
   padding-left: 8px; /* note this is added to tab-level padding, to match header */
 }
 
-.dashboard-builder .grid-container .dashboard-component-tabs {
+.dashboard-content .grid-container .dashboard-component-tabs {
   box-shadow: none;
   padding-left: 0;
 }
 
-.dashboard-builder > div:first-child {
+.dashboard-content > div:first-child {
   width: 100%;
   flex-grow: 1;
   position: relative;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
index 2bdf3cc..141c3e9 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
@@ -14,8 +14,6 @@
   opacity: 0.3;
 }
 
-.grid-container--resizing .dashboard-component-chart,
-.dashboard-builder--dragging .dashboard-component-chart,
-.dashboard-component-chart:hover {
+.dashboard-v2--editing .dashboard-component-chart:hover {
   box-shadow: inset 0 0 0 1px @gray-light;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
index caf31e7..9565112 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
@@ -2,10 +2,27 @@
   width: 100%;
 }
 
+/* gutters between elements in a column */
 .grid-column > :not(:only-child):not(.hover-menu):not(:last-child) {
   margin-bottom: 16px;
 }
 
+.dashboard-v2--editing .grid-column:after {
+  border: 1px dashed transparent;
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 1px;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
+}
+
+.dashboard-v2--editing .grid-column:hover:after {
+  border: 1px solid @gray-light;
+}
+
 .grid-column > .hover-menu--top {
   top: -20px;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
index f1d3d86..9347a4e 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
@@ -1,6 +1,6 @@
 .dashboard-component-divider {
   width: 100%;
-  padding: 24px 0; /* this is padding not margin to enable a larger mouse target */
+  padding: 16px 0; /* this is padding not margin to enable a larger mouse target */
   background-color: transparent;
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
index 30f14b8..956966d 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
@@ -7,10 +7,28 @@
   height: fit-content;
 }
 
+/* gutters between elements in a row */
 .grid-row > :not(:only-child):not(:last-child):not(.hover-menu) {
   margin-right: 16px;
 }
 
+/* hover indicator */
+.dashboard-v2--editing .grid-row:after {
+  border: 1px dashed transparent;
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 1px;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
+}
+
+.dashboard-v2--editing .grid-row:hover:after {
+  border: 1px solid @gray-light;
+}
+
 .grid-row.grid-row--empty {
   align-items: center; /* this centers the empty note content */
   height: 80px;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
index 7c55dee..45b8a42 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
@@ -9,6 +9,11 @@
   flex-direction: column;
 }
 
+/* gutters between rows */
+.grid-content > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget) {
+  margin-bottom: 16px;
+}
+
 .empty-grid-droptarget {
   width: 100%;
   height: 100%;
@@ -33,22 +38,3 @@
   pointer-events: none;
   z-index: 10;
 }
-
-
-.grid-container .grid-row:after,
-.grid-container .grid-column:after {
-  border: 1px dashed transparent;
-  content: "";
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  top: 1px;
-  left: 0;
-  z-index: 1;
-  pointer-events: none;
-}
-
-.grid-container .grid-row:hover:after,
-.grid-container .grid-column:hover:after {
-  border: 1px solid @gray-light;
-}

-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.

[incubator-superset] 02/02: [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers

Posted by cc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch chris--grid-root-and-spacer
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git

commit 3ffe470206d5bfed26e60c07418d097a31b3e3ef
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Wed Apr 4 11:39:56 2018 -0700

    [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers
---
 superset/assets/javascripts/dashboard/index.jsx    |  7 ++
 .../dashboard/v2/actions/messageToasts.js          | 47 ++++++++++++
 .../dashboard/v2/components/DashboardBuilder.jsx   |  2 +
 .../javascripts/dashboard/v2/components/Toast.jsx  | 87 ++++++++++++++++++++++
 .../dashboard/v2/components/ToastPresenter.jsx     | 39 ++++++++++
 .../dashboard/v2/containers/ToastPresenter.jsx     | 10 +++
 .../javascripts/dashboard/v2/reducers/index.js     |  2 +
 .../dashboard/v2/reducers/messageToasts.js         | 18 +++++
 .../v2/stylesheets/components/divider.less         |  2 +-
 .../v2/stylesheets/components/header.less          |  4 +-
 .../dashboard/v2/stylesheets/index.less            |  1 +
 .../dashboard/v2/stylesheets/toast.less            | 61 +++++++++++++++
 .../dashboard/v2/stylesheets/variables.less        |  8 ++
 .../javascripts/dashboard/v2/util/constants.js     |  6 ++
 .../javascripts/dashboard/v2/util/propShapes.jsx   |  7 ++
 15 files changed, 299 insertions(+), 2 deletions(-)

diff --git a/superset/assets/javascripts/dashboard/index.jsx b/superset/assets/javascripts/dashboard/index.jsx
index f7471f5..6f6d177 100644
--- a/superset/assets/javascripts/dashboard/index.jsx
+++ b/superset/assets/javascripts/dashboard/index.jsx
@@ -19,6 +19,7 @@ initJQueryAjax();
 const appContainer = document.getElementById('app');
 // const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
 // const initState = Object.assign({}, getInitialState(bootstrapData));
+
 const initState = {
   dashboardLayout: {
     past: [],
@@ -26,6 +27,12 @@ const initState = {
     future: [],
   },
   editMode: true,
+  messageToasts: [
+    { text: 'Info!', id: '157234', toastType: 'INFO_TOAST' },
+    { text: 'Success!', id: '1237545745', toastType: 'SUCCESS_TOAST' },
+    { text: 'Warning!', id: '154623', toastType: 'WARNING_TOAST' },
+    { text: 'Danger!', id: '9128346', toastType: 'DANGER_TOAST' },
+  ],
 };
 
 const store = createStore(
diff --git a/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js b/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
new file mode 100644
index 0000000..c49c94a
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
@@ -0,0 +1,47 @@
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+
+function getToastUuid(type) {
+  return `${Math.random().toString(16).slice(2)}-${type}-${Math.random().toString(16).slice(2)}`;
+}
+
+export const ADD_TOAST = 'ADD_TOAST';
+export function addToast({ toastType, text }) {
+  return {
+    type: ADD_TOAST,
+    payload: {
+      id: getToastUuid(toastType),
+      toastType,
+      text,
+    },
+  };
+}
+
+export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
+export function addInfoToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST }));
+}
+
+export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
+export function addSuccessToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST }));
+}
+
+export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
+export function addWarningToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST }));
+}
+
+export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
+export function addDangerToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: DANGER_TOAST }));
+}
+
+export const REMOVE_TOAST = 'REMOVE_TOAST';
+export function removeToast(id) {
+  return {
+    type: REMOVE_TOAST,
+    payload: {
+      id,
+    },
+  };
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index fc938b1..8e2d985 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -10,6 +10,7 @@ import DashboardGrid from '../containers/DashboardGrid';
 import IconButton from './IconButton';
 import DragDroppable from './dnd/DragDroppable';
 import DashboardComponent from '../containers/DashboardComponent';
+import ToastPresenter from '../containers/ToastPresenter';
 import WithPopoverMenu from './menu/WithPopoverMenu';
 
 import {
@@ -114,6 +115,7 @@ class DashboardBuilder extends React.Component {
           />
           {editMode && <BuilderComponentPane />}
         </div>
+        <ToastPresenter />
       </div>
     );
   }
diff --git a/superset/assets/javascripts/dashboard/v2/components/Toast.jsx b/superset/assets/javascripts/dashboard/v2/components/Toast.jsx
new file mode 100644
index 0000000..537388d
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/Toast.jsx
@@ -0,0 +1,87 @@
+import { Alert } from 'react-bootstrap';
+import cx from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { toastShape } from '../util/propShapes';
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+
+const propTypes = {
+  toast: toastShape.isRequired,
+  onCloseToast: PropTypes.func.isRequired,
+  delay: PropTypes.number,
+  duration: PropTypes.number, // if duration is >0, the toast will close on its own
+};
+
+const defaultProps = {
+  delay: 0,
+  duration: 0,
+};
+
+class Toast extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      visible: false,
+    };
+
+    this.showToast = this.showToast.bind(this);
+    this.handleClosePress = this.handleClosePress.bind(this);
+  }
+
+  componentDidMount() {
+    const { delay, duration } = this.props;
+
+    setTimeout(this.showToast, delay);
+
+    if (duration > 0) {
+      this.hideTimer = setTimeout(this.handleClosePress, delay + duration);
+    }
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this.hideTimer);
+  }
+
+  showToast() {
+    this.setState({ visible: true });
+  }
+
+  handleClosePress() {
+    clearTimeout(this.hideTimer);
+
+    this.setState({ visible: false }, () => {
+      // Wait for the transition
+      setTimeout(() => {
+        this.props.onCloseToast(this.props.toast.id);
+      }, 150);
+    });
+  }
+
+  render() {
+    const { visible } = this.state;
+    const { toast: { toastType, text } } = this.props;
+
+    return (
+      <Alert
+        onDismiss={this.handleClosePress}
+        bsClass={cx(
+          'alert',
+          'toast',
+          visible && 'toast--visible',
+          toastType === INFO_TOAST && 'toast--info',
+          toastType === SUCCESS_TOAST && 'toast--success',
+          toastType === WARNING_TOAST && 'toast--warning',
+          toastType === DANGER_TOAST && 'toast--danger',
+        )}
+      >
+        {text}
+      </Alert>
+    );
+  }
+}
+
+Toast.propTypes = propTypes;
+Toast.defaultProps = defaultProps;
+
+export default Toast;
diff --git a/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx b/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
new file mode 100644
index 0000000..95a0251
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
@@ -0,0 +1,39 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Toast from './Toast';
+import { toastShape } from '../util/propShapes';
+
+const propTypes = {
+  toasts: PropTypes.arrayOf(toastShape),
+  removeToast: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  toasts: [],
+};
+
+// eslint-disable-next-line react/prefer-stateless-function
+class ToastPresenter extends React.Component {
+  render() {
+    const { toasts, removeToast } = this.props;
+
+    return (
+      toasts.length > 0 &&
+        <div className="toast-presenter">
+          {toasts.map(toast => (
+            <Toast
+              key={toast.id}
+              toast={toast}
+              onCloseToast={removeToast}
+            />
+          ))}
+        </div>
+    );
+  }
+}
+
+ToastPresenter.propTypes = propTypes;
+ToastPresenter.defaultProps = defaultProps;
+
+export default ToastPresenter;
diff --git a/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx b/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
new file mode 100644
index 0000000..7e70abc
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
@@ -0,0 +1,10 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import ToastPresenter from '../components/ToastPresenter';
+
+import { removeToast } from '../actions/messageToasts';
+
+export default connect(
+  ({ messageToasts: toasts }) => ({ toasts }),
+  dispatch => bindActionCreators({ removeToast }, dispatch),
+)(ToastPresenter);
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index b824e9a..731734d 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -3,6 +3,7 @@ import undoable, { distinctState } from 'redux-undo';
 
 import dashboardLayout from './dashboardLayout';
 import editMode from './editMode';
+import messageToasts from './messageToasts';
 
 const undoableLayout = undoable(dashboardLayout, {
   limit: 15,
@@ -12,4 +13,5 @@ const undoableLayout = undoable(dashboardLayout, {
 export default combineReducers({
   dashboardLayout: undoableLayout,
   editMode,
+  messageToasts,
 });
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js b/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
new file mode 100644
index 0000000..3b40da4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
@@ -0,0 +1,18 @@
+import { ADD_TOAST, REMOVE_TOAST } from '../actions/messageToasts';
+
+export default function messageToastsReducer(toasts = [], action) {
+  switch (action.type) {
+    case ADD_TOAST: {
+      const { payload: { id, type, text } } = action;
+      return [...toasts, { id, type, text }];
+    }
+
+    case REMOVE_TOAST: {
+      const { payload: { id } } = action;
+      return [...toasts].filter(toast => toast.id !== id);
+    }
+
+    default:
+      return toasts;
+  }
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
index 9347a4e..e4625d3 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
@@ -1,6 +1,6 @@
 .dashboard-component-divider {
   width: 100%;
-  padding: 16px 0; /* this is padding not margin to enable a larger mouse target */
+  padding: 8px 0; /* this is padding not margin to enable a larger mouse target */
   background-color: transparent;
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
index 670155d..37c7598 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
@@ -36,9 +36,11 @@
   padding-right: 16px;
 }
 
-/* grids add margin between items, so don't double pad within columns
+/*
+ * grids add margin between items, so don't double pad within columns
  * we'll not worry about double padding on top as it can serve as a visual separator
  */
+// .grid-content > :not(:only-child):not(:last-child) .dashboard-component-header,
 .grid-column > :not(:only-child):not(:last-child) .dashboard-component-header {
   margin-bottom: -16px;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
index d2a41a8..49ff5da 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
@@ -8,3 +8,4 @@
 @import './popover-menu.less';
 @import './resizable.less';
 @import './components/index.less';
+@import './toast.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less b/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
new file mode 100644
index 0000000..b324137
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
@@ -0,0 +1,61 @@
+.toast-presenter {
+  position: fixed;
+  bottom: 16px;
+  left: 50%;
+  transform: translate(-50%, 0);
+  width: 500px;
+  z-index: 3000; // top of the world
+}
+
+.toast {
+  will-change: transform, opacity;
+  transform: translateY(-100%);
+  transition: transform .3s, opacity .3s;
+  opacity: 0;
+  position: relative;
+  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
+}
+
+.toast {
+  border-radius: 2px;
+  background: white;
+  color: @almost-black;
+}
+
+.toast > button {
+  color: @almost-black;
+}
+
+.toast > button:hover {
+  color: @gray-dark;
+}
+
+.toast--visible {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+.toast:after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 4px;
+  height: 100%;
+}
+
+.toast--info:after {
+  background: linear-gradient(to bottom, @pink, @purple);
+}
+
+.toast--success:after {
+  background: @success;
+}
+
+.toast--warning:after {
+  background: @warning;
+}
+
+.toast--danger:after {
+  background: @danger;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
index f3a61df..254af23 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
@@ -5,3 +5,11 @@
 @gray: #879399;
 @gray-light: #CFD8DC;
 @gray-bg: #f5f5f5;
+
+/* toasts */
+@pink: #E32364;
+@purple: #2C2261;
+
+@success: #00BFA5;
+@warning: #FFAB00;
+@danger: @pink;
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js
index 5dd6968..36ef71b 100644
--- a/superset/assets/javascripts/dashboard/v2/util/constants.js
+++ b/superset/assets/javascripts/dashboard/v2/util/constants.js
@@ -31,3 +31,9 @@ export const LARGE_HEADER = 'LARGE_HEADER';
 // Style types
 export const BACKGROUND_WHITE = 'BACKGROUND_WHITE';
 export const BACKGROUND_TRANSPARENT = 'BACKGROUND_TRANSPARENT';
+
+// Toast types
+export const INFO_TOAST = 'INFO_TOAST';
+export const SUCCESS_TOAST = 'SUCCESS_TOAST';
+export const WARNING_TOAST = 'WARNING_TOAST';
+export const DANGER_TOAST = 'DANGER_TOAST';
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
index d701cc2..8acc192 100644
--- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
 import componentTypes from './componentTypes';
 import backgroundStyleOptions from './backgroundStyleOptions';
 import headerStyleOptions from './headerStyleOptions';
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from './constants';
 
 export const componentShape = PropTypes.shape({ // eslint-disable-line
   id: PropTypes.string.isRequired,
@@ -22,3 +23,9 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
     background: PropTypes.oneOf(backgroundStyleOptions.map(opt => opt.value)),
   }),
 });
+
+export const toastShape = PropTypes.shape({
+  id: PropTypes.string.isRequired,
+  toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
+  text: PropTypes.string.isRequired,
+});

-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.