You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by GitBox <gi...@apache.org> on 2018/06/21 21:42:03 UTC

[GitHub] williaster closed pull request #5208: [dashboard v2] ui + ux fixes

williaster closed pull request #5208: [dashboard v2] ui + ux fixes
URL: https://github.com/apache/incubator-superset/pull/5208
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/images/loading.gif b/superset/assets/images/loading.gif
index ae5cbddd91..d82fc5d924 100644
Binary files a/superset/assets/images/loading.gif and b/superset/assets/images/loading.gif differ
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 84f0856892..4b2848085c 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -43,7 +43,9 @@ import {
 
 describe('dashboardLayout actions', () => {
   const mockState = {
-    dashboardState: {},
+    dashboardState: {
+      hasUnsavedChanges: true, // don't dispatch setUnsavedChanges() after every action
+    },
     dashboardInfo: {},
     dashboardLayout: {
       past: [],
@@ -62,9 +64,7 @@ describe('dashboardLayout actions', () => {
 
   describe('updateComponents', () => {
     it('should dispatch an updateLayout action', () => {
-      const { getState, dispatch } = setup({
-        dashboardState: { hasUnsavedChanges: true },
-      });
+      const { getState, dispatch } = setup();
       const nextComponents = { 1: {} };
       const thunk = updateComponents(nextComponents);
       thunk(dispatch, getState);
@@ -91,9 +91,7 @@ describe('dashboardLayout actions', () => {
 
   describe('deleteComponents', () => {
     it('should dispatch an deleteComponent action', () => {
-      const { getState, dispatch } = setup({
-        dashboardState: { hasUnsavedChanges: true },
-      });
+      const { getState, dispatch } = setup();
       const thunk = deleteComponent('id', 'parentId');
       thunk(dispatch, getState);
       expect(dispatch.callCount).to.equal(1);
@@ -135,14 +133,14 @@ describe('dashboardLayout actions', () => {
           },
         },
       });
+
+      expect(dispatch.callCount).to.equal(2);
     });
   });
 
   describe('createTopLevelTabs', () => {
     it('should dispatch a createTopLevelTabs action', () => {
-      const { getState, dispatch } = setup({
-        dashboardState: { hasUnsavedChanges: true },
-      });
+      const { getState, dispatch } = setup();
       const dropResult = {};
       const thunk = createTopLevelTabs(dropResult);
       thunk(dispatch, getState);
@@ -169,9 +167,7 @@ describe('dashboardLayout actions', () => {
 
   describe('deleteTopLevelTabs', () => {
     it('should dispatch a deleteTopLevelTabs action', () => {
-      const { getState, dispatch } = setup({
-        dashboardState: { hasUnsavedChanges: true },
-      });
+      const { getState, dispatch } = setup();
       const dropResult = {};
       const thunk = deleteTopLevelTabs(dropResult);
       thunk(dispatch, getState);
@@ -213,7 +209,6 @@ describe('dashboardLayout actions', () => {
 
     it('should update the size of the component', () => {
       const { getState, dispatch } = setup({
-        dashboardState: { hasUnsavedChanges: true },
         dashboardLayout,
       });
 
@@ -239,6 +234,8 @@ describe('dashboardLayout actions', () => {
           },
         },
       });
+
+      expect(dispatch.callCount).to.equal(2);
     });
 
     it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
@@ -265,11 +262,11 @@ describe('dashboardLayout actions', () => {
         dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
       };
 
-      const thunk1 = handleComponentDrop(dropResult);
-      thunk1(dispatch, getState);
+      const handleComponentDropThunk = handleComponentDrop(dropResult);
+      handleComponentDropThunk(dispatch, getState);
 
-      const thunk2 = dispatch.getCall(0).args[0];
-      thunk2(dispatch, getState);
+      const createComponentThunk = dispatch.getCall(0).args[0];
+      createComponentThunk(dispatch, getState);
 
       expect(dispatch.getCall(1).args[0]).to.deep.equal({
         type: CREATE_COMPONENT,
@@ -277,36 +274,47 @@ describe('dashboardLayout actions', () => {
           dropResult,
         },
       });
+
+      expect(dispatch.callCount).to.equal(2);
     });
 
     it('should move a component if the component is not new', () => {
       const { getState, dispatch } = setup({
-        dashboardLayout: { present: { id: { type: ROW_TYPE, children: [] } } },
+        dashboardLayout: {
+          // if 'dragging' is not only child will dispatch deleteComponent thunk
+          present: { id: { type: ROW_TYPE, children: ['_'] } },
+        },
       });
       const dropResult = {
         source: { id: 'id', index: 0, type: ROW_TYPE },
         destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
-        dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
+        dragging: { id: 'dragging', type: ROW_TYPE },
       };
 
-      const thunk = handleComponentDrop(dropResult);
-      thunk(dispatch, getState);
+      const handleComponentDropThunk = handleComponentDrop(dropResult);
+      handleComponentDropThunk(dispatch, getState);
 
-      expect(dispatch.getCall(0).args[0]).to.deep.equal({
+      const moveComponentThunk = dispatch.getCall(0).args[0];
+      moveComponentThunk(dispatch, getState);
+
+      expect(dispatch.getCall(1).args[0]).to.deep.equal({
         type: MOVE_COMPONENT,
         payload: {
           dropResult,
         },
       });
+
+      expect(dispatch.callCount).to.equal(2);
     });
 
     it('should dispatch a toast if the drop overflows the destination', () => {
       const { getState, dispatch } = setup({
         dashboardLayout: {
           present: {
-            source: { type: ROW_TYPE, meta: { width: 0 } },
-            destination: { type: ROW_TYPE, meta: { width: 0 } },
-            dragging: { type: CHART_TYPE, meta: { width: 100 } },
+            source: { type: ROW_TYPE },
+            destination: { type: ROW_TYPE, children: ['rowChild'] },
+            dragging: { type: CHART_TYPE, meta: { width: 1 } },
+            rowChild: { type: CHART_TYPE, meta: { width: 12 } },
           },
         },
       });
@@ -321,6 +329,8 @@ describe('dashboardLayout actions', () => {
       expect(dispatch.getCall(0).args[0].type).to.deep.equal(
         addInfoToast('').type,
       );
+
+      expect(dispatch.callCount).to.equal(1);
     });
 
     it('should delete a parent Row or Tabs if the moved child was the only child', () => {
@@ -358,6 +368,9 @@ describe('dashboardLayout actions', () => {
           parentId: 'parentId',
         },
       });
+
+      // move thunk, delete thunk, delete result actions
+      expect(dispatch.callCount).to.equal(3);
     });
 
     it('should create top-level tabs if dropped on root', () => {
@@ -380,6 +393,8 @@ describe('dashboardLayout actions', () => {
           dropResult,
         },
       });
+
+      expect(dispatch.callCount).to.equal(2);
     });
   });
 
@@ -413,9 +428,7 @@ describe('dashboardLayout actions', () => {
 
   describe('redoLayoutAction', () => {
     it('should dispatch a redux-undo .redo() action ', () => {
-      const { getState, dispatch } = setup({
-        dashboardState: { hasUnsavedChanges: true },
-      });
+      const { getState, dispatch } = setup();
       const thunk = redoLayoutAction();
       thunk(dispatch, getState);
 
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
index 1160d65249..d11c37f331 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
@@ -42,11 +42,11 @@ describe('DashboardGrid', () => {
     expect(wrapper.find(DashboardComponent)).to.have.length(2);
   });
 
-  it('should render an empty DragDroppables in editMode to increase the drop target zone', () => {
-    const withChildren = setup({ editMode: false });
-    const withoutChildren = setup({ editMode: true });
-    expect(withChildren.find(DragDroppable)).to.have.length(0);
-    expect(withoutChildren.find(DragDroppable)).to.have.length(1);
+  it('should render two empty DragDroppables in editMode to increase the drop target zone', () => {
+    const viewMode = setup({ editMode: false });
+    const editMode = setup({ editMode: true });
+    expect(viewMode.find(DragDroppable)).to.have.length(0);
+    expect(editMode.find(DragDroppable)).to.have.length(2);
   });
 
   it('should render grid column guides when resizing', () => {
diff --git a/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js b/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
index b153e1ec70..8e6f88964c 100644
--- a/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
@@ -7,6 +7,8 @@ import {
   CHART_TYPE,
   COLUMN_TYPE,
   ROW_TYPE,
+  HEADER_TYPE,
+  TAB_TYPE,
 } from '../../../../src/dashboard/util/componentTypes';
 
 describe('dropOverflowsParent', () => {
@@ -42,7 +44,7 @@ describe('dropOverflowsParent', () => {
     expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
   });
 
-  it('returns false if a parent DOES not have adequate width for child', () => {
+  it('returns false if a parent DOES have adequate width for child', () => {
     const dropResult = {
       source: { id: '_' },
       destination: { id: 'a' },
@@ -74,9 +76,41 @@ describe('dropOverflowsParent', () => {
     expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
   });
 
-  it('it should base result off of column width (instead of its children) if dropped on column', () => {
+  it('returns false if a child CAN shrink to available parent space', () => {
     const dropResult = {
-      source: { id: 'z' },
+      source: { id: '_' },
+      destination: { id: 'a' },
+      dragging: { id: 'z' },
+    };
+
+    const layout = {
+      a: {
+        id: 'a',
+        type: ROW_TYPE,
+        children: ['b', 'b'], // 2x b = 10
+      },
+      b: {
+        id: 'b',
+        type: CHART_TYPE,
+        meta: {
+          width: 5,
+        },
+      },
+      z: {
+        id: 'z',
+        type: CHART_TYPE,
+        meta: {
+          width: 10,
+        },
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+  });
+
+  it('returns true if a child CANNOT shrink to available parent space', () => {
+    const dropResult = {
+      source: { id: '_' },
       destination: { id: 'a' },
       dragging: { id: 'b' },
     };
@@ -85,24 +119,71 @@ describe('dropOverflowsParent', () => {
       a: {
         id: 'a',
         type: COLUMN_TYPE,
-        meta: { width: 10 },
+        meta: {
+          width: 6,
+        },
       },
+      // rows with children cannot shrink
       b: {
         id: 'b',
+        type: ROW_TYPE,
+        children: ['bChild', 'bChild', 'bChild'],
+      },
+      bChild: {
+        id: 'bChild',
         type: CHART_TYPE,
         meta: {
-          width: 2,
+          width: 3,
         },
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
+  });
+
+  it('returns true if a column has children that CANNOT shrink to available parent space', () => {
+    const dropResult = {
+      source: { id: '_' },
+      destination: { id: 'destination' },
+      dragging: { id: 'dragging' },
+    };
+
+    const layout = {
+      destination: {
+        id: 'destination',
+        type: ROW_TYPE,
+        children: ['b', 'b'], // 2x b = 10, 2 available
+      },
+      b: {
+        id: 'b',
+        type: CHART_TYPE,
+        meta: {
+          width: 5,
+        },
+      },
+      dragging: {
+        id: 'dragging',
+        type: COLUMN_TYPE,
+        meta: {
+          width: 10,
+        },
+        children: ['rowWithChildren'], // 2x b = width 10
+      },
+      rowWithChildren: {
+        id: 'rowWithChildren',
+        type: ROW_TYPE,
+        children: ['b', 'b'],
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
+    // remove children
     expect(
       dropOverflowsParent(dropResult, {
         ...layout,
-        a: { ...layout.a, meta: { width: 1 } },
+        dragging: { ...layout.dragging, children: [] },
       }),
-    ).to.equal(true);
+    ).to.equal(false);
   });
 
   it('should work with new components that are not in the layout', () => {
@@ -122,4 +203,25 @@ describe('dropOverflowsParent', () => {
 
     expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
   });
+
+  it('source/destination without widths should not overflow parent', () => {
+    const dropResult = {
+      source: { id: '_' },
+      destination: { id: 'tab' },
+      dragging: { id: 'header' },
+    };
+
+    const layout = {
+      tab: {
+        id: 'tab',
+        type: TAB_TYPE,
+      },
+      header: {
+        id: 'header',
+        type: HEADER_TYPE,
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+  });
 });
diff --git a/superset/assets/spec/javascripts/dashboard/util/getDetailedComponentWidth_spec.js b/superset/assets/spec/javascripts/dashboard/util/getDetailedComponentWidth_spec.js
new file mode 100644
index 0000000000..99e2282f7a
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getDetailedComponentWidth_spec.js
@@ -0,0 +1,223 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import getDetailedComponentWidth from '../../../../src/dashboard/util/getDetailedComponentWidth';
+import * as types from '../../../../src/dashboard/util/componentTypes';
+import {
+  GRID_COLUMN_COUNT,
+  GRID_MIN_COLUMN_COUNT,
+} from '../../../../src/dashboard/util/constants';
+
+describe('getDetailedComponentWidth', () => {
+  it('should return an object with width, minimumWidth, and occupiedWidth', () => {
+    expect(
+      getDetailedComponentWidth({ id: '_', components: {} }),
+    ).to.have.all.keys(['minimumWidth', 'occupiedWidth', 'width']);
+  });
+
+  describe('width', () => {
+    it('should be undefined if the component is not resizable and has no defined width', () => {
+      const empty = {
+        width: undefined,
+        occupiedWidth: undefined,
+        minimumWidth: undefined,
+      };
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.HEADER_TYPE },
+        }),
+      ).to.deep.equal(empty);
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.DIVIDER_TYPE },
+        }),
+      ).to.deep.equal(empty);
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.TAB_TYPE },
+        }),
+      ).to.deep.equal(empty);
+    });
+
+    it('should match component meta width for resizeable components', () => {
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.CHART_TYPE, meta: { width: 1 } },
+        }),
+      ).to.deep.equal({ width: 1, occupiedWidth: 1, minimumWidth: 1 });
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.MARKDOWN_TYPE, meta: { width: 2 } },
+        }),
+      ).to.deep.equal({ width: 2, occupiedWidth: 2, minimumWidth: 1 });
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.COLUMN_TYPE, meta: { width: 3 } },
+        }),
+        // note: occupiedWidth is zero for colunns/see test below
+      ).to.deep.equal({ width: 3, occupiedWidth: 0, minimumWidth: 1 });
+    });
+
+    it('should be GRID_COLUMN_COUNT for row components WITHOUT parents', () => {
+      expect(
+        getDetailedComponentWidth({
+          id: 'row',
+          components: { row: { id: 'row', type: types.ROW_TYPE } },
+        }),
+      ).to.deep.equal({
+        width: GRID_COLUMN_COUNT,
+        occupiedWidth: 0,
+        minimumWidth: GRID_MIN_COLUMN_COUNT,
+      });
+    });
+
+    it('should match parent width for row components WITH parents', () => {
+      expect(
+        getDetailedComponentWidth({
+          id: 'row',
+          components: {
+            row: { id: 'row', type: types.ROW_TYPE },
+            parent: {
+              id: 'parent',
+              type: types.COLUMN_TYPE,
+              children: ['row'],
+              meta: { width: 7 },
+            },
+          },
+        }),
+      ).to.deep.equal({
+        width: 7,
+        occupiedWidth: 0,
+        minimumWidth: GRID_MIN_COLUMN_COUNT,
+      });
+    });
+
+    it('should use either id or component (to support new components)', () => {
+      expect(
+        getDetailedComponentWidth({
+          id: 'id',
+          components: {
+            id: { id: 'id', type: types.CHART_TYPE, meta: { width: 6 } },
+          },
+        }).width,
+      ).to.equal(6);
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: 'id', type: types.CHART_TYPE, meta: { width: 6 } },
+        }).width,
+      ).to.equal(6);
+    });
+  });
+
+  describe('occupiedWidth', () => {
+    it('should reflect the sum of child widths for row components', () => {
+      expect(
+        getDetailedComponentWidth({
+          id: 'row',
+          components: {
+            row: {
+              id: 'row',
+              type: types.ROW_TYPE,
+              children: ['child', 'child'],
+            },
+            child: { id: 'child', meta: { width: 3.5 } },
+          },
+        }),
+      ).to.deep.equal({ width: 12, occupiedWidth: 7, minimumWidth: 7 });
+    });
+
+    it('should always be zero for column components', () => {
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.COLUMN_TYPE, meta: { width: 2 } },
+        }),
+      ).to.deep.equal({ width: 2, occupiedWidth: 0, minimumWidth: 1 });
+    });
+  });
+
+  describe('minimumWidth', () => {
+    it('should equal GRID_MIN_COLUMN_COUNT for resizable components', () => {
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.CHART_TYPE, meta: { width: 1 } },
+        }),
+      ).to.deep.equal({
+        width: 1,
+        minimumWidth: GRID_MIN_COLUMN_COUNT,
+        occupiedWidth: 1,
+      });
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.MARKDOWN_TYPE, meta: { width: 2 } },
+        }),
+      ).to.deep.equal({
+        width: 2,
+        minimumWidth: GRID_MIN_COLUMN_COUNT,
+        occupiedWidth: 2,
+      });
+
+      expect(
+        getDetailedComponentWidth({
+          component: { id: '', type: types.COLUMN_TYPE, meta: { width: 3 } },
+        }),
+      ).to.deep.equal({
+        width: 3,
+        minimumWidth: GRID_MIN_COLUMN_COUNT,
+        occupiedWidth: 0,
+      });
+    });
+
+    it('should equal the width of row children for column components with row children', () => {
+      expect(
+        getDetailedComponentWidth({
+          id: 'column',
+          components: {
+            column: {
+              id: 'column',
+              type: types.COLUMN_TYPE,
+              children: ['rowChild', 'ignoredChartChild'],
+              meta: { width: 12 },
+            },
+            rowChild: {
+              id: 'rowChild',
+              type: types.ROW_TYPE,
+              children: ['rowChildChild', 'rowChildChild'],
+            },
+            rowChildChild: {
+              id: 'rowChildChild',
+              meta: { width: 3.5 },
+            },
+            ignoredChartChild: {
+              id: 'ignoredChartChild',
+              meta: { width: 100 },
+            },
+          },
+        }),
+        // occupiedWidth is zero for colunns/see test below
+      ).to.deep.equal({ width: 12, occupiedWidth: 0, minimumWidth: 7 });
+    });
+
+    it('should equal occupiedWidth for row components', () => {
+      expect(
+        getDetailedComponentWidth({
+          id: 'row',
+          components: {
+            row: {
+              id: 'row',
+              type: types.ROW_TYPE,
+              children: ['child', 'child'],
+            },
+            child: { id: 'child', meta: { width: 3.5 } },
+          },
+        }),
+      ).to.deep.equal({ width: 12, occupiedWidth: 7, minimumWidth: 7 });
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js b/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
index 677c329a1f..8d00c186e2 100644
--- a/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
@@ -16,6 +16,7 @@ describe('newEntitiesFromDrop', () => {
       dropResult: {
         destination: { id: 'a', index: 0 },
         dragging: { type: CHART_TYPE },
+        source: { id: 'b', index: 0 },
       },
       layout: {
         a: {
@@ -37,6 +38,7 @@ describe('newEntitiesFromDrop', () => {
       dropResult: {
         destination: { id: 'a', index: 0 },
         dragging: { type: TABS_TYPE },
+        source: { id: 'b', index: 0 },
       },
       layout: {
         a: {
@@ -61,6 +63,7 @@ describe('newEntitiesFromDrop', () => {
       dropResult: {
         destination: { id: 'a', index: 0 },
         dragging: { type: CHART_TYPE },
+        source: { id: 'b', index: 0 },
       },
       layout: {
         a: {
diff --git a/superset/assets/src/SqlLab/components/QuerySearch.jsx b/superset/assets/src/SqlLab/components/QuerySearch.jsx
index 9d36d85218..45924e3141 100644
--- a/superset/assets/src/SqlLab/components/QuerySearch.jsx
+++ b/superset/assets/src/SqlLab/components/QuerySearch.jsx
@@ -2,14 +2,19 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Button } from 'react-bootstrap';
 import Select from 'react-select';
+import Loading from '../../components/Loading';
 import QueryTable from './QueryTable';
-import { now, epochTimeXHoursAgo,
-  epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
+import {
+  now,
+  epochTimeXHoursAgo,
+  epochTimeXDaysAgo,
+  epochTimeXYearsAgo,
+} from '../../modules/dates';
 import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
 import AsyncSelect from '../../components/AsyncSelect';
 import { t } from '../../locales';
 
-const $ = window.$ = require('jquery');
+const $ = (window.$ = require('jquery'));
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
@@ -47,13 +52,17 @@ class QuerySearch extends React.PureComponent {
     this.refreshQueries();
   }
   onUserClicked(userId) {
-    this.setState({ userId }, () => { this.refreshQueries(); });
+    this.setState({ userId }, () => {
+      this.refreshQueries();
+    });
   }
   onDbClicked(dbId) {
-    this.setState({ databaseId: dbId }, () => { this.refreshQueries(); });
+    this.setState({ databaseId: dbId }, () => {
+      this.refreshQueries();
+    });
   }
   onChange(db) {
-    const val = (db) ? db.value : null;
+    const val = db ? db.value : null;
     this.setState({ databaseId: val });
   }
   getTimeFromSelection(selection) {
@@ -77,25 +86,25 @@ class QuerySearch extends React.PureComponent {
     }
   }
   changeFrom(user) {
-    const val = (user) ? user.value : null;
+    const val = user ? user.value : null;
     this.setState({ from: val });
   }
   changeTo(status) {
-    const val = (status) ? status.value : null;
+    const val = status ? status.value : null;
     this.setState({ to: val });
   }
   changeUser(user) {
-    const val = (user) ? user.value : null;
+    const val = user ? user.value : null;
     this.setState({ userId: val });
   }
   insertParams(baseUrl, params) {
-    const validParams = params.filter(
-      function (p) { return p !== ''; },
-    );
+    const validParams = params.filter(function (p) {
+      return p !== '';
+    });
     return baseUrl + '?' + validParams.join('&');
   }
   changeStatus(status) {
-    const val = (status) ? status.value : null;
+    const val = status ? status.value : null;
     this.setState({ status: val });
   }
   changeSearch(event) {
@@ -120,7 +129,7 @@ class QuerySearch extends React.PureComponent {
     if (data.result.length === 0) {
       this.props.actions.addAlert({
         bsStyle: 'danger',
-        msg: t('It seems you don\'t have access to any database'),
+        msg: t("It seems you don't have access to any database"),
       });
     }
     return options;
@@ -175,8 +184,10 @@ class QuerySearch extends React.PureComponent {
             <Select
               name="select-from"
               placeholder={t('[From]-')}
-              options={TIME_OPTIONS
-                .slice(1, TIME_OPTIONS.length).map(xt => ({ value: xt, label: xt }))}
+              options={TIME_OPTIONS.slice(1, TIME_OPTIONS.length).map(xt => ({
+                value: xt,
+                label: xt,
+              }))}
               value={this.state.from}
               autosize={false}
               onChange={this.changeFrom}
@@ -206,29 +217,21 @@ class QuerySearch extends React.PureComponent {
             </Button>
           </div>
         </div>
-        {this.state.queriesLoading ?
-          (<img className="loading" alt="Loading..." src="/static/assets/images/loading.gif" />)
-          :
-          (
-            <div className="scrollbar-container">
-              <div
-                className="scrollbar-content"
-                style={{ height: this.props.height }}
-              >
-                <QueryTable
-                  columns={[
-                    'state', 'db', 'user', 'time',
-                    'progress', 'rows', 'sql', 'querylink',
-                  ]}
-                  onUserClicked={this.onUserClicked}
-                  onDbClicked={this.onDbClicked}
-                  queries={this.state.queriesArray}
-                  actions={this.props.actions}
-                />
-              </div>
+        {this.state.queriesLoading ? (
+          <Loading />
+        ) : (
+          <div className="scrollbar-container">
+            <div className="scrollbar-content" style={{ height: this.props.height }}>
+              <QueryTable
+                columns={['state', 'db', 'user', 'time', 'progress', 'rows', 'sql', 'querylink']}
+                onUserClicked={this.onUserClicked}
+                onDbClicked={this.onDbClicked}
+                queries={this.state.queriesArray}
+                actions={this.props.actions}
+              />
             </div>
-          )
-        }
+          </div>
+        )}
       </div>
     );
   }
diff --git a/superset/assets/src/SqlLab/components/ResultSet.jsx b/superset/assets/src/SqlLab/components/ResultSet.jsx
index f36a1640c7..bd446bba53 100644
--- a/superset/assets/src/SqlLab/components/ResultSet.jsx
+++ b/superset/assets/src/SqlLab/components/ResultSet.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { Alert, Button, ButtonGroup, ProgressBar } from 'react-bootstrap';
 import shortid from 'shortid';
 
+import Loading from '../../components/Loading';
 import VisualizeModal from './VisualizeModal';
 import HighlightedSql from './HighlightedSql';
 import FilterableTable from '../../components/FilterableTable/FilterableTable';
@@ -234,7 +235,7 @@ export default class ResultSet extends React.PureComponent {
     }
     return (
       <div>
-        <img className="loading" alt={t('Loading...')} src="/static/assets/images/loading.gif" />
+        <Loading />
         <QueryStateLabel query={query} />
         {progressBar}
         <div>
diff --git a/superset/assets/src/components/Loading.jsx b/superset/assets/src/components/Loading.jsx
index 810c5819cb..953e702ac4 100644
--- a/superset/assets/src/components/Loading.jsx
+++ b/superset/assets/src/components/Loading.jsx
@@ -5,7 +5,7 @@ const propTypes = {
   size: PropTypes.number,
 };
 const defaultProps = {
-  size: 25,
+  size: 50,
 };
 
 export default function Loading(props) {
@@ -15,17 +15,18 @@ export default function Loading(props) {
       alt="Loading..."
       src="/static/assets/images/loading.gif"
       style={{
-        width: props.size,
-        height: props.size,
+        width: Math.min(props.size, 50),
+        // height is auto
         padding: 0,
         margin: 0,
         position: 'absolute',
         left: '50%',
         top: '50%',
-        transform: 'translate(-50%, -60%)',
+        transform: 'translate(-50%, -50%)',
       }}
     />
   );
 }
+
 Loading.propTypes = propTypes;
 Loading.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index c4908b0513..bd01146143 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -2,16 +2,10 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 
 import { addInfoToast } from './messageToasts';
 import { setUnsavedChanges } from './dashboardState';
-import {
-  CHART_TYPE,
-  MARKDOWN_TYPE,
-  TABS_TYPE,
-  ROW_TYPE,
-} from '../util/componentTypes';
+import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
 import {
   DASHBOARD_ROOT_ID,
   NEW_COMPONENTS_SOURCE_ID,
-  GRID_MIN_COLUMN_COUNT,
   DASHBOARD_HEADER_ID,
 } from '../util/constants';
 import dropOverflowsParent from '../util/dropOverflowsParent';
@@ -117,22 +111,6 @@ export function resizeComponent({ id, width, height }) {
         },
       };
 
-      // set any resizable children to have a minimum width so that
-      // the chances that they are validly movable to future containers is maximized
-      component.children.forEach(childId => {
-        const child = dashboard[childId];
-        if ([CHART_TYPE, MARKDOWN_TYPE].includes(child.type)) {
-          updatedComponents[childId] = {
-            ...child,
-            meta: {
-              ...child.meta,
-              width: GRID_MIN_COLUMN_COUNT,
-              height: height || child.meta.height,
-            },
-          };
-        }
-      });
-
       dispatch(updateComponents(updatedComponents));
     }
   };
@@ -140,14 +118,12 @@ export function resizeComponent({ id, width, height }) {
 
 // Drag and drop --------------------------------------------------------------
 export const MOVE_COMPONENT = 'MOVE_COMPONENT';
-function moveComponent(dropResult) {
-  return {
-    type: MOVE_COMPONENT,
-    payload: {
-      dropResult,
-    },
-  };
-}
+const moveComponent = setUnsavedChangesAfterAction(dropResult => ({
+  type: MOVE_COMPONENT,
+  payload: {
+    dropResult,
+  },
+}));
 
 export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
 export function handleComponentDrop(dropResult) {
@@ -160,7 +136,7 @@ export function handleComponentDrop(dropResult) {
     if (overflowsParent) {
       return dispatch(
         addInfoToast(
-          `Parent does not have enough space for this component. Try decreasing its width or add it to a new row.`,
+          `There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
         ),
       );
     }
@@ -191,6 +167,7 @@ export function handleComponentDrop(dropResult) {
       if (
         (sourceComponent.type === TABS_TYPE ||
           sourceComponent.type === ROW_TYPE) &&
+        sourceComponent.children &&
         sourceComponent.children.length === 0
       ) {
         const parentId = findParentId({
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index c35a63756c..aafee5dcab 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import cx from 'classnames';
 import { StickyContainer, Sticky } from 'react-sticky';
+import ParentSize from '@vx/responsive/build/components/ParentSize';
 
 import NewColumn from './gridComponents/new/NewColumn';
 import NewDivider from './gridComponents/new/NewDivider';
@@ -13,6 +14,8 @@ import NewMarkdown from './gridComponents/new/NewMarkdown';
 import SliceAdder from '../containers/SliceAdder';
 import { t } from '../../locales';
 
+const SUPERSET_HEADER_HEIGHT = 59;
+
 const propTypes = {
   topOffset: PropTypes.number,
   toggleBuilderPane: PropTypes.func.isRequired,
@@ -42,62 +45,80 @@ class BuilderComponentPane extends React.PureComponent {
   render() {
     const { topOffset } = this.props;
     return (
-      <StickyContainer className="dashboard-builder-sidepane">
-        <Sticky topOffset={-topOffset}>
-          {({ style, calculatedHeight, isSticky }) => (
-            <div
-              className="viewport"
-              style={isSticky ? { ...style, top: topOffset } : null}
-            >
-              <div
-                className={cx('slider-container', this.state.slideDirection)}
-              >
-                <div className="component-layer slide-content">
-                  <div className="dashboard-builder-sidepane-header">
-                    <span>{t('Insert')}</span>
-                    <i
-                      className="fa fa-times trigger"
-                      onClick={this.props.toggleBuilderPane}
-                      role="none"
-                    />
-                  </div>
+      <div
+        className="dashboard-builder-sidepane"
+        style={{
+          height: `calc(100vh - ${topOffset + SUPERSET_HEADER_HEIGHT}px)`,
+        }}
+      >
+        <ParentSize>
+          {({ height }) => (
+            <StickyContainer>
+              <Sticky topOffset={-topOffset} bottomOffset={Infinity}>
+                {({ style, isSticky }) => (
                   <div
-                    className="new-component static"
-                    role="none"
-                    onClick={this.openSlicesPane}
+                    className="viewport"
+                    style={isSticky ? { ...style, top: topOffset } : null}
                   >
-                    <div className="new-component-placeholder fa fa-area-chart" />
-                    <div className="new-component-label">
-                      {t('Charts & filters')}
-                    </div>
+                    <div
+                      className={cx(
+                        'slider-container',
+                        this.state.slideDirection,
+                      )}
+                    >
+                      <div className="component-layer slide-content">
+                        <div className="dashboard-builder-sidepane-header">
+                          <span>{t('Insert')}</span>
+                          <i
+                            className="fa fa-times trigger"
+                            onClick={this.props.toggleBuilderPane}
+                            role="none"
+                          />
+                        </div>
+                        <div
+                          className="new-component static"
+                          role="none"
+                          onClick={this.openSlicesPane}
+                        >
+                          <div className="new-component-placeholder fa fa-area-chart" />
+                          <div className="new-component-label">
+                            {t('Your charts & filters')}
+                          </div>
 
-                    <i className="fa fa-arrow-right trigger" />
-                  </div>
+                          <i className="fa fa-arrow-right trigger" />
+                        </div>
 
-                  <NewTabs />
-                  <NewRow />
-                  <NewColumn />
+                        <NewTabs />
+                        <NewRow />
+                        <NewColumn />
 
-                  <NewHeader />
-                  <NewMarkdown />
-                  <NewDivider />
-                </div>
-                <div className="slices-layer slide-content">
-                  <div
-                    className="dashboard-builder-sidepane-header"
-                    onClick={this.closeSlicesPane}
-                    role="none"
-                  >
-                    <i className="fa fa-arrow-left trigger" />
-                    <span>{t('All components')}</span>
+                        <NewHeader />
+                        <NewMarkdown />
+                        <NewDivider />
+                      </div>
+                      <div className="slices-layer slide-content">
+                        <div
+                          className="dashboard-builder-sidepane-header"
+                          onClick={this.closeSlicesPane}
+                          role="none"
+                        >
+                          <i className="fa fa-arrow-left trigger" />
+                          <span>{t('All components')}</span>
+                        </div>
+                        <SliceAdder
+                          height={
+                            height + (isSticky ? SUPERSET_HEADER_HEIGHT : 0)
+                          }
+                        />
+                      </div>
+                    </div>
                   </div>
-                  <SliceAdder height={calculatedHeight} />
-                </div>
-              </div>
-            </div>
+                )}
+              </Sticky>
+            </StickyContainer>
           )}
-        </Sticky>
-      </StickyContainer>
+        </ParentSize>
+      </div>
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 99e93aa444..f069cd104b 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -77,10 +77,10 @@ class Dashboard extends React.PureComponent {
       eventNames: DASHBOARD_EVENT_NAMES,
     });
     Logger.start(this.actionLog);
+    this.initTs = new Date().getTime();
   }
 
   componentDidMount() {
-    this.ts_mount = new Date().getTime();
     Logger.append(LOG_ACTIONS_MOUNT_DASHBOARD);
   }
 
@@ -91,19 +91,22 @@ class Dashboard extends React.PureComponent {
         : 'v2';
       // log pane loads
       const loadedPaneIds = [];
-      const allPanesDidLoad = Object.entries(nextProps.loadStats).every(
+      let minQueryStartTime = Infinity;
+      const allVisiblePanesDidLoad = Object.entries(nextProps.loadStats).every(
         ([paneId, stats]) => {
-          const { didLoad, minQueryStartTime, ...restStats } = stats;
-
+          const {
+            didLoad,
+            minQueryStartTime: paneMinQueryStart,
+            ...restStats
+          } = stats;
           if (
             didLoad &&
             this.props.loadStats[paneId] &&
             !this.props.loadStats[paneId].didLoad
           ) {
-            const duration = new Date().getTime() - minQueryStartTime;
             Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, {
               ...restStats,
-              duration,
+              duration: new Date().getTime() - paneMinQueryStart,
               version,
             });
 
@@ -113,15 +116,18 @@ class Dashboard extends React.PureComponent {
           }
           if (this.isFirstLoad && didLoad && stats.slice_ids.length > 0) {
             loadedPaneIds.push(paneId);
+            minQueryStartTime = Math.min(minQueryStartTime, paneMinQueryStart);
           }
+
+          // return true if it is loaded, or it's index is not 0
           return didLoad || stats.index !== 0;
         },
       );
 
-      if (allPanesDidLoad && this.isFirstLoad) {
+      if (allVisiblePanesDidLoad && this.isFirstLoad) {
         Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
           pane_ids: loadedPaneIds,
-          duration: new Date().getTime() - this.ts_mount,
+          duration: new Date().getTime() - minQueryStartTime,
           version,
         });
         Logger.send(this.actionLog);
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 59a9152499..9621a4972a 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -26,6 +26,7 @@ import {
 } from '../util/constants';
 
 const TABS_HEIGHT = 47;
+const HEADER_HEIGHT = 67;
 
 const propTypes = {
   // redux
@@ -96,52 +97,52 @@ class DashboardBuilder extends React.Component {
       <StickyContainer
         className={cx('dashboard', editMode && 'dashboard--editing')}
       >
-        <DragDroppable
-          component={dashboardRoot}
-          parentComponent={null}
-          depth={DASHBOARD_ROOT_DEPTH}
-          index={0}
-          orientation="column"
-          onDrop={handleComponentDrop}
-          editMode
-          // you cannot drop on/displace tabs if they already exist
-          disableDragdrop={!editMode || topLevelTabs}
-        >
-          {({ dropIndicatorProps }) => (
-            <div>
-              <DashboardHeader />
-              {dropIndicatorProps && <div {...dropIndicatorProps} />}
-            </div>
+        <Sticky>
+          {({ style }) => (
+            <DragDroppable
+              component={dashboardRoot}
+              parentComponent={null}
+              depth={DASHBOARD_ROOT_DEPTH}
+              index={0}
+              orientation="column"
+              onDrop={handleComponentDrop}
+              editMode={editMode}
+              // you cannot drop on/displace tabs if they already exist
+              disableDragdrop={!!topLevelTabs}
+              style={{ zIndex: 100, ...style }}
+            >
+              {({ dropIndicatorProps }) => (
+                <div>
+                  <DashboardHeader />
+                  {dropIndicatorProps && <div {...dropIndicatorProps} />}
+                  {topLevelTabs && (
+                    <WithPopoverMenu
+                      shouldFocus={DashboardBuilder.shouldFocusTabs}
+                      menuItems={[
+                        <IconButton
+                          className="fa fa-level-down"
+                          label="Collapse tab content"
+                          onClick={this.handleDeleteTopLevelTabs}
+                        />,
+                      ]}
+                      editMode={editMode}
+                    >
+                      <DashboardComponent
+                        id={topLevelTabs.id}
+                        parentId={DASHBOARD_ROOT_ID}
+                        depth={DASHBOARD_ROOT_DEPTH + 1}
+                        index={0}
+                        renderTabContent={false}
+                        renderHoverMenu={false}
+                        onChangeTab={this.handleChangeTab}
+                      />
+                    </WithPopoverMenu>
+                  )}
+                </div>
+              )}
+            </DragDroppable>
           )}
-        </DragDroppable>
-
-        {topLevelTabs && (
-          <Sticky topOffset={50}>
-            {({ style }) => (
-              <WithPopoverMenu
-                shouldFocus={DashboardBuilder.shouldFocusTabs}
-                menuItems={[
-                  <IconButton
-                    className="fa fa-level-down"
-                    label="Collapse tab content"
-                    onClick={this.handleDeleteTopLevelTabs}
-                  />,
-                ]}
-                editMode={editMode}
-                style={{ zIndex: 100, ...style }}
-              >
-                <DashboardComponent
-                  id={topLevelTabs.id}
-                  parentId={DASHBOARD_ROOT_ID}
-                  depth={DASHBOARD_ROOT_DEPTH + 1}
-                  index={0}
-                  renderTabContent={false}
-                  onChangeTab={this.handleChangeTab}
-                />
-              </WithPopoverMenu>
-            )}
-          </Sticky>
-        )}
+        </Sticky>
 
         <div className="dashboard-content">
           <div className="grid-container">
@@ -187,7 +188,7 @@ class DashboardBuilder extends React.Component {
           {this.props.editMode &&
             this.props.showBuilderPane && (
               <BuilderComponentPane
-                topOffset={topLevelTabs ? TABS_HEIGHT : 0}
+                topOffset={HEADER_HEIGHT + (topLevelTabs ? TABS_HEIGHT : 0)}
                 toggleBuilderPane={this.props.toggleBuilderPane}
               />
             )}
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index f5ca6e5a7b..d85015e6b7 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -29,6 +29,7 @@ class DashboardGrid extends React.PureComponent {
     this.handleResizeStart = this.handleResizeStart.bind(this);
     this.handleResize = this.handleResize.bind(this);
     this.handleResizeStop = this.handleResizeStop.bind(this);
+    this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
     this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
     this.setGridRef = this.setGridRef.bind(this);
   }
@@ -38,7 +39,7 @@ class DashboardGrid extends React.PureComponent {
       return (
         resizeRef.getBoundingClientRect().bottom -
         this.grid.getBoundingClientRect().top -
-        1
+        2
       );
     }
     return null;
@@ -75,6 +76,19 @@ class DashboardGrid extends React.PureComponent {
     }));
   }
 
+  handleTopDropTargetDrop(dropResult) {
+    if (dropResult) {
+      this.props.handleComponentDrop({
+        ...dropResult,
+        destination: {
+          ...dropResult.destination,
+          // force appending as the first child if top drop target
+          index: 0,
+        },
+      });
+    }
+  }
+
   render() {
     const {
       gridComponent,
@@ -93,6 +107,26 @@ class DashboardGrid extends React.PureComponent {
     return width < 100 ? null : (
       <div className="dashboard-grid" ref={this.setGridRef}>
         <div className="grid-content">
+          {/* make the area above components droppable */}
+          {editMode && (
+            <DragDroppable
+              component={gridComponent}
+              depth={depth}
+              parentComponent={null}
+              index={0}
+              orientation="column"
+              onDrop={this.handleTopDropTargetDrop}
+              className="empty-droptarget"
+              editMode
+            >
+              {({ dropIndicatorProps }) =>
+                dropIndicatorProps && (
+                  <div className="drop-indicator drop-indicator--bottom" />
+                )
+              }
+            </DragDroppable>
+          )}
+
           {gridComponent.children.map((id, index) => (
             <DashboardComponent
               key={id}
@@ -117,7 +151,7 @@ class DashboardGrid extends React.PureComponent {
               index={gridComponent.children.length}
               orientation="column"
               onDrop={handleComponentDrop}
-              className="empty-grid-droptarget--bottom"
+              className="empty-droptarget"
               editMode
             >
               {({ dropIndicatorProps }) =>
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 5fa4afe167..3b1b6b1f36 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,12 +1,12 @@
 /* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
 
 import HeaderActionsDropdown from './HeaderActionsDropdown';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
+import UndoRedoKeylisteners from './UndoRedoKeylisteners';
 import V2PreviewModal from '../deprecated/V2PreviewModal';
 
 import { chartPropShape } from '../util/propShapes';
@@ -58,10 +58,14 @@ class Header extends React.PureComponent {
     super(props);
     this.state = {
       didNotifyMaxUndoHistoryToast: false,
+      emphasizeUndo: false,
+      hightlightRedo: false,
       showV2PreviewModal: props.isV2Preview,
     };
 
     this.handleChangeText = this.handleChangeText.bind(this);
+    this.handleCtrlZ = this.handleCtrlZ.bind(this);
+    this.handleCtrlY = this.handleCtrlY.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
     this.overwriteDashboard = this.overwriteDashboard.bind(this);
@@ -84,6 +88,11 @@ class Header extends React.PureComponent {
     }
   }
 
+  componentWillUnmount() {
+    clearTimeout(this.ctrlYTimeout);
+    clearTimeout(this.ctrlZTimeout);
+  }
+
   forceRefresh() {
     return this.props.fetchCharts(Object.values(this.props.charts), true);
   }
@@ -96,6 +105,26 @@ class Header extends React.PureComponent {
     }
   }
 
+  handleCtrlY() {
+    this.props.onRedo();
+    this.setState({ emphasizeRedo: true }, () => {
+      if (this.ctrlYTimeout) clearTimeout(this.ctrlYTimeout);
+      this.ctrlYTimeout = setTimeout(() => {
+        this.setState({ emphasizeRedo: false });
+      }, 100);
+    });
+  }
+
+  handleCtrlZ() {
+    this.props.onUndo();
+    this.setState({ emphasizeUndo: true }, () => {
+      if (this.ctrlZTimeout) clearTimeout(this.ctrlZTimeout);
+      this.ctrlZTimeout = setTimeout(() => {
+        this.setState({ emphasizeUndo: false });
+      }, 100);
+    });
+  }
+
   toggleEditMode() {
     this.props.setEditMode(!this.props.editMode);
   }
@@ -183,110 +212,117 @@ class Header extends React.PureComponent {
             )}
         </div>
 
-        <ButtonToolbar>
-          {userCanSaveAs && (
-            <ButtonGroup>
-              {editMode && (
+        {userCanSaveAs && (
+          <div className="button-container">
+            {editMode && (
+              <Button
+                bsSize="small"
+                onClick={onUndo}
+                disabled={undoLength < 1}
+                bsStyle={this.state.emphasizeUndo ? 'primary' : undefined}
+              >
+                <div title="Undo" className="undo-action fa fa-reply" />
+              </Button>
+            )}
+
+            {editMode && (
+              <Button
+                bsSize="small"
+                onClick={onRedo}
+                disabled={redoLength < 1}
+                bsStyle={this.state.emphasizeRedo ? 'primary' : undefined}
+              >
+                <div title="Redo" className="redo-action fa fa-share" />
+              </Button>
+            )}
+
+            {editMode && (
+              <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+                {showBuilderPane
+                  ? t('Hide components')
+                  : t('Insert components')}
+              </Button>
+            )}
+
+            {editMode &&
+              (hasUnsavedChanges || isV2Preview) && (
                 <Button
                   bsSize="small"
-                  onClick={onUndo}
-                  disabled={undoLength < 1}
+                  bsStyle={popButton ? 'primary' : undefined}
+                  onClick={this.overwriteDashboard}
                 >
-                  <div title="Undo" className="undo-action fa fa-reply" />
+                  {isV2Preview
+                    ? t('Persist as Dashboard v2')
+                    : t('Save changes')}
                 </Button>
               )}
 
-              {editMode && (
+            {!editMode &&
+              isV2Preview && (
                 <Button
                   bsSize="small"
-                  onClick={onRedo}
-                  disabled={redoLength < 1}
+                  onClick={this.toggleEditMode}
+                  bsStyle={popButton ? 'primary' : undefined}
+                  disabled={!userCanEdit}
                 >
-                  <div title="Redo" className="redo-action fa fa-share" />
+                  {t('Edit to persist Dashboard v2')}
                 </Button>
               )}
 
-              {editMode && (
-                <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
-                  {showBuilderPane
-                    ? t('Hide components')
-                    : t('Insert components')}
+            {!editMode &&
+              !isV2Preview &&
+              !hasUnsavedChanges && (
+                <Button
+                  bsSize="small"
+                  onClick={this.toggleEditMode}
+                  bsStyle={popButton ? 'primary' : undefined}
+                  disabled={!userCanEdit}
+                >
+                  {t('Edit dashboard')}
+                </Button>
+              )}
+
+            {editMode &&
+              !isV2Preview &&
+              !hasUnsavedChanges && (
+                <Button
+                  bsSize="small"
+                  onClick={this.toggleEditMode}
+                  bsStyle={undefined}
+                  disabled={!userCanEdit}
+                >
+                  {t('Switch to view mode')}
                 </Button>
               )}
 
-              {editMode &&
-                (hasUnsavedChanges || isV2Preview) && (
-                  <Button
-                    bsSize="small"
-                    bsStyle={popButton ? 'primary' : undefined}
-                    onClick={this.overwriteDashboard}
-                  >
-                    {isV2Preview
-                      ? t('Persist as Dashboard v2')
-                      : t('Save changes')}
-                  </Button>
-                )}
-
-              {!editMode &&
-                isV2Preview && (
-                  <Button
-                    bsSize="small"
-                    onClick={this.toggleEditMode}
-                    bsStyle={popButton ? 'primary' : undefined}
-                    disabled={!userCanEdit}
-                  >
-                    {t('Edit to persist Dashboard v2')}
-                  </Button>
-                )}
-
-              {!editMode &&
-                !isV2Preview &&
-                !hasUnsavedChanges && (
-                  <Button
-                    bsSize="small"
-                    onClick={this.toggleEditMode}
-                    bsStyle={popButton ? 'primary' : undefined}
-                    disabled={!userCanEdit}
-                  >
-                    {t('Edit dashboard')}
-                  </Button>
-                )}
-
-              {editMode &&
-                !isV2Preview &&
-                !hasUnsavedChanges && (
-                  <Button
-                    bsSize="small"
-                    onClick={this.toggleEditMode}
-                    bsStyle={undefined}
-                    disabled={!userCanEdit}
-                  >
-                    {t('Switch to view mode')}
-                  </Button>
-                )}
-
-              <HeaderActionsDropdown
-                addSuccessToast={this.props.addSuccessToast}
-                addDangerToast={this.props.addDangerToast}
-                dashboardId={dashboardInfo.id}
-                dashboardTitle={dashboardTitle}
-                layout={layout}
-                filters={filters}
-                expandedSlices={expandedSlices}
-                css={css}
-                onSave={onSave}
-                onChange={onChange}
-                forceRefreshAllCharts={this.forceRefresh}
-                startPeriodicRender={this.props.startPeriodicRender}
-                updateCss={updateCss}
-                editMode={editMode}
-                hasUnsavedChanges={hasUnsavedChanges}
-                userCanEdit={userCanEdit}
-                isV2Preview={isV2Preview}
+            <HeaderActionsDropdown
+              addSuccessToast={this.props.addSuccessToast}
+              addDangerToast={this.props.addDangerToast}
+              dashboardId={dashboardInfo.id}
+              dashboardTitle={dashboardTitle}
+              layout={layout}
+              filters={filters}
+              expandedSlices={expandedSlices}
+              css={css}
+              onSave={onSave}
+              onChange={onChange}
+              forceRefreshAllCharts={this.forceRefresh}
+              startPeriodicRender={this.props.startPeriodicRender}
+              updateCss={updateCss}
+              editMode={editMode}
+              hasUnsavedChanges={hasUnsavedChanges}
+              userCanEdit={userCanEdit}
+              isV2Preview={isV2Preview}
+            />
+
+            {editMode && (
+              <UndoRedoKeylisteners
+                onUndo={this.handleCtrlZ}
+                onRedo={this.handleCtrlY}
               />
-            </ButtonGroup>
-          )}
-        </ButtonToolbar>
+            )}
+          </div>
+        )}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 9e68278852..8674fc83ec 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -8,6 +8,7 @@ import SearchInput, { createFilter } from 'react-search-input';
 import AddSliceCard from './AddSliceCard';
 import AddSliceDragPreview from './dnd/AddSliceDragPreview';
 import DragDroppable from './dnd/DragDroppable';
+import Loading from '../../components/Loading';
 import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../util/componentTypes';
 import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
 import { slicePropShape } from '../util/propShapes';
@@ -207,13 +208,7 @@ class SliceAdder extends React.Component {
           </DropdownButton>
         </div>
 
-        {this.props.isLoading && (
-          <img
-            src="/static/assets/images/loading.gif"
-            className="loading"
-            alt="loading"
-          />
-        )}
+        {this.props.isLoading && <Loading />}
 
         {this.props.errorMessage && <div>{this.props.errorMessage}</div>}
 
diff --git a/superset/assets/src/dashboard/components/UndoRedoKeylisteners.jsx b/superset/assets/src/dashboard/components/UndoRedoKeylisteners.jsx
new file mode 100644
index 0000000000..5af0934a61
--- /dev/null
+++ b/superset/assets/src/dashboard/components/UndoRedoKeylisteners.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+  onUndo: PropTypes.func.isRequired,
+  onRedo: PropTypes.func.isRequired,
+};
+
+class UndoRedoKeylisteners extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleKeydown = this.handleKeydown.bind(this);
+  }
+
+  componentDidMount() {
+    document.addEventListener('keydown', this.handleKeydown);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('keydown', this.handleKeydown);
+  }
+
+  handleKeydown(event) {
+    const controlOrCommand = event.keyCode === 90 || event.metaKey;
+    if (controlOrCommand) {
+      const isZChar = event.key === 'z' || event.keyCode === 90;
+      const isYChar = event.key === 'y' || event.keyCode === 89;
+      const isEditingMarkdown = document.querySelector(
+        '.dashboard-markdown--editing',
+      );
+
+      if (!isEditingMarkdown && (isZChar || isYChar)) {
+        event.preventDefault();
+        const func = isZChar ? this.props.onUndo : this.props.onRedo;
+        func();
+      }
+    }
+  }
+
+  render() {
+    return null;
+  }
+}
+
+UndoRedoKeylisteners.propTypes = propTypes;
+
+export default UndoRedoKeylisteners;
diff --git a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
index 91fc0558b3..2c1128e9bb 100644
--- a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
+++ b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
@@ -11,11 +11,11 @@ import {
 
 const staticCardStyles = {
   position: 'fixed',
-  background: 'white',
+  background: 'rgba(255, 255, 255, 0.7)',
   pointerEvents: 'none',
   top: 0,
   left: 0,
-  zIndex: 100,
+  zIndex: 101, // this should be higher than top-level tabs
   width: 376 - 2 * 16,
 };
 
diff --git a/superset/assets/src/dashboard/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js
index cb98a6fcf0..a3b16aac4c 100644
--- a/superset/assets/src/dashboard/components/dnd/handleHover.js
+++ b/superset/assets/src/dashboard/components/dnd/handleHover.js
@@ -1,7 +1,7 @@
 import throttle from 'lodash.throttle';
 import getDropPosition from '../../util/getDropPosition';
 
-const HOVER_THROTTLE_MS = 150;
+const HOVER_THROTTLE_MS = 100;
 
 function handleHover(props, monitor, Component) {
   // this may happen due to throttling
diff --git a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
index a49a893a63..bd639e720a 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ReactMarkdown from 'react-markdown';
+import cx from 'classnames';
 import AceEditor from 'react-ace';
 import 'brace/mode/markdown';
 import 'brace/theme/textmate';
@@ -138,6 +139,7 @@ class Markdown extends React.PureComponent {
         onChange={this.handleMarkdownChange}
         width="100%"
         height="100%"
+        showGutter={false}
         editorProps={{ $blockScrolling: true }}
         value={
           // thisl allows "select all => delete" to give an empty editor
@@ -183,6 +185,8 @@ class Markdown extends React.PureComponent {
         ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
         : component.meta.width || GRID_MIN_COLUMN_COUNT;
 
+    const isEditing = this.state.editorMode === 'edit';
+
     return (
       <DragDroppable
         component={component}
@@ -207,7 +211,12 @@ class Markdown extends React.PureComponent {
             ]}
             editMode={editMode}
           >
-            <div className="dashboard-markdown">
+            <div
+              className={cx(
+                'dashboard-markdown',
+                isEditing && 'dashboard-markdown--editing',
+              )}
+            >
               <ResizableContainer
                 id={component.id}
                 adjustableWidth={parentComponent.type === ROW_TYPE}
@@ -230,7 +239,7 @@ class Markdown extends React.PureComponent {
                   ref={dragSourceRef}
                   className="dashboard-component dashboard-component-chart-holder"
                 >
-                  {editMode && this.state.editorMode === 'edit'
+                  {editMode && isEditing
                     ? this.renderEditMode()
                     : this.renderPreviewMode()}
                 </div>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 4cba2e6d7c..b91d8089fd 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -57,6 +57,7 @@ export default class Tab extends React.PureComponent {
     this.handleChangeText = this.handleChangeText.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
     this.handleDrop = this.handleDrop.bind(this);
+    this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
   }
 
   handleChangeFocus(nextFocus) {
@@ -89,21 +90,53 @@ export default class Tab extends React.PureComponent {
     this.props.onDropOnTab(dropResult);
   }
 
+  handleTopDropTargetDrop(dropResult) {
+    if (dropResult) {
+      this.props.handleComponentDrop({
+        ...dropResult,
+        destination: {
+          ...dropResult.destination,
+          // force appending as the first child if top drop target
+          index: 0,
+        },
+      });
+    }
+  }
+
   renderTabContent() {
     const {
       component: tabComponent,
       parentComponent: tabParentComponent,
-      index,
       depth,
       availableColumnCount,
       columnWidth,
       onResizeStart,
       onResize,
       onResizeStop,
+      editMode,
     } = this.props;
 
     return (
       <div className="dashboard-component-tabs-content">
+        {/* Make top of tab droppable */}
+        {editMode && (
+          <DragDroppable
+            component={tabComponent}
+            parentComponent={tabParentComponent}
+            orientation="column"
+            index={0}
+            depth={depth}
+            onDrop={this.handleTopDropTargetDrop}
+            editMode
+            className="empty-droptarget"
+          >
+            {({ dropIndicatorProps }) =>
+              dropIndicatorProps && (
+                <div className="drop-indicator drop-indicator--top" />
+              )
+            }
+          </DragDroppable>
+        )}
         {tabComponent.children.map((componentId, componentIndex) => (
           <DashboardComponent
             key={componentId}
@@ -119,21 +152,21 @@ export default class Tab extends React.PureComponent {
             onResizeStop={onResizeStop}
           />
         ))}
-        {/* Make the content of the tab component droppable in the case that there are no children */}
-        {tabComponent.children.length === 0 && (
+        {/* Make bottom of tab droppable */}
+        {editMode && (
           <DragDroppable
             component={tabComponent}
             parentComponent={tabParentComponent}
             orientation="column"
-            index={index}
+            index={tabComponent.children.length}
             depth={depth}
             onDrop={this.handleDrop}
             editMode
-            className="empty-tab-droptarget"
+            className="empty-droptarget"
           >
             {({ dropIndicatorProps }) =>
               dropIndicatorProps && (
-                <div className="drop-indicator drop-indicator--top" />
+                <div className="drop-indicator drop-indicator--bottom" />
               )
             }
           </DragDroppable>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 813961d228..01c0e600d6 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -24,6 +24,7 @@ const propTypes = {
   depth: PropTypes.number.isRequired,
   renderTabContent: PropTypes.bool, // whether to render tabs + content or just tabs
   editMode: PropTypes.bool.isRequired,
+  renderHoverMenu: PropTypes.bool,
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -43,6 +44,7 @@ const propTypes = {
 const defaultProps = {
   children: null,
   renderTabContent: true,
+  renderHoverMenu: true,
   availableColumnCount: 0,
   columnWidth: 0,
   onChangeTab() {},
@@ -132,6 +134,7 @@ class Tabs extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       renderTabContent,
+      renderHoverMenu,
       editMode,
     } = this.props;
 
@@ -153,19 +156,20 @@ class Tabs extends React.PureComponent {
           dragSourceRef: tabsDragSourceRef,
         }) => (
           <div className="dashboard-component dashboard-component-tabs">
-            {editMode && (
-              <HoverMenu innerRef={tabsDragSourceRef} position="left">
-                <DragHandle position="left" />
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-              </HoverMenu>
-            )}
+            {editMode &&
+              renderHoverMenu && (
+                <HoverMenu innerRef={tabsDragSourceRef} position="left">
+                  <DragHandle position="left" />
+                  <DeleteComponentButton
+                    onDelete={this.handleDeleteComponent}
+                  />
+                </HoverMenu>
+              )}
 
             <BootstrapTabs
               id={tabsComponent.id}
               activeKey={selectedTabIndex}
               onSelect={this.handleClickTab}
-              // these are important for performant loading of tabs. also, there is a
-              // react-bootstrap bug where mountOnEnter has no effect unless animation=true
               animation
               mountOnEnter
               unmountOnExit={false}
diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 29071cb18f..6f5b2e01bf 100644
--- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
 import ComponentLookup from '../components/gridComponents';
-import getTotalChildWidth from '../util/getChildWidth';
+import getDetailedComponentWidth from '../util/getDetailedComponentWidth';
 import { componentShape } from '../util/propShapes';
 import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
-import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
 
 import {
   createComponent,
@@ -40,23 +39,15 @@ function mapStateToProps(
 
   // rows and columns need more data about their child dimensions
   // doing this allows us to not pass the entire component lookup to all Components
-  if (props.component.type === ROW_TYPE) {
-    props.occupiedColumnCount = getTotalChildWidth({
+  const componentType = component.type;
+  if (componentType === ROW_TYPE || componentType === COLUMN_TYPE) {
+    const { occupiedWidth, minimumWidth } = getDetailedComponentWidth({
       id,
       components: dashboardLayout,
     });
-  } else if (props.component.type === COLUMN_TYPE) {
-    props.minColumnWidth = GRID_MIN_COLUMN_COUNT;
 
-    component.children.forEach(childId => {
-      // rows don't have widths, so find the width of its children
-      if (dashboardLayout[childId].type === ROW_TYPE) {
-        props.minColumnWidth = Math.max(
-          props.minColumnWidth,
-          getTotalChildWidth({ id: childId, components: dashboardLayout }),
-        );
-      }
-    });
+    if (componentType === ROW_TYPE) props.occupiedColumnCount = occupiedWidth;
+    if (componentType === COLUMN_TYPE) props.minColumnWidth = minimumWidth;
   }
 
   return props;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
index 6c2f62462b..3d3e46850e 100644
--- a/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
@@ -3,6 +3,7 @@ import $ from 'jquery';
 import PropTypes from 'prop-types';
 import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
 
+import Loading from '../../../../components/Loading';
 import ModalTrigger from '../../../../components/ModalTrigger';
 import { t } from '../../../../locales';
 
@@ -130,11 +131,7 @@ class SliceAdder extends React.Component {
     }
     const modalContent = (
       <div>
-        <img
-          src="/static/assets/images/loading.gif"
-          className={'loading ' + (hideLoad ? 'hidden' : '')}
-          alt={hideLoad ? '' : 'loading'}
-        />
+        {!hideLoad && <Loading />}
         <div className={this.errored ? '' : 'hidden'}>
           {this.state.errorMsg}
         </div>
diff --git a/superset/assets/src/dashboard/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index 396a56ca27..51cd02a162 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -3,7 +3,9 @@ import {
   DASHBOARD_GRID_ID,
   NEW_COMPONENTS_SOURCE_ID,
 } from '../util/constants';
+import componentIsResizable from '../util/componentIsResizable';
 import findParentId from '../util/findParentId';
+import getComponentWidthFromDrop from '../util/getComponentWidthFromDrop';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
@@ -104,6 +106,24 @@ const actionHandlers = {
       destination,
     });
 
+    if (componentIsResizable(nextEntities[dragging.id])) {
+      // update component width if it changed
+      const nextWidth =
+        getComponentWidthFromDrop({
+          dropResult,
+          layout: state,
+        }) || undefined; // don't set a 0 width
+      if ((nextEntities[dragging.id].meta || {}).width !== nextWidth) {
+        nextEntities[dragging.id] = {
+          ...nextEntities[dragging.id],
+          meta: {
+            ...nextEntities[dragging.id].meta,
+            width: nextWidth,
+          },
+        };
+      }
+    }
+
     // wrap the dragged component in a row depending on destination type
     const wrapInRow = shouldWrapChildInRow({
       parentType: destination.type,
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index bbcb7e1ca7..a81d33ae53 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -2,7 +2,6 @@
   flex: 0 0 @builder-pane-width;
   z-index: 10;
   position: relative;
-  box-shadow: -4px 0 4px 0 rgba(0, 0, 0, 0.1);
 
   .dashboard-builder-sidepane-header {
     font-size: 15px;
@@ -34,13 +33,14 @@
     overflow: hidden;
     width: @builder-pane-width;
     height: 100%;
+    box-shadow: -4px 0 4px 0 rgba(0, 0, 0, 0.1);
   }
 
   .slider-container {
     position: absolute;
     background: white;
     width: @builder-pane-width * 2;
-    height: 100%;
+    height: 100vh;
     display: flex;
     transition: all 0.5s ease;
 
diff --git a/superset/assets/src/dashboard/stylesheets/components/chart.less b/superset/assets/src/dashboard/stylesheets/components/chart.less
index 73914fba52..bc0005dbb5 100644
--- a/superset/assets/src/dashboard/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/stylesheets/components/chart.less
@@ -31,13 +31,14 @@
   .resizable-container:hover
   > .dashboard-component-chart-holder:after,
 .dashboard--editing .dashboard-component-chart-holder:hover:after {
-  border: 1px solid @gray-light;
+  border: 1px dashed @indicator-color;
+  z-index: 2;
 }
 
 .dashboard--editing
   .resizable-container.resizable-container--resizing:hover
   > .dashboard-component-chart-holder:after {
-  border: 1px solid @indicator-color;
+  border: 1px dashed @indicator-color;
 }
 
 .dashboard--editing
@@ -62,3 +63,38 @@
   /* disable chart interactions in edit mode */
   pointer-events: none;
 }
+
+.slice-header-controls-trigger {
+  padding: 0 16px;
+  position: absolute;
+  top: 0;
+  right: -16px; //increase the click-able area for the button
+
+  &:hover {
+    cursor: pointer;
+  }
+}
+
+.dot {
+  height: 4px;
+  width: 4px;
+  background-color: @gray;
+  border-radius: 50%;
+  margin: 2px 0;
+  display: inline-block;
+
+  .is-cached & {
+    background-color: @pink;
+    box-shadow: 0 0 5px 1.5px rgba(255, 0, 0, 0.3);
+  }
+
+  .vertical-dots-container & {
+    display: block;
+  }
+
+  a[role='menuitem'] & {
+    width: 8px;
+    height: 8px;
+    margin-right: 8px;
+  }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/components/column.less b/superset/assets/src/dashboard/stylesheets/components/column.less
index 2f26d95441..e923f8cc33 100644
--- a/superset/assets/src/dashboard/stylesheets/components/column.less
+++ b/superset/assets/src/dashboard/stylesheets/components/column.less
@@ -24,12 +24,16 @@
   .resizable-container.resizable-container--resizing:hover
   > .grid-column:after,
 .dashboard--editing .hover-menu:hover + .grid-column:after {
+  border: 1px dashed @indicator-color;
+  z-index: 2;
+}
+
+.dashboard--editing .grid-column:after {
   border: 1px dashed @gray-light;
-  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
 .grid-column--empty {
-  min-height: 72px;
+  min-height: 100px;
 }
 
 .grid-column--empty:before {
diff --git a/superset/assets/src/dashboard/stylesheets/components/header.less b/superset/assets/src/dashboard/stylesheets/components/header.less
index 940310336c..71b2176851 100644
--- a/superset/assets/src/dashboard/stylesheets/components/header.less
+++ b/superset/assets/src/dashboard/stylesheets/components/header.less
@@ -6,15 +6,28 @@
   color: @almost-black;
 }
 
+.dashboard--editing .dashboard-grid .dashboard-component-header:after {
+  border: 1px dashed transparent;
+  content: '';
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
+}
+
+.dashboard--editing .dashboard-grid .dashboard-component-header:hover:after {
+  border: 1px dashed @indicator-color;
+  z-index: 2;
+}
+
 .dashboard-header .dashboard-component-header {
   font-weight: 300;
   width: auto;
 }
 
-.dashboard-header .btn-group button {
-  margin-right: 8px;
-}
-
 .dashboard-header .undo-action,
 .dashboard-header .redo-action {
   line-height: 18px;
@@ -25,6 +38,11 @@
   cursor: move;
 }
 
+.header-style-option {
+  font-weight: 700;
+  color: @almost-black;
+}
+
 /* note: sizes should be a multiple of the 8px grid unit so that rows in the grid align */
 .header-small {
   font-size: 16px;
diff --git a/superset/assets/src/dashboard/stylesheets/components/markdown.less b/superset/assets/src/dashboard/stylesheets/components/markdown.less
index 2cfd92949d..87974d9ac6 100644
--- a/superset/assets/src/dashboard/stylesheets/components/markdown.less
+++ b/superset/assets/src/dashboard/stylesheets/components/markdown.less
@@ -14,3 +14,12 @@
     border: none;
   }
 }
+
+/* maximize editing space */
+.dashboard-markdown--editing {
+  .dashboard-component-chart-holder {
+    .with-popover-menu--focused & {
+      padding: 1px;
+    }
+  }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/components/row.less b/superset/assets/src/dashboard/stylesheets/components/row.less
index 382417eb00..06d98e66ce 100644
--- a/superset/assets/src/dashboard/stylesheets/components/row.less
+++ b/superset/assets/src/dashboard/stylesheets/components/row.less
@@ -32,13 +32,18 @@
   > .grid-row:after,
 .dashboard--editing .hover-menu:hover + .grid-row:after,
 .dashboard--editing .dashboard-component-tabs > .hover-menu:hover + div:after {
+  border: 1px dashed @indicator-color;
+  z-index: 2;
+}
+
+.dashboard--editing .grid-row:after,
+.dashboard--editing .dashboard-component-tabs > .hover-menu + div:after {
   border: 1px dashed @gray-light;
-  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
 .grid-row.grid-row--empty {
   align-items: center; /* this centers the empty note content */
-  height: 80px;
+  height: 100px;
 }
 
 .grid-row--empty:before {
diff --git a/superset/assets/src/dashboard/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
index b1124da0dd..cee524c659 100644
--- a/superset/assets/src/dashboard/stylesheets/components/tabs.less
+++ b/superset/assets/src/dashboard/stylesheets/components/tabs.less
@@ -1,89 +1,84 @@
 .dashboard-component-tabs {
   width: 100%;
   background-color: white;
-}
-
-.dashboard-component-tabs .dashboard-component-tabs-content {
-  min-height: 48px;
-  margin-top: 1px;
-}
-
-.dashboard-component-tabs-content .empty-tab-droptarget {
-  min-height: 24px;
-}
-
-.dashboard-component-tabs .nav-tabs {
-  border-bottom: none;
-}
-
-/* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
-.dashboard-component-tabs .nav-tabs > li {
-  margin: 0 16px;
-}
-
-.dashboard-component-tabs .nav-tabs > li > a {
-  color: @almost-black;
-  border: none;
-  padding: 12px 0 14px 0;
-  font-size: 15px;
-  margin-right: 0;
-}
-
-.dashboard-component-tabs .nav-tabs > li.active > a {
-  border: none;
-}
-
-.dashboard-component-tabs .nav-tabs > li.active > a:after {
-  content: '';
-  position: absolute;
-  height: 3px;
-  width: 100%;
-  bottom: 0;
-  background: linear-gradient(to right, #e32464, #2c2261);
-}
-
-.dashboard-component-tabs .nav-tabs > li > a:hover {
-  border: none;
-  background: inherit;
-  color: @almost-black;
-}
-
-.dashboard-component-tabs .nav-tabs > li > a:focus {
-  outline: none;
-  background: #fff;
-}
-
-.dashboard-component-tabs .nav-tabs > li .dragdroppable-tab {
-  cursor: move;
-}
-
-/* These expande the outline border + drop indicator for tabs */
-.dashboard-component-tabs .nav-tabs > li .drop-indicator {
-  top: -12px !important;
-  height: ~'calc(100% + 24px)' !important;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--left {
-  left: -12px !important;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--right {
-  right: -12px !important;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--top,
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--bottom {
-  left: -12px !important;
-  width: ~'calc(100% + 24px)' !important; /* escape for .less */
-  opacity: 0.4;
-}
-
-.dashboard-component-tabs li .fa-plus {
-  color: @gray-dark;
-  font-size: 14px;
-  margin-top: 3px;
-}
 
-.dashboard-component-tabs li .editable-title input[type='button'] {
-  cursor: pointer;
+  & .nav-tabs {
+    border-bottom: none;
+
+    /* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
+    & > li {
+      margin: 0 16px;
+
+      & > a {
+        color: @almost-black;
+        border: none;
+        padding: 12px 0 14px 0;
+        font-size: 15px;
+        margin-right: 0;
+      }
+
+      & > a:hover {
+        border: none;
+        background: inherit;
+        color: @almost-black;
+      }
+
+      & > a:focus {
+        outline: none;
+        background: #fff;
+      }
+
+      & .dragdroppable-tab {
+        cursor: move;
+      }
+
+      & .drop-indicator {
+        top: -12px !important;
+        height: ~'calc(100% + 24px)' !important;
+      }
+
+      & .drop-indicator--left {
+        left: -12px !important;
+      }
+      & .drop-indicator--right {
+        right: -12px !important;
+      }
+
+      & .drop-indicator--bottom,
+      & .drop-indicator--top {
+        left: -12px !important;
+        width: ~'calc(100% + 24px)' !important; /* escape for .less */
+        opacity: 0.4;
+      }
+
+      & .fa-plus {
+        color: @gray-dark;
+        font-size: 14px;
+        margin-top: 3px;
+      }
+
+      & .editable-title input[type='button'] {
+        cursor: pointer;
+      }
+    }
+
+    & li.active > a {
+      border: none;
+    }
+
+    & li.active > a:after {
+      content: '';
+      position: absolute;
+      height: 3px;
+      width: 100%;
+      bottom: 0;
+      background: linear-gradient(to right, #e32464, #2c2261);
+    }
+  }
+
+  & .dashboard-component-tabs-content {
+    min-height: 48px;
+    margin-top: 1px;
+    position: relative;
+  }
 }
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 3db5cdcf15..92f1ff10f6 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -78,7 +78,7 @@ body {
 .dashboard .dashboard-header {
   #save-dash-split-button {
     border-radius: 0;
-    margin-left: -8px;
+    margin-left: -9px;
     height: 30px;
     width: 30px;
     z-index: 10;
@@ -97,6 +97,15 @@ body {
       min-width: unset;
     }
   }
+
+  .button-container {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    & > :not(:last-child) {
+      margin-right: 8px;
+    }
+  }
 }
 
 .dashboard .chart-header,
@@ -120,40 +129,6 @@ body {
   }
 }
 
-.slice-header-controls-trigger {
-  padding: 0 16px;
-  position: absolute;
-  top: 0;
-  right: -16px; //increase the click-able area for the button
-
-  &:hover {
-    cursor: pointer;
-  }
-}
-
-.dot {
-  height: 4px;
-  width: 4px;
-  background-color: @gray;
-  border-radius: 50%;
-  margin: 2px 0;
-  display: inline-block;
-
-  .is-cached & {
-    background-color: @pink;
-  }
-
-  .vertical-dots-container & {
-    display: block;
-  }
-
-  a[role='menuitem'] & {
-    width: 8px;
-    height: 8px;
-    margin-right: 8px;
-  }
-}
-
 .modal img.loading {
   width: 50px;
   margin: 0;
@@ -205,6 +180,10 @@ body {
     line-height: 1em;
     cursor: pointer;
     opacity: 0.9;
+    flex-wrap: nowrap;
+    display: flex;
+    align-items: center;
+    white-space: nowrap;
 
     &:hover {
       opacity: 1;
diff --git a/superset/assets/src/dashboard/stylesheets/dnd.less b/superset/assets/src/dashboard/stylesheets/dnd.less
index 0a10c61c22..9b5ea896ee 100644
--- a/superset/assets/src/dashboard/stylesheets/dnd.less
+++ b/superset/assets/src/dashboard/stylesheets/dnd.less
@@ -3,7 +3,7 @@
 }
 
 .dragdroppable--dragging {
-  opacity: 0.15;
+  opacity: 0.2;
 }
 
 .dragdroppable-row {
@@ -76,3 +76,36 @@
   margin: -1px;
   width: 2px;
 }
+
+/* empty drop targets */
+.dashboard-component-tabs-content {
+  & > .empty-droptarget {
+    position: absolute;
+    width: 100%;
+  }
+
+  & > .empty-droptarget:first-child {
+    height: 16px;
+    top: -8px;
+    z-index: 10;
+  }
+
+  & > .empty-droptarget:last-child {
+    height: 12px;
+    bottom: 0px;
+  }
+}
+
+.grid-content {
+  /* note we don't do a :last-child selection because
+    assuming bottom empty-droptarget is last child is fragile */
+  & > .empty-droptarget {
+    width: 100%;
+    height: 100%;
+  }
+
+  & > .empty-droptarget:first-child {
+    height: 24px;
+    margin-top: -24px;
+  }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/grid.less b/superset/assets/src/dashboard/stylesheets/grid.less
index 9d09ac7017..1e22e1dfa0 100644
--- a/superset/assets/src/dashboard/stylesheets/grid.less
+++ b/superset/assets/src/dashboard/stylesheets/grid.less
@@ -1,5 +1,4 @@
 .grid-container {
-  min-height: 100%;
   position: relative;
   margin: 24px;
   /* without this, the grid will not get smaller upon toggling the builder panel on */
@@ -7,33 +6,21 @@
   width: 100%;
 }
 
-/* this is the ParentSize wrapper  */
+/* this is the ParentSize wrapper */
 .grid-container > div:first-child {
   height: inherit !important;
 }
 
 .grid-content {
-  min-height: 100%;
   display: flex;
   flex-direction: column;
-  margin-bottom: 100px;
 }
 
 /* gutters between rows */
-.grid-content
-  > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget--bottom):not(.empty-grid-droptarget--top) {
+.grid-content > div:not(:only-child):not(:last-child):not(.empty-droptarget) {
   margin-bottom: 16px;
 }
 
-.grid-content > .empty-grid-droptarget--top {
-  height: 24px;
-  margin-top: -24px;
-}
-.empty-grid-droptarget--bottom {
-  width: 100%;
-  height: 100%;
-}
-
 /* Editing guides */
 .grid-column-guide {
   position: absolute;
diff --git a/superset/assets/src/dashboard/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
index 0c70f58f64..3c790e4b6e 100644
--- a/superset/assets/src/dashboard/stylesheets/popover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/popover-menu.less
@@ -85,7 +85,7 @@
 
 .hover-dropdown li.dropdown-item:hover a,
 .popover-menu li.dropdown-item:hover a {
-  background: @gray-light;
+  background: @menu-hover;
 }
 
 .popover-dropdown .caret {
@@ -96,7 +96,7 @@
 
 .hover-dropdown li.dropdown-item.active a,
 .popover-menu li.dropdown-item.active a {
-  background: white;
+  background: @gray-light;
   font-weight: bold;
   color: @almost-black;
 }
@@ -125,6 +125,7 @@
   border: 1px solid @gray-light;
 }
 
+/* Create the transparent rect icon */
 .background-style-option.background--transparent:before {
   background-image: linear-gradient(45deg, @gray 25%, transparent 25%),
     linear-gradient(-45deg, @gray 25%, transparent 25%),
diff --git a/superset/assets/src/dashboard/util/dropOverflowsParent.js b/superset/assets/src/dashboard/util/dropOverflowsParent.js
index 328d8e3999..a5f99f3de4 100644
--- a/superset/assets/src/dashboard/util/dropOverflowsParent.js
+++ b/superset/assets/src/dashboard/util/dropOverflowsParent.js
@@ -1,45 +1,6 @@
-import { COLUMN_TYPE } from '../util/componentTypes';
-import { GRID_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID } from './constants';
-import findParentId from './findParentId';
-import getChildWidth from './getChildWidth';
-import newComponentFactory from './newComponentFactory';
+import getComponentWidthFromDrop from './getComponentWidthFromDrop';
 
 export default function doesChildOverflowParent(dropResult, layout) {
-  const { source, destination, dragging } = dropResult;
-
-  // moving a component within a container should never overflow
-  if (source.id === destination.id) {
-    return false;
-  }
-
-  const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
-  const grandparentId = findParentId({
-    childId: destination.id,
-    layout,
-  });
-
-  const child = isNewComponent
-    ? newComponentFactory(dragging.type)
-    : layout[dragging.id] || {};
-  const parent = layout[destination.id] || {};
-  const grandparent = layout[grandparentId] || {};
-
-  const childWidth = (child.meta && child.meta.width) || 0;
-
-  const grandparentCapacity =
-    grandparent.meta && typeof grandparent.meta.width === 'number'
-      ? grandparent.meta.width
-      : GRID_COLUMN_COUNT;
-
-  const parentCapacity =
-    parent.meta && typeof parent.meta.width === 'number'
-      ? parent.meta.width
-      : grandparentCapacity;
-
-  const occupiedParentWidth =
-    parent.type === COLUMN_TYPE
-      ? 0
-      : getChildWidth({ id: destination.id, components: layout });
-
-  return parentCapacity < occupiedParentWidth + childWidth;
+  const childWidth = getComponentWidthFromDrop({ dropResult, layout });
+  return typeof childWidth === 'number' && childWidth < 0;
 }
diff --git a/superset/assets/src/dashboard/util/findParentId.js b/superset/assets/src/dashboard/util/findParentId.js
index 9e47bf2a70..bf26d548bf 100644
--- a/superset/assets/src/dashboard/util/findParentId.js
+++ b/superset/assets/src/dashboard/util/findParentId.js
@@ -1,4 +1,4 @@
-export default function findParentId({ childId, layout = {} }) {
+function findParentId({ childId, layout = {} }) {
   let parentId = null;
 
   const ids = Object.keys(layout);
@@ -17,3 +17,15 @@ export default function findParentId({ childId, layout = {} }) {
 
   return parentId;
 }
+
+const cache = {};
+export default function findParentIdWithCache({ childId, layout = {} }) {
+  if (cache[childId]) {
+    const lastParent = layout[cache[childId]] || {};
+    if (lastParent.children && lastParent.children.includes(childId)) {
+      return lastParent.id;
+    }
+  }
+  cache[childId] = findParentId({ childId, layout });
+  return cache[childId];
+}
diff --git a/superset/assets/src/dashboard/util/getChildWidth.js b/superset/assets/src/dashboard/util/getChildWidth.js
deleted file mode 100644
index 69d2792a40..0000000000
--- a/superset/assets/src/dashboard/util/getChildWidth.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function getTotalChildWidth({ id, components }) {
-  const component = components[id];
-  if (!component) return 0;
-
-  let width = 0;
-
-  (component.children || []).forEach(childId => {
-    const child = components[childId] || {};
-    width += (child.meta || {}).width || 0;
-  });
-
-  return width;
-}
diff --git a/superset/assets/src/dashboard/util/getComponentWidthFromDrop.js b/superset/assets/src/dashboard/util/getComponentWidthFromDrop.js
new file mode 100644
index 0000000000..38c7c5a784
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getComponentWidthFromDrop.js
@@ -0,0 +1,57 @@
+import { NEW_COMPONENTS_SOURCE_ID } from './constants';
+import findParentId from './findParentId';
+import getDetailedComponentWidth from './getDetailedComponentWidth';
+import newComponentFactory from './newComponentFactory';
+
+export default function getComponentWidthFromDrop({
+  dropResult,
+  layout: components,
+}) {
+  const { source, destination, dragging } = dropResult;
+
+  const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
+  const component = isNewComponent
+    ? newComponentFactory(dragging.type)
+    : components[dragging.id] || {};
+
+  // moving a component within the same container shouldn't change its width
+  if (source.id === destination.id) {
+    return component.meta.width;
+  }
+
+  const draggingWidth = getDetailedComponentWidth({
+    component,
+    components,
+  });
+
+  const destinationWidth = getDetailedComponentWidth({
+    id: destination.id,
+    components,
+  });
+
+  let destinationCapacity =
+    destinationWidth.width - destinationWidth.occupiedWidth;
+
+  if (isNaN(destinationCapacity)) {
+    const grandparentWidth = getDetailedComponentWidth({
+      id: findParentId({
+        childId: destination.id,
+        layout: components,
+      }),
+      components,
+    });
+
+    destinationCapacity =
+      grandparentWidth.width - grandparentWidth.occupiedWidth;
+  }
+
+  if (isNaN(destinationCapacity) || isNaN(draggingWidth.width)) {
+    return draggingWidth.width;
+  } else if (destinationCapacity >= draggingWidth.width) {
+    return draggingWidth.width;
+  } else if (destinationCapacity >= draggingWidth.minimumWidth) {
+    return destinationCapacity;
+  }
+
+  return -1;
+}
diff --git a/superset/assets/src/dashboard/util/getDetailedComponentWidth.js b/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
new file mode 100644
index 0000000000..ee3096d671
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
@@ -0,0 +1,76 @@
+import findParentId from './findParentId';
+import { GRID_MIN_COLUMN_COUNT, GRID_COLUMN_COUNT } from './constants';
+import {
+  ROW_TYPE,
+  COLUMN_TYPE,
+  MARKDOWN_TYPE,
+  CHART_TYPE,
+} from './componentTypes';
+
+function getTotalChildWidth({ id, components }) {
+  const component = components[id];
+  if (!component) return 0;
+
+  let width = 0;
+
+  (component.children || []).forEach(childId => {
+    const child = components[childId] || {};
+    width += (child.meta || {}).width || 0;
+  });
+
+  return width;
+}
+
+export default function getDetailedComponentWidth({
+  // pass either an id, or a component
+  id,
+  component: passedComponent,
+  components = {},
+}) {
+  const result = {
+    width: undefined,
+    occupiedWidth: undefined,
+    minimumWidth: undefined,
+  };
+
+  const component = passedComponent || components[id];
+  if (!component) return result;
+
+  // note these remain as undefined if the component has no defined width
+  result.width = (component.meta || {}).width;
+  result.occupiedWidth = result.width;
+
+  if (component.type === ROW_TYPE) {
+    // not all rows have width 12, e
+    result.width =
+      getDetailedComponentWidth({
+        id: findParentId({
+          childId: component.id,
+          layout: components,
+        }),
+        components,
+      }).width || GRID_COLUMN_COUNT;
+    result.occupiedWidth = getTotalChildWidth({ id: component.id, components });
+    result.minimumWidth = result.occupiedWidth || GRID_MIN_COLUMN_COUNT;
+  } else if (component.type === COLUMN_TYPE) {
+    // find the width of the largest child, only rows count
+    result.minimumWidth = GRID_MIN_COLUMN_COUNT;
+    result.occupiedWidth = 0;
+    (component.children || []).forEach(childId => {
+      // rows don't have widths, so find the width of its children
+      if (components[childId].type === ROW_TYPE) {
+        result.minimumWidth = Math.max(
+          result.minimumWidth,
+          getTotalChildWidth({ id: childId, components }),
+        );
+      }
+    });
+  } else if (
+    component.type === MARKDOWN_TYPE ||
+    component.type === CHART_TYPE
+  ) {
+    result.minimumWidth = GRID_MIN_COLUMN_COUNT;
+  }
+
+  return result;
+}
diff --git a/superset/assets/src/dashboard/util/getDropPosition.js b/superset/assets/src/dashboard/util/getDropPosition.js
index 74dfcaa0e2..dd4add90b1 100644
--- a/superset/assets/src/dashboard/util/getDropPosition.js
+++ b/superset/assets/src/dashboard/util/getDropPosition.js
@@ -72,7 +72,7 @@ export default function getDropPosition(monitor, Component) {
   const siblingDropOrientation =
     orientation === 'row' ? 'horizontal' : 'vertical';
 
-  if (validChild && !validSibling) {
+  if (isDraggingOverShallow && validChild && !validSibling) {
     // easiest case, insert as child
     if (childDropOrientation === 'vertical') {
       return hasChildren ? DROP_RIGHT : DROP_LEFT;
diff --git a/superset/assets/src/dashboard/util/headerStyleOptions.js b/superset/assets/src/dashboard/util/headerStyleOptions.js
index 7efa040ef5..a37bd5fdfc 100644
--- a/superset/assets/src/dashboard/util/headerStyleOptions.js
+++ b/superset/assets/src/dashboard/util/headerStyleOptions.js
@@ -2,7 +2,19 @@ import { t } from '../../locales';
 import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from './constants';
 
 export default [
-  { value: SMALL_HEADER, label: t('Small'), className: 'header-small' },
-  { value: MEDIUM_HEADER, label: t('Medium'), className: 'header-medium' },
-  { value: LARGE_HEADER, label: t('Large'), className: 'header-large' },
+  {
+    value: SMALL_HEADER,
+    label: t('Small'),
+    className: 'header-style-option header-small',
+  },
+  {
+    value: MEDIUM_HEADER,
+    label: t('Medium'),
+    className: 'header-style-option header-medium',
+  },
+  {
+    value: LARGE_HEADER,
+    label: t('Large'),
+    className: 'header-style-option header-large',
+  },
 ];
diff --git a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
index 8abc9b9985..9054d44695 100644
--- a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
@@ -1,5 +1,7 @@
+import componentIsResizable from './componentIsResizable';
 import shouldWrapChildInRow from './shouldWrapChildInRow';
 import newComponentFactory from './newComponentFactory';
+import getComponentWidthFromDrop from './getComponentWidthFromDrop';
 
 import { ROW_TYPE, TABS_TYPE, TAB_TYPE } from './componentTypes';
 
@@ -10,6 +12,12 @@ export default function newEntitiesFromDrop({ dropResult, layout }) {
   const dropEntity = layout[destination.id];
   const dropType = dropEntity.type;
   let newDropChild = newComponentFactory(dragType, dragging.meta);
+
+  if (componentIsResizable(dragging)) {
+    newDropChild.meta.width = // don't set a 0 width
+      getComponentWidthFromDrop({ dropResult, layout }) || undefined;
+  }
+
   const wrapChildInRow = shouldWrapChildInRow({
     parentType: dropType,
     childType: dragType,
diff --git a/superset/assets/src/explore/components/DisplayQueryButton.jsx b/superset/assets/src/explore/components/DisplayQueryButton.jsx
index d098d2a9af..4aa3f96f50 100644
--- a/superset/assets/src/explore/components/DisplayQueryButton.jsx
+++ b/superset/assets/src/explore/components/DisplayQueryButton.jsx
@@ -9,6 +9,7 @@ import github from 'react-syntax-highlighter/dist/styles/github';
 import CopyToClipboard from './../../components/CopyToClipboard';
 import { getExploreUrlAndPayload } from '../exploreUtils';
 
+import Loading from '../../components/Loading';
 import ModalTrigger from './../../components/ModalTrigger';
 import Button from '../../components/Button';
 import { t } from '../../locales';
@@ -18,7 +19,7 @@ registerLanguage('html', html);
 registerLanguage('sql', sql);
 registerLanguage('json', json);
 
-const $ = window.$ = require('jquery');
+const $ = (window.$ = require('jquery'));
 
 const propTypes = {
   animation: PropTypes.bool,
@@ -80,8 +81,9 @@ export default class DisplayQueryButton extends React.PureComponent {
   }
   beforeOpen() {
     if (
-      ['loading', null].indexOf(this.props.chartStatus) >= 0
-      || !this.props.queryResponse || !this.props.queryResponse.query
+      ['loading', null].indexOf(this.props.chartStatus) >= 0 ||
+      !this.props.queryResponse ||
+      !this.props.queryResponse.query
     ) {
       this.fetchQuery();
     } else {
@@ -90,11 +92,7 @@ export default class DisplayQueryButton extends React.PureComponent {
   }
   renderModalBody() {
     if (this.state.isLoading) {
-      return (<img
-        className="loading"
-        alt="Loading..."
-        src="/static/assets/images/loading.gif"
-      />);
+      return <Loading />;
     } else if (this.state.error) {
       return <pre>{this.state.error}</pre>;
     } else if (this.state.query) {
diff --git a/superset/assets/src/explore/components/controls/DatasourceControl.jsx b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
index 404ba5e329..d63d6fe806 100644
--- a/superset/assets/src/explore/components/controls/DatasourceControl.jsx
+++ b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
@@ -3,11 +3,19 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Table } from 'reactable';
 import {
-  Row, Col, Collapse, Label, FormControl, Modal,
-  OverlayTrigger, Tooltip, Well,
+  Row,
+  Col,
+  Collapse,
+  Label,
+  FormControl,
+  Modal,
+  OverlayTrigger,
+  Tooltip,
+  Well,
 } from 'react-bootstrap';
 
 import ControlHeader from '../ControlHeader';
+import Loading from '../../../components/Loading';
 import { t } from '../../../locales';
 import ColumnOption from '../../../components/ColumnOption';
 import MetricOption from '../../../components/MetricOption';
@@ -68,7 +76,8 @@ export default class DatasourceControl extends React.PureComponent {
                 className="datasource-link"
               >
                 {ds.name}
-              </a>),
+              </a>
+            ),
             type: ds.type,
           }));
 
@@ -113,7 +122,9 @@ export default class DatasourceControl extends React.PureComponent {
           <div>
             <FormControl
               id="formControlsText"
-              inputRef={(ref) => { this.setSearchRef(ref); }}
+              inputRef={(ref) => {
+                this.setSearchRef(ref);
+              }}
               type="text"
               bsSize="sm"
               value={this.state.filter}
@@ -121,14 +132,8 @@ export default class DatasourceControl extends React.PureComponent {
               onChange={this.changeSearch}
             />
           </div>
-          {this.state.loading &&
-            <img
-              className="loading"
-              alt="Loading..."
-              src="/static/assets/images/loading.gif"
-            />
-          }
-          {this.state.datasources &&
+          {this.state.loading && <Loading />}
+          {this.state.datasources && (
             <Table
               columns={['name', 'type', 'schema', 'connection', 'creator']}
               className="table table-condensed"
@@ -138,9 +143,10 @@ export default class DatasourceControl extends React.PureComponent {
               filterBy={this.state.filter}
               hideFilterInput
             />
-          }
+          )}
         </Modal.Body>
-      </Modal>);
+      </Modal>
+    );
   }
   renderDatasource() {
     const datasource = this.props.datasource;
@@ -157,18 +163,23 @@ export default class DatasourceControl extends React.PureComponent {
             <Col md={6}>
               <strong>Columns</strong>
               {datasource.columns.map(col => (
-                <div key={col.column_name}><ColumnOption showType column={col} /></div>
+                <div key={col.column_name}>
+                  <ColumnOption showType column={col} />
+                </div>
               ))}
             </Col>
             <Col md={6}>
               <strong>Metrics</strong>
               {datasource.metrics.map(m => (
-                <div key={m.metric_name}><MetricOption metric={m} showType /></div>
+                <div key={m.metric_name}>
+                  <MetricOption metric={m} showType />
+                </div>
               ))}
             </Col>
           </Row>
         </Well>
-      </div>);
+      </div>
+    );
   }
   render() {
     return (
@@ -188,7 +199,7 @@ export default class DatasourceControl extends React.PureComponent {
           placement="right"
           overlay={
             <Tooltip id={'edit-datasource-tooltip'}>
-              {t('Edit the datasource\'s configuration')}
+              {t("Edit the datasource's configuration")}
             </Tooltip>
           }
         >
@@ -199,9 +210,7 @@ export default class DatasourceControl extends React.PureComponent {
         <OverlayTrigger
           placement="right"
           overlay={
-            <Tooltip id={'toggle-datasource-tooltip'}>
-              {t('Show datasource configuration')}
-            </Tooltip>
+            <Tooltip id={'toggle-datasource-tooltip'}>{t('Show datasource configuration')}</Tooltip>
           }
         >
           <a href="#">
@@ -211,11 +220,10 @@ export default class DatasourceControl extends React.PureComponent {
             />
           </a>
         </OverlayTrigger>
-        <Collapse in={this.state.showDatasource}>
-          {this.renderDatasource()}
-        </Collapse>
+        <Collapse in={this.state.showDatasource}>{this.renderDatasource()}</Collapse>
         {this.renderModal()}
-      </div>);
+      </div>
+    );
   }
 }
 
diff --git a/superset/assets/src/profile/components/TableLoader.jsx b/superset/assets/src/profile/components/TableLoader.jsx
index 1e67426eea..462e00993a 100644
--- a/superset/assets/src/profile/components/TableLoader.jsx
+++ b/superset/assets/src/profile/components/TableLoader.jsx
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Table, Tr, Td } from 'reactable';
 import $ from 'jquery';
-
+import Loading from '../../components/Loading';
 import '../../../stylesheets/reactable-pagination.css';
 
 const propTypes = {
@@ -29,6 +29,9 @@ export default class TableLoader extends React.PureComponent {
     });
   }
   render() {
+    if (this.state.isLoading) {
+      return <Loading />;
+    }
     const tableProps = Object.assign({}, this.props);
     let { columns } = this.props;
     if (!columns && this.state.data.length > 0) {
@@ -37,11 +40,14 @@ export default class TableLoader extends React.PureComponent {
     delete tableProps.dataEndpoint;
     delete tableProps.mutator;
     delete tableProps.columns;
-    if (this.state.isLoading) {
-      return <img alt="loading" width="25" src="/static/assets/images/loading.gif" />;
-    }
+
     return (
-      <Table {...tableProps} className="table" itemsPerPage={50} style={{ textTransform: 'capitalize' }}>
+      <Table
+        {...tableProps}
+        className="table"
+        itemsPerPage={50}
+        style={{ textTransform: 'capitalize' }}
+      >
         {this.state.data.map((row, i) => (
           <Tr key={i}>
             {columns.map((col) => {
@@ -49,9 +55,14 @@ export default class TableLoader extends React.PureComponent {
                 return (
                   <Td key={col} column={col} value={row['_' + col]}>
                     {row[col]}
-                  </Td>);
+                  </Td>
+                );
               }
-              return <Td key={col} column={col}>{row[col]}</Td>;
+              return (
+                <Td key={col} column={col}>
+                  {row[col]}
+                </Td>
+              );
             })}
           </Tr>
         ))}
diff --git a/superset/assets/src/welcome/DashboardTable.jsx b/superset/assets/src/welcome/DashboardTable.jsx
index 78d4bdd57b..f7f3007ecc 100644
--- a/superset/assets/src/welcome/DashboardTable.jsx
+++ b/superset/assets/src/welcome/DashboardTable.jsx
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
 import PropTypes from 'prop-types';
 import { Table, Tr, Td, Thead, Th, unsafe } from 'reactable';
 
+import Loading from '../components/Loading';
 import '../../stylesheets/reactable-pagination.css';
 
 const $ = window.$ = require('jquery');
@@ -60,12 +61,7 @@ export default class DashboardTable extends React.PureComponent {
         </Table>
       );
     }
-    return (
-      <img
-        className="loading"
-        alt="Loading..."
-        src="/static/assets/images/loading.gif"
-      />);
+    return <Loading />;
   }
 }
 DashboardTable.propTypes = propTypes;
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index 2add7976f8..9d1a39b935 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -2051,6 +2051,10 @@ clap@^1.0.9:
   dependencies:
     chalk "^1.1.3"
 
+classnames@2.x, classnames@^2.1.2:
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
+
 classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
   version "2.2.5"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
@@ -3591,6 +3595,10 @@ execa@^0.7.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+exenv@^1.2.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+
 exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -5731,7 +5739,7 @@ lodash.isarray@^3.0.0:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
 
-lodash.isequal@^4.1.1:
+lodash.isequal@^4.0.0, lodash.isequal@^4.1.1:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
 
@@ -7953,6 +7961,15 @@ react-bootstrap-slider@2.0.1:
     react "^15.6.1"
     react-dom "^15.6.1"
 
+react-bootstrap-table@^4.0.2:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/react-bootstrap-table/-/react-bootstrap-table-4.3.1.tgz#f704be55b7f6bf0557d2fc5bec6d25fd307d0cde"
+  dependencies:
+    classnames "^2.1.2"
+    prop-types "^15.5.10"
+    react-modal "^3.1.7"
+    react-s-alert "^1.3.2"
+
 react-bootstrap@^0.31.5:
   version "0.31.5"
   resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.5.tgz#57040fa8b1274e1e074803c21a1b895fdabea05a"
@@ -8013,6 +8030,13 @@ react-dnd@^2.5.4:
     object-assign "^4.1.0"
     prop-types "^15.5.10"
 
+react-draggable@3.x:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d"
+  dependencies:
+    classnames "^2.2.5"
+    prop-types "^15.6.0"
+
 "react-draggable@^2.2.6 || ^3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4"
@@ -8028,6 +8052,16 @@ react-gravatar@^2.6.1:
     md5 "^2.1.0"
     query-string "^4.2.2"
 
+react-grid-layout@0.16.5:
+  version "0.16.5"
+  resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.5.tgz#1ff12d12afa875c11fe05802f7509e52bfe9a2cb"
+  dependencies:
+    classnames "2.x"
+    lodash.isequal "^4.0.0"
+    prop-types "15.x"
+    react-draggable "3.x"
+    react-resizable "1.x"
+
 react-html-attributes@^1.3.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.4.1.tgz#97b5ec710da68833598c8be6f89ac436216840a5"
@@ -8047,6 +8081,10 @@ react-input-autosize@^2.1.2:
   dependencies:
     prop-types "^15.5.8"
 
+react-lifecycles-compat@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+
 react-map-gl@^3.0.4:
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/react-map-gl/-/react-map-gl-3.0.5.tgz#8797b4a1a85be1404a2409f43f577ad939475a60"
@@ -8069,6 +8107,15 @@ react-markdown@^3.3.0:
     unist-util-visit "^1.3.0"
     xtend "^4.0.1"
 
+react-modal@^3.1.7:
+  version "3.4.5"
+  resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.4.5.tgz#75a7eefb8f4c8247278d5ce1c41249d7785d9f69"
+  dependencies:
+    exenv "^1.2.0"
+    prop-types "^15.5.10"
+    react-lifecycles-compat "^3.0.0"
+    warning "^3.0.0"
+
 react-onclickoutside@^5.9.0:
   version "5.11.1"
   resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623"
@@ -8096,13 +8143,19 @@ react-redux@^5.0.2:
     loose-envify "^1.1.0"
     prop-types "^15.5.10"
 
-react-resizable@^1.3.3:
+react-resizable@1.x, react-resizable@^1.3.3:
   version "1.7.5"
   resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
   dependencies:
     prop-types "15.x"
     react-draggable "^2.2.6 || ^3.0.3"
 
+react-s-alert@^1.3.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/react-s-alert/-/react-s-alert-1.4.1.tgz#ef3665a9d98c4cf2e448fc2d84e48aeca799bb5a"
+  dependencies:
+    babel-runtime "^6.23.0"
+
 react-search-input@^0.11.3:
   version "0.11.3"
   resolved "https://registry.yarnpkg.com/react-search-input/-/react-search-input-0.11.3.tgz#3dd1f9fc584b6bc40a6ee133ae042b6fbb7ae8dd"


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org