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:21 UTC

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

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.