You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/22 00:54:15 UTC

[incubator-superset] branch dashboard-builder updated (ec1c879 -> 00ff778)

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

ccwilliams pushed a change to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git.


    omit ec1c879  [fix] layout converter fix (#5218)
    omit e979d6d  [dashboard v2] ui + ux fixes (#5208)
    omit c4e07f2  [fix] new dashboard state (#5213)
    omit da45e9c  [dash builder fix] combine markdown and slice name, slice picker height (#5165)
    omit 4d13dae  Fix: Should pass slice_can_edit flag down (#5159)
    omit 1592f72  [dashboard v2]  better grid drop ux, fix tab bugs 🐛 (#5151)
    omit aa649be  Fix dashboard position row data (#5131)
    omit b497ccc  add slice from explore view (#5141)
    omit 6d3303a  [dashboard v2] add v1 switch (#5126)
    omit 54ff295  Fix: update slices list when add/remove multiple slices (#5138)
    omit ec55c78  [dashboard v2] logging updates (#5087)
    omit 72bea06  [dashboard v2] tests! (#5066)
    omit 6619271  [dashboard v2] fix bugs from rebase
    omit c10b5d7  [dashboard v2] check for default_filters before json_loads-ing them (#5064)
    omit 0b886c7  Dashboard save button (#4979)
    omit 8f25e6a  fix dashboard server-side unit tests (#5009)
    omit b62cea5  Markdown for dashboard (#4962)
    omit 0f35350  add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893)
    omit 8d03599  [dashboard builder] improve perf (#4855)
    omit 9ebffcc  Dashboard builder rebased + linted (#4849)
    omit 14eb8db  [dashboard builder] git mv to src/ post-rebase
    omit 4e90e06  [dashboard builder] static layout + toasts (#4763)
    omit 883eff0  [dashboard-builder] add top-level tabs + undo-redo (#4626)
    omit a96aa2f  fix rebase error, clean up css organization and use @less vars
    omit 2c4a6be  [dashboard builder] Add dir structure for dashboard/v2, simplified Header, split pane, Draggable side panel
     add 05061a7  Fix time shift color assignements (#5065)
     add 1aaa73b  Translate string to array for multi fields in getControlsState (#5057)
     add 3207116  Revert "[get_df] Adding support for multi-statement SQL" (#5078)
     add 42d0597  Use a dummy version number on master (#5000)
     add e30215c  Add 24 hours refresh for dashboard (#5068)
     add c18ef89  [bugfix] fix visualization with adhocMetric (#5080)
     add 4592677  Fix python2 str() in visualization (#5093)
     add ae50845  Proper error handling in Hive Queries (#4428)
     add 6c3e469  Add more time grains (#5083)
     add 7dbb45e  add CnOvit to Superset users list (#5094)
     add 0511d1f  [get_df] Adding support for multi-statement SQL (#5086)
     add f611797  Bump dep on pydruid to 0.4.3 (#5098)
     add 21967f4  Add Lime to Superset user list.
     add e8b2598  Merge pull request #5109 from cxmcc/patch-1
     add 875d0b5  Override time grain in annotations (#5084)
     add 1aced9b  force limit only when there is no existing limit
     add d38315a  reuse_regex_logic
     add a9d7faf  add tests
     add cefc206  Merge pull request #5023 from timifasubaa/fix_sqllab_commit
     add f3778c3  fixing LIKE constant name (#5110)
     add 4ecd95a  [bugfix] deck.gl on druid always shows animation (#5107)
     add ff4b103  Fixing time table viz for adhoc metrics (#5117)
     add 2861110  Refactor NULL handling into method, disable for DECK.gl vizes (#5106)
     add 40fadfc  adding null checks to adhoc filter popover (#5111)
     add 1d3e96b  Allow multiple time shifts (#5067)
     add 556ef44  docs: Add new Athena URI scheme awsathena+rest:// (#5112)
     add cc0942a  updating adhoc metric filtering (#5105)
     add dc21e0d  URL shortner for dashboards (#4760)
     add ffd65ce  Pin FAB to 1.10.0 (#5133)
     add d2bc4ec   Bump celery to 4.1.1 (#5134)
     add b71f551  Optimize presto SQL Lab query performance. (#5132)
     add f102eab  [crud] Improving performance (#5136)
     add 0abbc98  pin kombu dependency (#5150)
     add 0545d11  [migrations] Fix time grain SQLA (#5135)
     add 57e1256  Improve time shift (#5140)
     add 5b35f75  empty lists are invalid comparators (#5160)
     add 1b4406d  [migration] Adding migration to remove empty in/not-in filters (#5161)
     add b53b240  [Explore][Adhoc Metrics/ Filters] disabled message for custom sql tab in druid (#5162)
     add 915e6e9  Restore translations as of 459cb701fb2140fcce8b97a1839a9511574375c7 (#5156)
     add 7d1c035  [druid] Fixing Druid version check (#5028)
     add 0a276ff  Init docker for local development environment. (#4193)
     add a3477ab  Pin FAB and bump a bunch of JS libs (#5122)
     add 3de79b6  Fix bullet chart rendering (#5108)
     add 280200f  Fixing tooltip displaying metrics in heatmap (#5055)
     add 52a933f  [migrations] Cleanup recent migrations (#5155)
     add 502b617  [migrations] Cleaning up migration logic (#5167)
     add 7f30b48  fetch datasources from broker endpoint when refresh new datasources (#5183)
     add d6846d9  Adding column only if it doesn't already exist (#5179)
     add 95bb175  fix empty metrics
     add 6e37d3f  Merge pull request #5190 from timifasubaa/fix_null_metrics
     add 30111bf  Repoint .istambul.yml to the right location (#5187)
     add a109543  Introduce class attr BaseViz.enforce_numerical_metrics (#5176)
     add 585dbe6  fix Formula type annotation, it doesn't show up since #4630 (#5181)
     add 6f05b48  Adding the MetricsControl to the timeseries_limit_metric field
     add 4776828  Adding tests for adhoc metric as timeseries_limit_metric
     add b380a57  Fixing sortby adhoc metrics for table viz
     add 66ffcb6  Merge pull request #5118 from michellethomas/add_metrics_control_sort_by
     add 7a107fa  pass_error_message_separately
     add 7fa5559  remove resolution link prop
     add 4b7a14d  Merge pull request #5194 from timifasubaa/pass_error_link_separately
     add b60ac5f  Fixing issue with table viz for table with no metrics (#5205)
     add 8cdc9ca  [pie-chart] Restricting query to single metric (#5203)
     add de0aaf4  [webpack] setup lazy loading for all visualizations (#4727)
     add d5ebc43  [explore] fix autocomplete on verbose names (#5204)
     add 7b49b6c  Revert "[webpack] setup lazy loading for all visualizations" (#5219)
     add 00fad1c  README: update Maieutical Labs url (#5227)
     add ccf2110  Bump Celery to 4.2.0 (#5222)
     add c89933d  [sql lab] quote schema and table name (#5195)
     add c670621  setup: improve description (#5226)
     add 0509d7a  [Explore] Enable Rich tooltip by default (#5215)
     add d7d5327  [CRUD] Improving performance by disabling editing Associated Chart] (#5238)
     add 480ddfc  [CRUD] disable user change slices from dashboardmodelview (#5217)
     add 1fc4ee0  [perf] add webpack 4 + SplitChunks + lazy load visualizations (#5240)
     add d483ed1  [adhoc-filters] Adding adhoc-filters to all viz types (#5206)
     add 5c106b9  [bubble-chart] Fixing issue w/ metric names (#5237)
     add a84f430  Describe the use of custom OAuth2 authorization servers (#5220)
     add 70679d4  Pin botocore version (#5184)
     add 93cdf60  [sqllab] Fix sql lab resolution link (#5216)
     add 62427c8  Revert "[perf] add webpack 4 + SplitChunks + lazy load visualizations" (#5253)
     add 2a3d297  Allow users to view dashboards they own (#4520)
     add 409ac68  [sql lab] Fix issue around VARBINARY type in Presto (#5121)
     add 13cbf80  [Explore] Handle empty metrics control data (#5241)
     add eb67651  Moving homogenize_types to after no data exception (#5214)
     add 15c8e5b  [timeseries table] use verbose date in tooltip by default (#5263)
     add 0e5293b  Update db_engine_specs.py (#5264)
     new 1a03632  [dashboard builder] Add dir structure for dashboard/v2, simplified Header, split pane, Draggable side panel
     new c62af8c  fix rebase error, clean up css organization and use @less vars
     new abc3ec0  [dashboard-builder] add top-level tabs + undo-redo (#4626)
     new 67e78c3  [dashboard builder] static layout + toasts (#4763)
     new 996b083  [dashboard builder] git mv to src/ post-rebase
     new 5ff440b  Dashboard builder rebased + linted (#4849)
     new 2205204  [dashboard builder] improve perf (#4855)
     new ff057d8  add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893)
     new d6dcec2  Markdown for dashboard (#4962)
     new 82ab75e  fix dashboard server-side unit tests (#5009)
     new ae37277  Dashboard save button (#4979)
     new a02d858  [dashboard v2] check for default_filters before json_loads-ing them (#5064)
     new 439fa14  [dashboard v2] fix bugs from rebase
     new 4bee613  [dashboard v2] tests! (#5066)
     new 69ab5f7  [dashboard v2] logging updates (#5087)
     new a61f6f7  Fix: update slices list when add/remove multiple slices (#5138)
     new 0e0c768  [dashboard v2] add v1 switch (#5126)
     new 7ad60ba  add slice from explore view (#5141)
     new 017e797  Fix dashboard position row data (#5131)
     new b1386ab  [dashboard v2]  better grid drop ux, fix tab bugs 🐛 (#5151)
     new f973397  Fix: Should pass slice_can_edit flag down (#5159)
     new 0860341  [dash builder fix] combine markdown and slice name, slice picker height (#5165)
     new da81674  [fix] new dashboard state (#5213)
     new 85ae429  [dashboard v2] ui + ux fixes (#5208)
     new 4a48825  [fix] layout converter fix (#5218)
     new 00ff778  [dashboard v2] remove webpack-cli, fresh yarn.lock post-rebase

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (ec1c879)
            \
             N -- N -- N   refs/heads/dashboard-builder (00ff778)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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


Summary of changes:
 .gitignore                                         |    7 +
 README.md                                          |    4 +-
 UPDATING.md                                        |    6 +
 tests/__init__.py => babel-node                    |    0
 contrib/docker/Dockerfile                          |   60 +
 contrib/docker/docker-build.sh                     |    5 +
 contrib/docker/docker-compose.yml                  |   48 +
 contrib/docker/docker-entrypoint.sh                |   12 +
 contrib/docker/docker-init.sh                      |   24 +
 contrib/docker/superset_config.py                  |   48 +
 docs/installation.rst                              |  103 +-
 package-lock.json                                  |  743 ++
 requirements.txt                                   |    7 +-
 setup.py                                           |   16 +-
 superset/assets/.istanbul.yml                      |    2 +-
 superset/assets/backendSync.json                   |   24 -
 superset/assets/package.json                       |   48 +-
 .../components/AlteredSliceTag_spec.jsx            |   80 +-
 .../components/URLShortLinkButton_spec.jsx         |    8 +-
 .../spec/javascripts/explore/AdhocFilter_spec.js   |   20 +
 .../explore/components/AdhocFilterControl_spec.jsx |   52 -
 ...AdhocFilterEditPopoverSimpleTabContent_spec.jsx |    4 +-
 .../components/ControlPanelsContainer_spec.jsx     |    2 +-
 .../explore/components/FilterControl_spec.jsx      |  248 -
 .../javascripts/explore/components/Filter_spec.jsx |  115 -
 .../explore/components/MetricsControl_spec.jsx     |   32 +-
 .../javascripts/visualizations/nvd3_viz_spec.jsx   |    6 +-
 superset/assets/src/SqlLab/actions.js              |   16 +-
 .../src/SqlLab/components/HighlightedSql.jsx       |    4 +-
 .../assets/src/SqlLab/components/ResultSet.jsx     |    6 +-
 .../assets/src/SqlLab/components/SouthPane.jsx     |    1 -
 superset/assets/src/SqlLab/reducers.js             |    7 +-
 superset/assets/src/chart/chartAction.js           |    4 +
 superset/assets/src/components/AlteredSliceTag.jsx |   12 +-
 superset/assets/src/components/MetricOption.jsx    |    2 +-
 .../assets/src/components/StackTraceMessage.jsx    |    7 +
 .../components/URLShortLinkButton.jsx              |   24 +-
 .../dashboard/components/RefreshIntervalModal.jsx  |    5 +
 superset/assets/src/explore/AdhocFilter.js         |   10 +-
 .../explore/components/AdhocFilterEditPopover.jsx  |   33 +-
 .../AdhocFilterEditPopoverSimpleTabContent.jsx     |    8 +-
 .../explore/components/AdhocMetricEditPopover.jsx  |   45 +-
 .../src/explore/components/DisplayQueryButton.jsx  |   12 +-
 .../explore/components/ExploreActionButtons.jsx    |   11 +-
 .../components/controls/AdhocFilterControl.jsx     |   92 +-
 .../components/controls/AnnotationLayer.jsx        |   31 +-
 .../src/explore/components/controls/Filter.jsx     |  187 -
 .../explore/components/controls/FilterControl.jsx  |  155 -
 .../explore/components/controls/MetricsControl.jsx |   27 +-
 .../explore/components/controls/SelectControl.jsx  |    2 +
 .../src/explore/components/controls/index.js       |    2 -
 superset/assets/src/explore/constants.js           |    5 +-
 superset/assets/src/explore/controls.jsx           |   79 +-
 superset/assets/src/explore/main.css               |    7 +
 superset/assets/src/explore/store.js               |   11 +-
 superset/assets/src/explore/visTypes.js            |  132 +-
 superset/assets/src/modules/colors.js              |    3 +-
 superset/assets/src/modules/dates.js               |    2 +-
 superset/assets/src/utils/common.js                |    3 +
 superset/assets/src/visualizations/heatmap.js      |    3 +-
 superset/assets/src/visualizations/nvd3_vis.css    |   31 +
 superset/assets/src/visualizations/nvd3_vis.js     |   23 +-
 superset/assets/src/visualizations/table.js        |    6 +-
 superset/assets/src/visualizations/time_table.jsx  |   20 +-
 superset/assets/webpack.config.js                  |    2 +-
 superset/assets/yarn.lock                          | 4602 ++++-----
 superset/connectors/druid/models.py                |   75 +-
 superset/connectors/druid/views.py                 |    4 +-
 superset/connectors/sqla/models.py                 |   13 +-
 superset/connectors/sqla/views.py                  |    4 +-
 superset/data/__init__.py                          |   61 +-
 superset/db_engine_specs.py                        |   63 +-
 .../versions/130915240929_is_sqllab_viz_flow.py    |    2 +-
 .../80a67c5192fa_single_pie_chart_metric.py        |   73 +
 .../versions/afb7730f6a9c_remove_empty_filters.py  |   57 +
 .../versions/bddc498dd179_adhoc_filters.py         |   97 +
 .../versions/c5756bec8b47_time_grain_sqla.py       |   63 +
 superset/migrations/versions/f231d82b9b26_.py      |    3 -
 superset/models/core.py                            |    7 +-
 superset/sql_lab.py                                |   11 +-
 superset/translations/de/LC_MESSAGES/messages.json | 2425 ++++-
 superset/translations/de/LC_MESSAGES/messages.mo   |  Bin 87713 -> 63955 bytes
 superset/translations/de/LC_MESSAGES/messages.po   | 7772 ++++-----------
 superset/translations/en/LC_MESSAGES/messages.json |    2 +-
 superset/translations/en/LC_MESSAGES/messages.mo   |  Bin 87713 -> 88509 bytes
 superset/translations/en/LC_MESSAGES/messages.po   | 2459 +++--
 superset/translations/es/LC_MESSAGES/messages.json | 2425 ++++-
 superset/translations/es/LC_MESSAGES/messages.mo   |  Bin 87713 -> 64912 bytes
 superset/translations/es/LC_MESSAGES/messages.po   | 8389 +++++------------
 superset/translations/fr/LC_MESSAGES/messages.json | 3346 ++++++-
 superset/translations/fr/LC_MESSAGES/messages.mo   |  Bin 87712 -> 90477 bytes
 superset/translations/fr/LC_MESSAGES/messages.po   | 5361 +++--------
 superset/translations/it/LC_MESSAGES/messages.json | 3145 ++++++-
 superset/translations/it/LC_MESSAGES/messages.mo   |  Bin 87723 -> 83973 bytes
 superset/translations/it/LC_MESSAGES/messages.po   | 6301 ++++---------
 superset/translations/ja/LC_MESSAGES/messages.json | 2443 ++++-
 superset/translations/ja/LC_MESSAGES/messages.mo   |  Bin 87681 -> 65233 bytes
 superset/translations/ja/LC_MESSAGES/messages.po   | 7766 ++++-----------
 superset/translations/messages.pot                 | 1648 ++--
 .../translations/pt_BR/LC_MESSAGES/messages.json   | 2643 +++++-
 .../translations/pt_BR/LC_MESSAGES/messages.mo     |  Bin 87676 -> 73720 bytes
 .../translations/pt_BR/LC_MESSAGES/messages.po     | 9570 ++++++-------------
 superset/translations/ru/LC_MESSAGES/messages.json | 3145 ++++++-
 superset/translations/ru/LC_MESSAGES/messages.mo   |  Bin 87714 -> 113812 bytes
 superset/translations/ru/LC_MESSAGES/messages.po   | 9869 ++++++--------------
 superset/translations/zh/LC_MESSAGES/messages.json |    2 +-
 superset/translations/zh/LC_MESSAGES/messages.mo   |  Bin 87716 -> 86943 bytes
 superset/translations/zh/LC_MESSAGES/messages.po   | 5940 ++++--------
 superset/utils.py                                  |    7 +-
 superset/views/base.py                             |    4 +-
 superset/views/core.py                             |   53 +-
 superset/viz.py                                    |   77 +-
 tests/celery_tests.py                              |   30 +-
 tests/core_tests.py                                |    3 +-
 tests/dashboard_tests.py                           |   36 +
 tests/db_engine_specs_test.py                      |   13 +
 tests/druid_tests.py                               |    5 +-
 tests/model_tests.py                               |    6 +
 118 files changed, 41920 insertions(+), 50854 deletions(-)
 copy tests/__init__.py => babel-node (100%)
 create mode 100644 contrib/docker/Dockerfile
 create mode 100644 contrib/docker/docker-build.sh
 create mode 100644 contrib/docker/docker-compose.yml
 create mode 100644 contrib/docker/docker-entrypoint.sh
 create mode 100644 contrib/docker/docker-init.sh
 create mode 100644 contrib/docker/superset_config.py
 create mode 100644 package-lock.json
 rename superset/assets/spec/javascripts/{explore => }/components/URLShortLinkButton_spec.jsx (76%)
 delete mode 100644 superset/assets/spec/javascripts/explore/components/FilterControl_spec.jsx
 delete mode 100644 superset/assets/spec/javascripts/explore/components/Filter_spec.jsx
 rename superset/assets/src/{explore => }/components/URLShortLinkButton.jsx (65%)
 delete mode 100644 superset/assets/src/explore/components/controls/Filter.jsx
 delete mode 100644 superset/assets/src/explore/components/controls/FilterControl.jsx
 create mode 100644 superset/migrations/versions/80a67c5192fa_single_pie_chart_metric.py
 create mode 100644 superset/migrations/versions/afb7730f6a9c_remove_empty_filters.py
 create mode 100644 superset/migrations/versions/bddc498dd179_adhoc_filters.py
 create mode 100644 superset/migrations/versions/c5756bec8b47_time_grain_sqla.py


[incubator-superset] 01/26: [dashboard builder] Add dir structure for dashboard/v2, simplified Header, split pane, Draggable side panel

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

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

commit 1a03632281ff44b918bcdec4fc5e2526441f9c7a
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Tue Jan 16 14:50:08 2018 -0800

    [dashboard builder] Add dir structure for dashboard/v2, simplified Header, split pane, Draggable side panel
    
    [grid] add <DashboardGrid />, <ResizableContainer />, and initial grid components.
    
    [grid] gridComponents/ directory, add fixtures/ directory and test layout, add <Column />
    
    [grid] working grid with gutters
    
    [grid] design tweaks and polish, add <Tabs />
    
    [header] add gradient header logo and favicon
    
    [dnd] begin adding dnd functionality
    
    [dnd] add util/isValidChild.js
    
    [react-beautiful-dnd] iterate on dnd until blocked
    
    [dnd] refactor to use react-dnd
    
    [react-dnd] refactor to use composable <DashboardComponent /> structure
    
    [dnd] factor out DashboardComponent, let components render dropInidcator and set draggableRef, add draggable tabs
    
    [dnd] refactor to use redux, add DashboardComponent and DashboardGrid containers
    
    [dragdroppable] rename horizontal/vertical => row/column
    
    [builder] refactor into HoverMenu, add WithPopoverMenu
    
    [builder] add editable header and disableDragDrop prop for Dragdroppable's
    
    [builder] make tabs editable
    
    [builder] add generic popover dropdown and header row style editability
    
    [builder] add hover rowStyle dropdown, make row styles editable
    
    [builder] add some new component icons, add popover with delete to charts
    
    [builder] add preview icons, add popover menu to rows.
    
    [builder] add IconButton and RowStyleDropdown
    
    [resizable] use ResizableContainer instead of DimensionProvider, fix resize and delete bugs
    
    [builder] fix bug with spacer
    
    [builder] clean up, header.size => header.headerSize
    
    [builder] support more drag/drop combinations by wrapping some components in rows upon drop. fix within list drop index. refactor some utils.
    
    [builder][tabs] fix broken add tab button
    
    [dashboard builder] don't pass dashboard layout to all dashboard components, improve drop indicator logic, fix delete component pure component bug
    
    [dnd] refactor drop position logic
---
 superset/assets/images/favicon.png                 | Bin 6927 -> 18166 bytes
 superset/assets/images/superset-logo@2x.png        | Bin 4132 -> 0 bytes
 superset/assets/javascripts/dashboard/v2/.eslintrc |  29 ++
 .../javascripts/dashboard/v2/actions/index.js      |  69 ++++
 .../v2/components/BuilderComponentPane.jsx         |  39 ++
 .../dashboard/v2/components/Dashboard.jsx          |  44 ++
 .../dashboard/v2/components/DashboardBuilder.jsx   |  40 ++
 .../dashboard/v2/components/DashboardGrid.jsx      | 171 ++++++++
 .../dashboard/v2/components/DashboardHeader.jsx    |  62 +++
 .../v2/components/DeleteComponentButton.jsx        |  23 ++
 .../dashboard/v2/components/IconButton.jsx         |  40 ++
 .../dashboard/v2/components/StaticDashboard.jsx    |  19 +
 .../dashboard/v2/components/dnd/DragDroppable.jsx  | 100 +++++
 .../dashboard/v2/components/dnd/DragHandle.jsx     |  38 ++
 .../dashboard/v2/components/dnd/dnd.css            |  60 +++
 .../v2/components/dnd/dragDroppableConfig.js       |  67 +++
 .../dashboard/v2/components/dnd/handleDrop.js      |  63 +++
 .../dashboard/v2/components/dnd/handleHover.js     |  37 ++
 .../v2/components/gridComponents/Chart.jsx         | 126 ++++++
 .../v2/components/gridComponents/Column.jsx        | 144 +++++++
 .../v2/components/gridComponents/Divider.jsx       |  64 +++
 .../v2/components/gridComponents/Header.jsx        | 151 +++++++
 .../dashboard/v2/components/gridComponents/Row.jsx | 186 +++++++++
 .../v2/components/gridComponents/Spacer.jsx        | 113 +++++
 .../dashboard/v2/components/gridComponents/Tab.jsx | 162 ++++++++
 .../v2/components/gridComponents/Tabs.jsx          | 211 ++++++++++
 .../v2/components/gridComponents/components.css    | 455 +++++++++++++++++++++
 .../v2/components/gridComponents/grid.css          |  17 +
 .../v2/components/gridComponents/index.js          |  43 ++
 .../gridComponents/new/DraggableNewComponent.jsx   |  39 ++
 .../v2/components/gridComponents/new/NewChart.jsx  |  24 ++
 .../v2/components/gridComponents/new/NewColumn.jsx |  24 ++
 .../components/gridComponents/new/NewDivider.jsx   |  24 ++
 .../v2/components/gridComponents/new/NewHeader.jsx |  24 ++
 .../v2/components/gridComponents/new/NewRow.jsx    |  23 ++
 .../v2/components/gridComponents/new/NewSpacer.jsx |  24 ++
 .../v2/components/gridComponents/new/NewTabs.jsx   |  24 ++
 .../dashboard/v2/components/menu/HoverMenu.jsx     |  36 ++
 .../v2/components/menu/PopoverDropdown.jsx         |  64 +++
 .../v2/components/menu/RowStyleDropdown.jsx        |  46 +++
 .../v2/components/menu/WithPopoverMenu.jsx         | 100 +++++
 .../v2/components/resizable/ResizableContainer.jsx | 184 +++++++++
 .../v2/components/resizable/ResizableHandle.jsx    |  25 ++
 .../v2/components/resizable/resizable.css          |  72 ++++
 .../dashboard/v2/containers/DashboardComponent.jsx |  68 +++
 .../dashboard/v2/containers/DashboardGrid.jsx      |  23 ++
 .../dashboard/v2/fixtures/testLayout.js            | 161 ++++++++
 .../javascripts/dashboard/v2/reducers/dashboard.js | 112 +++++
 .../javascripts/dashboard/v2/reducers/index.js     |   6 +
 .../dashboard/v2/util/componentIsResizable.js      |  15 +
 .../dashboard/v2/util/componentTypes.js            |  23 ++
 .../javascripts/dashboard/v2/util/constants.js     |  30 ++
 .../dashboard/v2/util/countChildRowsAndColumns.js  |  14 +
 .../javascripts/dashboard/v2/util/dnd-reorder.js   |  54 +++
 .../dashboard/v2/util/getDropPosition.js           |  88 ++++
 .../dashboard/v2/util/headerStyleOptions.js        |   8 +
 .../javascripts/dashboard/v2/util/isValidChild.js  |  69 ++++
 .../dashboard/v2/util/newComponentFactory.js       |  45 ++
 .../dashboard/v2/util/newComponentIdToType.js      |  35 ++
 .../dashboard/v2/util/newEntitiesFromDrop.js       |  55 +++
 .../javascripts/dashboard/v2/util/propShapes.jsx   |  24 ++
 .../dashboard/v2/util/resizableConfig.js           |  30 ++
 .../dashboard/v2/util/rowStyleOptions.js           |   7 +
 .../dashboard/v2/util/shouldWrapChildInRow.js      |  30 ++
 superset/assets/package.json                       |   5 +-
 superset/assets/src/components/EditableTitle.jsx   |  36 +-
 .../dashboard/components/DashboardContainer.jsx    |   2 +-
 superset/assets/src/dashboard/index.jsx            |  21 +-
 superset/assets/stylesheets/dashboard-v2.css       |  42 ++
 superset/assets/stylesheets/superset.less          |  24 +-
 superset/config.py                                 |   2 +-
 superset/templates/appbuilder/navbar.html          |   5 +-
 superset/templates/superset/dashboard.html         |   1 -
 73 files changed, 4323 insertions(+), 18 deletions(-)

diff --git a/superset/assets/images/favicon.png b/superset/assets/images/favicon.png
index 55316fa..f03cd5c 100644
Binary files a/superset/assets/images/favicon.png and b/superset/assets/images/favicon.png differ
diff --git a/superset/assets/images/superset-logo@2x.png b/superset/assets/images/superset-logo@2x.png
deleted file mode 100644
index 839f617..0000000
Binary files a/superset/assets/images/superset-logo@2x.png and /dev/null differ
diff --git a/superset/assets/javascripts/dashboard/v2/.eslintrc b/superset/assets/javascripts/dashboard/v2/.eslintrc
new file mode 100644
index 0000000..70efc15
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/.eslintrc
@@ -0,0 +1,29 @@
+{
+  "rules": {
+    "prefer-template": 2,
+    "new-cap": 2,
+    "no-restricted-syntax": 2,
+    "guard-for-in": 2,
+    "prefer-arrow-callback": 2,
+    "func-names": 2,
+    "react/jsx-no-bind": 2,
+    "no-confusing-arrow": 2,
+    "jsx-a11y/no-static-element-interactions": 2,
+    "jsx-a11y/anchor-has-content": 2,
+    "react/require-default-props": 2,
+    "no-plusplus": 2,
+    "no-mixed-operators": 2,
+    "no-continue": 2,
+    "no-bitwise": 2,
+    "no-undef": 2,
+    "no-multi-assign": 2,
+    "no-restricted-properties": 2,
+    "no-prototype-builtins": 2,
+    "jsx-a11y/href-no-hash": 2,
+    "class-methods-use-this": 2,
+    "import/no-named-as-default": 2,
+    "import/prefer-default-export": 2,
+    "react/no-unescaped-entities": 2,
+    "react/no-string-refs": 2,
+  }
+}
diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js
new file mode 100644
index 0000000..005a77e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/actions/index.js
@@ -0,0 +1,69 @@
+export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
+export function updateComponents(nextComponents) {
+  return {
+    type: UPDATE_COMPONENTS,
+    payload: {
+      nextComponents,
+    },
+  };
+}
+
+export const DELETE_COMPONENT = 'DELETE_COMPONENT';
+export function deleteComponent(id, parentId) {
+  return {
+    type: DELETE_COMPONENT,
+    payload: {
+      id,
+      parentId,
+    },
+  };
+}
+
+export const CREATE_COMPONENT = 'CREATE_COMPONENT';
+export function createComponent(dropResult) {
+  return {
+    type: CREATE_COMPONENT,
+    payload: {
+      dropResult,
+    },
+  };
+}
+
+
+// Drag and drop --------------------------------------------------------------
+export const MOVE_COMPONENT = 'MOVE_COMPONENT';
+export function moveComponent(dropResult) {
+  return {
+    type: MOVE_COMPONENT,
+    payload: {
+      dropResult,
+    },
+  };
+}
+
+export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
+export function handleComponentDrop(dropResult) {
+  return (dispatch) => {
+    if (
+      dropResult.destination
+      && dropResult.source
+      && !( // ensure it has moved
+        dropResult.destination.droppableId === dropResult.source.droppableId
+        && dropResult.destination.index === dropResult.source.index
+      )
+    ) {
+      return dispatch(moveComponent(dropResult));
+
+      // new components don't have a source
+    } else if (dropResult.destination && !dropResult.source) {
+      return dispatch(createComponent(dropResult));
+    }
+    return null;
+  };
+}
+
+// Resize ---------------------------------------------------------------------
+
+// export function dashboardComponentResizeStart() {}
+// export function dashboardComponentResize() {}
+// export function dashboardComponentResizeStop() {}
diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
new file mode 100644
index 0000000..86f3788
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import NewChart from './gridComponents/new/NewChart';
+import NewColumn from './gridComponents/new/NewColumn';
+import NewDivider from './gridComponents/new/NewDivider';
+import NewHeader from './gridComponents/new/NewHeader';
+import NewRow from './gridComponents/new/NewRow';
+import NewSpacer from './gridComponents/new/NewSpacer';
+import NewTabs from './gridComponents/new/NewTabs';
+
+const propTypes = {
+  editMode: PropTypes.bool,
+};
+
+class BuilderComponentPane extends React.PureComponent {
+  render() {
+    return (
+      <div className="dashboard-builder-sidepane">
+        <div className="dashboard-builder-sidepane-header">
+          Insert components
+        </div>
+        <NewChart />
+        <NewHeader />
+
+        <NewDivider />
+        <NewSpacer />
+
+        <NewTabs />
+        <NewRow />
+        <NewColumn />
+      </div>
+    );
+  }
+}
+
+BuilderComponentPane.propTypes = propTypes;
+
+export default BuilderComponentPane;
diff --git a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
new file mode 100644
index 0000000..5936006
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DashboardBuilder from './DashboardBuilder';
+import StaticDashboard from './StaticDashboard';
+import DashboardHeader from './DashboardHeader';
+
+import '../../../../stylesheets/dashboard-v2.css';
+
+const propTypes = {
+  actions: PropTypes.shape({
+    updateDashboardTitle: PropTypes.func.isRequired,
+    setEditMode: PropTypes.func.isRequired,
+  }),
+  editMode: PropTypes.bool,
+};
+
+const defaultProps = {
+  editMode: true,
+};
+
+class Dashboard extends React.Component {
+  render() {
+    const { editMode, actions } = this.props;
+    const { setEditMode, updateDashboardTitle } = actions;
+    return (
+      <div className="dashboard-v2">
+        <DashboardHeader
+          editMode={true}
+          setEditMode={setEditMode}
+          updateDashboardTitle={updateDashboardTitle}
+        />
+
+        {true ?
+          <DashboardBuilder /> : <StaticDashboard />}
+      </div>
+    );
+  }
+}
+
+Dashboard.propTypes = propTypes;
+Dashboard.defaultProps = defaultProps;
+
+export default Dashboard;
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
new file mode 100644
index 0000000..94069b7
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import HTML5Backend from 'react-dnd-html5-backend';
+import { DragDropContext } from 'react-dnd';
+import cx from 'classnames';
+
+import BuilderComponentPane from './BuilderComponentPane';
+import DashboardGrid from '../containers/DashboardGrid';
+
+import './dnd/dnd.css';
+
+const propTypes = {
+  editMode: PropTypes.bool,
+};
+
+const defaultProps = {
+  editMode: true,
+};
+
+class DashboardBuilder extends React.Component {
+  constructor(props) {
+    super(props);
+    // this component might control the state of the side pane etc. in the future
+    this.state = {};
+  }
+
+  render() {
+    return (
+      <div className={cx('dashboard-builder')}>
+        <DashboardGrid />
+        <BuilderComponentPane />
+      </div>
+    );
+  }
+}
+
+DashboardBuilder.propTypes = propTypes;
+DashboardBuilder.defaultProps = defaultProps;
+
+export default DragDropContext(HTML5Backend)(DashboardBuilder);
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
new file mode 100644
index 0000000..6cbcee5
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -0,0 +1,171 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ParentSize from '@vx/responsive/build/components/ParentSize';
+import cx from 'classnames';
+
+import DragDroppable from './dnd/DragDroppable';
+import DashboardComponent from '../containers/DashboardComponent';
+
+import {
+  DASHBOARD_ROOT_ID,
+  GRID_GUTTER_SIZE,
+  GRID_COLUMN_COUNT,
+} from '../util/constants';
+
+import './gridComponents/grid.css';
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+};
+
+class DashboardGrid extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isResizing: false,
+      rowGuideTop: null,
+    };
+
+    this.handleResizeStart = this.handleResizeStart.bind(this);
+    this.handleResize = this.handleResize.bind(this);
+    this.handleResizeStop = this.handleResizeStop.bind(this);
+    this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
+  }
+
+  getRowGuidePosition(resizeRef) {
+    if (resizeRef && this.grid) {
+      return resizeRef.getBoundingClientRect().bottom - this.grid.getBoundingClientRect().top - 1;
+    }
+    return null;
+  }
+
+  handleResizeStart({ ref, direction }) {
+    let rowGuideTop = null;
+    if (direction === 'bottom' || direction === 'bottomRight') {
+      rowGuideTop = this.getRowGuidePosition(ref);
+    }
+
+    this.setState(() => ({
+      isResizing: true,
+      rowGuideTop,
+    }));
+  }
+
+  handleResize({ ref, direction }) {
+    if (direction === 'bottom' || direction === 'bottomRight') {
+      this.setState(() => ({ rowGuideTop: this.getRowGuidePosition(ref) }));
+    }
+  }
+
+  handleResizeStop({ id, widthMultiple, heightMultiple }) {
+    const { dashboard: components, updateComponents } = this.props;
+    const component = components[id];
+    if (
+      component &&
+      (component.meta.width !== widthMultiple || component.meta.height !== heightMultiple)
+    ) {
+      updateComponents({
+        [id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            width: widthMultiple || component.meta.width,
+            height: heightMultiple || component.meta.height,
+          },
+        },
+      });
+    }
+    this.setState(() => ({
+      isResizing: false,
+      rowGuideTop: null,
+    }));
+  }
+
+  render() {
+    const { dashboard: components, handleComponentDrop } = this.props;
+    const { isResizing, rowGuideTop } = this.state;
+    const rootComponent = components[DASHBOARD_ROOT_ID];
+
+    return (
+      <div
+        ref={(ref) => { this.grid = ref; }}
+        className={cx(
+          'grid-container',
+          isResizing && 'grid-container--resizing',
+        )}
+      >
+        <ParentSize>
+          {({ width }) => {
+            // account for (COLUMN_COUNT - 1) gutters
+            const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
+            const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
+
+            return width < 50 ? null : (
+              <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
+                {(rootComponent.children || []).map((id, index) => (
+                  <DashboardComponent
+                    key={id}
+                    id={id}
+                    parentId={rootComponent.id}
+                    depth={0}
+                    index={index}
+                    availableColumnCount={GRID_COLUMN_COUNT}
+                    columnWidth={columnWidth}
+                    onResizeStart={this.handleResizeStart}
+                    onResize={this.handleResize}
+                    onResizeStop={this.handleResizeStop}
+                  />
+                ))}
+
+                {rootComponent.children.length === 0 &&
+                  <DragDroppable
+                    component={rootComponent}
+                    parentComponent={null}
+                    index={0}
+                    orientation="column"
+                    onDrop={handleComponentDrop}
+                  >
+                    {({ dropIndicatorProps }) => (
+                      <div style={{ width: '100%', height: '100%' }}>
+                        {dropIndicatorProps && <div {...dropIndicatorProps} />}
+                      </div>
+                    )}
+                  </DragDroppable>}
+
+                {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
+                  <div
+                    key={`grid-column-${i}`}
+                    className="grid-column-guide"
+                    style={{
+                      left: (i * GRID_GUTTER_SIZE) + (i * columnWidth),
+                      width: columnWidth,
+                    }}
+                  />
+                ))}
+
+                {isResizing && rowGuideTop &&
+                  <div
+                    className="grid-row-guide"
+                    style={{
+                      top: rowGuideTop,
+                      width,
+                    }}
+                  />}
+              </div>
+            );
+          }}
+        </ParentSize>
+      </div>
+    );
+  }
+}
+
+DashboardGrid.propTypes = propTypes;
+DashboardGrid.defaultProps = defaultProps;
+
+export default DashboardGrid;
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
new file mode 100644
index 0000000..8ffe677
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
+
+import Button from '../../../components/Button';
+import EditableTitle from '../../../components/EditableTitle';
+
+const propTypes = {
+  updateDashboardTitle: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
+  setEditMode: PropTypes.func.isRequired,
+};
+
+class Header extends React.Component {
+  constructor(props) {
+    super(props);
+    this.handleSaveTitle = this.handleSaveTitle.bind(this);
+    this.toggleEditMode = this.toggleEditMode.bind(this);
+  }
+
+  handleSaveTitle(title) {
+    this.props.updateDashboardTitle(title);
+  }
+
+  toggleEditMode() {
+    this.props.setEditMode(!this.props.editMode);
+  }
+
+  render() {
+    const { editMode } = this.props;
+    return (
+      <div className="dashboard-header">
+        <h1>
+          <EditableTitle
+            title={'Example header'}
+            canEdit={false}
+            onSaveTitle={() => {}}
+            showTooltip={false}
+          />
+        </h1>
+        <ButtonToolbar>
+          <DropdownButton title="Actions" bsSize="small" id="btn-dashboard-actions">
+            <MenuItem>Action 1</MenuItem>
+            <MenuItem>Action 2</MenuItem>
+            <MenuItem>Action 3</MenuItem>
+          </DropdownButton>
+
+          <Button
+            bsStyle="primary"
+            onClick={this.toggleEditMode}
+          >
+            {editMode ? 'Save changes' : 'Edit dashboard'}
+          </Button>
+        </ButtonToolbar>
+      </div>
+    );
+  }
+}
+
+Header.propTypes = propTypes;
+
+export default Header;
diff --git a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx
new file mode 100644
index 0000000..18efff4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import IconButton from './IconButton';
+
+const propTypes = {
+  onDelete: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+};
+
+export default class DeleteComponentButton extends React.PureComponent {
+  render() {
+    const { onDelete } = this.props;
+    return (
+      <IconButton onClick={onDelete} className="fa fa-trash" />
+    );
+  }
+}
+
+DeleteComponentButton.propTypes = propTypes;
+DeleteComponentButton.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
new file mode 100644
index 0000000..98044c9
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+const propTypes = {
+  onClick: PropTypes.func.isRequired,
+  className: PropTypes.string,
+};
+
+const defaultProps = {
+  className: null,
+};
+
+export default class IconButton extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleClick = this.handleClick.bind(this);
+  }
+
+  handleClick(event) {
+    event.preventDefault();
+    const { onClick } = this.props;
+    onClick(event);
+  }
+
+  render() {
+    const { className } = this.props;
+    return (
+      <div
+        className={cx('icon-button', className)}
+        onClick={this.handleClick}
+        tabIndex="0"
+        role="button"
+      />
+    );
+  }
+}
+
+IconButton.propTypes = propTypes;
+IconButton.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx
new file mode 100644
index 0000000..4fd2397
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+const propTypes = {
+};
+
+class StaticDashboard extends React.Component {
+  render() {
+    return (
+      <div>
+        Static dashboard ...
+      </div>
+    );
+  }
+}
+
+StaticDashboard.propTypes = propTypes;
+
+export default StaticDashboard;
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
new file mode 100644
index 0000000..320872b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DragSource, DropTarget } from 'react-dnd';
+import cx from 'classnames';
+
+import { dragConfig, dropConfig } from './dragDroppableConfig';
+import { componentShape } from '../../util/propShapes';
+
+
+const propTypes = {
+  children: PropTypes.func,
+  component: componentShape.isRequired,
+  parentComponent: componentShape,
+  disableDragDrop: PropTypes.bool,
+  orientation: PropTypes.oneOf(['row', 'column']),
+  index: PropTypes.number.isRequired,
+
+  // from react-dnd
+  isDragging: PropTypes.bool.isRequired,
+  isDraggingOver: PropTypes.bool.isRequired,
+  isDraggingOverShallow: PropTypes.bool.isRequired,
+  droppableRef: PropTypes.func.isRequired,
+  dragSourceRef: PropTypes.func.isRequired,
+  dragPreviewRef: PropTypes.func.isRequired,
+
+  // from redux
+  onDrop: PropTypes.func,
+};
+
+const defaultProps = {
+  parentComponent: null,
+  disableDragDrop: false,
+  children() {},
+  onDrop() {},
+  orientation: 'row',
+};
+
+class DragDroppable extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dropIndicator: null, // this gets set/modified by the react-dnd HOCs
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  render() {
+    const {
+      children,
+      orientation,
+      droppableRef,
+      dragSourceRef,
+      dragPreviewRef,
+      isDragging,
+      isDraggingOver,
+    } = this.props;
+
+    const { dropIndicator } = this.state;
+
+    return (
+      <div
+        ref={(ref) => {
+          this.ref = ref;
+          dragPreviewRef(ref);
+          droppableRef(ref);
+        }}
+        className={cx(
+          'dragdroppable',
+          orientation === 'row' && 'dragdroppable-row',
+          orientation === 'column' && 'dragdroppable-column',
+          isDragging && 'dragdroppable--dragging',
+        )}
+      >
+        {children({
+          dragSourceRef,
+          dropIndicatorProps: isDraggingOver && dropIndicator && {
+            className: 'drop-indicator',
+            style: dropIndicator,
+          },
+        })}
+      </div>
+    );
+  }
+}
+
+DragDroppable.propTypes = propTypes;
+DragDroppable.defaultProps = defaultProps;
+
+// note that the composition order here determines using
+// component.method() vs decoratedComponentInstance.method() in the drag/drop config
+export default DropTarget(...dropConfig)(
+  DragSource(...dragConfig)(DragDroppable),
+);
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx
new file mode 100644
index 0000000..36d1e6b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+const propTypes = {
+  position: PropTypes.oneOf(['left', 'top']),
+  innerRef: PropTypes.func,
+  dotCount: PropTypes.number,
+};
+
+const defaultProps = {
+  position: 'left',
+  innerRef: null,
+  dotCount: 8,
+};
+
+export default class DragHandle extends React.PureComponent {
+  render() {
+    const { innerRef, position, dotCount } = this.props;
+    return (
+      <div
+        ref={innerRef}
+        className={cx(
+          'drag-handle',
+          position === 'left' && 'drag-handle--left',
+          position === 'top' && 'drag-handle--top',
+        )}
+      >
+        {Array(dotCount).fill(null).map((_, i) => (
+          <div key={`handle-dot-${i}`} className="drag-handle-dot" />
+        ))}
+      </div>
+    );
+  }
+}
+
+DragHandle.propTypes = propTypes;
+DragHandle.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css
new file mode 100644
index 0000000..fb010e0
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css
@@ -0,0 +1,60 @@
+.dragdroppable {
+  position: relative;
+}
+
+.dragdroppable--dragging {
+  opacity: 0.25;
+}
+
+.dragdroppable-row {
+  width: 100%;
+}
+
+.grid-container .dragdroppable-row:after,
+.grid-container .dragdroppable-column:after {
+  border: 1px dashed transparent;
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 1px;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
+}
+
+  .grid-container .dragdroppable-row:hover:after,
+  .grid-container .dragdroppable-column:hover:after {
+    border: 1px dashed #aaa;
+  }
+
+/* Drag handle */
+.drag-handle {
+  overflow: hidden;
+  width: 16px;
+  cursor: move;
+}
+
+.drag-handle--left {
+  width: 8px;
+}
+
+.drag-handle--top {
+  /*margin: 10px auto;*/
+}
+
+.drag-handle-dot {
+  float: left;
+  height: 2px;
+  margin: 1px;
+  width: 2px
+}
+
+.drag-handle-dot:after {
+  content: "";
+  background: #aaa;
+  float: left;
+  height: 2px;
+  margin: -1px;
+  width: 2px;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
new file mode 100644
index 0000000..e6d5533
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
@@ -0,0 +1,67 @@
+import handleHover from './handleHover';
+import handleDrop from './handleDrop';
+
+// note: the 'type' hook is not useful for us as dropping is contigent on other properties
+const TYPE = 'DRAG_DROPPABLE';
+
+export const dragConfig = [
+  TYPE,
+  {
+    canDrag(props) {
+      return !props.disableDragDrop;
+    },
+    beginDrag(props /* , monitor, component */) {
+      const { component, index, parentComponent } = props;
+      return {
+        draggableId: component.id,
+        index,
+        parentId: parentComponent && parentComponent.id,
+        type: component.type,
+      };
+    },
+  },
+  function dragStateToProps(connect, monitor) {
+    return {
+      dragSourceRef: connect.dragSource(),
+      dragPreviewRef: connect.dragPreview(),
+      isDragging: monitor.isDragging(),
+    };
+  },
+];
+
+export const dropConfig = [
+  TYPE,
+  {
+    hover(props, monitor, component) {
+      if (
+        component
+        && component.decoratedComponentInstance
+        && component.decoratedComponentInstance.mounted
+      ) {
+        handleHover(
+          props,
+          monitor,
+          component.decoratedComponentInstance,
+        );
+      }
+    },
+    // note:
+    //  the react-dnd api requires that the drop() method return a result or undefined
+    //  monitor.didDrop() cannot be used because it returns true only for the most-nested target
+    drop(props, monitor, component) {
+      const Component = component.decoratedComponentInstance;
+      const dropResult = monitor.getDropResult();
+      if ((!dropResult || !dropResult.destination) && Component.mounted) {
+        return handleDrop(props, monitor, Component);
+      }
+      return undefined;
+    },
+  },
+  function dropStateToProps(connect, monitor) {
+    return {
+      droppableRef: connect.dropTarget(),
+      isDraggingOver: monitor.isOver(),
+      isDraggingOverShallow: monitor.isOver({ shallow: true }),
+    };
+  },
+];
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
new file mode 100644
index 0000000..cf790da
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
@@ -0,0 +1,63 @@
+import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+
+export default function handleDrop(props, monitor, Component) {
+  // this may happen due to throttling
+  if (!Component.mounted) return undefined;
+
+  Component.setState(() => ({ dropIndicator: null }));
+  const dropPosition = getDropPosition(monitor, Component);
+
+  if (!dropPosition) {
+    return undefined;
+  }
+
+  const {
+    parentComponent,
+    component,
+    index: componentIndex,
+    onDrop,
+    orientation,
+  } = Component.props;
+
+  const draggingItem = monitor.getItem();
+
+  const dropAsChildOrSibling =
+    (orientation === 'row' && (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
+    (orientation === 'column' && (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
+    ? 'sibling' : 'child';
+
+  const dropResult = {
+    source: draggingItem.parentId ? {
+      droppableId: draggingItem.parentId,
+      index: draggingItem.index,
+    } : null,
+    draggableId: draggingItem.draggableId,
+  };
+
+  // simplest case, append as child
+  if (dropAsChildOrSibling === 'child') {
+    dropResult.destination = {
+      droppableId: component.id,
+      index: component.children.length,
+    };
+  } else {
+    // if the item is in the same list with a smaller index, you must account for the
+    // "missing" index upon movement within the list
+    const sameParent = parentComponent && draggingItem.parentId === parentComponent.id;
+    const sameParentLowerIndex = sameParent && draggingItem.index < componentIndex;
+
+    let nextIndex = sameParentLowerIndex ? componentIndex - 1 : componentIndex;
+    if (dropPosition === DROP_BOTTOM || dropPosition === DROP_RIGHT) {
+      nextIndex += 1;
+    }
+
+    dropResult.destination = {
+      droppableId: parentComponent.id,
+      index: nextIndex,
+    };
+  }
+
+  onDrop(dropResult);
+
+  return dropResult;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
new file mode 100644
index 0000000..1eadef4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
@@ -0,0 +1,37 @@
+import throttle from 'lodash.throttle';
+import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+
+const HOVER_THROTTLE_MS = 200;
+
+function handleHover(props, monitor, Component) {
+  // this may happen due to throttling
+  if (!Component.mounted) return;
+
+  const dropPosition = getDropPosition(monitor, Component);
+
+  if (!dropPosition) {
+    Component.setState(() => ({ dropIndicator: null }));
+    return;
+  }
+
+  // @TODO
+  // drop-indicator
+  // drop-indicator--top/right/bottom/left
+  Component.setState(() => ({
+    dropIndicator: {
+      top: dropPosition === DROP_BOTTOM ? '100%' : 0,
+      left: dropPosition === DROP_RIGHT ? '100%' : 0,
+      height: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? '100%' : 3,
+      width: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? '100%' : 3,
+      minHeight: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? 16 : null,
+      minWidth: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? 16 : null,
+      margin: 'auto',
+      backgroundColor: '#44C0FF',
+      position: 'absolute',
+      zIndex: 10,
+    },
+  }));
+}
+
+// this is called very frequently by react-dnd
+export default throttle(handleHover, HOVER_THROTTLE_MS);
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
new file mode 100644
index 0000000..9daa8cf
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DeleteComponentButton from '../DeleteComponentButton';
+import DragDroppable from '../dnd/DragDroppable';
+import DragHandle from '../dnd/DragHandle';
+import HoverMenu from '../menu/HoverMenu';
+import ResizableContainer from '../resizable/ResizableContainer';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+import { componentShape } from '../../util/propShapes';
+
+import {
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+} from '../../util/constants';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  deleteComponent: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+};
+
+class Chart extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+    };
+
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+  }
+
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: nextFocus }));
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  render() {
+    const { isFocused } = this.state;
+
+    const {
+      component,
+      parentComponent,
+      index,
+      depth,
+      availableColumnCount,
+      columnWidth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+    } = this.props;
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        index={index}
+        onDrop={handleComponentDrop}
+        disableDragDrop={isFocused}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <ResizableContainer
+            id={component.id}
+            adjustableWidth={depth <= 1}
+            adjustableHeight
+            widthStep={columnWidth}
+            widthMultiple={component.meta.width}
+            heightMultiple={component.meta.height}
+            minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+            minHeightMultiple={GRID_MIN_ROW_UNITS}
+            maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
+            onResizeStart={onResizeStart}
+            onResize={onResize}
+            onResizeStop={onResizeStop}
+          >
+            <HoverMenu innerRef={dragSourceRef} position="top">
+              <DragHandle position="top" />
+            </HoverMenu>
+
+            <WithPopoverMenu
+              onChangeFocus={this.handleChangeFocus}
+              menuItems={[
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
+              ]}
+            >
+              <div className="dashboard-component dashboard-component-chart">
+                <div className="fa fa-area-chart" />
+              </div>
+
+              {dropIndicatorProps && <div {...dropIndicatorProps} />}
+            </WithPopoverMenu>
+          </ResizableContainer>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Chart.propTypes = propTypes;
+Chart.defaultProps = defaultProps;
+
+export default Chart;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
new file mode 100644
index 0000000..8409bc1
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
@@ -0,0 +1,144 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import DashboardComponent from '../../containers/DashboardComponent';
+import DeleteComponentButton from '../DeleteComponentButton';
+import DragDroppable from '../dnd/DragDroppable';
+import DragHandle from '../dnd/DragHandle';
+import HoverMenu from '../menu/HoverMenu';
+import ResizableContainer from '../resizable/ResizableContainer';
+import { componentShape } from '../../util/propShapes';
+
+import { GRID_GUTTER_SIZE, GRID_MIN_COLUMN_COUNT } from '../../util/constants';
+
+const GUTTER = 'GUTTER';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+  // occupiedRowCount: PropTypes.number,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  deleteComponent: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  // occupiedRowCount: null,
+};
+
+class Column extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  render() {
+    const {
+      component: columnComponent,
+      parentComponent,
+      index,
+      availableColumnCount,
+      columnWidth,
+      // occupiedRowCount,
+      depth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+    } = this.props;
+
+    const columnItems = [];
+
+    (columnComponent.children || []).forEach((id, childIndex) => {
+      columnItems.push(id);
+      if (childIndex < columnComponent.children.length - 1) {
+        columnItems.push(GUTTER);
+      }
+    });
+
+    return (
+      <DragDroppable
+        component={columnComponent}
+        parentComponent={parentComponent}
+        orientation="column"
+        index={index}
+        onDrop={handleComponentDrop}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <ResizableContainer
+            id={columnComponent.id}
+            adjustableWidth
+            adjustableHeight={false}
+            widthStep={columnWidth}
+            widthMultiple={columnComponent.meta.width}
+            // heightMultiple={occupiedRowCount}
+            minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+            maxWidthMultiple={availableColumnCount + (columnComponent.meta.width || 0)}
+            onResizeStart={onResizeStart}
+            onResize={onResize}
+            onResizeStop={onResizeStop}
+          >
+            <div
+              className={cx(
+                'grid-column',
+                columnItems.length === 0 && 'grid-column--empty',
+              )}
+            >
+              <HoverMenu innerRef={dragSourceRef} position="top">
+                <DragHandle position="top" />
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+              </HoverMenu>
+
+              {columnItems.map((componentId, itemIndex) => {
+                if (componentId === GUTTER) {
+                  return <div key={`gutter-${itemIndex}`} style={{ height: GRID_GUTTER_SIZE }} />;
+                }
+
+                return (
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={columnComponent.id}
+                    depth={depth + 1}
+                    index={itemIndex / 2} // account for gutters!
+                    availableColumnCount={availableColumnCount}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                  />
+                );
+              })}
+
+              {dropIndicatorProps && <div {...dropIndicatorProps} />}
+            </div>
+          </ResizableContainer>
+        )}
+      </DragDroppable>
+
+    );
+  }
+}
+
+Column.propTypes = propTypes;
+Column.defaultProps = defaultProps;
+
+export default Column;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
new file mode 100644
index 0000000..29437e1
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DragDroppable from '../dnd/DragDroppable';
+import HoverMenu from '../menu/HoverMenu';
+import DeleteComponentButton from '../DeleteComponentButton';
+import { componentShape } from '../../util/propShapes';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+  deleteComponent: PropTypes.func.isRequired,
+};
+
+class Divider extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  render() {
+    const {
+      component,
+      parentComponent,
+      index,
+      handleComponentDrop,
+    } = this.props;
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation="row"
+        index={index}
+        onDrop={handleComponentDrop}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <div ref={dragSourceRef}>
+            <HoverMenu position="left">
+              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+            </HoverMenu>
+
+            <div className="dashboard-component dashboard-component-divider" />
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
+          </div>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Divider.propTypes = propTypes;
+
+export default Divider;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
new file mode 100644
index 0000000..967b483
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import DragDroppable from '../dnd/DragDroppable';
+import DragHandle from '../dnd/DragHandle';
+import EditableTitle from '../../../../components/EditableTitle';
+import HoverMenu from '../menu/HoverMenu';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+import RowStyleDropdown from '../menu/RowStyleDropdown';
+import DeleteComponentButton from '../DeleteComponentButton';
+import PopoverDropdown from '../menu/PopoverDropdown';
+import headerStyleOptions from '../../util/headerStyleOptions';
+import rowStyleOptions from '../../util/rowStyleOptions';
+import { componentShape } from '../../util/propShapes';
+import { SMALL_HEADER, ROW_TRANSPARENT } from '../../util/constants';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+
+  // redux
+  handleComponentDrop: PropTypes.func.isRequired,
+  deleteComponent: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+};
+
+class Header extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+    };
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
+    this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
+    this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize');
+    this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle');
+    this.handleChangeText = this.handleUpdateMeta.bind(this, 'text');
+  }
+
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: nextFocus }));
+  }
+
+  handleUpdateMeta(metaKey, nextValue) {
+    const { updateComponents, component } = this.props;
+    if (nextValue && component.meta[metaKey] !== nextValue) {
+      updateComponents({
+        [component.id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            [metaKey]: nextValue,
+          },
+        },
+      });
+    }
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  render() {
+    const { isFocused } = this.state;
+
+    const {
+      component,
+      parentComponent,
+      index,
+      handleComponentDrop,
+    } = this.props;
+
+    const headerStyle = headerStyleOptions.find(
+      opt => opt.value === (component.meta.headerSize || SMALL_HEADER),
+    );
+
+    const rowStyle = rowStyleOptions.find(
+      opt => opt.value === (component.meta.rowStyle || ROW_TRANSPARENT),
+    );
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation="row"
+        index={index}
+        onDrop={handleComponentDrop}
+        disableDragDrop={isFocused}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <div ref={dragSourceRef}>
+            <HoverMenu position="left">
+              <DragHandle position="left" />
+            </HoverMenu>
+
+            <WithPopoverMenu
+              onChangeFocus={this.handleChangeFocus}
+              menuItems={[
+                <PopoverDropdown
+                  id={`${component.id}-header-style`}
+                  options={headerStyleOptions}
+                  value={component.meta.headerSize}
+                  onChange={this.handleChangeSize}
+                  renderTitle={option => `${option.label} header`}
+                />,
+                <RowStyleDropdown
+                  id={`${component.id}-row-style`}
+                  value={component.meta.rowStyle}
+                  onChange={this.handleChangeRowStyle}
+                />,
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
+              ]}
+            >
+              <div
+                className={cx(
+                  'dashboard-component',
+                  'dashboard-component-header',
+                  headerStyle.className,
+                  rowStyle.className,
+                )}
+              >
+                <EditableTitle
+                  title={component.meta.text}
+                  canEdit={isFocused}
+                  onSaveTitle={this.handleChangeText}
+                  showTooltip={false}
+                />
+              </div>
+            </WithPopoverMenu>
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
+          </div>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Header.propTypes = propTypes;
+Header.defaultProps = defaultProps;
+
+export default Header;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
new file mode 100644
index 0000000..632a3f3
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -0,0 +1,186 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import DragDroppable from '../dnd/DragDroppable';
+import DragHandle from '../dnd/DragHandle';
+import DashboardComponent from '../../containers/DashboardComponent';
+import DeleteComponentButton from '../DeleteComponentButton';
+import HoverMenu from '../menu/HoverMenu';
+import IconButton from '../IconButton';
+import RowStyleDropdown from '../menu/RowStyleDropdown';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+
+import { componentShape } from '../../util/propShapes';
+import rowStyleOptions from '../../util/rowStyleOptions';
+import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants';
+
+const GUTTER = 'GUTTER';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  occupiedColumnCount: PropTypes.number.isRequired,
+  occupiedRowCount: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  handleComponentDrop: PropTypes.func.isRequired,
+  deleteComponent: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  rowHeight: null,
+};
+
+class Row extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+    };
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
+    this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle');
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
+  }
+
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+  }
+
+  handleUpdateMeta(metaKey, nextValue) {
+    const { updateComponents, component } = this.props;
+    if (nextValue && component.meta[metaKey] !== nextValue) {
+      updateComponents({
+        [component.id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            [metaKey]: nextValue,
+          },
+        },
+      });
+    }
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, component, parentId } = this.props;
+    deleteComponent(component.id, parentId);
+  }
+
+  render() {
+    const {
+      component: rowComponent,
+      parentComponent,
+      index,
+      availableColumnCount,
+      columnWidth,
+      occupiedColumnCount,
+      occupiedRowCount,
+      depth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+    } = this.props;
+
+    const rowItems = [];
+    console.log('render row', rowComponent);
+
+    // this adds a gutter between each child in the row.
+    (rowComponent.children || []).forEach((id, childIndex) => {
+      rowItems.push(id);
+      if (childIndex < rowComponent.children.length - 1) {
+        rowItems.push(GUTTER);
+      }
+    });
+
+    const rowStyle = rowStyleOptions.find(
+      opt => opt.value === (rowComponent.meta.rowStyle || ROW_TRANSPARENT),
+    );
+
+    return (
+      <DragDroppable
+        component={rowComponent}
+        parentComponent={parentComponent}
+        orientation="row"
+        index={index}
+        onDrop={handleComponentDrop}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <WithPopoverMenu
+            isFocused={this.state.isFocused}
+            onChangeFocus={this.handleChangeFocus}
+            disableClick
+            menuItems={[
+              <RowStyleDropdown
+                id={`${rowComponent.id}-row-style`}
+                value={rowComponent.meta.rowStyle}
+                onChange={this.handleChangeRowStyle}
+              />,
+            ]}
+          >
+
+            <div
+              className={cx(
+                'grid-row',
+                rowItems.length === 0 && 'grid-row--empty',
+                rowStyle.className,
+              )}
+            >
+              <HoverMenu innerRef={dragSourceRef} position="left">
+                <DragHandle position="left" />
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                <IconButton
+                  onClick={this.handleChangeFocus}
+                  className="fa fa-cog"
+                />
+              </HoverMenu>
+
+              {rowItems.map((componentId, itemIndex) => {
+                if (componentId === GUTTER) {
+                  return <div key={`gutter-${itemIndex}`} style={{ width: GRID_GUTTER_SIZE }} />;
+                }
+
+                return (
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={rowComponent.id}
+                    depth={depth + 1}
+                    index={itemIndex / 2} // account for gutters!
+                    availableColumnCount={availableColumnCount - occupiedColumnCount}
+                    occupiedRowCount={occupiedRowCount}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                  />
+                );
+              })}
+
+              {dropIndicatorProps && <div {...dropIndicatorProps} />}
+            </div>
+          </WithPopoverMenu>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Row.propTypes = propTypes;
+Row.defaultProps = defaultProps;
+
+export default Row;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
new file mode 100644
index 0000000..4b54edd
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DeleteComponentButton from '../DeleteComponentButton';
+import DragDroppable from '../dnd/DragDroppable';
+// import DragHandle from '../dnd/DragHandle';
+import HoverMenu from '../menu/HoverMenu';
+import ResizableContainer from '../resizable/ResizableContainer';
+import { componentShape } from '../../util/propShapes';
+
+import {
+//   GRID_MIN_COLUMN_COUNT,
+  // GRID_MIN_ROW_UNITS,
+} from '../../util/constants';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  occupiedRowCount: PropTypes.number,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  deleteComponent: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  occupiedRowCount: null,
+};
+
+class Spacer extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  render() {
+    const {
+      component,
+      parentComponent,
+      index,
+      depth,
+      availableColumnCount,
+      columnWidth,
+      occupiedRowCount,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+    } = this.props;
+
+    const orientation = depth % 2 === 0 ? 'row' : 'column';
+    const hoverMenuPosition = orientation === 'row' ? 'left' : 'top';
+    const adjustableWidth = orientation === 'column';
+    const adjustableHeight = orientation === 'row';
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation={orientation}
+        index={index}
+        onDrop={handleComponentDrop}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <ResizableContainer
+            id={component.id}
+            adjustableWidth={adjustableWidth}
+            adjustableHeight={adjustableHeight}
+            widthStep={columnWidth}
+            widthMultiple={component.meta.width}
+            heightMultiple={adjustableHeight ? component.meta.height || 1 : undefined}
+            staticHeightMultiple={!adjustableHeight ? occupiedRowCount || 5 : undefined}
+            minWidthMultiple={1}
+            minHeightMultiple={1}
+            maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
+            onResizeStart={onResizeStart}
+            onResize={onResize}
+            onResizeStop={onResizeStop}
+          >
+            <HoverMenu position={hoverMenuPosition}>
+              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+            </HoverMenu>
+
+            <div ref={dragSourceRef} className="grid-spacer" />
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
+          </ResizableContainer>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Spacer.propTypes = propTypes;
+Spacer.defaultProps = defaultProps;
+
+export default Spacer;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
new file mode 100644
index 0000000..74cd9ae
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DashboardComponent from '../../containers/DashboardComponent';
+import DragDroppable from '../dnd/DragDroppable';
+import EditableTitle from '../../../../components/EditableTitle';
+import DeleteComponentButton from '../DeleteComponentButton';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+import { componentShape } from '../../util/propShapes';
+
+export const RENDER_TAB = 'RENDER_TAB';
+export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+  renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
+  onDropOnTab: PropTypes.func,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // redux
+  handleComponentDrop: PropTypes.func.isRequired,
+  deleteComponent: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  onDropOnTab: null,
+};
+
+export default class Tab extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+    };
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
+    this.handleChangeText = this.handleChangeText.bind(this);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.handleDrop = this.handleDrop.bind(this);
+  }
+
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: nextFocus }));
+  }
+
+  handleChangeText(nextTabText) {
+    const { updateComponents, component } = this.props;
+    if (nextTabText && nextTabText !== component.meta.text) {
+      updateComponents({
+        [component.id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            text: nextTabText,
+          },
+        },
+      });
+    }
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  handleDrop(dropResult) {
+    const { handleComponentDrop, onDropOnTab } = this.props;
+    handleComponentDrop(dropResult);
+    if (onDropOnTab) onDropOnTab(dropResult);
+  }
+
+  renderTabContent() {
+    const {
+      component: tabComponent,
+      depth,
+      availableColumnCount,
+      columnWidth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+    } = this.props;
+
+    return (
+      <div className="dashboard-component-tabs-content">
+        {tabComponent.children.map((componentId, componentIndex) => (
+          <DashboardComponent
+            key={componentId}
+            id={componentId}
+            parentId={tabComponent.id}
+            depth={depth}
+            index={componentIndex}
+            onDrop={this.handleDrop}
+            availableColumnCount={availableColumnCount}
+            columnWidth={columnWidth}
+            onResizeStart={onResizeStart}
+            onResize={onResize}
+            onResizeStop={onResizeStop}
+          />
+        ))}
+      </div>
+    );
+  }
+
+  renderTab() {
+    const { isFocused } = this.state;
+    const {
+      component,
+      parentComponent,
+      index,
+    } = this.props;
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation="column"
+        index={index}
+        onDrop={this.handleDrop}
+        disableDragDrop={isFocused}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <div className="dragdroppable-tab" ref={dragSourceRef}>
+            <WithPopoverMenu
+              onChangeFocus={this.handleChangeFocus}
+              menuItems={parentComponent.children.length <= 1 ? [] : [
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
+              ]}
+            >
+              <EditableTitle
+                title={component.meta.text}
+                canEdit={isFocused}
+                onSaveTitle={this.handleChangeText}
+                showTooltip={false}
+              />
+            </WithPopoverMenu>
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
+          </div>
+        )}
+      </DragDroppable>
+    );
+  }
+
+  render() {
+    const { renderType } = this.props;
+    return renderType === RENDER_TAB ? this.renderTab() : this.renderTabContent();
+  }
+}
+
+Tab.propTypes = propTypes;
+Tab.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
new file mode 100644
index 0000000..1e2e64c
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
@@ -0,0 +1,211 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
+
+import DragDroppable from '../dnd/DragDroppable';
+import DragHandle from '../dnd/DragHandle';
+import DashboardComponent from '../../containers/DashboardComponent';
+import DeleteComponentButton from '../DeleteComponentButton';
+import HoverMenu from '../menu/HoverMenu';
+import { componentShape } from '../../util/propShapes';
+import { NEW_TAB_ID } from '../../util/constants';
+import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
+
+const NEW_TAB_INDEX = -1;
+const MAX_TAB_COUNT = 5;
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  createComponent: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+  onChangeTab: PropTypes.func,
+  deleteComponent: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  onChangeTab: null,
+  children: null,
+};
+
+class Tabs extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      tabIndex: 0,
+    };
+    this.handleClicKTab = this.handleClicKTab.bind(this);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.handleDropOnTab = this.handleDropOnTab.bind(this);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const maxIndex = Math.max(0, nextProps.component.children.length - 1);
+    if (this.state.tabIndex > maxIndex) {
+      this.setState(() => ({ tabIndex: maxIndex }));
+    }
+  }
+
+  handleClicKTab(tabIndex) {
+    const { onChangeTab, component, createComponent } = this.props;
+
+    if (tabIndex !== NEW_TAB_INDEX && tabIndex !== this.state.tabIndex) {
+      this.setState(() => ({ tabIndex }));
+      if (onChangeTab) {
+        onChangeTab({ tabIndex, tabId: component.children[tabIndex] });
+      }
+    } else if (tabIndex === NEW_TAB_INDEX) {
+      createComponent({
+        destination: {
+          droppableId: component.id,
+          index: component.children.length,
+        },
+        draggableId: NEW_TAB_ID,
+      });
+    }
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  handleDropOnTab(dropResult) {
+    const { component } = this.props;
+
+    // Ensure dropped tab is visible
+    const { destination } = dropResult;
+    if (destination) {
+      const dropTabIndex = destination.droppableId === component.id
+        ? destination.index // dropped ON tabs
+        : component.children.indexOf(destination.droppableId); // dropped IN tab
+
+      if (dropTabIndex > -1) {
+        setTimeout(() => {
+          this.handleClicKTab(dropTabIndex);
+        }, 30);
+      }
+    }
+  }
+
+  render() {
+    const {
+      depth,
+      component: tabsComponent,
+      parentComponent,
+      index,
+      availableColumnCount,
+      columnWidth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+    } = this.props;
+
+    const { tabIndex: selectedTabIndex } = this.state;
+    const { children: tabIds } = tabsComponent;
+
+    return (
+      <DragDroppable
+        component={tabsComponent}
+        parentComponent={parentComponent}
+        orientation="row"
+        index={index}
+        onDrop={handleComponentDrop}
+      >
+        {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
+          <div className="dashboard-component dashboard-component-tabs">
+            <HoverMenu innerRef={tabsDragSourceRef} position="left">
+              <DragHandle position="left" />
+              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+            </HoverMenu>
+
+            <BootstrapTabs
+              id={tabsComponent.id}
+              activeKey={selectedTabIndex}
+              onSelect={this.handleClicKTab}
+              animation={false}
+            >
+              {tabIds.map((tabId, tabIndex) => (
+                // react-bootstrap doesn't render a Tab if we move this to its own Tab.jsx so we
+                // use `renderType` to indicate what the DashboardComponent should render. This
+                // prevents us from passing the entire dashboard component lookup to render Tabs.jsx
+                <BootstrapTab
+                  key={tabId}
+                  eventKey={tabIndex}
+                  title={
+                    <DashboardComponent
+                      id={tabId}
+                      parentId={tabsComponent.id}
+                      depth={depth}
+                      index={tabIndex}
+                      renderType={RENDER_TAB}
+                      availableColumnCount={availableColumnCount}
+                      columnWidth={columnWidth}
+                      onResizeStart={onResizeStart}
+                      onResize={onResize}
+                      onResizeStop={onResizeStop}
+                      onDropOnTab={this.handleDropOnTab}
+                    />
+                  }
+                >
+                  {/*
+                    react-bootstrap renders all children with display:none, so we don't
+                    render potentially-expensive charts (this also enables lazy loading
+                    their content)
+                  */}
+                  {tabIndex === selectedTabIndex &&
+                    <DashboardComponent
+                      id={tabId}
+                      parentId={tabsComponent.id}
+                      depth={depth}
+                      index={tabIndex}
+                      renderType={RENDER_TAB_CONTENT}
+                      availableColumnCount={availableColumnCount}
+                      columnWidth={columnWidth}
+                      onResizeStart={onResizeStart}
+                      onResize={onResize}
+                      onResizeStop={onResizeStop}
+                      onDropOnTab={this.handleDropOnTab}
+                    />}
+                </BootstrapTab>
+              ))}
+
+              {tabIds.length < MAX_TAB_COUNT &&
+                <BootstrapTab
+                  eventKey={NEW_TAB_INDEX}
+                  title={<div className="fa fa-plus-square" />}
+                />}
+
+            </BootstrapTabs>
+
+            {tabsDropIndicatorProps
+              && tabsDropIndicatorProps.style
+              && tabsDropIndicatorProps.style.width === '100%'
+              && <div {...tabsDropIndicatorProps} />}
+
+          </div>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Tabs.propTypes = propTypes;
+Tabs.defaultProps = defaultProps;
+
+export default Tabs;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css
new file mode 100644
index 0000000..a88ea09
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css
@@ -0,0 +1,455 @@
+/* Header */
+.dashboard-component-header {
+  width: 100%;
+  line-height: 1em;
+  font-weight: 700;
+  background-color: inherit;
+  padding: 16px 0;
+  color: #263238;
+}
+
+.header-small {
+  font-size: 16px;
+}
+
+.header-medium {
+  font-size: 22px;
+}
+
+.header-large {
+  font-size: 32px;
+}
+
+  .dragdroppable-row .dragdroppable-row .dashboard-component-header,
+  .dragdroppable-row .dragdroppable-row .dashboard-component-divider {
+    padding-left: 16px;
+    padding-right: 16px;
+  }
+
+/* Chart */
+.dashboard-component-chart {
+  width: 100%;
+  height: 100%;
+  color: #879399;
+  background-color: #fff;
+  padding: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.dashboard-component-chart .fa {
+  font-size: 100px;
+  opacity: 0.3;
+}
+
+.grid-container--resizing .dashboard-component-chart,
+.dashboard-builder--dragging .dashboard-component-chart,
+.dashboard-component-chart:hover {
+  box-shadow: inset 0 0 0 1px #CFD8DC;
+}
+
+/* Divider */
+.dashboard-component-divider {
+  width: 100%;
+  padding: 24px 0; /* this is padding not margin to enable a larger mouse target */
+  background-color: transparent;
+}
+
+.dashboard-component-divider:after {
+  content: "";
+  height: 1px;
+  width: 100%;
+  background-color: #CFD8DC;
+  display: block;
+}
+
+.new-component-placeholder.divider-placeholder:after {
+  content: "";
+  height: 2px;
+  width: 100%;
+  background-color: #CFD8DC;
+}
+
+.dragdroppable .dashboard-component-divider {
+  cursor: move;
+}
+
+/* Tabs -- this overwrites Superset bootstrap theme tab styling */
+.dashboard-component-tabs {
+  width: 100%;
+  background-color: white;
+}
+.dashboard-component-tabs .dashboard-component-tabs-content {
+  min-height: 48px;
+  margin-top: 1px;
+}
+
+.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 {
+  padding: 0 16px;
+}
+
+.dashboard-component-tabs .nav-tabs > li > a {
+  color: #263238;
+  border: none;
+  padding: 12px 0 14px 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: #000000;
+}
+
+
+.dashboard-component-tabs .nav-tabs > li > a:focus {
+  outline: none;
+  background: #fff;
+}
+
+.dashboard-component-tabs .nav-tabs > li .dragdroppable-tab {
+  cursor: move;
+}
+
+.dashboard-component-tabs .nav-tabs > li .drop-indicator {
+  height: 40px !important;
+  top: -10px !important;
+  opacity: 0.5;
+}
+
+.dashboard-component-tabs .fa-plus-square {
+  background: linear-gradient(135deg, #E32464, #2C2261);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  display: initial;
+  font-size: 16px;
+}
+
+/* New components */
+.new-component {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  align-items: center;
+  padding: 16px;
+  background: white;
+}
+
+.new-component-placeholder {
+  position: relative;
+  background: #f5f5f5;
+  width: 40px;
+  height: 40px;
+  margin-right: 16px;
+  box-shadow: 0 0 1px #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #aaa;
+  font-size: 1.5em;
+}
+
+/* Spacer */
+.grid-container {
+   flex-grow: 1;
+   min-width: 66%;
+   margin: 24px 32px;
+   height: 100%;
+   position: relative;
+}
+
+.new-component-placeholder.spacer-placeholder {
+  font-size: 1em;
+}
+
+.new-component-placeholder.fa-window-restore {
+  font-size: 1em;
+}
+
+.new-component-placeholder.spacer-placeholder:after {
+  content: "";
+  position: absolute;
+  height: 60%;
+  width: 60%;
+  border: 1px dashed #aaa;
+}
+
+/* columns and rows */
+.grid-column {
+  width: 100%;
+  min-height: 56px;
+}
+
+.grid-column > .hover-menu--top {
+  top: -20px;
+}
+
+.grid-row {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  width: 100%;
+  height: fit-content;
+  background-color: transparent;
+}
+
+.grid-row--transparent {
+  background-color: transparent;
+}
+
+.grid-row--white {
+  background-color: #fff;
+}
+
+.dashboard-component-header.grid-row--white {
+  padding-left: 16px;
+}
+
+.grid-row.grid-row--empty {
+  align-items: center; /* this centers the empty note content */
+  height: 80px;
+}
+
+.grid-row--empty:after {
+  position: absolute;
+  top: 0;
+  left: 0;
+  content: "Empty row";
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  color: #aaa;
+}
+
+.grid-column--empty:after {
+  content: "Empty column";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #CFD8DC;
+}
+
+/* spacer */
+.grid-spacer {
+  width: 100%;
+  height: 100%;
+  background-color: transparent;
+}
+
+.dragdroppable .grid-spacer {
+  cursor: move;
+}
+
+.dragdroppable:hover .grid-spacer {
+  box-shadow: inset 0 0 0 1px #CFD8DC;
+}
+
+/* popover menu */
+.with-popover-menu {
+  position: relative;
+  outline: none;
+}
+
+.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */
+  width: 100%;
+  height: 100%;
+}
+
+.with-popover-menu--focused:after {
+  content: "";
+  position: absolute;
+  top: 1;
+  left: -1;
+  width: 100%;
+  height: 100%;
+  box-shadow: inset 0 0 0 2px #44C0FF;
+  pointer-events: none;
+  z-index: 9;
+}
+
+.popover-menu {
+  position: absolute;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+  left: 1px;
+  top: -42px;
+  height: 40px;
+  padding: 0 16px;
+  background: #fff;
+  box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2);
+  font-size: 14px;
+  cursor: default;
+  z-index: 10;
+}
+
+.popover-menu .menu-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+/* vertical spacer after each menu item */
+.popover-menu .menu-item:not(:only-child):not(:last-child):after {
+  content: "";
+  width: 1;
+  height: 100%;
+  background: #CFD8DC;
+  margin: 0 16px;
+}
+
+.popover-menu .popover-dropdown.btn {
+  border: none;
+  padding: 0;
+  font-size: inherit;
+  color: #000;
+}
+
+.popover-menu .popover-dropdown.btn:hover,
+.popover-menu .popover-dropdown.btn:active,
+.popover-menu .popover-dropdown.btn:focus,
+.hover-dropdown .btn:hover,
+.hover-dropdown .btn:active,
+.hover-dropdown .btn:focus {
+  background: initial;
+  box-shadow: none;
+}
+
+.hover-dropdown li.dropdown-item:hover a,
+.popover-menu li.dropdown-item:hover a {
+  background: #CFD8DC;
+}
+
+.popover-dropdown .caret { /* without this the caret doesn't take up full width / is clipped */
+  width: auto;
+  border-top-color: transparent;
+}
+
+
+.hover-dropdown li.dropdown-item.active a,
+.popover-menu li.dropdown-item.active a {
+  background: #fff;
+  font-weight: bold;
+  color: #000;
+}
+
+/* row style menu */
+.row-style-option {
+  display: inline-block;
+}
+
+.row-style-option:before {
+  content: "";
+  width: 1em;
+  height: 1em;
+  margin-right: 8px;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.row-style-option.grid-row--white {
+  padding-left: 0;
+  background: transparent;
+}
+
+.row-style-option.grid-row--white:before {
+  background: #fff;
+  border: 1px solid #CFD8DC;
+}
+
+.row-style-option.grid-row--transparent:before {
+  background: #CFD8DC;
+}
+
+/* hover menu */
+.hover-menu {
+  opacity: 0;
+  position: absolute;
+  z-index: 2;
+}
+
+.hover-menu--left {
+  width: 20px;
+  height: 100%;
+  top: 0;
+  left: -20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) {
+  margin-bottom: 8px;
+}
+
+.dragdroppable-row .dragdroppable-row .hover-menu--left {
+  left: 1px;
+}
+
+.hover-menu--top {
+  width: 100%;
+  height: 20px;
+  top: 0;
+  left: 0;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
+  margin-right: 8px;
+}
+
+.dragdroppable:hover .hover-menu,
+.dragdroppable .hover-menu:hover {
+  opacity: 1;
+}
+
+
+/* Menu fa buttons */
+.icon-button {
+  color: #879399;
+  font-size: 1em;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  outline: none;
+}
+
+.icon-button:hover,
+.icon-button:active,
+.icon-button:focus {
+  color: #484848;
+  outline: none;
+  text-decoration: none;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css
new file mode 100644
index 0000000..6119eab
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css
@@ -0,0 +1,17 @@
+/* Editing guides */
+.grid-column-guide {
+  position: absolute;
+  top: 0;
+  height: 100%;
+  background-color: rgba(68, 192, 255, 0.05);
+  pointer-events: none;
+  box-shadow: inset 0 0 0 1px rgba(68, 192, 255, 0.5);
+}
+
+.grid-row-guide {
+  position: absolute;
+  left: 0;
+  height: 1;
+  background-color: #44C0FF;
+  pointer-events: none;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
new file mode 100644
index 0000000..c84864e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
@@ -0,0 +1,43 @@
+import './components.css';
+
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  INVISIBLE_ROW_TYPE,
+  ROW_TYPE,
+  SPACER_TYPE,
+  TAB_TYPE,
+  TABS_TYPE,
+} from '../../util/componentTypes';
+
+import Chart from './Chart';
+import Column from './Column';
+import Divider from './Divider';
+import Header from './Header';
+import Row from './Row';
+import Spacer from './Spacer';
+import Tab from './Tab';
+import Tabs from './Tabs';
+
+export { default as Chart } from './Chart';
+export { default as Column } from './Column';
+export { default as Divider } from './Divider';
+export { default as Header } from './Header';
+export { default as Row } from './Row';
+export { default as Spacer } from './Spacer';
+export { default as Tab } from './Tab';
+export { default as Tabs } from './Tabs';
+
+export default {
+  [CHART_TYPE]: Chart,
+  [COLUMN_TYPE]: Column,
+  [DIVIDER_TYPE]: Divider,
+  [HEADER_TYPE]: Header,
+  [INVISIBLE_ROW_TYPE]: Row,
+  [ROW_TYPE]: Row,
+  [SPACER_TYPE]: Spacer,
+  [TAB_TYPE]: Tab,
+  [TABS_TYPE]: Tabs,
+};
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
new file mode 100644
index 0000000..c4d8d62
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import DragDroppable from '../../dnd/DragDroppable';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  type: PropTypes.string.isRequired,
+  label: PropTypes.string.isRequired,
+  className: PropTypes.string,
+};
+
+const defaultProps = {
+  className: null,
+};
+
+export default class DraggableNewComponent extends React.PureComponent {
+  render() {
+    const { label, id, type, className } = this.props;
+    return (
+      <DragDroppable
+        component={{ type, id }}
+        parentComponent={null}
+        index={0}
+      >
+        {({ dragSourceRef }) => (
+          <div ref={dragSourceRef} className="new-component">
+            <div className={cx('new-component-placeholder', className)} />
+            {label}
+          </div>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+DraggableNewComponent.propTypes = propTypes;
+DraggableNewComponent.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx
new file mode 100644
index 0000000..0255755
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { CHART_TYPE } from '../../../util/componentTypes';
+import { NEW_CHART_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewChart extends React.PureComponent {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_CHART_ID}
+        type={CHART_TYPE}
+        label="Chart"
+        className="fa fa-area-chart"
+      />
+    );
+  }
+}
+
+DraggableNewChart.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx
new file mode 100644
index 0000000..654c60b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { COLUMN_TYPE } from '../../../util/componentTypes';
+import { NEW_COLUMN_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewColumn extends React.PureComponent {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_COLUMN_ID}
+        type={COLUMN_TYPE}
+        label="Column"
+        className="fa fa-long-arrow-down"
+      />
+    );
+  }
+}
+
+DraggableNewColumn.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx
new file mode 100644
index 0000000..5d70041
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { DIVIDER_TYPE } from '../../../util/componentTypes';
+import { NEW_DIVIDER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewDivider extends React.PureComponent {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_DIVIDER_ID}
+        type={DIVIDER_TYPE}
+        label="Divider"
+        className="divider-placeholder"
+      />
+    );
+  }
+}
+
+DraggableNewDivider.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx
new file mode 100644
index 0000000..d207a9c
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { HEADER_TYPE } from '../../../util/componentTypes';
+import { NEW_HEADER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewHeader extends React.Component {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_HEADER_ID}
+        type={HEADER_TYPE}
+        label="Header"
+        className="fa fa-header"
+      />
+    );
+  }
+}
+
+DraggableNewHeader.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx
new file mode 100644
index 0000000..1d9ab10
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+
+import { ROW_TYPE } from '../../../util/componentTypes';
+import { NEW_ROW_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewRow extends React.PureComponent {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_ROW_ID}
+        type={ROW_TYPE}
+        label="Row"
+        className="fa fa-long-arrow-right"
+      />
+    );
+  }
+}
+
+DraggableNewRow.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx
new file mode 100644
index 0000000..7287770
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { SPACER_TYPE } from '../../../util/componentTypes';
+import { NEW_SPACER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewChart extends React.PureComponent {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_SPACER_ID}
+        type={SPACER_TYPE}
+        label="Spacer"
+        className="spacer-placeholder fa fa-arrows"
+      />
+    );
+  }
+}
+
+DraggableNewChart.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx
new file mode 100644
index 0000000..a473281
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { TABS_TYPE } from '../../../util/componentTypes';
+import { NEW_TABS_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+const propTypes = {
+};
+
+export default class DraggableNewTabs extends React.PureComponent {
+  render() {
+    return (
+      <DraggableNewComponent
+        id={NEW_TABS_ID}
+        type={TABS_TYPE}
+        label="Tabs"
+        className="fa fa-window-restore"
+      />
+    );
+  }
+}
+
+DraggableNewTabs.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx
new file mode 100644
index 0000000..c238d02
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+const propTypes = {
+  position: PropTypes.oneOf(['left', 'top']),
+  innerRef: PropTypes.func,
+  children: PropTypes.node,
+};
+
+const defaultProps = {
+  position: 'left',
+  innerRef: null,
+  children: null,
+};
+
+export default class HoverMenu extends React.PureComponent {
+  render() {
+    const { innerRef, position, children } = this.props;
+    return (
+      <div
+        ref={innerRef}
+        className={cx(
+          'hover-menu',
+          position === 'left' && 'hover-menu--left',
+          position === 'top' && 'hover-menu--top',
+        )}
+      >
+        {children}
+      </div>
+    );
+  }
+}
+
+HoverMenu.propTypes = propTypes;
+HoverMenu.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx
new file mode 100644
index 0000000..6a56eab
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  options: PropTypes.arrayOf(
+    PropTypes.shape({
+      value: PropTypes.string.isRequired,
+      label: PropTypes.string.isRequired,
+      className: PropTypes.string,
+    }),
+  ).isRequired,
+  onChange: PropTypes.func.isRequired,
+  value: PropTypes.string.isRequired,
+  renderButton: PropTypes.func,
+  renderOption: PropTypes.func,
+};
+
+const defaultProps = {
+  renderButton: option => option.label,
+  renderOption: option => <div className={option.className}>{option.label}</div>,
+};
+
+class PopoverDropdown extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleSelect = this.handleSelect.bind(this);
+  }
+
+  handleSelect(nextValue) {
+    this.props.onChange(nextValue);
+  }
+
+  render() {
+    const { id, value, options, renderButton, renderOption } = this.props;
+    const selected = options.find(opt => opt.value === value);
+    return (
+      <DropdownButton
+        id={id}
+        bsSize="small"
+        title={renderButton(selected)}
+        className="popover-dropdown"
+      >
+        {options.map(option => (
+          <MenuItem
+            key={option.value}
+            eventKey={option.value}
+            active={option.value === value}
+            onSelect={this.handleSelect}
+            className="dropdown-item"
+          >
+            {renderOption(option)}
+          </MenuItem>
+        ))}
+      </DropdownButton>
+    );
+  }
+}
+
+PopoverDropdown.propTypes = propTypes;
+PopoverDropdown.defaultProps = defaultProps;
+
+export default PopoverDropdown;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx
new file mode 100644
index 0000000..d3c7eff
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import rowStyleOptions from '../../util/rowStyleOptions';
+import PopoverDropdown from './PopoverDropdown';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  value: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+function renderButton(option) {
+  return (
+    <div className={cx('row-style-option', option.className)}>
+      {`${option.label} background`}
+    </div>
+  );
+}
+
+function renderOption(option) {
+  return (
+    <div className={cx('row-style-option', option.className)}>
+      {option.label}
+    </div>
+  );
+}
+
+export default class RowStyleDropdown extends React.PureComponent {
+  render() {
+    const { id, value, onChange } = this.props;
+    return (
+      <PopoverDropdown
+        id={id}
+        options={rowStyleOptions}
+        value={value}
+        onChange={onChange}
+        renderButton={renderButton}
+        renderOption={renderOption}
+      />
+    );
+  }
+}
+
+RowStyleDropdown.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
new file mode 100644
index 0000000..7fb24cd
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+const propTypes = {
+  children: PropTypes.node,
+  disableClick: PropTypes.bool,
+  menuItems: PropTypes.arrayOf(PropTypes.node),
+  onChangeFocus: PropTypes.func,
+  isFocused: PropTypes.bool,
+};
+
+const defaultProps = {
+  children: null,
+  disableClick: false,
+  onChangeFocus: null,
+  onPressDelete() {},
+  menuItems: [],
+  isFocused: false,
+};
+
+class WithPopoverMenu extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: props.isFocused,
+    };
+    this.setRef = this.setRef.bind(this);
+    this.handleClick = this.handleClick.bind(this);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.isFocused && !this.state.isFocused) {
+      document.addEventListener('click', this.handleClick, true);
+      document.addEventListener('drag', this.handleClick, true);
+      this.setState({ isFocused: true });
+    }
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('click', this.handleClick, true);
+    document.removeEventListener('drag', this.handleClick, true);
+  }
+
+  setRef(ref) {
+    this.container = ref;
+  }
+
+  handleClick(event) {
+    const { onChangeFocus } = this.props;
+    if (!this.state.isFocused) {
+      // if not focused, set focus and add a window event listener to capture outside clicks
+      // this enables us to not set a click listener for ever item on a dashboard
+      document.addEventListener('click', this.handleClick, true);
+      document.addEventListener('drag', this.handleClick, true);
+      this.setState(() => ({ isFocused: true }));
+      if (onChangeFocus) {
+        onChangeFocus(true);
+      }
+    } else if (!this.container.contains(event.target)) {
+      document.removeEventListener('click', this.handleClick, true);
+      document.removeEventListener('drag', this.handleClick, true);
+      this.setState(() => ({ isFocused: false }));
+      if (onChangeFocus) {
+        onChangeFocus(false);
+      }
+    }
+  }
+
+  render() {
+    const { children, menuItems, disableClick } = this.props;
+    const { isFocused } = this.state;
+
+    return (
+      <div
+        ref={this.setRef}
+        onClick={!disableClick && this.handleClick}
+        role="button" // @TODO consider others?
+        tabIndex="0"
+        className={cx(
+          'with-popover-menu',
+          isFocused && 'with-popover-menu--focused',
+        )}
+      >
+        {children}
+        {isFocused && menuItems.length ?
+          <div className="popover-menu" >
+            {menuItems.map((node, i) => (
+              <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
+            ))}
+          </div> : null}
+      </div>
+    );
+  }
+}
+
+WithPopoverMenu.propTypes = propTypes;
+WithPopoverMenu.defaultProps = defaultProps;
+
+export default WithPopoverMenu;
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
new file mode 100644
index 0000000..bd590ae
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -0,0 +1,184 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Resizable from 're-resizable';
+import cx from 'classnames';
+
+import ResizableHandle from './ResizableHandle';
+import resizableConfig from '../../util/resizableConfig';
+import {
+  GRID_BASE_UNIT,
+  GRID_ROW_HEIGHT_UNIT,
+  GRID_GUTTER_SIZE,
+} from '../../util/constants';
+
+import './resizable.css';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  children: PropTypes.node,
+  adjustableWidth: PropTypes.bool,
+  adjustableHeight: PropTypes.bool,
+  gutterWidth: PropTypes.number,
+  widthStep: PropTypes.number,
+  heightStep: PropTypes.number,
+  widthMultiple: PropTypes.number,
+  heightMultiple: PropTypes.number,
+  minWidthMultiple: PropTypes.number,
+  maxWidthMultiple: PropTypes.number,
+  minHeightMultiple: PropTypes.number,
+  maxHeightMultiple: PropTypes.number,
+  staticHeightMultiple: PropTypes.number,
+  onResizeStop: PropTypes.func,
+  onResize: PropTypes.func,
+  onResizeStart: PropTypes.func,
+};
+
+const defaultProps = {
+  children: null,
+  adjustableWidth: true,
+  adjustableHeight: true,
+  gutterWidth: GRID_GUTTER_SIZE,
+  widthStep: GRID_BASE_UNIT,
+  heightStep: GRID_ROW_HEIGHT_UNIT,
+  widthMultiple: null,
+  heightMultiple: null,
+  minWidthMultiple: 1,
+  maxWidthMultiple: Infinity,
+  minHeightMultiple: 1,
+  maxHeightMultiple: Infinity,
+  staticHeightMultiple: null,
+  onResizeStop: null,
+  onResize: null,
+  onResizeStart: null,
+};
+
+// because columns are not actually multiples of a single variable (width = n*cols + (n-1)*gutters)
+// we snap to the base unit and then snap to actual column multiples on stop
+const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT];
+
+class ResizableContainer extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isResizing: false,
+    };
+
+    this.handleResizeStart = this.handleResizeStart.bind(this);
+    this.handleResize = this.handleResize.bind(this);
+    this.handleResizeStop = this.handleResizeStop.bind(this);
+  }
+
+  handleResizeStart(event, direction, ref) {
+    const { id, onResizeStart } = this.props;
+
+    if (onResizeStart) {
+      onResizeStart({ id, direction, ref });
+    }
+
+    this.setState(() => ({ isResizing: true }));
+  }
+
+  handleResize(event, direction, ref) {
+    const { onResize, id } = this.props;
+    if (onResize) {
+      onResize({ id, direction, ref });
+    }
+  }
+
+  handleResizeStop(event, direction, ref, delta) {
+    const {
+      id,
+      onResizeStop,
+      widthStep,
+      heightStep,
+      widthMultiple,
+      heightMultiple,
+      adjustableHeight,
+      adjustableWidth,
+      gutterWidth,
+    } = this.props;
+
+    if (onResizeStop) {
+      const nextWidthMultiple =
+        Math.round(widthMultiple + (delta.width / (widthStep + gutterWidth)));
+      const nextHeightMultiple =
+        Math.round(heightMultiple + (delta.height / heightStep));
+
+      onResizeStop({
+        id,
+        widthMultiple: adjustableWidth ? nextWidthMultiple : null,
+        heightMultiple: adjustableHeight ? nextHeightMultiple : null,
+      });
+
+      this.setState(() => ({ isResizing: false }));
+    }
+  }
+
+  render() {
+    const {
+      children,
+      adjustableWidth,
+      adjustableHeight,
+      widthStep,
+      heightStep,
+      staticHeightMultiple,
+      widthMultiple,
+      heightMultiple,
+      minWidthMultiple,
+      maxWidthMultiple,
+      minHeightMultiple,
+      maxHeightMultiple,
+      gutterWidth,
+    } = this.props;
+
+    const size = {
+      width: adjustableWidth
+        ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth : undefined,
+      height: adjustableHeight
+        ? heightStep * heightMultiple
+        : (staticHeightMultiple && staticHeightMultiple * heightStep) || undefined,
+    };
+
+    let enableConfig = resizableConfig.widthAndHeight;
+    if (!adjustableHeight) enableConfig = resizableConfig.widthOnly;
+    else if (!adjustableWidth) enableConfig = resizableConfig.heightOnly;
+
+    const { isResizing } = this.state;
+
+    return (
+      <Resizable
+        enable={enableConfig}
+        grid={snapToGrid}
+        minWidth={adjustableWidth
+          ? (minWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
+          : size.width}
+        minHeight={adjustableHeight
+          ? (minHeightMultiple * heightStep)
+          : size.height}
+        maxWidth={adjustableWidth
+          ? (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
+          : size.width}
+        maxHeight={adjustableHeight
+          ? (maxHeightMultiple * heightStep)
+          : size.height}
+        size={size}
+        onResizeStart={this.handleResizeStart}
+        onResize={this.handleResize}
+        onResizeStop={this.handleResizeStop}
+        handleComponent={ResizableHandle}
+        className={cx(
+          'grid-resizable-container',
+          isResizing && 'grid-resizable-container--resizing',
+        )}
+      >
+        {children}
+      </Resizable>
+    );
+  }
+}
+
+ResizableContainer.propTypes = propTypes;
+ResizableContainer.defaultProps = defaultProps;
+
+export default ResizableContainer;
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx
new file mode 100644
index 0000000..9536f6b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+export function BottomRightResizeHandle() {
+  return (
+    <div className="resize-handle resize-handle--bottom-right" />
+  );
+}
+
+export function RightResizeHandle() {
+  return (
+    <div className="resize-handle resize-handle--right" />
+  );
+}
+
+export function BottomResizeHandle() {
+  return (
+    <div className="resize-handle resize-handle--bottom" />
+  );
+}
+
+export default {
+  right: RightResizeHandle,
+  bottom: BottomResizeHandle,
+  bottomRight: BottomRightResizeHandle,
+};
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css
new file mode 100644
index 0000000..1d5de72
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css
@@ -0,0 +1,72 @@
+.grid-resizable-container {
+  background-color: transparent;
+  position: relative;
+}
+
+/* after ensures border visibility on top of any children */
+.grid-resizable-container--resizing:after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  box-shadow: inset 0 0 0 2px #44C0FF;
+}
+
+.resize-handle {
+  opacity: 0;
+}
+
+  .grid-resizable-container:hover .resize-handle,
+  .grid-resizable-container--resizing .resize-handle {
+    opacity: 1;
+  }
+
+.resize-handle--bottom-right {
+  position: absolute;
+  border: solid;
+  border-width: 0 1.5px 1.5px 0;
+  border-right-color: #879399;
+  border-bottom-color: #879399;
+  right: 16;
+  bottom: 16;
+  width: 8px;
+  height: 8px;
+}
+
+.resize-handle--right {
+  width: 2px;
+  height: 20px;
+  right: -2px;
+  top: 47%;
+  position: absolute;
+  border-left: 1px solid #879399;
+  border-right: 1px solid #879399;
+}
+
+  .grid-spacer + span .resize-handle--right {
+    right: 3px;
+  }
+
+.resize-handle--bottom {
+  height: 2px;
+  width: 20px;
+  bottom: 10px;
+  left: 47%;
+  position: absolute;
+  border-top: 1px solid #879399;
+  border-bottom: 1px solid #879399;
+}
+
+.grid-resizable-container--resizing > span .resize-handle {
+  border-color: #44C0FF;
+}
+
+/* re-resizable sets an empty div to 100% width and height, which doesn't
+  play well with many 100% height containers we need
+ */
+.grid-resizable-container ~ div {
+  width: auto !important;
+  height: auto !important;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
new file mode 100644
index 0000000..1340781
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import ComponentLookup from '../components/gridComponents';
+import countChildRowsAndColumns from '../util/countChildRowsAndColumns';
+import { componentShape } from '../util/propShapes';
+import { ROW_TYPE } from '../util/componentTypes';
+
+import {
+  createComponent,
+  deleteComponent,
+  updateComponents,
+  handleComponentDrop,
+} from '../actions';
+
+const propTypes = {
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  createComponent: PropTypes.func.isRequired,
+  deleteComponent: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+};
+
+function mapStateToProps({ dashboard = {} }, ownProps) {
+  const { id, parentId } = ownProps;
+  const props = {
+    component: dashboard[id],
+    parentComponent: dashboard[parentId],
+  };
+
+  // row is a special component that needs extra dims about its children
+  // doing this allows us to not pass the entire component lookup to all Components
+  if (props.component.type === ROW_TYPE) {
+    const { rowCount, columnCount } = countChildRowsAndColumns({
+      component: props.component,
+      components: dashboard,
+    });
+
+    props.occupiedRowCount = rowCount;
+    props.occupiedColumnCount = columnCount;
+  }
+
+  return props;
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators({
+    createComponent,
+    deleteComponent,
+    updateComponents,
+    handleComponentDrop,
+  }, dispatch);
+}
+
+class DashboardComponent extends React.PureComponent {
+  render() {
+    const { component } = this.props;
+    const Component = ComponentLookup[component.type];
+    return Component ? <Component {...this.props} /> : null;
+  }
+}
+
+DashboardComponent.propTypes = propTypes;
+
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardComponent);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
new file mode 100644
index 0000000..741151b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
@@ -0,0 +1,23 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import DashboardGrid from '../components/DashboardGrid';
+
+import {
+  updateComponents,
+  handleComponentDrop,
+} from '../actions';
+
+function mapStateToProps({ dashboard = {} }) {
+  return {
+    dashboard,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators({
+    updateComponents,
+    handleComponentDrop,
+  }, dispatch);
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js
new file mode 100644
index 0000000..c3ce897
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js
@@ -0,0 +1,161 @@
+import {
+  COLUMN_TYPE,
+  HEADER_TYPE,
+  ROW_TYPE,
+  SPACER_TYPE,
+  TAB_TYPE,
+  TABS_TYPE,
+  CHART_TYPE,
+  DIVIDER_TYPE,
+  GRID_ROOT_TYPE,
+} from '../util/componentTypes';
+
+import { DASHBOARD_ROOT_ID } from '../util/constants';
+
+export default {
+  [DASHBOARD_ROOT_ID]: {
+    type: GRID_ROOT_TYPE,
+    id: DASHBOARD_ROOT_ID,
+    children: [
+      // 'header0',
+      // 'row0',
+      // 'divider0',
+      // 'row1',
+      // 'tabs0',
+      // 'divider1',
+    ],
+  },
+  // row0: {
+  //   id: 'row0',
+  //   type: INVISIBLE_ROW_TYPE,
+  //   children: [
+  //     // 'charta',
+  //     // 'chartb',
+  //     // 'chartc',
+  //   ],
+  // },
+  // row1: {
+  //   id: 'row1',
+  //   type: ROW_TYPE,
+  //   children: [
+  //     'header1',
+  //   ],
+  // },
+  // row2: {
+  //   id: 'row2',
+  //   type: ROW_TYPE,
+  //   children: [
+  //     'chartd',
+  //     'spacer0',
+  //     'charte',
+  //   ],
+  // },
+  // tabs0: {
+  //   id: 'tabs0',
+  //   type: TABS_TYPE,
+  //   children: [
+  //     'tab0',
+  //     'tab1',
+  //     'tab3',
+  //   ],
+  //   meta: {
+  //   },
+  // },
+  // tab0: {
+  //   id: 'tab0',
+  //   type: TAB_TYPE,
+  //   children: [
+  //     // 'row2',
+  //   ],
+  //   meta: {
+  //     text: 'Tab A',
+  //   },
+  // },
+  // tab1: {
+  //   id: 'tab1',
+  //   type: TAB_TYPE,
+  //   children: [
+  //   ],
+  //   meta: {
+  //     text: 'Tab B',
+  //   },
+  // },
+  // tab3: {
+  //   id: 'tab3',
+  //   type: TAB_TYPE,
+  //   children: [
+  //   ],
+  //   meta: {
+  //     text: 'Tab C',
+  //   },
+  // },
+  // header0: {
+  //   id: 'header0',
+  //   type: HEADER_TYPE,
+  //   meta: {
+  //     text: 'Header 1',
+  //   },
+  // },
+  // header1: {
+  //   id: 'header1',
+  //   type: HEADER_TYPE,
+  //   meta: {
+  //     text: 'Header 2',
+  //   },
+  // },
+  // divider0: {
+  //   id: 'divider0',
+  //   type: DIVIDER_TYPE,
+  // },
+  // divider1: {
+  //   id: 'divider1',
+  //   type: DIVIDER_TYPE,
+  // },
+  // charta: {
+  //   id: 'charta',
+  //   type: CHART_TYPE,
+  //   meta: {
+  //     width: 3,
+  //     height: 10,
+  //   },
+  // },
+  // chartb: {
+  //   id: 'chartb',
+  //   type: CHART_TYPE,
+  //   meta: {
+  //     width: 3,
+  //     height: 10,
+  //   },
+  // },
+  // chartc: {
+  //   id: 'chartc',
+  //   type: CHART_TYPE,
+  //   meta: {
+  //     width: 3,
+  //     height: 10,
+  //   },
+  // },
+  // chartd: {
+  //   id: 'chartd',
+  //   type: CHART_TYPE,
+  //   meta: {
+  //     width: 3,
+  //     height: 10,
+  //   },
+  // },
+  // charte: {
+  //   id: 'charte',
+  //   type: CHART_TYPE,
+  //   meta: {
+  //     width: 3,
+  //     height: 10,
+  //   },
+  // },
+  // spacer0: {
+  //   id: 'spacer0',
+  //   type: SPACER_TYPE,
+  //   meta: {
+  //     width: 1,
+  //   },
+  // },
+};
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
new file mode 100644
index 0000000..19fa9d7
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
@@ -0,0 +1,112 @@
+import newComponentFactory from '../util/newComponentFactory';
+import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
+import reorderItem from '../util/dnd-reorder';
+import shouldWrapChildInRow from '../util/shouldWrapChildInRow';
+import { ROW_TYPE } from '../util/componentTypes';
+
+import {
+  UPDATE_COMPONENTS,
+  DELETE_COMPONENT,
+  CREATE_COMPONENT,
+  MOVE_COMPONENT,
+} from '../actions';
+
+const actionHandlers = {
+  [UPDATE_COMPONENTS](state, action) {
+    const { payload: { nextComponents } } = action;
+    return {
+      ...state,
+      ...nextComponents,
+    };
+  },
+
+  [DELETE_COMPONENT](state, action) {
+    const { payload: { id, parentId } } = action;
+
+    if (!parentId || !id || !state[id] || !state[parentId]) return state;
+
+    const nextComponents = { ...state };
+
+    // recursively find children to remove
+    let deleteCount = 0;
+    function recursivelyDeleteChildren(componentId, componentParentId) {
+      // delete child and it's children
+      const component = nextComponents[componentId];
+      delete nextComponents[componentId];
+      deleteCount += 1;
+      const { children = [] } = component;
+      children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); });
+
+      const parent = nextComponents[componentParentId];
+      if (parent) { // may have been deleted in another recursion
+        const componentIndex = (parent.children || []).indexOf(componentId);
+        if (componentIndex > -1) {
+          const nextChildren = [...parent.children];
+          nextChildren.splice(componentIndex, 1);
+          nextComponents[componentParentId] = {
+            ...parent,
+            children: nextChildren,
+          };
+        }
+      }
+    }
+
+    recursivelyDeleteChildren(id, parentId);
+    console.log('deleted', deleteCount, 'total components', nextComponents);
+
+    return nextComponents;
+  },
+
+  [CREATE_COMPONENT](state, action) {
+    const { payload: { dropResult } } = action;
+    const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+    return {
+      ...state,
+      ...newEntities,
+    };
+  },
+
+  [MOVE_COMPONENT](state, action) {
+    const { payload: { dropResult } } = action;
+    const { source, destination, draggableId } = dropResult;
+
+    if (!source || !destination || !draggableId) return state;
+
+    const nextEntities = reorderItem({
+      entitiesMap: state,
+      source,
+      destination,
+    });
+
+    // wrap the dragged component in a row depening on destination type
+    const destinationType = (state[destination.droppableId] || {}).type;
+    const draggableType = (state[draggableId] || {}).type;
+    const wrapInRow = shouldWrapChildInRow({
+      parentType: destinationType,
+      childType: draggableType,
+    });
+
+    if (wrapInRow) {
+      const destinationEntity = nextEntities[destination.droppableId];
+      const destinationChildren = destinationEntity.children;
+      const newRow = newComponentFactory(ROW_TYPE);
+      newRow.children = [destinationChildren[destination.index]];
+      destinationChildren[destination.index] = newRow.id;
+      nextEntities[newRow.id] = newRow;
+    }
+
+    return {
+      ...state,
+      ...nextEntities,
+    };
+  },
+};
+
+export default function dashboardReducer(state = {}, action) {
+  if (action.type in actionHandlers) {
+    const handler = actionHandlers[action.type];
+    return handler(state, action);
+  }
+
+  return state;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
new file mode 100644
index 0000000..103fda0
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -0,0 +1,6 @@
+import { combineReducers } from 'redux';
+import dashboard from './dashboard';
+
+export default combineReducers({
+  dashboard,
+});
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js
new file mode 100644
index 0000000..ab701a7
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js
@@ -0,0 +1,15 @@
+import {
+  SPACER_TYPE,
+  COLUMN_TYPE,
+  CHART_TYPE,
+  MARKDOWN_TYPE,
+} from './componentTypes';
+
+export default function componentIsResizable(entity) {
+  return [
+    SPACER_TYPE,
+    COLUMN_TYPE,
+    CHART_TYPE,
+    MARKDOWN_TYPE,
+  ].indexOf(entity.type) > -1;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
new file mode 100644
index 0000000..fd5d294
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
@@ -0,0 +1,23 @@
+export const CHART_TYPE = 'DASHBOARD_CHART_TYPE';
+export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE';
+export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE';
+export const GRID_ROOT_TYPE = 'DASHBOARD_GRID_ROOT_TYPE';
+export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE';
+export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE';
+export const ROW_TYPE = 'DASHBOARD_ROW_TYPE';
+export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE';
+export const TABS_TYPE = 'DASHBOARD_TABS_TYPE';
+export const TAB_TYPE = 'DASHBOARD_TAB_TYPE';
+
+export default {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DIVIDER_TYPE,
+  GRID_ROOT_TYPE,
+  HEADER_TYPE,
+  MARKDOWN_TYPE,
+  ROW_TYPE,
+  SPACER_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+};
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js
new file mode 100644
index 0000000..44a0f0e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/constants.js
@@ -0,0 +1,30 @@
+// Ids
+export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID';
+export const NEW_CHART_ID = 'NEW_CHART_ID';
+export const NEW_COLUMN_ID = 'NEW_COLUMN_ID';
+export const NEW_DIVIDER_ID = 'NEW_DIVIDER_ID';
+export const NEW_HEADER_ID = 'NEW_HEADER_ID';
+export const NEW_MARKDOWN_ID = 'NEW_MARKDOWN_ID';
+export const NEW_ROW_ID = 'NEW_ROW_ID';
+export const NEW_SPACER_ID = 'NEW_SPACER_ID';
+export const NEW_TAB_ID = 'NEW_TAB_ID';
+export const NEW_TABS_ID = 'NEW_TABS_ID';
+
+// grid constants
+export const GRID_BASE_UNIT = 8;
+export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
+export const GRID_ROW_HEIGHT_UNIT = 2 * GRID_BASE_UNIT;
+export const GRID_COLUMN_COUNT = 12;
+export const GRID_MIN_COLUMN_COUNT = 3;
+export const GRID_MIN_ROW_UNITS = 5;
+export const GRID_MAX_ROW_UNITS = 100;
+export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
+
+// Header types
+export const SMALL_HEADER = 'SMALL_HEADER';
+export const MEDIUM_HEADER = 'MEDIUM_HEADER';
+export const LARGE_HEADER = 'LARGE_HEADER';
+
+// Row types
+export const ROW_WHITE = 'ROW_WHITE';
+export const ROW_TRANSPARENT = 'ROW_TRANSPARENT';
diff --git a/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js b/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js
new file mode 100644
index 0000000..dbc63cd
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js
@@ -0,0 +1,14 @@
+export default function countChildRowsAndColumns({ component, components }) {
+  let columnCount = 0;
+  let rowCount = 0;
+
+  (component.children || []).forEach((childId) => {
+    const childComponent = components[childId];
+    columnCount += (childComponent.meta || {}).width || 0;
+    if ((childComponent.meta || {}).height) {
+      rowCount = Math.max(rowCount, childComponent.meta.height);
+    }
+  });
+
+  return { columnCount, rowCount };
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
new file mode 100644
index 0000000..5ebca8c
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
@@ -0,0 +1,54 @@
+export function reorder(list, startIndex, endIndex) {
+  const result = [...list];
+  const [removed] = result.splice(startIndex, 1);
+  result.splice(endIndex, 0, removed);
+
+  return result;
+}
+
+export default function reorderItem({
+  entitiesMap,
+  source,
+  destination,
+}) {
+  const current = [...entitiesMap[source.droppableId].children];
+  const next = [...entitiesMap[destination.droppableId].children];
+  const target = current[source.index];
+
+  // moving to same list
+  if (source.droppableId === destination.droppableId) {
+    const reordered = reorder(
+      current,
+      source.index,
+      destination.index,
+    );
+
+    const result = {
+      ...entitiesMap,
+      [source.droppableId]: {
+        ...entitiesMap[source.droppableId],
+        children: reordered,
+      },
+    };
+
+    return result;
+  }
+
+  // moving to different list
+  current.splice(source.index, 1); // remove from original
+  next.splice(destination.index, 0, target); // insert into next
+
+  const result = {
+    ...entitiesMap,
+    [source.droppableId]: {
+      ...entitiesMap[source.droppableId],
+      children: current,
+    },
+    [destination.droppableId]: {
+      ...entitiesMap[destination.droppableId],
+      children: next,
+    },
+  };
+
+  return result;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
new file mode 100644
index 0000000..e1dfbd3
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
@@ -0,0 +1,88 @@
+import isValidChild from './isValidChild';
+
+export const DROP_TOP = 'DROP_TOP';
+export const DROP_RIGHT = 'DROP_RIGHT';
+export const DROP_BOTTOM = 'DROP_BOTTOM';
+export const DROP_LEFT = 'DROP_LEFT';
+
+const SIBLING_DROP_THRESHOLD = 10;
+
+export default function getDropPosition(monitor, Component) {
+  const {
+    parentComponent,
+    component,
+    orientation,
+    isDraggingOverShallow,
+  } = Component.props;
+
+  const draggingItem = monitor.getItem();
+
+  // if dropped self on self, do nothing
+  if (!draggingItem || draggingItem.draggableId === component.id || !isDraggingOverShallow) {
+    return null;
+  }
+
+  const validChild = isValidChild({
+    parentType: component.type,
+    childType: draggingItem.type,
+  });
+
+  const validSibling = isValidChild({
+    parentType: parentComponent && parentComponent.type,
+    childType: draggingItem.type,
+  });
+
+  if (!validChild && !validSibling) {
+    return null;
+  }
+
+  const hasChildren = component.children.length > 0;
+  const childDropOrientation = orientation === 'row' ? 'vertical' : 'horizontal';
+  const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical';
+
+  if (validChild && !validSibling) { // easiest case, insert as child
+    if (childDropOrientation === 'vertical') {
+      return hasChildren ? DROP_RIGHT : DROP_LEFT;
+    }
+    return hasChildren ? DROP_BOTTOM : DROP_TOP;
+  }
+
+  const refBoundingRect = Component.ref.getBoundingClientRect();
+  const clientOffset = monitor.getClientOffset();
+
+  // Drop based on mouse position relative to component center
+  if (validSibling && !validChild) {
+    if (siblingDropOrientation === 'vertical') {
+      const refMiddleX =
+        refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2);
+      return clientOffset.x < refMiddleX ? DROP_LEFT : DROP_RIGHT;
+    }
+    const refMiddleY = refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2);
+    return clientOffset.y < refMiddleY ? DROP_TOP : DROP_BOTTOM;
+  }
+
+  // either is valid, so choose location based on boundary deltas
+  if (validSibling && validChild) {
+    const deltaTop = Math.abs(clientOffset.y - refBoundingRect.top);
+    const deltaBottom = Math.abs(clientOffset.y - refBoundingRect.bottom);
+    const deltaLeft = Math.abs(clientOffset.x - refBoundingRect.left);
+    const deltaRight = Math.abs(clientOffset.x - refBoundingRect.right);
+
+    // if near enough to a sibling boundary, drop there
+    if (siblingDropOrientation === 'vertical') {
+      if (deltaLeft < SIBLING_DROP_THRESHOLD) return DROP_LEFT;
+      if (deltaRight < SIBLING_DROP_THRESHOLD) return DROP_RIGHT;
+    } else {
+      if (deltaTop < SIBLING_DROP_THRESHOLD) return DROP_TOP;
+      if (deltaBottom < SIBLING_DROP_THRESHOLD) return DROP_BOTTOM;
+    }
+
+    // drop as child
+    if (childDropOrientation === 'vertical') {
+      return hasChildren ? DROP_RIGHT : DROP_LEFT;
+    }
+    return hasChildren ? DROP_BOTTOM : DROP_TOP;
+  }
+
+  return null;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js
new file mode 100644
index 0000000..309d482
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js
@@ -0,0 +1,8 @@
+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' },
+];
diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
new file mode 100644
index 0000000..c8921ec
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
@@ -0,0 +1,69 @@
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  GRID_ROOT_TYPE,
+  MARKDOWN_TYPE,
+  ROW_TYPE,
+  SPACER_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from './componentTypes';
+
+const typeToValidChildType = {
+  // while some components are wrapped in Rows, most types are valid root children
+  [GRID_ROOT_TYPE]: {
+    [CHART_TYPE]: true,
+    [COLUMN_TYPE]: true,
+    [DIVIDER_TYPE]: true,
+    [HEADER_TYPE]: true,
+    [ROW_TYPE]: true,
+    [SPACER_TYPE]: true,
+    [TABS_TYPE]: true,
+  },
+
+  [ROW_TYPE]: {
+    [CHART_TYPE]: true,
+    [MARKDOWN_TYPE]: true,
+    [COLUMN_TYPE]: true,
+    [SPACER_TYPE]: true,
+  },
+
+  [TABS_TYPE]: {
+    [TAB_TYPE]: true,
+  },
+
+  [TAB_TYPE]: {
+    [CHART_TYPE]: true,
+    [COLUMN_TYPE]: true,
+    [DIVIDER_TYPE]: true,
+    [HEADER_TYPE]: true,
+    [ROW_TYPE]: true,
+    [SPACER_TYPE]: true,
+  },
+
+  [COLUMN_TYPE]: {
+    [CHART_TYPE]: true,
+    [MARKDOWN_TYPE]: true,
+    [HEADER_TYPE]: true,
+    [SPACER_TYPE]: true,
+  },
+
+  // these have no valid children
+  [CHART_TYPE]: {},
+  [MARKDOWN_TYPE]: {},
+  [DIVIDER_TYPE]: {},
+  [HEADER_TYPE]: {},
+  [SPACER_TYPE]: {},
+};
+
+export default function isValidChild({ parentType, childType }) {
+  if (!parentType || !childType) return false;
+
+  const isValid = Boolean(
+    typeToValidChildType[parentType][childType],
+  );
+
+  return isValid;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
new file mode 100644
index 0000000..c1ed03e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
@@ -0,0 +1,45 @@
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  MARKDOWN_TYPE,
+  ROW_TYPE,
+  SPACER_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from './componentTypes';
+
+import {
+  MEDIUM_HEADER,
+  ROW_TRANSPARENT,
+} from './constants';
+
+const typeToDefaultMetaData = {
+  [CHART_TYPE]: { width: 3, height: 15 },
+  [COLUMN_TYPE]: { width: 3 },
+  [DIVIDER_TYPE]: null,
+  [HEADER_TYPE]: { text: 'New header', headerSize: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT },
+  [MARKDOWN_TYPE]: { width: 3, height: 15 },
+  [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT },
+  [SPACER_TYPE]: {},
+  [TABS_TYPE]: null,
+  [TAB_TYPE]: { text: 'New Tab' },
+};
+
+// @TODO this should be replaced by a more robust algorithm
+function uuid(type) {
+  return `${type}-${Math.random().toString(16)}`;
+}
+
+export default function entityFactory(type) {
+  return {
+    version: 'v0',
+    type,
+    id: uuid(type),
+    children: [],
+    meta: {
+      ...typeToDefaultMetaData[type],
+    },
+  };
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js b/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js
new file mode 100644
index 0000000..38d1c7c
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js
@@ -0,0 +1,35 @@
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  MARKDOWN_TYPE,
+  ROW_TYPE,
+  SPACER_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from './componentTypes';
+
+import {
+  NEW_CHART_ID,
+  NEW_COLUMN_ID,
+  NEW_DIVIDER_ID,
+  NEW_HEADER_ID,
+  NEW_MARKDOWN_ID,
+  NEW_ROW_ID,
+  NEW_SPACER_ID,
+  NEW_TABS_ID,
+  NEW_TAB_ID,
+} from './constants';
+
+export default {
+  [NEW_CHART_ID]: CHART_TYPE, // @TODO we will have to encode real chart ids => type in the future
+  [NEW_COLUMN_ID]: COLUMN_TYPE,
+  [NEW_DIVIDER_ID]: DIVIDER_TYPE,
+  [NEW_HEADER_ID]: HEADER_TYPE,
+  [NEW_MARKDOWN_ID]: MARKDOWN_TYPE,
+  [NEW_ROW_ID]: ROW_TYPE,
+  [NEW_SPACER_ID]: SPACER_TYPE,
+  [NEW_TABS_ID]: TABS_TYPE,
+  [NEW_TAB_ID]: TAB_TYPE,
+};
diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
new file mode 100644
index 0000000..a0d92fa
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
@@ -0,0 +1,55 @@
+import newComponentIdToType from './newComponentIdToType';
+import shouldWrapChildInRow from './shouldWrapChildInRow';
+import newComponentFactory from './newComponentFactory';
+
+import {
+  ROW_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from './componentTypes';
+
+export default function newEntitiesFromDrop({ dropResult, components }) {
+  const { draggableId, destination } = dropResult;
+
+  const dragType = newComponentIdToType[draggableId];
+  const dropEntity = components[destination.droppableId];
+
+  if (!dropEntity) {
+    console.warn('Drop target entity', destination.droppableId, 'not found');
+    return null;
+  }
+
+  if (!dragType) {
+    console.warn('Drag type not found for id', draggableId);
+    return null;
+  }
+
+  const dropType = dropEntity.type;
+  let newDropChild = newComponentFactory(dragType);
+  const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
+
+  const newEntities = {
+    [newDropChild.id]: newDropChild,
+  };
+
+  if (wrapChildInRow) {
+    const rowWrapper = newComponentFactory(ROW_TYPE);
+    rowWrapper.children = [newDropChild.id];
+    newEntities[rowWrapper.id] = rowWrapper;
+    newDropChild = rowWrapper;
+  } else if (dragType === TABS_TYPE) { // create a new tab component
+    const tabChild = newComponentFactory(TAB_TYPE);
+    newDropChild.children = [tabChild.id];
+    newEntities[tabChild.id] = tabChild;
+  }
+
+  const nextDropChildren = [...dropEntity.children];
+  nextDropChildren.splice(destination.index, 0, newDropChild.id);
+
+  newEntities[destination.droppableId] = {
+    ...dropEntity,
+    children: nextDropChildren,
+  };
+
+  return newEntities;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
new file mode 100644
index 0000000..be84965
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
@@ -0,0 +1,24 @@
+import PropTypes from 'prop-types';
+import componentTypes from './componentTypes';
+import rowStyleOptions from './rowStyleOptions';
+import headerStyleOptions from './headerStyleOptions';
+
+export const componentShape = PropTypes.shape({ // eslint-disable-line
+  id: PropTypes.string.isRequired,
+  type: PropTypes.oneOf(
+    Object.values(componentTypes),
+  ).isRequired,
+  children: PropTypes.arrayOf(PropTypes.string),
+  meta: PropTypes.shape({
+    // Dimensions
+    width: PropTypes.number,
+    height: PropTypes.number,
+
+    // Header
+    text: PropTypes.string,
+    headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)),
+
+    // Row
+    rowStyle: PropTypes.oneOf(rowStyleOptions.map(opt => opt.value)),
+  }),
+});
diff --git a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
new file mode 100644
index 0000000..40e9af6
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
@@ -0,0 +1,30 @@
+// config for a ResizableContainer
+
+const adjustableWidthAndHeight = {
+  top: false,
+  right: false,
+  bottom: false,
+  left: false,
+  topRight: false,
+  bottomRight: true,
+  bottomLeft: false,
+  topLeft: false,
+};
+
+const adjustableWidth = {
+  ...adjustableWidthAndHeight,
+  right: true,
+  bottomRight: false,
+};
+
+const adjustableHeight = {
+  ...adjustableWidthAndHeight,
+  bottom: true,
+  bottomRight: false,
+};
+
+export default {
+  widthAndHeight: adjustableWidthAndHeight,
+  widthOnly: adjustableWidth,
+  heightOnly: adjustableHeight,
+};
diff --git a/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js
new file mode 100644
index 0000000..ad42492
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js
@@ -0,0 +1,7 @@
+import { t } from '../../../locales';
+import { ROW_TRANSPARENT, ROW_WHITE } from './constants';
+
+export default [
+  { value: ROW_TRANSPARENT, label: t('Transparent'), className: 'grid-row--transparent' },
+  { value: ROW_WHITE, label: t('White'), className: 'grid-row--white' },
+];
diff --git a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
new file mode 100644
index 0000000..487e247
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
@@ -0,0 +1,30 @@
+import {
+  GRID_ROOT_TYPE,
+  CHART_TYPE,
+  COLUMN_TYPE,
+  MARKDOWN_TYPE,
+  TAB_TYPE,
+} from './componentTypes';
+
+const typeToWrapChildLookup = {
+  [GRID_ROOT_TYPE]: {
+    [CHART_TYPE]: true,
+    [COLUMN_TYPE]: true,
+    [MARKDOWN_TYPE]: true,
+  },
+
+  [TAB_TYPE]: {
+    [CHART_TYPE]: true,
+    [COLUMN_TYPE]: true,
+    [MARKDOWN_TYPE]: true,
+  },
+};
+
+export default function shouldWrapChildInRow({ parentType, childType }) {
+  if (!parentType || !childType) return false;
+
+  const wrapChildLookup = typeToWrapChildLookup[parentType];
+  if (!wrapChildLookup) return false;
+
+  return Boolean(wrapChildLookup[childType]);
+}
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 1bcb5d6..b3379f3 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -41,9 +41,9 @@
   },
   "homepage": "http://superset.apache.org/",
   "dependencies": {
-    "//": "known issues with react-bootstrap>=0.32",
     "@data-ui/event-flow": "^0.0.54",
     "@data-ui/sparkline": "^0.0.54",
+    "@vx/responsive": "0.0.153",
     "babel-register": "^6.24.1",
     "bootstrap": "^3.3.6",
     "bootstrap-slider": "^10.0.0",
@@ -83,6 +83,7 @@
     "parse-iso-duration": "^1.0.0",
     "po2json": "^0.4.5",
     "prop-types": "^15.6.0",
+    "re-resizable": "^4.3.1",
     "react": "^15.6.2",
     "react-ace": "^5.0.1",
     "react-addons-css-transition-group": "^15.6.0",
@@ -93,6 +94,8 @@
     "react-bootstrap-table": "^4.3.1",
     "react-color": "^2.13.8",
     "react-datetime": "2.14.0",
+    "react-dnd": "^2.5.4",
+    "react-dnd-html5-backend": "^2.5.4",
     "react-dom": "^15.6.2",
     "react-gravatar": "^2.6.1",
     "react-grid-layout": "0.16.6",
diff --git a/superset/assets/src/components/EditableTitle.jsx b/superset/assets/src/components/EditableTitle.jsx
index b773340..1497676 100644
--- a/superset/assets/src/components/EditableTitle.jsx
+++ b/superset/assets/src/components/EditableTitle.jsx
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import cx from 'classnames';
 import TooltipWrapper from './TooltipWrapper';
 import { t } from '../locales';
 
@@ -27,8 +28,10 @@ class EditableTitle extends React.PureComponent {
     this.handleClick = this.handleClick.bind(this);
     this.handleBlur = this.handleBlur.bind(this);
     this.handleChange = this.handleChange.bind(this);
+    this.handleKeyDown = this.handleKeyDown.bind(this);
     this.handleKeyPress = this.handleKeyPress.bind(this);
   }
+
   componentWillReceiveProps(nextProps) {
     if (nextProps.title !== this.state.title) {
       this.setState({
@@ -37,8 +40,9 @@ class EditableTitle extends React.PureComponent {
       });
     }
   }
+
   handleClick() {
-    if (!this.props.canEdit) {
+    if (!this.props.canEdit || this.state.isEditing) {
       return;
     }
 
@@ -46,6 +50,7 @@ class EditableTitle extends React.PureComponent {
       isEditing: true,
     });
   }
+
   handleBlur() {
     if (!this.props.canEdit) {
       return;
@@ -67,9 +72,31 @@ class EditableTitle extends React.PureComponent {
       this.setState({
         lastTitle: this.state.title,
       });
+    }
+
+    if (this.props.title !== this.state.title) {
       this.props.onSaveTitle(this.state.title);
     }
   }
+
+  handleKeyDown(ev) {
+    // this entire method exists to support using EditableTitle as the title of a
+    // react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
+    //
+    // tl;dr when a Tab EditableTitle is being edited, typically the Tab it's within has been
+    // clicked and is focused/active. for accessibility, when focused the Tab <a /> intercepts
+    // the ' ' key (among others, including all arrows) and onChange() doesn't fire. somehow
+    // keydown is still called so we can detect this and manually add a ' ' to the current title
+    if (ev.key === ' ') {
+      let title = ev.target.value;
+      const titleLength = (title || '').length;
+      if (title && title[titleLength - 1] !== ' ') {
+        title = `${title} `;
+        this.setState(() => ({ title }));
+      }
+    }
+  }
+
   handleChange(ev) {
     if (!this.props.canEdit) {
       return;
@@ -79,6 +106,7 @@ class EditableTitle extends React.PureComponent {
       title: ev.target.value,
     });
   }
+
   handleKeyPress(ev) {
     if (ev.key === 'Enter') {
       ev.preventDefault();
@@ -86,12 +114,14 @@ class EditableTitle extends React.PureComponent {
       this.handleBlur();
     }
   }
+
   render() {
     let input = (
       <input
         required
         type={this.state.isEditing ? 'text' : 'button'}
         value={this.state.title}
+        onKeyDown={this.handleKeyDown}
         onChange={this.handleChange}
         onBlur={this.handleBlur}
         onClick={this.handleClick}
@@ -110,7 +140,9 @@ class EditableTitle extends React.PureComponent {
       );
     }
     return (
-      <span className="editable-title">{input}</span>
+      <span className={cx('editable-title', this.props.canEdit && 'editable-title--editable')}>
+        {input}
+      </span>
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
index a18a5d2..6df72ff 100644
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ b/superset/assets/src/dashboard/components/DashboardContainer.jsx
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 
 import * as dashboardActions from '../actions';
 import * as chartActions from '../../chart/chartAction';
-import Dashboard from './Dashboard';
+import Dashboard from '../v2/components/Dashboard';
 
 function mapStateToProps({ charts, dashboard, impressionId }) {
   return {
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index 774e071..c9236bd 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -8,17 +8,29 @@ import { initEnhancer } from '../reduxUtils';
 import { appSetup } from '../common';
 import { initJQueryAjax } from '../modules/utils';
 import DashboardContainer from './components/DashboardContainer';
-import rootReducer, { getInitialState } from './reducers';
+// import rootReducer, { getInitialState } from './reducers';
+
+import testLayout from './v2/fixtures/testLayout';
+import rootReducer from './v2/reducers/';
 
 appSetup();
 initJQueryAjax();
 
 const appContainer = document.getElementById('app');
-const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
-const initState = Object.assign({}, getInitialState(bootstrapData));
+// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
+// const initState = Object.assign({}, getInitialState(bootstrapData));
+const initState = {
+  dashboard: testLayout,
+};
 
 const store = createStore(
-  rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
+  rootReducer,
+  initState,
+  compose(
+    applyMiddleware(thunk),
+    initEnhancer(false),
+  ),
+);
 
 ReactDOM.render(
   <Provider store={store}>
@@ -26,4 +38,3 @@ ReactDOM.render(
   </Provider>,
   appContainer,
 );
-
diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css
new file mode 100644
index 0000000..534a17e
--- /dev/null
+++ b/superset/assets/stylesheets/dashboard-v2.css
@@ -0,0 +1,42 @@
+.dashboard-v2 {
+  margin-top: -20px;
+  position: relative;
+  color: #263238;
+}
+
+.dashboard-header {
+  background: white;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 24px;
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
+  margin-bottom: 2px;
+}
+
+.dashboard-builder {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  height: auto;
+}
+
+.dashboard-builder-sidepane {
+  background: white;
+  flex: 0 0 376px;
+  box-shadow: 0 0 0 1px #ccc; /* @TODO color */
+}
+
+.dashboard-builder-sidepane-header {
+  font-size: 16;
+  font-weight: 700;
+  border-bottom: 1px solid #ccc;
+  padding: 16px;
+}
+
+/* @TODO remove upon new theme */
+.btn.btn-primary {
+  background: #263238 !important;
+  color: white !important;
+}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 035acce..6513b6f 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -208,6 +208,17 @@ div.widget {
     }
   }
 }
+/* brand icon */
+.navbar-brand > img.logo {
+  margin-left: 15px;
+  width: 36px;
+  display: inline;
+}
+.navbar-brand > span {
+  margin-left: 2px;
+  font-size: 15px;
+  font-weight: bold;
+}
 
 .navbar .alert {
     padding: 5px 10px;
@@ -228,23 +239,26 @@ table.table-no-hover tr:hover {
 }
 
 .editable-title input {
-    padding: 2px 6px 3px 6px;
+  outline: none;
+  background: transparent;
+  border: none;
+  box-shadow: none;
+  padding-left: 0;
 }
 
 .editable-title input[type="button"] {
     border-color: transparent;
     background: transparent;
+    font-size: inherit;
+    line-height: inherit;
     white-space: normal;
     text-align: left;
 }
 
-.editable-title input[type="button"]:hover {
+.editable-title--editable input[type="button"]:hover {
     cursor: text;
 }
 
-.editable-title input[type="button"]:focus {
-    outline: none;
-}
 .m-r-5 {
     margin-right: 5px;
 }
diff --git a/superset/config.py b/superset/config.py
index 530b126..ea5a520 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -97,7 +97,7 @@ ENABLE_PROXY_FIX = False
 APP_NAME = 'Superset'
 
 # Uncomment to setup an App icon
-APP_ICON = '/static/assets/images/superset-logo@2x.png'
+APP_ICON = '/static/assets/images/favicon.png'
 
 # Druid query timezone
 # tz.tzutc() : Using utc timezone
diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html
index 27f2fee..c2f0668 100644
--- a/superset/templates/appbuilder/navbar.html
+++ b/superset/templates/appbuilder/navbar.html
@@ -12,9 +12,11 @@
       </button>
       <a class="navbar-brand" href="/superset/profile/{{ current_user.username }}/">
         <img
-          width="126" src="{{ appbuilder.app_icon }}"
+          class="logo"
+          src="{{ appbuilder.app_icon }}"
           alt="{{ appbuilder.app_name }}"
         />
+        <span>Superset</span>
       </a>
     </div>
     <div class="navbar-collapse collapse">
@@ -47,4 +49,3 @@
     </div>
   </div>
 </div>
-
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index 1a158d9..25633da 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -3,7 +3,6 @@
 {% block body %}
 <div
   id="app"
-  class="dashboard container-fluid"
   data-bootstrap="{{ bootstrap_data }}"
 >
 </div>


[incubator-superset] 25/26: [fix] layout converter fix (#5218)

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

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

commit 4a488258b92dee87a61c34502dee389672a3d972
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Thu Jun 21 16:42:32 2018 -0700

    [fix] layout converter fix (#5218)
    
    * [fix] layout converter fix
    
    * add changed_on into initial sliceEntity data
    
    * add unit tests for SliceAdder component
    
    * remove old fixtures file
---
 .../dashboard/components/Dashboard_spec.jsx        |   2 +-
 .../dashboard/components/SliceAdder_spec.jsx       | 154 +++++++++++++++++++
 .../components/gridComponents/Chart_spec.jsx       |   5 +-
 .../assets/spec/javascripts/dashboard/fixtures.jsx | 171 ---------------------
 .../dashboard/fixtures/mockSliceEntities.js        | 140 ++++++++++++++++-
 .../assets/src/dashboard/containers/SliceAdder.js  |  28 ----
 .../src/dashboard/reducers/getInitialState.js      |   1 +
 .../src/dashboard/util/dashboardLayoutConverter.js |   6 +-
 superset/assets/src/dashboard/util/propShapes.jsx  |   4 +-
 superset/models/core.py                            |   1 +
 tests/core_tests.py                                |   8 +
 11 files changed, 313 insertions(+), 207 deletions(-)

diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
index 2094040..e855009 100644
--- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -13,7 +13,7 @@ import datasources from '../fixtures/mockDatasource';
 import dashboardInfo from '../fixtures/mockDashboardInfo';
 import { dashboardLayout } from '../fixtures/mockDashboardLayout';
 import dashboardState from '../fixtures/mockDashboardState';
-import sliceEntities from '../fixtures/mockSliceEntities';
+import { sliceEntitiesForChart as sliceEntities } from '../fixtures/mockSliceEntities';
 
 import { CHART_TYPE } from '../../../../src/dashboard/util/componentTypes';
 import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
diff --git a/superset/assets/spec/javascripts/dashboard/components/SliceAdder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/SliceAdder_spec.jsx
new file mode 100644
index 0000000..da0f7df
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/SliceAdder_spec.jsx
@@ -0,0 +1,154 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it, beforeEach, afterEach } from 'mocha';
+import sinon from 'sinon';
+import { expect } from 'chai';
+
+import { List } from 'react-virtualized';
+
+import SliceAdder from '../../../../src/dashboard/components/SliceAdder';
+import { sliceEntitiesForDashboard as mockSliceEntities } from '../fixtures/mockSliceEntities';
+
+describe('SliceAdder', () => {
+  const mockEvent = {
+    key: 'Enter',
+    target: {
+      value: 'mock event target',
+    },
+    preventDefault: () => {},
+  };
+  const props = {
+    ...mockSliceEntities,
+    fetchAllSlices: () => {},
+    selectedSliceIds: [127, 128],
+    userId: '1',
+    height: 100,
+  };
+  const errorProps = {
+    ...props,
+    errorMessage: 'this is error',
+  };
+
+  describe('SliceAdder.sortByComparator', () => {
+    it('should sort by timestamp descending', () => {
+      const sortedTimestamps = Object.values(props.slices)
+        .sort(SliceAdder.sortByComparator('changed_on'))
+        .map(slice => slice.changed_on);
+      expect(
+        sortedTimestamps.every((currentTimestamp, index) => {
+          if (index === 0) {
+            return true;
+          }
+          return currentTimestamp < sortedTimestamps[index - 1];
+        }),
+      ).to.equal(true);
+    });
+
+    it('should sort by slice_name', () => {
+      const sortedNames = Object.values(props.slices)
+        .sort(SliceAdder.sortByComparator('slice_name'))
+        .map(slice => slice.slice_name);
+      const expectedNames = Object.values(props.slices)
+        .map(slice => slice.slice_name)
+        .sort();
+      expect(sortedNames).to.deep.equal(expectedNames);
+    });
+  });
+
+  it('render List', () => {
+    const wrapper = shallow(<SliceAdder {...props} />);
+    wrapper.setState({ filteredSlices: Object.values(props.slices) });
+    expect(wrapper.find(List)).to.have.length(1);
+  });
+
+  it('render error', () => {
+    const wrapper = shallow(<SliceAdder {...errorProps} />);
+    wrapper.setState({ filteredSlices: Object.values(props.slices) });
+    expect(wrapper.text()).to.have.string(errorProps.errorMessage);
+  });
+
+  it('componentDidMount', () => {
+    sinon.spy(SliceAdder.prototype, 'componentDidMount');
+    sinon.spy(props, 'fetchAllSlices');
+
+    shallow(<SliceAdder {...props} />, {
+      lifecycleExperimental: true,
+    });
+    expect(SliceAdder.prototype.componentDidMount.calledOnce).to.equal(true);
+    expect(props.fetchAllSlices.calledOnce).to.equal(true);
+
+    SliceAdder.prototype.componentDidMount.restore();
+    props.fetchAllSlices.restore();
+  });
+
+  describe('componentWillReceiveProps', () => {
+    let wrapper;
+    beforeEach(() => {
+      wrapper = shallow(<SliceAdder {...props} />);
+      wrapper.setState({ filteredSlices: Object.values(props.slices) });
+      sinon.spy(wrapper.instance(), 'setState');
+    });
+    afterEach(() => {
+      wrapper.instance().setState.restore();
+    });
+
+    it('fetch slices should update state', () => {
+      wrapper.instance().componentWillReceiveProps({
+        ...props,
+        lastUpdated: new Date().getTime(),
+      });
+      expect(wrapper.instance().setState.calledOnce).to.equal(true);
+
+      const stateKeys = Object.keys(
+        wrapper.instance().setState.lastCall.args[0],
+      );
+      expect(stateKeys).to.include('filteredSlices');
+    });
+
+    it('select slices should update state', () => {
+      wrapper.instance().componentWillReceiveProps({
+        ...props,
+        selectedSliceIds: [127],
+      });
+      expect(wrapper.instance().setState.calledOnce).to.equal(true);
+
+      const stateKeys = Object.keys(
+        wrapper.instance().setState.lastCall.args[0],
+      );
+      expect(stateKeys).to.include('selectedSliceIdsSet');
+    });
+  });
+
+  describe('should rerun filter and sort', () => {
+    let wrapper;
+    let spy;
+    beforeEach(() => {
+      wrapper = shallow(<SliceAdder {...props} />);
+      wrapper.setState({ filteredSlices: Object.values(props.slices) });
+      spy = sinon.spy(wrapper.instance(), 'getFilteredSortedSlices');
+    });
+    afterEach(() => {
+      spy.restore();
+    });
+
+    it('searchUpdated', () => {
+      const newSearchTerm = 'new search term';
+      wrapper.instance().searchUpdated(newSearchTerm);
+      expect(spy.calledOnce).to.equal(true);
+      expect(spy.lastCall.args[0]).to.equal(newSearchTerm);
+    });
+
+    it('handleSelect', () => {
+      const newSortBy = 1;
+      wrapper.instance().handleSelect(newSortBy);
+      expect(spy.calledOnce).to.equal(true);
+      expect(spy.lastCall.args[1]).to.equal(newSortBy);
+    });
+
+    it('handleKeyPress', () => {
+      wrapper.instance().handleKeyPress(mockEvent);
+      expect(spy.calledOnce).to.equal(true);
+      expect(spy.lastCall.args[0]).to.equal(mockEvent.target.value);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
index 5fff313..dcd7119 100644
--- a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
@@ -9,7 +9,10 @@ import SliceHeader from '../../../../../src/dashboard/components/SliceHeader';
 import ChartContainer from '../../../../../src/chart/ChartContainer';
 
 import mockDatasource from '../../fixtures/mockDatasource';
-import sliceEntities, { sliceId } from '../../fixtures/mockSliceEntities';
+import {
+  sliceEntitiesForChart as sliceEntities,
+  sliceId,
+} from '../../fixtures/mockSliceEntities';
 import chartQueries, {
   sliceId as queryId,
 } from '../../fixtures/mockChartQueries';
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
deleted file mode 100644
index 7a12454..0000000
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import getInitialState from '../../../src/dashboard/reducers/getInitialState';
-
-export const defaultFilters = {
-  256: { region: [] },
-  257: { country_name: ['United States'] },
-};
-export const regionFilter = {
-  datasource: null,
-  description: null,
-  description_markeddown: '',
-  edit_url: '/slicemodelview/edit/256',
-  form_data: {
-    datasource: '2__table',
-    date_filter: false,
-    filters: [
-      {
-        col: 'country_name',
-        op: 'in',
-        val: ['United States', 'France', 'Japan'],
-      },
-    ],
-    granularity_sqla: null,
-    groupby: ['region', 'country_name'],
-    having: '',
-    instant_filtering: true,
-    metric: 'sum__SP_POP_TOTL',
-    show_druid_time_granularity: false,
-    show_druid_time_origin: false,
-    show_sqla_time_column: false,
-    show_sqla_time_granularity: false,
-    since: '100 years ago',
-    slice_id: 256,
-    time_grain_sqla: null,
-    until: 'now',
-    viz_type: 'filter_box',
-    where: '',
-  },
-  slice_id: 256,
-  slice_name: 'Region Filters',
-  slice_url:
-    '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20256%7D',
-};
-export const countryFilter = {
-  datasource: null,
-  description: null,
-  description_markeddown: '',
-  edit_url: '/slicemodelview/edit/257',
-  form_data: {
-    datasource: '2__table',
-    date_filter: false,
-    filters: [],
-    granularity_sqla: null,
-    groupby: ['country_name'],
-    having: '',
-    instant_filtering: true,
-    metric: 'sum__SP_POP_TOTL',
-    show_druid_time_granularity: false,
-    show_druid_time_origin: false,
-    show_sqla_time_column: false,
-    show_sqla_time_granularity: false,
-    since: '100 years ago',
-    slice_id: 257,
-    time_grain_sqla: null,
-    until: 'now',
-    viz_type: 'filter_box',
-    where: '',
-  },
-  slice_id: 257,
-  slice_name: 'Country Filters',
-  slice_url:
-    '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20257%7D',
-};
-export const slice = {
-  datasource: null,
-  description: null,
-  description_markeddown: '',
-  edit_url: '/slicemodelview/edit/248',
-  form_data: {
-    annotation_layers: [],
-    bottom_margin: 'auto',
-    color_scheme: 'bnbColors',
-    contribution: false,
-    datasource: '2__table',
-    filters: [],
-    granularity_sqla: null,
-    groupby: [],
-    having: '',
-    left_margin: 'auto',
-    limit: 50,
-    line_interpolation: 'linear',
-    metrics: ['sum__SP_POP_TOTL'],
-    num_period_compare: '',
-    order_desc: true,
-    period_ratio_type: 'growth',
-    resample_fillmethod: null,
-    resample_how: null,
-    resample_rule: null,
-    rich_tooltip: true,
-    rolling_type: 'None',
-    show_brush: false,
-    show_legend: true,
-    show_markers: false,
-    since: '1961-01-01T00:00:00',
-    slice_id: 248,
-    time_compare: null,
-    time_grain_sqla: null,
-    timeseries_limit_metric: null,
-    until: '2014-12-31T00:00:00',
-    viz_type: 'line',
-    where: '',
-    x_axis_format: 'smart_date',
-    x_axis_label: '',
-    x_axis_showminmax: true,
-    y_axis_bounds: [null, null],
-    y_axis_format: '.3s',
-    y_axis_label: '',
-    y_axis_showminmax: true,
-    y_log_scale: false,
-  },
-  slice_id: 248,
-  slice_name: 'Filtered Population',
-  slice_url:
-    '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
-};
-
-const mockDashboardData = {
-  css: '',
-  dash_edit_perm: true,
-  dash_save_perm: true,
-  dashboard_title: 'Births',
-  id: 2,
-  metadata: {
-    default_filters: JSON.stringify(defaultFilters),
-    filter_immune_slices: [256],
-    timed_refresh_immune_slices: [],
-    filter_immune_slice_fields: {},
-    expanded_slices: {},
-  },
-  position_json: [
-    {
-      size_x: 4,
-      slice_id: '256',
-      row: 0,
-      size_y: 4,
-      col: 5,
-    },
-    {
-      size_x: 4,
-      slice_id: '248',
-      row: 0,
-      size_y: 4,
-      col: 1,
-    },
-  ],
-  slug: 'births',
-  slices: [regionFilter, slice, countryFilter],
-  standalone_mode: false,
-};
-export const {
-  dashboardState,
-  dashboardInfo,
-  charts,
-  datasources,
-  sliceEntities,
-  dashboardLayout,
-} = getInitialState({
-  common: {},
-  dashboard_data: mockDashboardData,
-  datasources: {},
-  user_id: '1',
-});
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
index 7c43bea..3644c10 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
@@ -3,7 +3,7 @@ import { datasourceId } from './mockDatasource';
 
 export const sliceId = id;
 
-export default {
+export const sliceEntitiesForChart = {
   slices: {
     [sliceId]: {
       slice_id: sliceId,
@@ -37,3 +37,141 @@ export default {
   errorMessage: null,
   lastUpdated: 0,
 };
+
+export const sliceEntitiesForDashboard = {
+  slices: {
+    127: {
+      slice_id: 127,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D',
+      slice_name: 'Region Filter',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/127',
+      viz_type: 'filter_box',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332615,
+    },
+    128: {
+      slice_id: 128,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20128%7D',
+      slice_name: "World's Population",
+      form_data: {},
+      edit_url: '/slicemodelview/edit/128',
+      viz_type: 'big_number',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332628,
+    },
+    129: {
+      slice_id: 129,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20129%7D',
+      slice_name: 'Most Populated Countries',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/129',
+      viz_type: 'table',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332637,
+    },
+    130: {
+      slice_id: 130,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20130%7D',
+      slice_name: 'Growth Rate',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/130',
+      viz_type: 'line',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332645,
+    },
+    131: {
+      slice_id: 131,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20131%7D',
+      slice_name: '% Rural',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/131',
+      viz_type: 'world_map',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332654,
+    },
+    132: {
+      slice_id: 132,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20132%7D',
+      slice_name: 'Life Expectancy VS Rural %',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/132',
+      viz_type: 'bubble',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332663,
+    },
+    133: {
+      slice_id: 133,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20133%7D',
+      slice_name: 'Rural Breakdown',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/133',
+      viz_type: 'sunburst',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332673,
+    },
+    134: {
+      slice_id: 134,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20134%7D',
+      slice_name: "World's Pop Growth",
+      form_data: {},
+      edit_url: '/slicemodelview/edit/134',
+      viz_type: 'area',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332680,
+    },
+    135: {
+      slice_id: 135,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20135%7D',
+      slice_name: 'Box plot',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/135',
+      viz_type: 'box_plot',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332688,
+    },
+    136: {
+      slice_id: 136,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20136%7D',
+      slice_name: 'Treemap',
+      form_data: {},
+      edit_url: '/slicemodelview/edit/136',
+      viz_type: 'treemap',
+      datasource: '2__table',
+      description: null,
+      description_markeddown: '',
+      modified: '23 hours ago',
+      changed_on: 1529453332700,
+    },
+  },
+  isLoading: false,
+  errorMessage: null,
+  lastUpdated: 0,
+};
diff --git a/superset/assets/src/dashboard/containers/SliceAdder.js b/superset/assets/src/dashboard/containers/SliceAdder.js
deleted file mode 100644
index e3d931d..0000000
--- a/superset/assets/src/dashboard/containers/SliceAdder.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-
-import { fetchAllSlices } from '../actions/sliceEntities';
-import SliceAdder from '../components/SliceAdder';
-
-function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
-  return {
-    userId: dashboardInfo.userId,
-    selectedSliceIds: dashboardState.sliceIds,
-    slices: sliceEntities.slices,
-    isLoading: sliceEntities.isLoading,
-    errorMessage: sliceEntities.errorMessage,
-    lastUpdated: sliceEntities.lastUpdated,
-    editMode: dashboardState.editMode,
-  };
-}
-
-function mapDispatchToProps(dispatch) {
-  return bindActionCreators(
-    {
-      fetchAllSlices,
-    },
-    dispatch,
-  );
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index e529bf4..13283ec 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -93,6 +93,7 @@ export default function(bootstrapData) {
         description: slice.description,
         description_markeddown: slice.description_markeddown,
         modified: slice.modified ? slice.modified.replace(/<[^>]*>/g, '') : '',
+        changed_on: new Date(slice.changed_on).getTime(),
       };
 
       sliceIds.add(key);
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index c1d855c..9b66e41 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -398,12 +398,12 @@ export function convertToLayout(positions) {
 function mergePosition(position, bottomLine, maxColumn) {
   const { col, size_x, size_y } = position;
   const endColumn = col + size_x > maxColumn ? bottomLine.length : col + size_x;
-  const nextSectionStart =
+  const sectionLength =
     bottomLine.slice(col).findIndex(value => value > bottomLine[col]) + 1;
 
   const currentBottom =
-    nextSectionStart > 0 && nextSectionStart < size_x
-      ? Math.max.apply(null, bottomLine.slice(col, col + size_x + 1))
+    sectionLength > 0 && sectionLength < size_x
+      ? Math.max.apply(null, bottomLine.slice(col, col + size_x))
       : bottomLine[col];
   bottomLine.fill(currentBottom + size_y, col, endColumn);
 }
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index 3427520..f155190 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -59,8 +59,8 @@ export const slicePropShape = PropTypes.shape({
   datasource: PropTypes.string,
   datasource_name: PropTypes.string,
   datasource_link: PropTypes.string,
-  changedOn: PropTypes.number,
-  modified: PropTypes.string,
+  changed_on: PropTypes.number.isRequired,
+  modified: PropTypes.string.isRequired,
   viz_type: PropTypes.string.isRequired,
   description: PropTypes.string,
   description_markeddown: PropTypes.string,
diff --git a/superset/models/core.py b/superset/models/core.py
index 02109e7..bd5fc9d 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -190,6 +190,7 @@ class Slice(Model, AuditMixinNullable, ImportMixin):
             'slice_name': self.slice_name,
             'slice_url': self.slice_url,
             'modified': self.modified(),
+            'changed_on': self.changed_on.isoformat(),
         }
 
     @property
diff --git a/tests/core_tests.py b/tests/core_tests.py
index dd6e3d8..6a4f153 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -227,6 +227,14 @@ class CoreTests(SupersetTestCase):
         assert len(resp) > 0
         assert 'Carbon Dioxide' in resp
 
+    def test_slice_data(self):
+        # slice data should have some required attributes
+        self.login(username='admin')
+        slc = self.get_slice('Girls', db.session)
+        slc_data_attributes = slc.data.keys()
+        assert('changed_on' in slc_data_attributes)
+        assert('modified' in slc_data_attributes)
+
     def test_slices(self):
         # Testing by hitting the two supported end points for all slices
         self.login(username='admin')


[incubator-superset] 08/26: add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893)

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

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

commit ff057d86bd5205de8e5f8230bd68e14da9854a88
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue May 8 11:33:14 2018 -0700

    add sticky tabs + sidepane, better tabs perf, better container hierarchy, better chart header (#4893)
    
    * dashboard header, slice header UI improvement
    
    * add slider and sticky
    
    * dashboard header, slice header UI improvement
    
    * make builder pane floating
    
    * [dashboard builder] add sticky top-level tabs, refactor for performant tabs
    
    * [dashboard builder] visually distinct containers, icons for undo-redo, fix some isValidChild bugs
    
    * [dashboard builder] better undo redo <> save changes state, notify upon reaching undo limit
    
    * [dashboard builder] hook up edit + create component actions to saved-state pop.
    
    * [dashboard builder] visual refinement, refactor Dashboard header content and updates into layout for undo-redo, refactor save dashboard modal to use toasts instead of notify.
    
    * [dashboard builder] refactor chart name update logic to use layout for undo redo, save slice name changes on dashboard save
    
    * add slider and sticky
    
    * [dashboard builder] fix layout converter slice_id + chartId type casting, don't change grid size upon edit (perf)
    
    * [dashboard builder] don't set version key in getInitialState
    
    * [dashboard builder] make top level tabs addition/removal undoable, fix double sticky tabs + side panel.
    
    * [dashboard builder] fix sticky tabs offset bug
    
    * [dashboard builder] fix drag preview width, css polish, fix rebase issue
    
    * [dashboard builder] fix side pane labels and hove z-index
---
 superset/assets/package.json                       |   1 +
 .../{dashboard => }/components/ActionMenuItem.jsx  |   2 +-
 .../src/dashboard/actions/dashboardLayout.js       | 118 +++++++++++-
 .../assets/src/dashboard/actions/dashboardState.js |  43 ++++-
 .../assets/src/dashboard/actions/sliceEntities.js  |  35 ----
 .../dashboard/components/BuilderComponentPane.jsx  | 114 ++++++++----
 .../assets/src/dashboard/components/Controls.jsx   |  52 ++----
 .../assets/src/dashboard/components/Dashboard.jsx  |   7 +-
 .../src/dashboard/components/DashboardBuilder.jsx  | 134 ++++++++++----
 .../src/dashboard/components/DashboardGrid.jsx     | 204 +++++++++++----------
 .../assets/src/dashboard/components/Header.jsx     |  58 ++++--
 .../assets/src/dashboard/components/SaveModal.jsx  |  32 ++--
 .../assets/src/dashboard/components/SliceAdder.jsx |   6 +-
 .../src/dashboard/components/SliceHeader.jsx       |  26 +--
 .../dashboard/components/SliceHeaderControls.jsx   |  86 +++++----
 .../components/dnd/AddSliceDragPreview.jsx         |  17 +-
 .../dashboard/components/gridComponents/Chart.jsx  |  70 +++----
 .../components/gridComponents/ChartHolder.jsx      |  19 +-
 .../dashboard/components/gridComponents/Column.jsx |  25 ++-
 .../dashboard/components/gridComponents/Row.jsx    |  23 +--
 .../dashboard/components/gridComponents/Tab.jsx    |   2 +-
 .../dashboard/components/gridComponents/Tabs.jsx   |  42 ++---
 .../dashboard/components/menu/WithPopoverMenu.jsx  |   5 +-
 superset/assets/src/dashboard/containers/Chart.jsx |   4 +-
 .../assets/src/dashboard/containers/Dashboard.jsx  |   2 -
 .../dashboard/containers/DashboardComponent.jsx    |   8 +-
 .../src/dashboard/containers/DashboardHeader.jsx   |  38 ++--
 .../assets/src/dashboard/containers/SliceAdder.js  |  28 +++
 .../src/dashboard/reducers/dashboardState.js       |  11 +-
 .../src/dashboard/reducers/getInitialState.js      |  44 ++++-
 .../dashboard/reducers/undoableDashboardLayout.js  |   5 +-
 .../dashboard/stylesheets/builder-sidepane.less    |  78 +++++---
 .../dashboard/stylesheets/components/chart.less    |   5 -
 .../dashboard/stylesheets/components/column.less   |   6 +-
 .../dashboard/stylesheets/components/header.less   |  10 +-
 .../src/dashboard/stylesheets/components/row.less  |   6 +-
 .../src/dashboard/stylesheets/components/tabs.less |  13 +-
 .../src/dashboard/stylesheets/dashboard.less       |  98 +++++++---
 superset/assets/src/dashboard/stylesheets/dnd.less |   4 +-
 .../assets/src/dashboard/stylesheets/grid.less     |   9 +-
 .../src/dashboard/stylesheets/hover-menu.less      |  53 +++++-
 .../src/dashboard/stylesheets/popover-menu.less    |  28 +--
 .../src/dashboard/stylesheets/variables.less       |   4 +
 superset/assets/src/dashboard/util/constants.js    |   4 +
 .../src/dashboard/util/dashboardLayoutConverter.js |  63 ++-----
 superset/assets/src/dashboard/util/isValidChild.js |   9 +-
 .../src/dashboard/util/newComponentFactory.js      |   1 -
 superset/assets/src/dashboard/util/propShapes.jsx  |   1 -
 superset/assets/src/theme.js                       |   1 -
 superset/assets/src/visualizations/nvd3_vis.js     |   2 +
 superset/assets/stylesheets/superset.less          |   6 +-
 superset/views/core.py                             |  34 +++-
 52 files changed, 1058 insertions(+), 638 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index ab440f0..6f3b20a 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -107,6 +107,7 @@
     "react-select-fast-filter-options": "^0.2.1",
     "react-sortable-hoc": "^0.8.3",
     "react-split-pane": "^0.1.66",
+    "react-sticky": "^6.0.2",
     "react-syntax-highlighter": "^7.0.4",
     "react-virtualized": "9.19.1",
     "react-virtualized-select": "^2.4.0",
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/components/ActionMenuItem.jsx
similarity index 94%
rename from superset/assets/src/dashboard/components/ActionMenuItem.jsx
rename to superset/assets/src/components/ActionMenuItem.jsx
index a0ecb78..e6c4447 100644
--- a/superset/assets/src/dashboard/components/ActionMenuItem.jsx
+++ b/superset/assets/src/components/ActionMenuItem.jsx
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { MenuItem } from 'react-bootstrap';
 
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
 
 export function MenuItemContent({ faIcon, text, tooltip, children }) {
   return (
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index 5a04de5..c64ea0d 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -1,3 +1,5 @@
+import { ActionCreators as UndoActionCreators } from 'redux-undo';
+
 import { addInfoToast } from './messageToasts';
 import { setUnsavedChanges } from './dashboardState';
 import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
@@ -5,13 +7,14 @@ import {
   DASHBOARD_ROOT_ID,
   NEW_COMPONENTS_SOURCE_ID,
   GRID_MIN_COLUMN_COUNT,
+  DASHBOARD_HEADER_ID,
 } from '../util/constants';
 import dropOverflowsParent from '../util/dropOverflowsParent';
 import findParentId from '../util/findParentId';
 
 // Component CRUD -------------------------------------------------------------
 export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
-export function updateComponents(nextComponents) {
+function updateLayoutComponents(nextComponents) {
   return {
     type: UPDATE_COMPONENTS,
     payload: {
@@ -20,8 +23,34 @@ export function updateComponents(nextComponents) {
   };
 }
 
+export function updateComponents(nextComponents) {
+  return (dispatch, getState) => {
+    dispatch(updateLayoutComponents(nextComponents));
+
+    if (!getState().dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
+  };
+}
+
+export function updateDashboardTitle(text) {
+  return (dispatch, getState) => {
+    const { dashboardLayout } = getState();
+    dispatch(
+      updateComponents({
+        [DASHBOARD_HEADER_ID]: {
+          ...dashboardLayout.present[DASHBOARD_HEADER_ID],
+          meta: {
+            text,
+          },
+        },
+      }),
+    );
+  };
+}
+
 export const DELETE_COMPONENT = 'DELETE_COMPONENT';
-export function deleteComponent(id, parentId) {
+function deleteLayoutComponent(id, parentId) {
   return {
     type: DELETE_COMPONENT,
     payload: {
@@ -31,8 +60,18 @@ export function deleteComponent(id, parentId) {
   };
 }
 
+export function deleteComponent(id, parentId) {
+  return (dispatch, getState) => {
+    dispatch(deleteLayoutComponent(id, parentId));
+
+    if (!getState().dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
+  };
+}
+
 export const CREATE_COMPONENT = 'CREATE_COMPONENT';
-export function createComponent(dropResult) {
+function createLayoutComponent(dropResult) {
   return {
     type: CREATE_COMPONENT,
     payload: {
@@ -41,9 +80,19 @@ export function createComponent(dropResult) {
   };
 }
 
+export function createComponent(dropResult) {
+  return (dispatch, getState) => {
+    dispatch(createLayoutComponent(dropResult));
+
+    if (!getState().dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
+  };
+}
+
 // Tabs -----------------------------------------------------------------------
 export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
-export function createTopLevelTabs(dropResult) {
+function createTopLevelTabsAction(dropResult) {
   return {
     type: CREATE_TOP_LEVEL_TABS,
     payload: {
@@ -52,19 +101,39 @@ export function createTopLevelTabs(dropResult) {
   };
 }
 
+export function createTopLevelTabs(dropResult) {
+  return (dispatch, getState) => {
+    dispatch(createTopLevelTabsAction(dropResult));
+
+    if (!getState().dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
+  };
+}
+
 export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
-export function deleteTopLevelTabs() {
+function deleteTopLevelTabsAction() {
   return {
     type: DELETE_TOP_LEVEL_TABS,
     payload: {},
   };
 }
 
+export function deleteTopLevelTabs(dropResult) {
+  return (dispatch, getState) => {
+    dispatch(deleteTopLevelTabsAction(dropResult));
+
+    if (!getState().dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
+  };
+}
+
 // Resize ---------------------------------------------------------------------
 export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
 export function resizeComponent({ id, width, height }) {
   return (dispatch, getState) => {
-    const { dashboardLayout: undoableLayout } = getState();
+    const { dashboardLayout: undoableLayout, dashboardState } = getState();
     const { present: dashboard } = undoableLayout;
     const component = dashboard[id];
     const widthChanged = width && component.meta.width !== width;
@@ -99,7 +168,9 @@ export function resizeComponent({ id, width, height }) {
       });
 
       dispatch(updateComponents(updatedComponents));
-      dispatch(setUnsavedChanges(true));
+      if (!dashboardState.hasUnsavedChanges) {
+        dispatch(setUnsavedChanges(true));
+      }
     }
   };
 }
@@ -149,9 +220,10 @@ export function handleComponentDrop(dropResult) {
       dispatch(moveComponent(dropResult));
     }
 
+    const { dashboardLayout: undoableLayout, dashboardState } = getState();
+
     // if we moved a Tab and the parent Tabs no longer has children, delete it.
     if (!isNewComponent) {
-      const { dashboardLayout: undoableLayout } = getState();
       const { present: layout } = undoableLayout;
       const sourceComponent = layout[source.id];
 
@@ -167,8 +239,36 @@ export function handleComponentDrop(dropResult) {
       }
     }
 
-    dispatch(setUnsavedChanges(true));
+    if (!dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
 
     return null;
   };
 }
+
+// Undo redo ------------------------------------------------------------------
+export function undoLayoutAction() {
+  return (dispatch, getState) => {
+    dispatch(UndoActionCreators.undo());
+
+    const { dashboardLayout, dashboardState } = getState();
+
+    if (
+      dashboardLayout.past.length === 0 &&
+      !dashboardState.maxUndoHistoryExceeded
+    ) {
+      dispatch(setUnsavedChanges(false));
+    }
+  };
+}
+
+export function redoLayoutAction() {
+  return (dispatch, getState) => {
+    dispatch(UndoActionCreators.redo());
+
+    if (!getState().dashboardState.hasUnsavedChanges) {
+      dispatch(setUnsavedChanges(true));
+    }
+  };
+}
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index d80ec83..10c0a26 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -1,10 +1,12 @@
 /* eslint camelcase: 0 */
 import $ from 'jquery';
+import { ActionCreators as UndoActionCreators } from 'redux-undo';
 
 import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
 import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
 import { applyDefaultFormData } from '../../explore/stores/store';
+import { addWarningToast } from './messageToasts';
 
 export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
 export function setUnsavedChanges(hasUnsavedChanges) {
@@ -21,11 +23,6 @@ export function removeFilter(sliceId, col, vals, refresh = true) {
   return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
 }
 
-export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
-export function updateDashboardTitle(title) {
-  return { type: UPDATE_DASHBOARD_TITLE, title };
-}
-
 export const ADD_SLICE = 'ADD_SLICE';
 export function addSlice(slice) {
   return { type: ADD_SLICE, slice };
@@ -84,6 +81,14 @@ export function onSave() {
   return { type: ON_SAVE };
 }
 
+export function saveDashboard() {
+  return dispatch => {
+    dispatch(onSave());
+    // clear layout undo history
+    dispatch(UndoActionCreators.clearHistory());
+  };
+}
+
 export function fetchCharts(chartList = [], force = false, interval = 0) {
   return (dispatch, getState) => {
     const timeout = getState().dashboardInfo.common.conf
@@ -168,9 +173,31 @@ export function addSliceToDashboard(id) {
   };
 }
 
-export function removeSliceFromDashboard(chart) {
+export function removeSliceFromDashboard(id) {
   return dispatch => {
-    dispatch(removeSlice(chart.id));
-    dispatch(removeChart(chart.id));
+    dispatch(removeSlice(id));
+    dispatch(removeChart(id));
+  };
+}
+
+// Undo history ---------------------------------------------------------------
+export const SET_MAX_UNDO_HISTORY_EXCEEDED = 'SET_MAX_UNDO_HISTORY_EXCEEDED';
+export function setMaxUndoHistoryExceeded(maxUndoHistoryExceeded = true) {
+  return {
+    type: SET_MAX_UNDO_HISTORY_EXCEEDED,
+    payload: { maxUndoHistoryExceeded },
+  };
+}
+
+export function maxUndoHistoryToast() {
+  return (dispatch, getState) => {
+    const { dashboardLayout } = getState();
+    const historyLength = dashboardLayout.past.length;
+
+    return dispatch(
+      addWarningToast(
+        `You have used all ${historyLength} undo slots and will not be able to fully undo subsequent actions. You may save your current state to reset the history.`,
+      ),
+    );
   };
 }
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index 6922753..37781f9 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -1,41 +1,6 @@
 /* eslint camelcase: 0 */
-/* global notify */
 import $ from 'jquery';
 
-export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
-export function updateSliceName(key, sliceName) {
-  return { type: UPDATE_SLICE_NAME, key, sliceName };
-}
-
-export function saveSliceName(slice, sliceName) {
-  const oldName = slice.slice_name;
-  return dispatch => {
-    const sliceParams = {};
-    sliceParams.slice_id = slice.slice_id;
-    sliceParams.action = 'overwrite';
-    sliceParams.slice_name = sliceName;
-
-    const url = `${slice.slice_url}&${Object.keys(sliceParams)
-      .map(key => `${key}=${sliceParams[key]}`)
-      .join('&')}`;
-    const key = slice.slice_id;
-    return $.ajax({
-      url,
-      type: 'POST',
-      success: () => {
-        dispatch(updateSliceName(key, sliceName));
-        notify.success('This slice name was saved successfully.');
-      },
-      error: () => {
-        // if server-side reject the overwrite action,
-        // revert to old state
-        dispatch(updateSliceName(key, oldName));
-        notify.error("You don't have the rights to alter this slice");
-      },
-    });
-  };
-}
-
 export const SET_ALL_SLICES = 'SET_ALL_SLICES';
 export function setAllSlices(slices) {
   return { type: SET_ALL_SLICES, slices };
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index e5bc74c..b42650e 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -1,70 +1,106 @@
+/* eslint-env browser */
+import PropTypes from 'prop-types';
 import React from 'react';
 import cx from 'classnames';
+import { StickyContainer, Sticky } from 'react-sticky';
 
 import NewColumn from './gridComponents/new/NewColumn';
 import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
 import NewTabs from './gridComponents/new/NewTabs';
-import SliceAdderContainer from '../containers/SliceAdder';
+import SliceAdder from '../containers/SliceAdder';
+import { t } from '../../locales';
+
+const propTypes = {
+  topOffset: PropTypes.number,
+};
+
+const defaultProps = {
+  topOffset: 0,
+};
 
 class BuilderComponentPane extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      showSlices: false,
+      slideDirection: 'slide-out',
     };
 
-    this.openSlicesPane = this.showSlices.bind(this, true);
-    this.closeSlicesPane = this.showSlices.bind(this, false);
+    this.openSlicesPane = this.slide.bind(this, 'slide-in');
+    this.closeSlicesPane = this.slide.bind(this, 'slide-out');
   }
 
-  showSlices(show) {
+  slide(direction) {
     this.setState({
-      showSlices: show,
+      slideDirection: direction,
     });
   }
 
   render() {
+    const { topOffset } = this.props;
     return (
-      <div className="dashboard-builder-sidepane">
-        <div className="dashboard-builder-sidepane-header">
-          Insert components
-          {this.state.showSlices && (
-            <i
-              className="fa fa-times close trigger"
-              onClick={this.closeSlicesPane}
-              role="none"
-            />
-          )}
-        </div>
+      <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">
+                    {t('Saved components')}
+                  </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('Charts & filters')}
+                    </div>
 
-        <div className="component-layer">
-          <div
-            className="dragdroppable dragdroppable-row"
-            onClick={this.openSlicesPane}
-            role="none"
-          >
-            <div className="new-component static">
-              <div className="new-component-placeholder fa fa-area-chart" />
-              Chart
-              <i className="fa fa-arrow-right open trigger" />
-            </div>
-          </div>
+                    <i className="fa fa-arrow-right trigger" />
+                  </div>
 
-          <NewHeader />
-          <NewDivider />
-          <NewTabs />
-          <NewRow />
-          <NewColumn />
-        </div>
+                  <div className="dashboard-builder-sidepane-header">
+                    {t('Containers')}
+                  </div>
+                  <NewTabs />
+                  <NewRow />
+                  <NewColumn />
 
-        <div className={cx('slices-layer', this.state.showSlices && 'show')}>
-          <SliceAdderContainer />
-        </div>
-      </div>
+                  <div className="dashboard-builder-sidepane-header">
+                    {t('More components')}
+                  </div>
+                  <NewHeader />
+                  <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" />
+                    {t('All components')}
+                  </div>
+                  <SliceAdder height={calculatedHeight} />
+                </div>
+              </div>
+            </div>
+          )}
+        </Sticky>
+      </StickyContainer>
     );
   }
 }
 
+BuilderComponentPane.propTypes = propTypes;
+BuilderComponentPane.defaultProps = defaultProps;
+
 export default BuilderComponentPane;
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index 06b4f7f..07b6c33 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -2,11 +2,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import $ from 'jquery';
-import { DropdownButton } from 'react-bootstrap';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
 
 import RefreshIntervalModal from './RefreshIntervalModal';
 import SaveModal from './SaveModal';
-import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
 import { t } from '../../locales';
 
 function updateDom(css) {
@@ -28,6 +27,8 @@ function updateDom(css) {
 }
 
 const propTypes = {
+  addSuccessToast: PropTypes.func.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
   layout: PropTypes.object.isRequired,
@@ -100,23 +101,18 @@ class Controls extends React.PureComponent {
           id="bg-nested-dropdown"
           pullRight
         >
-          <ActionMenuItem
-            text={t('Force Refresh')}
-            tooltip={t('Force refresh the whole dashboard')}
-            onClick={forceRefreshAllCharts}
-          />
+          <MenuItem onClick={forceRefreshAllCharts}>
+            {t('Force refresh dashboard')}
+          </MenuItem>
           <RefreshIntervalModal
             onChange={refreshInterval =>
               startPeriodicRender(refreshInterval * 1000)
             }
-            triggerNode={
-              <MenuItemContent
-                text={t('Set autorefresh')}
-                tooltip={t('Set the auto-refresh interval for this session')}
-              />
-            }
+            triggerNode={<span>{t('Set auto-refresh interval')}</span>}
           />
           <SaveModal
+            addSuccessToast={this.props.addSuccessToast}
+            addDangerToast={this.props.addDangerToast}
             dashboardId={this.props.dashboardInfo.id}
             dashboardTitle={dashboardTitle}
             layout={layout}
@@ -124,33 +120,19 @@ class Controls extends React.PureComponent {
             expandedSlices={expandedSlices}
             onSave={onSave}
             css={this.state.css}
-            triggerNode={
-              <MenuItemContent
-                text={editMode ? t('Save') : t('Save as')}
-                tooltip={t('Save the dashboard')}
-              />
-            }
+            triggerNode={<span>{editMode ? t('Save') : t('Save as')}</span>}
             isMenuItem
           />
           {editMode && (
-            <ActionMenuItem
-              text={t('Edit properties')}
-              tooltip={t("Edit the dashboards's properties")}
-              onClick={() => {
-                window.location = `/dashboardmodelview/edit/${
-                  this.props.dashboardInfo.id
-                }`;
-              }}
-            />
+            <MenuItem
+              target="_blank"
+              href={`/dashboardmodelview/edit/${this.props.dashboardInfo.id}`}
+            >
+              {t('Edit dashboard metadata')}
+            </MenuItem>
           )}
           {editMode && (
-            <ActionMenuItem
-              text={t('Email')}
-              tooltip={t('Email a link to this dashboard')}
-              onClick={() => {
-                window.location = emailLink;
-              }}
-            />
+            <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
           )}
         </DropdownButton>
       </span>
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 2d85ebf..369ed46 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -27,7 +27,6 @@ import '../stylesheets/index.less';
 const propTypes = {
   actions: PropTypes.shape({
     addSliceToDashboard: PropTypes.func.isRequired,
-    onChange: PropTypes.func.isRequired,
     removeSliceFromDashboard: PropTypes.func.isRequired,
     runQuery: PropTypes.func.isRequired,
   }).isRequired,
@@ -98,16 +97,12 @@ class Dashboard extends React.PureComponent {
         key => currentChartIds.indexOf(key) === -1,
       );
       this.props.actions.addSliceToDashboard(newChartId);
-      this.props.actions.onChange();
     } else if (currentChartIds.length > nextChartIds.length) {
       // remove chart
       const removedChartId = currentChartIds.find(
         key => nextChartIds.indexOf(key) === -1,
       );
-      this.props.actions.removeSliceFromDashboard(
-        this.props.charts[removedChartId],
-      );
-      this.props.actions.onChange();
+      this.props.actions.removeSliceFromDashboard(removedChartId);
     }
   }
 
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 79eb35d..7f92948 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -1,8 +1,14 @@
+/* eslint-env browser */
 import cx from 'classnames';
-import React from 'react';
-import PropTypes from 'prop-types';
-import HTML5Backend from 'react-dnd-html5-backend';
 import { DragDropContext } from 'react-dnd';
+import HTML5Backend from 'react-dnd-html5-backend';
+// ParentSize uses resize observer so the dashboard will update size
+// when its container size changes, due to e.g., builder side panel opening
+import ParentSize from '@vx/responsive/build/components/ParentSize';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Sticky, StickyContainer } from 'react-sticky';
+import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
 
 import BuilderComponentPane from './BuilderComponentPane';
 import DashboardHeader from '../containers/DashboardHeader';
@@ -19,6 +25,8 @@ import {
   DASHBOARD_ROOT_DEPTH,
 } from '../util/constants';
 
+const TABS_HEIGHT = 47;
+
 const propTypes = {
   // redux
   dashboardLayout: PropTypes.object.isRequired,
@@ -52,31 +60,35 @@ class DashboardBuilder extends React.Component {
 
   handleChangeTab({ tabIndex }) {
     this.setState(() => ({ tabIndex }));
+    setTimeout(() => {
+      if (window)
+        window.scrollTo({
+          top: 0,
+          behavior: 'smooth',
+        });
+    }, 100);
   }
 
   render() {
-    const { tabIndex } = this.state;
     const {
       handleComponentDrop,
       dashboardLayout,
       deleteTopLevelTabs,
       editMode,
     } = this.props;
+
+    const { tabIndex } = this.state;
     const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
     const topLevelTabs =
       rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
 
-    const gridComponentId = topLevelTabs
-      ? topLevelTabs.children[
-          Math.min(topLevelTabs.children.length - 1, tabIndex)
-        ]
-      : DASHBOARD_GRID_ID;
-
-    const gridComponent = dashboardLayout[gridComponentId];
+    const childIds = topLevelTabs ? topLevelTabs.children : [DASHBOARD_GRID_ID];
 
     return (
-      <div className={cx('dashboard', editMode && 'dashboard--editing')}>
+      <StickyContainer
+        className={cx('dashboard', editMode && 'dashboard--editing')}
+      >
         {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
           <DashboardHeader />
         ) : (
@@ -99,38 +111,84 @@ class DashboardBuilder extends React.Component {
         )}
 
         {topLevelTabs && (
-          <WithPopoverMenu
-            shouldFocus={DashboardBuilder.shouldFocusTabs}
-            menuItems={[
-              <IconButton
-                className="fa fa-level-down"
-                label="Collapse tab content"
-                onClick={deleteTopLevelTabs}
-              />,
-            ]}
-            editMode={editMode}
-          >
-            <DashboardComponent
-              id={topLevelTabs.id}
-              parentId={DASHBOARD_ROOT_ID}
-              depth={DASHBOARD_ROOT_DEPTH + 1}
-              index={0}
-              renderTabContent={false}
-              onChangeTab={this.handleChangeTab}
-            />
-          </WithPopoverMenu>
+          <Sticky topOffset={50}>
+            {({ style }) => (
+              <WithPopoverMenu
+                shouldFocus={DashboardBuilder.shouldFocusTabs}
+                menuItems={[
+                  <IconButton
+                    className="fa fa-level-down"
+                    label="Collapse tab content"
+                    onClick={deleteTopLevelTabs}
+                  />,
+                ]}
+                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>
         )}
 
         <div className="dashboard-content">
-          <DashboardGrid
-            gridComponent={gridComponent}
-            depth={DASHBOARD_ROOT_DEPTH + 1}
-          />
+          <div className="grid-container">
+            <ParentSize>
+              {({ width }) => (
+                /*
+                  We use a TabContainer irrespective of whether top-level tabs exist to maintain
+                  a consistent React component tree. This avoids expensive mounts/unmounts of
+                  the entire dashboard upon adding/removing top-level tabs, which would otherwise
+                  happen because of React's diffing algorithm
+                */
+                <TabContainer
+                  id={DASHBOARD_GRID_ID}
+                  activeKey={tabIndex}
+                  onSelect={this.handleChangeTab}
+                  // 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}
+                >
+                  <TabContent>
+                    {childIds.map((id, index) => (
+                      // Matching the key of the first TabPane irrespective of topLevelTabs
+                      // lets us keep the same React component tree when !!topLevelTabs changes.
+                      // This avoids expensive mounts/unmounts of the entire dashboard.
+                      <TabPane
+                        key={index === 0 ? DASHBOARD_GRID_ID : id}
+                        eventKey={index}
+                      >
+                        <DashboardGrid
+                          gridComponent={dashboardLayout[id]}
+                          depth={DASHBOARD_ROOT_DEPTH + 1}
+                          width={width}
+                        />
+                      </TabPane>
+                    ))}
+                  </TabContent>
+                </TabContainer>
+              )}
+            </ParentSize>
+          </div>
+
           {this.props.editMode &&
-            this.props.showBuilderPane && <BuilderComponentPane />}
+            this.props.showBuilderPane && (
+              <BuilderComponentPane
+                topOffset={topLevelTabs ? TABS_HEIGHT : 0}
+              />
+            )}
         </div>
         <ToastPresenter />
-      </div>
+      </StickyContainer>
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index 3e6fc0c..77503bb 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -1,8 +1,5 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-// ParentSize uses resize observer so the dashboard will update size
-// when its container size changes, due to e.g., builder side panel opening
-import ParentSize from '@vx/responsive/build/components/ParentSize';
 
 import { componentShape } from '../util/propShapes';
 import DashboardComponent from '../containers/DashboardComponent';
@@ -16,6 +13,7 @@ const propTypes = {
   gridComponent: componentShape.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
   resizeComponent: PropTypes.func.isRequired,
+  width: PropTypes.number.isRequired,
 };
 
 const defaultProps = {};
@@ -28,6 +26,7 @@ class DashboardGrid extends React.PureComponent {
       rowGuideTop: null,
     };
 
+    this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
     this.handleResizeStart = this.handleResizeStart.bind(this);
     this.handleResize = this.handleResize.bind(this);
     this.handleResizeStop = this.handleResizeStop.bind(this);
@@ -77,100 +76,117 @@ 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, handleComponentDrop, depth, editMode } = this.props;
+    const {
+      gridComponent,
+      handleComponentDrop,
+      depth,
+      editMode,
+      width,
+    } = this.props;
+
+    const columnPlusGutterWidth =
+      (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
+
+    const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
     const { isResizing, rowGuideTop } = this.state;
 
-    return (
-      <div className="grid-container" ref={this.setGridRef}>
-        <ParentSize>
-          {({ width }) => {
-            const columnPlusGutterWidth =
-              (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
-            const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
-            return width < 50 ? null : (
-              <div className="grid-content">
-                {editMode && (
-                  <DragDroppable
-                    component={gridComponent}
-                    depth={depth}
-                    parentComponent={null}
-                    index={0}
-                    orientation="column"
-                    onDrop={handleComponentDrop}
-                    editMode
-                  >
-                    {({ dropIndicatorProps }) =>
-                      dropIndicatorProps && (
-                        <div className="drop-indicator drop-indicator--bottom" />
-                      )
-                    }
-                  </DragDroppable>
-                )}
-
-                {gridComponent.children.map((id, index) => (
-                  <DashboardComponent
-                    key={id}
-                    id={id}
-                    parentId={gridComponent.id}
-                    depth={depth + 1}
-                    index={index}
-                    availableColumnCount={GRID_COLUMN_COUNT}
-                    columnWidth={columnWidth}
-                    onResizeStart={this.handleResizeStart}
-                    onResize={this.handleResize}
-                    onResizeStop={this.handleResizeStop}
-                  />
-                ))}
-
-                {/* render an empty drop target */}
-                {editMode && (
-                  <DragDroppable
-                    component={gridComponent}
-                    depth={depth}
-                    parentComponent={null}
-                    index={gridComponent.children.length}
-                    orientation="column"
-                    onDrop={handleComponentDrop}
-                    className="empty-grid-droptarget"
-                    editMode
-                  >
-                    {({ dropIndicatorProps }) =>
-                      dropIndicatorProps && (
-                        <div className="drop-indicator drop-indicator--top" />
-                      )
-                    }
-                  </DragDroppable>
-                )}
-
-                {isResizing &&
-                  Array(GRID_COLUMN_COUNT)
-                    .fill(null)
-                    .map((_, i) => (
-                      <div
-                        key={`grid-column-${i}`}
-                        className="grid-column-guide"
-                        style={{
-                          left: i * GRID_GUTTER_SIZE + i * columnWidth,
-                          width: columnWidth,
-                        }}
-                      />
-                    ))}
-
-                {isResizing &&
-                  rowGuideTop && (
-                    <div
-                      className="grid-row-guide"
-                      style={{
-                        top: rowGuideTop,
-                        width,
-                      }}
-                    />
-                  )}
-              </div>
-            );
-          }}
-        </ParentSize>
+    return width < 100 ? null : (
+      <div className="dashboard-grid" ref={this.setGridRef}>
+        <div className="grid-content">
+          {/* empty drop target makes top droppable */}
+          {editMode && (
+            <DragDroppable
+              component={gridComponent}
+              depth={depth}
+              parentComponent={null}
+              index={0}
+              orientation="column"
+              onDrop={this.handleTopDropTargetDrop}
+              className="empty-grid-droptarget--top"
+              editMode
+            >
+              {({ dropIndicatorProps }) =>
+                dropIndicatorProps && (
+                  <div className="drop-indicator drop-indicator--bottom" />
+                )
+              }
+            </DragDroppable>
+          )}
+
+          {gridComponent.children.map((id, index) => (
+            <DashboardComponent
+              key={id}
+              id={id}
+              parentId={gridComponent.id}
+              depth={depth + 1}
+              index={index}
+              availableColumnCount={GRID_COLUMN_COUNT}
+              columnWidth={columnWidth}
+              onResizeStart={this.handleResizeStart}
+              onResize={this.handleResize}
+              onResizeStop={this.handleResizeStop}
+            />
+          ))}
+
+          {/* empty drop target makes bottom droppable */}
+          {editMode && (
+            <DragDroppable
+              component={gridComponent}
+              depth={depth}
+              parentComponent={null}
+              index={gridComponent.children.length}
+              orientation="column"
+              onDrop={handleComponentDrop}
+              className="empty-grid-droptarget--bottom"
+              editMode
+            >
+              {({ dropIndicatorProps }) =>
+                dropIndicatorProps && (
+                  <div className="drop-indicator drop-indicator--top" />
+                )
+              }
+            </DragDroppable>
+          )}
+
+          {isResizing &&
+            Array(GRID_COLUMN_COUNT)
+              .fill(null)
+              .map((_, i) => (
+                <div
+                  key={`grid-column-${i}`}
+                  className="grid-column-guide"
+                  style={{
+                    left: i * GRID_GUTTER_SIZE + i * columnWidth,
+                    width: columnWidth,
+                  }}
+                />
+              ))}
+
+          {isResizing &&
+            rowGuideTop && (
+              <div
+                className="grid-row-guide"
+                style={{
+                  top: rowGuideTop,
+                  width,
+                }}
+              />
+            )}
+        </div>
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 242102e..21b01db 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -6,12 +6,14 @@ import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-// import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
 import SaveModal from './SaveModal';
 import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
+import { UNDO_LIMIT } from '../util/constants';
 
 const propTypes = {
+  addSuccessToast: PropTypes.func.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
   charts: PropTypes.objectOf(chartPropShape).isRequired,
@@ -31,23 +33,45 @@ const propTypes = {
   showBuilderPane: PropTypes.bool.isRequired,
   toggleBuilderPane: PropTypes.func.isRequired,
   hasUnsavedChanges: PropTypes.bool.isRequired,
+  maxUndoHistoryExceeded: PropTypes.bool.isRequired,
 
   // redux
   onUndo: PropTypes.func.isRequired,
   onRedo: PropTypes.func.isRequired,
-  canUndo: PropTypes.bool.isRequired,
-  canRedo: PropTypes.bool.isRequired,
+  undoLength: PropTypes.number.isRequired,
+  redoLength: PropTypes.number.isRequired,
+  setMaxUndoHistoryExceeded: PropTypes.func.isRequired,
+  maxUndoHistoryToast: PropTypes.func.isRequired,
 };
 
 class Header extends React.PureComponent {
   constructor(props) {
     super(props);
+    this.state = {
+      didNotifyMaxUndoHistoryToast: false,
+    };
 
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
   }
 
+  componentWillReceiveProps(nextProps) {
+    if (
+      UNDO_LIMIT - nextProps.undoLength <= 0 &&
+      !this.state.didNotifyMaxUndoHistoryToast
+    ) {
+      this.setState(() => ({ didNotifyMaxUndoHistoryToast: true }));
+      this.props.maxUndoHistoryToast();
+    }
+    if (
+      nextProps.undoLength > UNDO_LIMIT &&
+      !this.props.maxUndoHistoryExceeded
+    ) {
+      this.props.setMaxUndoHistoryExceeded();
+    }
+  }
+
   forceRefresh() {
     return this.props.fetchCharts(Object.values(this.props.charts), true);
   }
@@ -72,8 +96,8 @@ class Header extends React.PureComponent {
       expandedSlices,
       onUndo,
       onRedo,
-      canUndo,
-      canRedo,
+      undoLength,
+      redoLength,
       onChange,
       onSave,
       editMode,
@@ -91,9 +115,9 @@ class Header extends React.PureComponent {
             title={dashboardTitle}
             canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
             onSaveTitle={this.handleChangeText}
-            showTooltip={editMode}
+            showTooltip={false}
           />
-          <span className="favstar m-r-5">
+          <span className="favstar m-l-5">
             <FaveStar
               itemId={this.props.dashboardInfo.id}
               fetchFaveStar={this.props.fetchFaveStar}
@@ -106,14 +130,22 @@ class Header extends React.PureComponent {
           {userCanEdit && (
             <ButtonGroup>
               {editMode && (
-                <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
-                  Undo
+                <Button
+                  bsSize="small"
+                  onClick={onUndo}
+                  disabled={undoLength < 1}
+                >
+                  <div title="Undo" className="undo-action fa fa-reply" />
                 </Button>
               )}
 
               {editMode && (
-                <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
-                  Redo
+                <Button
+                  bsSize="small"
+                  onClick={onRedo}
+                  disabled={redoLength < 1}
+                >
+                  <div title="Redo" className="redo-action fa fa-share" />
                 </Button>
               )}
 
@@ -135,6 +167,8 @@ class Header extends React.PureComponent {
                 </Button>
               ) : (
                 <SaveModal
+                  addSuccessToast={this.props.addSuccessToast}
+                  addDangerToast={this.props.addDangerToast}
                   dashboardId={this.props.dashboardInfo.id}
                   dashboardTitle={dashboardTitle}
                   layout={layout}
@@ -154,6 +188,8 @@ class Header extends React.PureComponent {
           )}
 
           <Controls
+            addSuccessToast={this.props.addSuccessToast}
+            addDangerToast={this.props.addDangerToast}
             dashboardInfo={this.props.dashboardInfo}
             dashboardTitle={dashboardTitle}
             layout={layout}
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 41c6364..1d287d6 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,4 +1,4 @@
-/* global notify, window */
+/* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
 import $ from 'jquery';
@@ -10,6 +10,8 @@ import { t } from '../../locales';
 import Checkbox from '../../components/Checkbox';
 
 const propTypes = {
+  addSuccessToast: PropTypes.func.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
   dashboardId: PropTypes.number.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
   expandedSlices: PropTypes.object.isRequired,
@@ -61,31 +63,31 @@ class SaveModal extends React.PureComponent {
     });
   }
 
+  // @TODO this should all be moved to actions
   saveDashboardRequest(data, url, saveType) {
-    const saveModal = this.modal;
-    const onSaveDashboard = this.props.onSave;
     $.ajax({
       type: 'POST',
       url,
       data: {
         data: JSON.stringify(data),
       },
-      success(resp) {
-        saveModal.close();
-        onSaveDashboard();
+      success: resp => {
+        this.modal.close();
+        this.props.onSave();
         if (saveType === 'newDashboard') {
           window.location = `/superset/dashboard/${resp.id}/`;
         } else {
-          notify.success(t('This dashboard was saved successfully.'));
+          this.props.addSuccessToast(
+            t('This dashboard was saved successfully.'),
+          );
         }
       },
-      error(error) {
-        saveModal.close();
+      error: error => {
+        this.modal.close();
         const errorMsg = getAjaxErrorMsg(error);
-        notify.error(
-          `${t(
-            'Sorry, there was an error saving this dashboard: ',
-          )} ${errorMsg}`,
+        this.props.addDangerToast(
+          `${t('Sorry, there was an error saving this dashboard: ')}
+          ${errorMsg}`,
         );
       },
     });
@@ -115,7 +117,9 @@ class SaveModal extends React.PureComponent {
       this.saveDashboardRequest(data, url, saveType);
     } else if (saveType === 'newDashboard') {
       if (!newDashName) {
-        notify.error('You must pick a name for the new dashboard');
+        this.props.addDangerToast(
+          t('You must pick a name for the new dashboard'),
+        );
       } else {
         data.dashboard_title = newDashName;
         url = `/superset/copy_dash/${dashboardId}/`;
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 37ce21f..05c4270 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,3 +1,4 @@
+/* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
@@ -20,12 +21,14 @@ const propTypes = {
   userId: PropTypes.string.isRequired,
   selectedSliceIds: PropTypes.object,
   editMode: PropTypes.bool,
+  height: PropTypes.number,
 };
 
 const defaultProps = {
   selectedSliceIds: new Set(),
   editMode: false,
   errorMessage: '',
+  height: window.innerHeight,
 };
 
 const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
@@ -179,6 +182,7 @@ class SliceAdder extends React.Component {
           </DropdownButton>
 
           <SearchInput
+            className="search-input"
             onChange={this.searchUpdated}
             onKeyPress={this.handleKeyPress}
           />
@@ -198,7 +202,7 @@ class SliceAdder extends React.Component {
           this.state.filteredSlices.length > 0 && (
             <List
               width={376}
-              height={500}
+              height={this.props.height}
               rowCount={this.state.filteredSlices.length}
               rowHeight={136}
               rowRenderer={this.rowRenderer}
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index bcdaedf..0c572d8 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -20,6 +20,7 @@ const propTypes = {
   editMode: PropTypes.bool,
   annotationQuery: PropTypes.object,
   annotationError: PropTypes.object,
+  sliceName: PropTypes.string,
 };
 
 const defaultProps = {
@@ -36,21 +37,10 @@ const defaultProps = {
   cachedDttm: null,
   isCached: false,
   isExpanded: false,
+  sliceName: '',
 };
 
 class SliceHeader extends React.PureComponent {
-  constructor(props) {
-    super(props);
-
-    this.onSaveTitle = this.onSaveTitle.bind(this);
-  }
-
-  onSaveTitle(newTitle) {
-    if (this.props.updateSliceName) {
-      this.props.updateSliceName(this.props.slice.slice_id, newTitle);
-    }
-  }
-
   render() {
     const {
       slice,
@@ -62,6 +52,7 @@ class SliceHeader extends React.PureComponent {
       exploreChart,
       exportCSV,
       innerRef,
+      sliceName,
     } = this.props;
 
     const annoationsLoading = t('Annotation layers are still loading.');
@@ -71,13 +62,10 @@ class SliceHeader extends React.PureComponent {
       <div className="chart-header" ref={innerRef}>
         <div className="header">
           <EditableTitle
-            title={slice.slice_name}
-            canEdit={!!this.props.updateSliceName && this.props.editMode}
-            onSaveTitle={this.onSaveTitle}
-            noPermitTooltip={
-              "You don't have the rights to alter this dashboard."
-            }
-            showTooltip={!!this.props.updateSliceName && this.props.editMode}
+            title={sliceName}
+            canEdit={this.props.editMode}
+            onSaveTitle={this.props.updateSliceName}
+            showTooltip={this.props.editMode}
           />
           {!!Object.values(this.props.annotationQuery).length && (
             <TooltipWrapper
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index 0dae6f8..e793bc2 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -2,9 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 import moment from 'moment';
-import { DropdownButton } from 'react-bootstrap';
+import { Dropdown, MenuItem } from 'react-bootstrap';
 
-import { ActionMenuItem } from './ActionMenuItem';
 import { t } from '../../locales';
 
 const propTypes = {
@@ -28,6 +27,14 @@ const defaultProps = {
   isExpanded: false,
 };
 
+const VerticalDotsTrigger = () => (
+  <div className="vertical-dots-container">
+    <span className="dot" />
+    <span className="dot" />
+    <span className="dot" />
+  </div>
+);
+
 class SliceHeaderControls extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -57,56 +64,47 @@ class SliceHeaderControls extends React.PureComponent {
     const slice = this.props.slice;
     const isCached = this.props.isCached;
     const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
-    const refreshTooltip = isCached
-      ? t('Served from data cached %s . Click to force refresh.', cachedWhen)
-      : t('Force refresh data');
+    const refreshTooltip = isCached ? t('Cached %s', cachedWhen) : '';
 
     // @TODO account for
     //  dashboard.dashboard.superset_can_explore
     //  dashboard.dashboard.slice_can_edit
     return (
-      <DropdownButton
-        title=""
+      <Dropdown
         id={`slice_${slice.slice_id}-controls`}
-        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', {
-          'is-cached': isCached,
-        })}
+        className={cx(isCached && 'is-cached')}
         pullRight
-        noCaret
       >
-        <ActionMenuItem
-          text={t('Force refresh data')}
-          tooltip={refreshTooltip}
-          onClick={this.props.forceRefresh}
-        />
-
-        {slice.description && (
-          <ActionMenuItem
-            text={t('Toggle chart description')}
-            tooltip={t('Toggle chart description')}
-            onClick={this.toggleExpandSlice}
-          />
-        )}
-
-        <ActionMenuItem
-          text={t('Edit chart')}
-          tooltip={t("Edit the chart's properties")}
-          href={slice.edit_url}
-          target="_blank"
-        />
-
-        <ActionMenuItem
-          text={t('Export CSV')}
-          tooltip={t('Export CSV')}
-          onClick={this.exportCSV}
-        />
-
-        <ActionMenuItem
-          text={t('Explore chart')}
-          tooltip={t('Explore chart')}
-          onClick={this.exploreChart}
-        />
-      </DropdownButton>
+        <Dropdown.Toggle className="slice-header-controls-trigger" noCaret>
+          <VerticalDotsTrigger />
+        </Dropdown.Toggle>
+
+        <Dropdown.Menu>
+          <MenuItem onClick={this.props.forceRefresh}>
+            {isCached && <span className="dot" />}
+            {t('Force refresh')}
+            {isCached && (
+              <div className="refresh-tooltip">{refreshTooltip}</div>
+            )}
+          </MenuItem>
+
+          <MenuItem divider />
+
+          {slice.description && (
+            <MenuItem onClick={this.toggleExpandSlice}>
+              {t('Toggle chart description')}
+            </MenuItem>
+          )}
+
+          <MenuItem href={slice.edit_url} target="_blank">
+            {t('Edit chart metadata')}
+          </MenuItem>
+
+          <MenuItem onClick={this.exportCSV}>{t('Export CSV')}</MenuItem>
+
+          <MenuItem onClick={this.exploreChart}>{t('Explore chart')}</MenuItem>
+        </Dropdown.Menu>
+      </Dropdown>
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
index 94cab42..91fc055 100644
--- a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
+++ b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
@@ -9,6 +9,16 @@ import {
   CHART_TYPE,
 } from '../../util/componentTypes';
 
+const staticCardStyles = {
+  position: 'fixed',
+  background: 'white',
+  pointerEvents: 'none',
+  top: 0,
+  left: 0,
+  zIndex: 100,
+  width: 376 - 2 * 16,
+};
+
 const propTypes = {
   dragItem: PropTypes.shape({
     index: PropTypes.number.isRequired,
@@ -41,12 +51,7 @@ function AddSliceDragPreview({ dragItem, slices, isDragging, currentOffset }) {
   return !shouldRender ? null : (
     <AddSliceCard
       style={{
-        position: 'fixed',
-        background: 'white',
-        pointerEvents: 'none',
-        top: 0,
-        left: 0,
-        zIndex: 100,
+        ...staticCardStyles,
         transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
       }}
       sliceName={slice.slice_name}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 54e1536..4742d71 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -13,16 +13,17 @@ const propTypes = {
   id: PropTypes.number.isRequired,
   width: PropTypes.number.isRequired,
   height: PropTypes.number.isRequired,
+  updateSliceName: PropTypes.func.isRequired,
 
   // from redux
   chart: PropTypes.shape(chartPropType).isRequired,
   formData: PropTypes.object.isRequired,
   datasource: PropTypes.object.isRequired,
   slice: slicePropShape.isRequired,
+  sliceName: PropTypes.string.isRequired,
   timeout: PropTypes.number.isRequired,
   filters: PropTypes.object.isRequired,
   refreshChart: PropTypes.func.isRequired,
-  saveSliceName: PropTypes.func.isRequired,
   toggleExpandSlice: PropTypes.func.isRequired,
   addFilter: PropTypes.func.isRequired,
   removeFilter: PropTypes.func.isRequired,
@@ -150,6 +151,8 @@ class Chart extends React.Component {
       isExpanded,
       editMode,
       formData,
+      updateSliceName,
+      sliceName,
       toggleExpandSlice,
       timeout,
     } = this.props;
@@ -161,25 +164,21 @@ class Chart extends React.Component {
     const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice && slice.viz_type);
 
     return (
-      <div
-        className={cx(
-          'dashboard-chart',
-          isOverflowable && 'dashboard-chart--overflowable',
-        )}
-      >
+      <div>
         <SliceHeader
           innerRef={this.setHeaderRef}
           slice={slice}
           isExpanded={!!isExpanded}
           isCached={isCached}
           cachedDttm={cachedDttm}
-          updateSliceName={this.updateSliceName}
           toggleExpandSlice={toggleExpandSlice}
           forceRefresh={this.forceRefresh}
           editMode={editMode}
           annotationQuery={chart.annotationQuery}
           exploreChart={this.exploreChart}
           exportCSV={this.exportCSV}
+          updateSliceName={updateSliceName}
+          sliceName={sliceName}
         />
 
         {/*
@@ -199,30 +198,37 @@ class Chart extends React.Component {
             />
           )}
 
-        <ChartContainer
-          containerId={`slice-container-${id}`}
-          chartId={id}
-          datasource={datasource}
-          formData={formData}
-          headerHeight={this.getHeaderHeight()}
-          height={this.getChartHeight()}
-          width={width}
-          timeout={timeout}
-          vizType={slice.viz_type}
-          addFilter={this.addFilter}
-          getFilters={this.getFilters}
-          removeFilter={this.removeFilter}
-          annotationData={chart.annotationData}
-          chartAlert={chart.chartAlert}
-          chartStatus={chart.chartStatus}
-          chartUpdateEndTime={chart.chartUpdateEndTime}
-          chartUpdateStartTime={chart.chartUpdateStartTime}
-          latestQueryFormData={chart.latestQueryFormData}
-          lastRendered={chart.lastRendered}
-          queryResponse={chart.queryResponse}
-          queryRequest={chart.queryRequest}
-          triggerQuery={chart.triggerQuery}
-        />
+        <div
+          className={cx(
+            'dashboard-chart',
+            isOverflowable && 'dashboard-chart--overflowable',
+          )}
+        >
+          <ChartContainer
+            containerId={`slice-container-${id}`}
+            chartId={id}
+            datasource={datasource}
+            formData={formData}
+            headerHeight={this.getHeaderHeight()}
+            height={this.getChartHeight()}
+            width={width}
+            timeout={timeout}
+            vizType={slice.viz_type}
+            addFilter={this.addFilter}
+            getFilters={this.getFilters}
+            removeFilter={this.removeFilter}
+            annotationData={chart.annotationData}
+            chartAlert={chart.chartAlert}
+            chartStatus={chart.chartStatus}
+            chartUpdateEndTime={chart.chartUpdateEndTime}
+            chartUpdateStartTime={chart.chartUpdateStartTime}
+            latestQueryFormData={chart.latestQueryFormData}
+            lastRendered={chart.lastRendered}
+            queryResponse={chart.queryResponse}
+            queryRequest={chart.queryRequest}
+            triggerQuery={chart.triggerQuery}
+          />
+        </div>
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index a684230..bc9f430 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
 import Chart from '../../containers/Chart';
 import DeleteComponentButton from '../DeleteComponentButton';
 import DragDroppable from '../dnd/DragDroppable';
-import DragHandle from '../dnd/DragHandle';
 import HoverMenu from '../menu/HoverMenu';
 import ResizableContainer from '../resizable/ResizableContainer';
 import { componentShape } from '../../util/propShapes';
@@ -35,6 +34,7 @@ const propTypes = {
 
   // dnd
   deleteComponent: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
@@ -49,6 +49,7 @@ class ChartHolder extends React.Component {
 
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this);
   }
 
   handleChangeFocus(nextFocus) {
@@ -60,6 +61,19 @@ class ChartHolder extends React.Component {
     deleteComponent(id, parentId);
   }
 
+  handleUpdateSliceName(nextName) {
+    const { component, updateComponents } = this.props;
+    updateComponents({
+      [component.id]: {
+        ...component,
+        meta: {
+          ...component.meta,
+          chartName: nextName,
+        },
+      },
+    });
+  }
+
   render() {
     const { isFocused } = this.state;
 
@@ -119,10 +133,11 @@ class ChartHolder extends React.Component {
                 id={component.meta.chartId}
                 width={widthMultiple * columnWidth}
                 height={component.meta.height * GRID_BASE_UNIT - CHART_MARGIN}
+                sliceName={component.meta.chartName}
+                updateSliceName={this.handleUpdateSliceName}
               />
               {editMode && (
                 <HoverMenu position="top">
-                  <DragHandle position="top" />
                   <DeleteComponentButton
                     onDelete={this.handleDeleteComponent}
                   />
diff --git a/superset/assets/src/dashboard/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
index a71d732..7249034 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
@@ -142,6 +142,18 @@ class Column extends React.PureComponent {
               ]}
               editMode={editMode}
             >
+              {editMode && (
+                <HoverMenu innerRef={dragSourceRef} position="top">
+                  <DragHandle position="top" />
+                  <DeleteComponentButton
+                    onDelete={this.handleDeleteComponent}
+                  />
+                  <IconButton
+                    onClick={this.handleChangeFocus}
+                    className="fa fa-cog"
+                  />
+                </HoverMenu>
+              )}
               <div
                 className={cx(
                   'grid-column',
@@ -149,19 +161,6 @@ class Column extends React.PureComponent {
                   backgroundStyle.className,
                 )}
               >
-                {editMode && (
-                  <HoverMenu innerRef={dragSourceRef} position="top">
-                    <DragHandle position="top" />
-                    <DeleteComponentButton
-                      onDelete={this.handleDeleteComponent}
-                    />
-                    <IconButton
-                      onClick={this.handleChangeFocus}
-                      className="fa fa-cog"
-                    />
-                  </HoverMenu>
-                )}
-
                 {columnItems.map((componentId, itemIndex) => (
                   <DashboardComponent
                     key={componentId}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
index 91f200d..3119a08 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
@@ -128,6 +128,16 @@ class Row extends React.PureComponent {
             ]}
             editMode={editMode}
           >
+            {editMode && (
+              <HoverMenu innerRef={dragSourceRef} position="left">
+                <DragHandle position="left" />
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                <IconButton
+                  onClick={this.handleChangeFocus}
+                  className="fa fa-cog"
+                />
+              </HoverMenu>
+            )}
             <div
               className={cx(
                 'grid-row',
@@ -135,19 +145,6 @@ class Row extends React.PureComponent {
                 backgroundStyle.className,
               )}
             >
-              {editMode && (
-                <HoverMenu innerRef={dragSourceRef} position="left">
-                  <DragHandle position="left" />
-                  <DeleteComponentButton
-                    onDelete={this.handleDeleteComponent}
-                  />
-                  <IconButton
-                    onClick={this.handleChangeFocus}
-                    className="fa fa-cog"
-                  />
-                </HoverMenu>
-              )}
-
               {rowItems.map((componentId, itemIndex) => (
                 <DashboardComponent
                   key={componentId}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index d73bc0c..63619c1 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -136,7 +136,7 @@ export default class Tab extends React.PureComponent {
         // disable drag drop of top-level Tab's to prevent invalid nesting of a child in
         // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
         // reusult in circular children
-        disableDragDrop={isFocused || depth === DASHBOARD_ROOT_DEPTH + 1}
+        disableDragDrop={depth === DASHBOARD_ROOT_DEPTH + 1}
         editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 585041f..813961d 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -164,7 +164,11 @@ class Tabs extends React.PureComponent {
               id={tabsComponent.id}
               activeKey={selectedTabIndex}
               onSelect={this.handleClickTab}
-              animation={false}
+              // 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}
             >
               {tabIds.map((tabId, tabIndex) => (
                 // react-bootstrap doesn't render a Tab if we move this to its own Tab.jsx so we
@@ -187,27 +191,21 @@ class Tabs extends React.PureComponent {
                     />
                   }
                 >
-                  {/*
-                    react-bootstrap renders all children with display:none, so we don't
-                    render potentially-expensive charts (this also enables lazy loading
-                    their content)
-                  */}
-                  {tabIndex === selectedTabIndex &&
-                    renderTabContent && (
-                      <DashboardComponent
-                        id={tabId}
-                        parentId={tabsComponent.id}
-                        depth={depth} // see isValidChild.js for why tabs don't increment child depth
-                        index={tabIndex}
-                        renderType={RENDER_TAB_CONTENT}
-                        availableColumnCount={availableColumnCount}
-                        columnWidth={columnWidth}
-                        onResizeStart={onResizeStart}
-                        onResize={onResize}
-                        onResizeStop={onResizeStop}
-                        onDropOnTab={this.handleDropOnTab}
-                      />
-                    )}
+                  {renderTabContent && (
+                    <DashboardComponent
+                      id={tabId}
+                      parentId={tabsComponent.id}
+                      depth={depth} // see isValidChild.js for why tabs don't increment child depth
+                      index={tabIndex}
+                      renderType={RENDER_TAB_CONTENT}
+                      availableColumnCount={availableColumnCount}
+                      columnWidth={columnWidth}
+                      onResizeStart={onResizeStart}
+                      onResize={onResize}
+                      onResizeStop={onResizeStop}
+                      onDropOnTab={this.handleDropOnTab}
+                    />
+                  )}
                 </BootstrapTab>
               ))}
 
diff --git a/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
index 8a87fca..2a047ac 100644
--- a/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
@@ -10,6 +10,7 @@ const propTypes = {
   isFocused: PropTypes.bool,
   shouldFocus: PropTypes.func,
   editMode: PropTypes.bool.isRequired,
+  style: PropTypes.object,
 };
 
 const defaultProps = {
@@ -20,6 +21,7 @@ const defaultProps = {
   menuItems: [],
   isFocused: false,
   shouldFocus: (event, container) => container.contains(event.target),
+  style: null,
 };
 
 class WithPopoverMenu extends React.PureComponent {
@@ -84,7 +86,7 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   render() {
-    const { children, menuItems, editMode } = this.props;
+    const { children, menuItems, editMode, style } = this.props;
     const { isFocused } = this.state;
 
     return (
@@ -96,6 +98,7 @@ class WithPopoverMenu extends React.PureComponent {
           'with-popover-menu',
           editMode && isFocused && 'with-popover-menu--focused',
         )}
+        style={style}
       >
         {children}
         {editMode &&
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 470176b..61627d2 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -8,7 +8,7 @@ import {
 } from '../actions/dashboardState';
 import { refreshChart } from '../../chart/chartAction';
 import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
-import { saveSliceName } from '../actions/sliceEntities';
+import { updateComponents } from '../actions/dashboardLayout';
 import Chart from '../components/gridComponents/Chart';
 
 function mapStateToProps(
@@ -46,7 +46,7 @@ function mapStateToProps(
 function mapDispatchToProps(dispatch) {
   return bindActionCreators(
     {
-      saveSliceName,
+      updateComponents,
       toggleExpandSlice,
       addFilter,
       refreshChart,
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
index 9af0e81..bcf2ace 100644
--- a/superset/assets/src/dashboard/containers/Dashboard.jsx
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
 import {
   addSliceToDashboard,
   removeSliceFromDashboard,
-  onChange,
 } from '../actions/dashboardState';
 import { runQuery } from '../../chart/chartAction';
 import Dashboard from '../components/Dashboard';
@@ -37,7 +36,6 @@ function mapDispatchToProps(dispatch) {
     actions: bindActionCreators(
       {
         addSliceToDashboard,
-        onChange,
         removeSliceFromDashboard,
         runQuery,
       },
diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 650313e..29071cb 100644
--- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -26,13 +26,7 @@ const propTypes = {
 };
 
 function mapStateToProps(
-  {
-    dashboardLayout: undoableLayout,
-    dashboardState,
-    sliceEntities,
-    charts,
-    datasources,
-  },
+  { dashboardLayout: undoableLayout, dashboardState },
   ownProps,
 ) {
   const dashboardLayout = undoableLayout.present;
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index 2b3431a..fe7e7bb 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -1,8 +1,8 @@
-import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
 import DashboardHeader from '../components/Header';
+
 import {
   setEditMode,
   toggleBuilderPane,
@@ -10,11 +10,21 @@ import {
   saveFaveStar,
   fetchCharts,
   startPeriodicRender,
-  updateDashboardTitle,
   onChange,
-  onSave,
+  saveDashboard,
+  setMaxUndoHistoryExceeded,
+  maxUndoHistoryToast,
 } from '../actions/dashboardState';
-import { handleComponentDrop } from '../actions/dashboardLayout';
+
+import {
+  undoLayoutAction,
+  redoLayoutAction,
+  updateDashboardTitle,
+} from '../actions/dashboardLayout';
+
+import { addSuccessToast, addDangerToast } from '../actions/messageToasts';
+
+import { DASHBOARD_HEADER_ID } from '../util/constants';
 
 function mapStateToProps({
   dashboardLayout: undoableLayout,
@@ -24,16 +34,19 @@ function mapStateToProps({
 }) {
   return {
     dashboardInfo,
-    canUndo: undoableLayout.past.length > 0,
-    canRedo: undoableLayout.future.length > 0,
+    undoLength: undoableLayout.past.length,
+    redoLength: undoableLayout.future.length,
     layout: undoableLayout.present,
     filters: dashboard.filters,
-    dashboardTitle: dashboard.title,
+    dashboardTitle: (
+      (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
+    ).text,
     expandedSlices: dashboard.expandedSlices,
     charts,
     userId: dashboardInfo.userId,
     isStarred: !!dashboard.isStarred,
     hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
+    maxUndoHistoryExceeded: !!dashboard.maxUndoHistoryExceeded,
     editMode: !!dashboard.editMode,
     showBuilderPane: !!dashboard.showBuilderPane,
   };
@@ -42,9 +55,10 @@ function mapStateToProps({
 function mapDispatchToProps(dispatch) {
   return bindActionCreators(
     {
-      handleComponentDrop,
-      onUndo: UndoActionCreators.undo,
-      onRedo: UndoActionCreators.redo,
+      addSuccessToast,
+      addDangerToast,
+      onUndo: undoLayoutAction,
+      onRedo: redoLayoutAction,
       setEditMode,
       toggleBuilderPane,
       fetchFaveStar,
@@ -53,7 +67,9 @@ function mapDispatchToProps(dispatch) {
       startPeriodicRender,
       updateDashboardTitle,
       onChange,
-      onSave,
+      onSave: saveDashboard,
+      setMaxUndoHistoryExceeded,
+      maxUndoHistoryToast,
     },
     dispatch,
   );
diff --git a/superset/assets/src/dashboard/containers/SliceAdder.js b/superset/assets/src/dashboard/containers/SliceAdder.js
new file mode 100644
index 0000000..e3d931d
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/SliceAdder.js
@@ -0,0 +1,28 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { fetchAllSlices } from '../actions/sliceEntities';
+import SliceAdder from '../components/SliceAdder';
+
+function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
+  return {
+    userId: dashboardInfo.userId,
+    selectedSliceIds: dashboardState.sliceIds,
+    slices: sliceEntities.slices,
+    isLoading: sliceEntities.isLoading,
+    errorMessage: sliceEntities.errorMessage,
+    lastUpdated: sliceEntities.lastUpdated,
+    editMode: dashboardState.editMode,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators(
+    {
+      fetchAllSlices,
+    },
+    dispatch,
+  );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 7b5a17a..2d44399 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -9,6 +9,7 @@ import {
   REMOVE_SLICE,
   REMOVE_FILTER,
   SET_EDIT_MODE,
+  SET_MAX_UNDO_HISTORY_EXCEEDED,
   SET_UNSAVED_CHANGES,
   TOGGLE_BUILDER_PANE,
   TOGGLE_EXPAND_SLICE,
@@ -55,6 +56,10 @@ export default function dashboardStateReducer(state = {}, action) {
     [SET_EDIT_MODE]() {
       return { ...state, editMode: action.editMode };
     },
+    [SET_MAX_UNDO_HISTORY_EXCEEDED]() {
+      const { maxUndoHistoryExceeded = true } = action.payload;
+      return { ...state, maxUndoHistoryExceeded };
+    },
     [TOGGLE_BUILDER_PANE]() {
       return { ...state, showBuilderPane: !state.showBuilderPane };
     },
@@ -72,7 +77,11 @@ export default function dashboardStateReducer(state = {}, action) {
       return { ...state, hasUnsavedChanges: true };
     },
     [ON_SAVE]() {
-      return { ...state, hasUnsavedChanges: false };
+      return {
+        ...state,
+        hasUnsavedChanges: false,
+        maxUndoHistoryExceeded: false,
+      };
     },
 
     // filters
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index d0b4d7b..ba24b36 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -7,7 +7,8 @@ import { getParam } from '../../modules/utils';
 import { applyDefaultFormData } from '../../explore/stores/store';
 import { getColorFromScheme } from '../../modules/colors';
 import layoutConverter from '../util/dashboardLayoutConverter';
-import { DASHBOARD_ROOT_ID } from '../util/constants';
+import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
+import { DASHBOARD_HEADER_TYPE, CHART_TYPE } from '../util/componentTypes';
 
 export default function(bootstrapData) {
   const { user_id, datasources, common } = bootstrapData;
@@ -35,22 +36,39 @@ export default function(bootstrapData) {
   }
 
   // dashboard layout
-  const positionJson = dashboard.position_json;
-  let layout;
-  if (!positionJson || !positionJson[DASHBOARD_ROOT_ID]) {
-    layout = layoutConverter(dashboard);
-  } else {
-    layout = positionJson;
-  }
+  const { position_json: positionJson } = dashboard;
+
+  const layout =
+    !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2'
+      ? layoutConverter(dashboard)
+      : positionJson;
+
+  // store the header as a layout component so we can undo/redo changes
+  layout[DASHBOARD_HEADER_ID] = {
+    id: DASHBOARD_HEADER_ID,
+    type: DASHBOARD_HEADER_TYPE,
+    meta: {
+      text: dashboard.dashboard_title,
+    },
+  };
 
   const dashboardLayout = {
     past: [],
     present: layout,
     future: [],
   };
+
   delete dashboard.position_json;
   delete dashboard.css;
 
+  // creat a lookup to sync layout names with slice names
+  const chartIdToLayoutId = {};
+  Object.values(layout).forEach(layoutComponent => {
+    if (layoutComponent.type === CHART_TYPE) {
+      chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id;
+    }
+  });
+
   const chartQueries = {};
   const slices = {};
   const sliceIds = new Set();
@@ -76,6 +94,14 @@ export default function(bootstrapData) {
     };
 
     sliceIds.add(key);
+
+    // sync layout names with current slice names in case a slice was edited
+    // in explore since the layout was updated. name updates go through layout for undo/redo
+    // functionality and python updates slice names based on layout upon dashboard save
+    const layoutId = chartIdToLayoutId[key];
+    if (layoutId && layout[layoutId]) {
+      layout[layoutId].meta.chartName = slice.slice_name;
+    }
   });
 
   return {
@@ -99,7 +125,6 @@ export default function(bootstrapData) {
       common,
     },
     dashboardState: {
-      title: dashboard.dashboard_title,
       sliceIds,
       refresh: false,
       filters,
@@ -107,6 +132,7 @@ export default function(bootstrapData) {
       editMode: false,
       showBuilderPane: false,
       hasUnsavedChanges: false,
+      maxUndoHistoryExceeded: false,
     },
     dashboardLayout,
     messageToasts: [],
diff --git a/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
index b78c273..45e36ee 100644
--- a/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
@@ -1,4 +1,5 @@
 import undoable, { includeAction } from 'redux-undo';
+import { UNDO_LIMIT } from '../util/constants';
 import {
   UPDATE_COMPONENTS,
   DELETE_COMPONENT,
@@ -13,7 +14,9 @@ import {
 import dashboardLayout from './dashboardLayout';
 
 export default undoable(dashboardLayout, {
-  limit: 15,
+  // +1 because length of history seems max out at limit - 1
+  // +1 again so we can detect if we've exceeded the limit
+  limit: UNDO_LIMIT + 2,
   filter: includeAction([
     UPDATE_COMPONENTS,
     DELETE_COMPONENT,
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index bdf342b..d45da4f 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -1,53 +1,67 @@
 .dashboard-builder-sidepane {
-  background: white;
-  flex: 0 0 376px;
-  border: 1px solid @gray-light;
+  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;
     font-weight: 700;
+    border-top: 1px solid @gray-light;
     border-bottom: 1px solid @gray-light;
-    padding: 14px;
+    padding: 16px;
   }
 
   .trigger {
-    height: 25px;
+    height: 18px;
     width: 25px;
-    color: @gray;
-    position: relative;
+    color: @almost-black;
+    opacity: 1;
+  }
+
+  .viewport {
+    position: absolute;
+    transform: none !important;
+    background: white;
+    overflow: hidden;
+    width: @builder-pane-width;
+    height: 100%;
+  }
+
+  .slider-container {
+    position: absolute;
+    background: white;
+    width: @builder-pane-width * 2;
+    height: 100%;
+    display: flex;
+    transition: all 0.5s ease;
+
+    &.slide-in {
+      left: -@builder-pane-width;
+    }
 
-    &.close {
-      top: 3px;
+    &.slide-out {
+      left: 0;
     }
 
-    &.open {
-      position: absolute;
-      right: 14px;
+    .slide-content {
+      width: @builder-pane-width;
     }
   }
 
+  .component-layer .new-component.static,
+  .slices-layer .dashboard-builder-sidepane-header {
+    cursor: pointer;
+  }
+
   .component-layer {
     .new-component.static {
       cursor: pointer;
     }
   }
 
-  .slices-layer {
-    position: absolute;
-    width: 2px;
-    top: 51px;
-    right: 0;
-    background: white;
-    transition-property: width;
-    transition-duration: 1s;
-    transition-timing-function: ease;
-    overflow: hidden;
-
-    &.show {
-      width: 374px;
-    }
+  .new-component-label {
+    flex-grow: 1;
   }
 
   .chart-card-container {
@@ -89,21 +103,27 @@
       display: flex;
       padding: 16px;
 
+      /* the input is wrapped in a div */
+      .search-input {
+        flex-grow: 1;
+        margin-left: 16px;
+      }
+
       .dropdown.btn-group button,
       input {
         font-size: 14px;
         line-height: 16px;
         padding: 7px 12px;
         height: 32px;
+        border: 1px solid @gray-light;
       }
 
       input {
-        margin-left: 16px;
-        width: 169px;
-        border: 1px solid @gray;
+        width: 100%;
 
         &:focus {
           outline: none;
+          border-color: @gray;
         }
       }
     }
diff --git a/superset/assets/src/dashboard/stylesheets/components/chart.less b/superset/assets/src/dashboard/stylesheets/components/chart.less
index dc366a1..73914fb 100644
--- a/superset/assets/src/dashboard/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/stylesheets/components/chart.less
@@ -62,8 +62,3 @@
   /* disable chart interactions in edit mode */
   pointer-events: none;
 }
-
-.dashboard-chart .chart-header {
-  font-size: 16px;
-  font-weight: bold;
-}
diff --git a/superset/assets/src/dashboard/stylesheets/components/column.less b/superset/assets/src/dashboard/stylesheets/components/column.less
index 5fcb442..2f26d95 100644
--- a/superset/assets/src/dashboard/stylesheets/components/column.less
+++ b/superset/assets/src/dashboard/stylesheets/components/column.less
@@ -23,15 +23,11 @@
 .dashboard--editing
   .resizable-container.resizable-container--resizing:hover
   > .grid-column:after,
-.dashboard--editing .grid-column:hover:after {
+.dashboard--editing .hover-menu:hover + .grid-column:after {
   border: 1px dashed @gray-light;
   box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
-.grid-column > .hover-menu--top {
-  top: -20px;
-}
-
 .grid-column--empty {
   min-height: 72px;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/components/header.less b/superset/assets/src/dashboard/stylesheets/components/header.less
index 8b93164..9403103 100644
--- a/superset/assets/src/dashboard/stylesheets/components/header.less
+++ b/superset/assets/src/dashboard/stylesheets/components/header.less
@@ -1,6 +1,6 @@
 .dashboard-component-header {
   width: 100%;
-  line-height: 1em;
+  line-height: 1.1;
   font-weight: 700;
   padding: 16px 0;
   color: @almost-black;
@@ -15,7 +15,13 @@
   margin-right: 8px;
 }
 
-.dragdroppable-row .dashboard-component-header {
+.dashboard-header .undo-action,
+.dashboard-header .redo-action {
+  line-height: 18px;
+  font-size: 12px;
+}
+
+.dashboard--editing .dragdroppable-row .dashboard-component-header {
   cursor: move;
 }
 
diff --git a/superset/assets/src/dashboard/stylesheets/components/row.less b/superset/assets/src/dashboard/stylesheets/components/row.less
index 7df5675..382417e 100644
--- a/superset/assets/src/dashboard/stylesheets/components/row.less
+++ b/superset/assets/src/dashboard/stylesheets/components/row.less
@@ -14,7 +14,8 @@
 }
 
 /* hover indicator */
-.dashboard--editing .grid-row:after {
+.dashboard--editing .grid-row:after,
+.dashboard--editing .dashboard-component-tabs > .hover-menu:hover + div:after {
   border: 1px dashed transparent;
   content: '';
   position: absolute;
@@ -29,7 +30,8 @@
 .dashboard--editing
   .resizable-container.resizable-container--resizing:hover
   > .grid-row:after,
-.dashboard--editing .grid-row:hover:after {
+.dashboard--editing .hover-menu:hover + .grid-row:after,
+.dashboard--editing .dashboard-component-tabs > .hover-menu:hover + div:after {
   border: 1px dashed @gray-light;
   box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
diff --git a/superset/assets/src/dashboard/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
index f67c151..02039b4 100644
--- a/superset/assets/src/dashboard/stylesheets/components/tabs.less
+++ b/superset/assets/src/dashboard/stylesheets/components/tabs.less
@@ -30,12 +30,12 @@
 }
 
 .dashboard-component-tabs .nav-tabs > li.active > a:after {
-  content: "";
+  content: '';
   position: absolute;
   height: 3px;
   width: 100%;
   bottom: 0;
-  background: linear-gradient(to right, #E32464, #2C2261);
+  background: linear-gradient(to right, #e32464, #2c2261);
 }
 
 .dashboard-component-tabs .nav-tabs > li > a:hover {
@@ -53,9 +53,10 @@
   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;
+  height: ~'calc(100% + 24px)' !important;
 }
 
 .dashboard-component-tabs .nav-tabs > li .drop-indicator--left {
@@ -69,7 +70,7 @@
 .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 */
+  width: ~'calc(100% + 24px)' !important; /* escape for .less */
   opacity: 0.4;
 }
 
@@ -78,3 +79,7 @@
   font-size: 14px;
   margin-top: 3px;
 }
+
+.dashboard-component-tabs li .editable-title input[type='button'] {
+  cursor: pointer;
+}
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 03c804b..8d8c8be 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -1,52 +1,94 @@
-// @import './less/cosmo/variables.less';
-
 .dashboard .chart-header {
   position: relative;
+  font-size: 16px;
+  font-weight: bold;
 
   .dropdown.btn-group {
     position: absolute;
     right: 0;
   }
 
+  .dropdown-toggle.btn.btn-default {
+    background: none;
+    border: none;
+    box-shadow: none;
+  }
+
   .dropdown-menu.dropdown-menu-right {
-    right: 7px;
-    top: -3px;
+    top: 20px;
   }
-}
 
-.slice-header-controls-trigger {
-  border: 0;
-  padding: 0 0 0 20px;
-  background: none;
-  outline: none;
-  box-shadow: none;
-  color: #263238;
-
-  &.is-cached {
-    color: red;
+  .divider {
+    margin: 5px 0;
   }
 
-  &:hover,
-  &:focus {
-    background: none;
-    cursor: pointer;
+  .fa-circle {
+    position: absolute;
+    left: 7px;
+    top: 18px;
+    font-size: 4px;
+    color: @pink;
   }
 
-  .controls-container.dropdown-menu {
-    top: 0;
-    left: unset;
-    right: 10px;
+  .refresh-tooltip {
+    display: block;
+    height: 16px;
+    margin: 3px 0;
+    color: @gray;
+  }
+}
 
-    &.is-open {
-      display: block;
-    }
+.dashboard .chart-header,
+.dashboard .dashboard-header {
+  .dropdown-menu {
+    padding: 9px 0;
+  }
 
-    & li {
-      white-space: nowrap;
+  .dropdown-menu li a {
+    padding: 3px 16px;
+    color: @almost-black;
+    line-height: 16px;
+    font-size: 14px;
+    letter-spacing: 0.4px;
+
+    &:hover,
+    &:focus {
+      background: @menu-hover;
+      color: @almost-black;
     }
   }
 }
 
+.slice-header-controls-trigger {
+  padding: 0 16px;
+  position: absolute;
+  top: 0;
+  right: -22px;
+
+  &: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;
+    margin-right: 6px;
+  }
+
+  .vertical-dots-container & {
+    display: block;
+  }
+}
+
+
 .modal img.loading {
   width: 50px;
   margin: 0;
diff --git a/superset/assets/src/dashboard/stylesheets/dnd.less b/superset/assets/src/dashboard/stylesheets/dnd.less
index 835b62b..0a10c61 100644
--- a/superset/assets/src/dashboard/stylesheets/dnd.less
+++ b/superset/assets/src/dashboard/stylesheets/dnd.less
@@ -65,11 +65,11 @@
   float: left;
   height: 2px;
   margin: 1px;
-  width: 2px
+  width: 2px;
 }
 
 .drag-handle-dot:after {
-  content: "";
+  content: '';
   background: #aaa;
   float: left;
   height: 2px;
diff --git a/superset/assets/src/dashboard/stylesheets/grid.less b/superset/assets/src/dashboard/stylesheets/grid.less
index a12ac97..9d09ac7 100644
--- a/superset/assets/src/dashboard/stylesheets/grid.less
+++ b/superset/assets/src/dashboard/stylesheets/grid.less
@@ -20,11 +20,16 @@
 }
 
 /* gutters between rows */
-.grid-content > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget) {
+.grid-content
+  > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget--bottom):not(.empty-grid-droptarget--top) {
   margin-bottom: 16px;
 }
 
-.empty-grid-droptarget {
+.grid-content > .empty-grid-droptarget--top {
+  height: 24px;
+  margin-top: -24px;
+}
+.empty-grid-droptarget--bottom {
   width: 100%;
   height: 100%;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/hover-menu.less b/superset/assets/src/dashboard/stylesheets/hover-menu.less
index 77edb06..4f62401 100644
--- a/superset/assets/src/dashboard/stylesheets/hover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/hover-menu.less
@@ -1,14 +1,16 @@
 .hover-menu {
   opacity: 0;
   position: absolute;
-  z-index: 2;
+  z-index: 10;
+  font-size: 14px;
 }
 
 .hover-menu--left {
   width: 24px;
-  height: 100%;
-  top: 0;
+  top: 50%;
+  transform: translate(0, -50%);
   left: -24px;
+  padding: 8px 0;
   display: flex;
   flex-direction: column;
   justify-content: center;
@@ -19,21 +21,52 @@
   margin-bottom: 12px;
 }
 
-.dragdroppable-row .dragdroppable-row .hover-menu--left {
-  left: 1px;
-}
-
 .hover-menu--top {
-  width: 100%;
   height: 24px;
-  top: 0;
-  left: 0;
+  top: -24px;
+  left: 50%;
+  transform: translate(-50%);
+  padding: 0 8px;
   display: flex;
   flex-direction: row;
   justify-content: center;
   align-items: center;
 }
 
+/* Special cases */
+
+/* A row within a column has inset hover menu */
+.dragdroppable-column .dragdroppable-row .hover-menu--left {
+  left: -12px;
+  background: white;
+  border: 1px solid @gray-light;
+}
+
+/* A column within a column or tabs has inset hover menu */
+.dragdroppable-column .dragdroppable-column .hover-menu--top,
+.dashboard-component-tabs .dragdroppable-column .hover-menu--top {
+  top: -12px;
+  background: white;
+  border: 1px solid @gray-light;
+}
+
+/* move Tabs hover menu to top near actual Tabs */
+.dashboard-component-tabs > .hover-menu--left {
+  top: 0;
+  transform: unset;
+  background: transparent;
+}
+
+/* push Chart actions to upper right */
+.dragdroppable-column .dashboard-component-chart-holder > .hover-menu--top {
+  right: 8px;
+  top: 8px;
+  background: transparent;
+  border: none;
+  transform: unset;
+  left: unset;
+}
+
 .hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
   margin-right: 12px;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
index 848949b..d69006c 100644
--- a/superset/assets/src/dashboard/stylesheets/popover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/popover-menu.less
@@ -3,13 +3,14 @@
   outline: none;
 }
 
-.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */
+.grid-row.grid-row--empty .with-popover-menu {
+  /* drop indicator doesn't show up without this */
   width: 100%;
   height: 100%;
 }
 
 .with-popover-menu--focused:after {
-  content: "";
+  content: '';
   position: absolute;
   top: 1;
   left: -1;
@@ -34,15 +35,15 @@
   box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2);
   font-size: 14px;
   cursor: default;
-  z-index: 10;
+  z-index: 1000;
 }
 
 /* the focus menu doesn't account for parent padding */
 .dashboard-component-tabs li .with-popover-menu--focused:after {
   top: -12px;
-  left: -2px;
-  width: ~"calc(100% + 4px)"; /* escape for .less */
-  height: ~"calc(100% + 28px)";
+  left: -8px;
+  width: ~'calc(100% + 16px)'; /* escape for .less */
+  height: ~'calc(100% + 28px)';
 }
 
 .dashboard-component-tabs li .popover-menu {
@@ -57,7 +58,7 @@
 
 /* vertical spacer after each menu item */
 .popover-menu .menu-item:not(:only-child):not(:last-child):after {
-  content: "";
+  content: '';
   width: 1;
   height: 100%;
   background: @gray-light;
@@ -86,12 +87,12 @@
   background: @gray-light;
 }
 
-.popover-dropdown .caret { /* without this the caret doesn't take up full width / is clipped */
+.popover-dropdown .caret {
+  /* without this the caret doesn't take up full width / is clipped */
   width: auto;
   border-top-color: transparent;
 }
 
-
 .hover-dropdown li.dropdown-item.active a,
 .popover-menu li.dropdown-item.active a {
   background: white;
@@ -105,7 +106,7 @@
 }
 
 .background-style-option:before {
-  content: "";
+  content: '';
   width: 1em;
   height: 1em;
   margin-right: 8px;
@@ -124,7 +125,10 @@
 }
 
 .background-style-option.background--transparent:before {
-  background-image: linear-gradient(45deg, @gray 25%, transparent 25%), linear-gradient(-45deg, @gray 25%, transparent 25%), linear-gradient(45deg, transparent 75%, @gray 75%), linear-gradient(-45deg, transparent 75%, @gray 75%);
+  background-image: linear-gradient(45deg, @gray 25%, transparent 25%),
+    linear-gradient(-45deg, @gray 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, @gray 75%),
+    linear-gradient(-45deg, transparent 75%, @gray 75%);
   background-size: 8px 8px;
-  background-position: 0 0, 0 4px, 4px -4px, -4px 0px
+  background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/variables.less b/superset/assets/src/dashboard/stylesheets/variables.less
index 254af23..8f53f99 100644
--- a/superset/assets/src/dashboard/stylesheets/variables.less
+++ b/superset/assets/src/dashboard/stylesheets/variables.less
@@ -5,6 +5,10 @@
 @gray: #879399;
 @gray-light: #CFD8DC;
 @gray-bg: #f5f5f5;
+@menu-hover: #F2F3F5;
+
+/* builder component pane */
+@builder-pane-width: 374px;
 
 /* toasts */
 @pink: #E32364;
diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index f35614c..d682687 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -2,6 +2,7 @@
 export const DASHBOARD_GRID_ID = 'DASHBOARD_GRID_ID';
 export const DASHBOARD_HEADER_ID = 'DASHBOARD_HEADER_ID';
 export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID';
+export const DASHBOARD_VERSION_KEY = 'DASHBOARD_VERSION_KEY';
 
 export const NEW_COMPONENTS_SOURCE_ID = 'NEW_COMPONENTS_SOURCE_ID';
 export const NEW_CHART_ID = 'NEW_CHART_ID';
@@ -37,3 +38,6 @@ export const INFO_TOAST = 'INFO_TOAST';
 export const SUCCESS_TOAST = 'SUCCESS_TOAST';
 export const WARNING_TOAST = 'WARNING_TOAST';
 export const DANGER_TOAST = 'DANGER_TOAST';
+
+// undo-redo
+export const UNDO_LIMIT = 50;
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index f04b50e..f3f6061 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -5,14 +5,14 @@ import {
   ROW_TYPE,
   COLUMN_TYPE,
   CHART_TYPE,
-  DASHBOARD_HEADER_TYPE,
   DASHBOARD_ROOT_TYPE,
   DASHBOARD_GRID_TYPE,
 } from './componentTypes';
+
 import {
   DASHBOARD_GRID_ID,
-  DASHBOARD_HEADER_ID,
   DASHBOARD_ROOT_ID,
+  DASHBOARD_VERSION_KEY,
 } from './constants';
 
 const MAX_RECURSIVE_LEVEL = 6;
@@ -55,7 +55,6 @@ function getBoundary(positions) {
 
 function getRowContainer() {
   return {
-    version: 'v2',
     type: ROW_TYPE,
     id: `DASHBOARD_ROW_TYPE-${generateId()}`,
     children: [],
@@ -67,7 +66,6 @@ function getRowContainer() {
 
 function getColContainer() {
   return {
-    version: 'v2',
     type: COLUMN_TYPE,
     id: `DASHBOARD_COLUMN_TYPE-${generateId()}`,
     children: [],
@@ -78,24 +76,19 @@ function getColContainer() {
 }
 
 function getChartHolder(item) {
-  const { row, col, size_x, size_y, slice_id } = item;
-  const converted = {
-    row: Math.round(row / GRID_RATIO),
-    col: Math.floor((col - 1) / GRID_RATIO) + 1,
-    size_x: Math.max(1, Math.floor(size_x / GRID_RATIO)),
-    size_y: Math.max(1, Math.round(size_y / GRID_RATIO)),
-    slice_id,
-  };
+  const { size_x, size_y, slice_id } = item;
+
+  const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
+  const height = Math.max(1, Math.round(size_y / GRID_RATIO));
 
   return {
-    version: 'v2',
     type: CHART_TYPE,
     id: `DASHBOARD_CHART_TYPE-${generateId()}`,
     children: [],
     meta: {
-      width: converted.size_x,
-      height: Math.round(converted.size_y * 100 / ROW_HEIGHT),
-      chartId: slice_id,
+      width,
+      height: Math.round(height * 100 / ROW_HEIGHT),
+      chartId: parseInt(slice_id, 10),
     },
   };
 }
@@ -111,21 +104,6 @@ function getChildrenSum(items, attr, layout) {
   );
 }
 
-// function getChildrenMax(items, attr, layout) {
-//   return Math.max.apply(null, items.map((childId) => {
-//     const child = layout[childId];
-//     if (child.type === ROW_TYPE && attr === 'width') {
-//       // rows don't have widths themselves
-//       return getChildrenSum(child.children, attr, layout);
-//     } else if (child.type === COLUMN_TYPE && attr === 'height') {
-//       // columns don't have heights themselves
-//       return getChildrenSum(child.children, attr, layout);
-//     }
-//
-//     return child.meta[attr];
-//   }));
-// }
-
 function sortByRowId(item1, item2) {
   return item1.row - item2.row;
 }
@@ -289,10 +267,10 @@ export default function(dashboard) {
 
   // position data clean up. some dashboard didn't have position_json
   let { position_json } = dashboard;
-  const posDict = {};
+  const positionDict = {};
   if (Array.isArray(position_json)) {
     position_json.forEach(position => {
-      posDict[position.slice_id] = position;
+      positionDict[position.slice_id] = position;
     });
   } else {
     position_json = [];
@@ -303,25 +281,25 @@ export default function(dashboard) {
     Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
   );
   let newSliceCounter = 0;
-  dashboard.slices.forEach(slice => {
-    const sliceId = slice.slice_id;
-    let pos = posDict[sliceId];
-    if (!pos) {
+  dashboard.slices.forEach(({ slice_id }) => {
+    let position = positionDict[slice_id];
+    if (!position) {
       // append new slices to dashboard bottom, 3 slices per row
-      pos = {
+      position = {
         col: (newSliceCounter % 3) * 16 + 1,
         row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
         size_x: 16,
         size_y: 16,
-        slice_id: String(sliceId),
+        slice_id,
       };
       newSliceCounter += 1;
     }
 
-    positions.push(pos);
+    positions.push(position);
   });
 
   const root = {
+    [DASHBOARD_VERSION_KEY]: 'v2',
     [DASHBOARD_ROOT_ID]: {
       type: DASHBOARD_ROOT_TYPE,
       id: DASHBOARD_ROOT_ID,
@@ -332,11 +310,8 @@ export default function(dashboard) {
       id: DASHBOARD_GRID_ID,
       children: [],
     },
-    [DASHBOARD_HEADER_ID]: {
-      type: DASHBOARD_HEADER_TYPE,
-      id: DASHBOARD_HEADER_ID,
-    },
   };
+
   doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
 
   // remove row's width/height and col's height
diff --git a/superset/assets/src/dashboard/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
index d789f45..a885c31 100644
--- a/superset/assets/src/dashboard/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -33,6 +33,7 @@ const depthOne = rootDepth + 1;
 const depthTwo = rootDepth + 2;
 const depthThree = rootDepth + 3;
 const depthFour = rootDepth + 4;
+const depthFive = rootDepth + 5;
 
 // when moving components around the depth of child is irrelevant, note these are parent depths
 const parentMaxDepthLookup = {
@@ -53,7 +54,7 @@ const parentMaxDepthLookup = {
   [ROW_TYPE]: {
     [CHART_TYPE]: depthFour,
     [MARKDOWN_TYPE]: depthFour,
-    [COLUMN_TYPE]: depthTwo,
+    [COLUMN_TYPE]: depthFour,
   },
 
   [TABS_TYPE]: {
@@ -70,9 +71,9 @@ const parentMaxDepthLookup = {
   },
 
   [COLUMN_TYPE]: {
-    [CHART_TYPE]: depthThree,
-    [HEADER_TYPE]: depthThree,
-    [MARKDOWN_TYPE]: depthThree,
+    [CHART_TYPE]: depthFive,
+    [HEADER_TYPE]: depthFive,
+    [MARKDOWN_TYPE]: depthFive,
     [ROW_TYPE]: depthThree,
   },
 
diff --git a/superset/assets/src/dashboard/util/newComponentFactory.js b/superset/assets/src/dashboard/util/newComponentFactory.js
index 4e2de37..8d259af 100644
--- a/superset/assets/src/dashboard/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/util/newComponentFactory.js
@@ -34,7 +34,6 @@ function uuid(type) {
 
 export default function entityFactory(type, meta) {
   return {
-    version: 'v0',
     type,
     id: uuid(type),
     children: [],
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index 73a10b0..c8e1981 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -66,7 +66,6 @@ export const slicePropShape = PropTypes.shape({
 });
 
 export const dashboardStatePropShape = PropTypes.shape({
-  title: PropTypes.string.isRequired,
   sliceIds: PropTypes.object.isRequired,
   refresh: PropTypes.bool.isRequired,
   filters: PropTypes.object,
diff --git a/superset/assets/src/theme.js b/superset/assets/src/theme.js
index 68a7a8a..34fc0c0 100644
--- a/superset/assets/src/theme.js
+++ b/superset/assets/src/theme.js
@@ -1,3 +1,2 @@
-import '../stylesheets/less/index.less';
 import '../stylesheets/react-select/select.less';
 import '../stylesheets/superset.less';
diff --git a/superset/assets/src/visualizations/nvd3_vis.js b/superset/assets/src/visualizations/nvd3_vis.js
index e94e740..4619d4d 100644
--- a/superset/assets/src/visualizations/nvd3_vis.js
+++ b/superset/assets/src/visualizations/nvd3_vis.js
@@ -490,6 +490,8 @@ export default function nvd3Vis(slice, payload) {
         chart.showLegend(fd.show_legend);
       }
     }
+    // This is needed for correct chart dimensions if a chart is rendered in a hidden container
+    chart.width(width);
     chart.height(height);
     slice.container.css('height', height + 'px');
 
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index d756551..0e8ffad 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -114,7 +114,6 @@ span.title-block {
 }
 
 .nvtooltip {
-    //position: relative !important;
     z-index: 888;
     transition: opacity 0ms linear;
     -moz-transition: opacity 0ms linear;
@@ -238,13 +237,14 @@ table.table-no-hover tr:hover {
   line-height: inherit;
   white-space: normal;
   text-align: left;
+  cursor: initial;
 }
 
-.editable-title.editable-title--editable {
+.editable-title.editable-title--editable input[type="button"] {
   cursor: pointer;
 }
 
-.editable-title.editable-title--editing {
+.editable-title.editable-title--editing input[type="button"] {
   cursor: text;
 }
 
diff --git a/superset/views/core.py b/superset/views/core.py
index c086a6d..26356c5 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1559,10 +1559,16 @@ class Superset(BaseSupersetView):
                 session.add(new_slice)
                 session.flush()
                 new_slice.dashboards.append(dash)
-                old_to_new_sliceids['{}'.format(slc.id)] =\
-                    '{}'.format(new_slice.id)
-            for d in data['positions']:
-                d['slice_id'] = old_to_new_sliceids[d['slice_id']]
+                old_to_new_sliceids[slc.id] = new_slice.id
+
+            # update chartId of layout entities
+            for value in data['positions'].values():
+                if isinstance(value, dict) and value.get('meta') \
+                    and value.get('meta').get('chartId'):
+
+                    old_id = value.get('meta').get('chartId')
+                    new_id = old_to_new_sliceids[old_id]
+                    value['meta']['chartId'] = new_id
         else:
             dash.slices = original_dash.slices
         dash.params = original_dash.params
@@ -1585,6 +1591,7 @@ class Superset(BaseSupersetView):
                 .filter_by(id=dashboard_id).first())
         check_ownership(dash, raise_if_false=True)
         data = json.loads(request.form.get('data'))
+        original_slice_names = {(slc.id): slc.slice_name for slc in dash.slices}
         self._set_dash_metadata(dash, data)
         session.merge(dash)
         session.commit()
@@ -1596,15 +1603,30 @@ class Superset(BaseSupersetView):
         positions = data['positions']
         # find slices in the position data
         slice_ids = []
+        slice_id_to_name = {}
         for value in positions.values():
-            if value.get('meta') and value.get('meta').get('chartId'):
-                slice_ids.append(int(value.get('meta').get('chartId')))
+            if isinstance(value, dict) and value.get('meta') \
+                and value.get('meta').get('chartId'):
+
+                slice_id = value.get('meta').get('chartId')
+                slice_ids.append(slice_id)
+                slice_id_to_name[slice_id] = value.get('meta').get('chartName')
+
         session = db.session()
         Slice = models.Slice  # noqa
         current_slices = session.query(Slice).filter(
             Slice.id.in_(slice_ids)).all()
 
         dashboard.slices = current_slices
+
+        # update slice names. this assumes user has permissions to update the slice
+        for slc in dashboard.slices:
+            new_name = slice_id_to_name[slc.id]
+            if slc.slice_name != new_name:
+                slc.slice_name = new_name
+                session.merge(slc)
+                session.flush()
+
         dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
         md = dashboard.params_dict
         dashboard.css = data.get('css')


[incubator-superset] 24/26: [dashboard v2] ui + ux fixes (#5208)

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

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

commit 85ae429031ca34d2a8d1234e029bab77c098d179
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Thu Jun 21 14:42:00 2018 -0700

    [dashboard v2] ui + ux fixes (#5208)
    
    * [dashboard v2] use <Loading /> throughout, small loading gif, improve row/column visual hierarchy, add cached data pop
    
    * [dashboard v2] lots of polish
    
    * [dashboard v2] remove markdown padding on edit, more opaque slice drag preview, unsavedChanges=true upon moving a component, fix initial load logging.
    
    * [dashboard v2] gray loading.gif, sticky header, undo/redo keyboard shortcuts, fix move component saved changes update, v0 double scrollbar fix
    
    * [dashboard v2] move UndoRedoKeylisteners into Header, render only in edit mode, show visual feedback for keyboard shortcut, hide hover menu in top-level tabs
    
    * [dashboard v2] fix grid + sidepane height issues
    
    * [dashboard v2] add auto-resize functionality, update tests. cache findParentId results.
    
    * [dashboard v2][tests] add getDetailedComponentWidth_spec.js
    
    * [dashboard v2] fix lint
---
 superset/assets/images/loading.gif                 | Bin 1945878 -> 79023 bytes
 .../dashboard/actions/dashboardLayout_spec.js      |  71 ++++---
 .../dashboard/components/DashboardGrid_spec.jsx    |  10 +-
 .../dashboard/util/dropOverflowsParent_spec.js     | 118 ++++++++++-
 .../util/getDetailedComponentWidth_spec.js         | 223 +++++++++++++++++++++
 .../dashboard/util/newEntitiesFromDrop_spec.js     |   3 +
 .../assets/src/SqlLab/components/QuerySearch.jsx   |  79 ++++----
 .../assets/src/SqlLab/components/ResultSet.jsx     |   3 +-
 superset/assets/src/components/Loading.jsx         |   9 +-
 .../src/dashboard/actions/dashboardLayout.js       |  41 +---
 .../dashboard/components/BuilderComponentPane.jsx  | 119 ++++++-----
 .../assets/src/dashboard/components/Dashboard.jsx  |  22 +-
 .../src/dashboard/components/DashboardBuilder.jsx  |  93 ++++-----
 .../src/dashboard/components/DashboardGrid.jsx     |  38 +++-
 .../assets/src/dashboard/components/Header.jsx     | 214 ++++++++++++--------
 .../assets/src/dashboard/components/SliceAdder.jsx |   9 +-
 .../dashboard/components/UndoRedoKeylisteners.jsx  |  47 +++++
 .../components/dnd/AddSliceDragPreview.jsx         |   4 +-
 .../src/dashboard/components/dnd/handleHover.js    |   2 +-
 .../components/gridComponents/Markdown.jsx         |  13 +-
 .../dashboard/components/gridComponents/Tab.jsx    |  45 ++++-
 .../dashboard/components/gridComponents/Tabs.jsx   |  20 +-
 .../dashboard/containers/DashboardComponent.jsx    |  21 +-
 .../deprecated/v1/components/SliceAdder.jsx        |   7 +-
 .../src/dashboard/reducers/dashboardLayout.js      |  20 ++
 .../dashboard/stylesheets/builder-sidepane.less    |   4 +-
 .../dashboard/stylesheets/components/chart.less    |  40 +++-
 .../dashboard/stylesheets/components/column.less   |   8 +-
 .../dashboard/stylesheets/components/header.less   |  26 ++-
 .../dashboard/stylesheets/components/markdown.less |   9 +
 .../src/dashboard/stylesheets/components/row.less  |   9 +-
 .../src/dashboard/stylesheets/components/tabs.less | 163 ++++++++-------
 .../src/dashboard/stylesheets/dashboard.less       |  49 ++---
 superset/assets/src/dashboard/stylesheets/dnd.less |  35 +++-
 .../assets/src/dashboard/stylesheets/grid.less     |  17 +-
 .../src/dashboard/stylesheets/popover-menu.less    |   5 +-
 .../src/dashboard/util/dropOverflowsParent.js      |  45 +----
 superset/assets/src/dashboard/util/findParentId.js |  14 +-
 .../assets/src/dashboard/util/getChildWidth.js     |  13 --
 .../dashboard/util/getComponentWidthFromDrop.js    |  57 ++++++
 .../dashboard/util/getDetailedComponentWidth.js    |  76 +++++++
 .../assets/src/dashboard/util/getDropPosition.js   |   2 +-
 .../src/dashboard/util/headerStyleOptions.js       |  18 +-
 .../src/dashboard/util/newEntitiesFromDrop.js      |   8 +
 .../src/explore/components/DisplayQueryButton.jsx  |  14 +-
 .../components/controls/DatasourceControl.jsx      |  58 +++---
 .../assets/src/profile/components/TableLoader.jsx  |  25 ++-
 superset/assets/src/welcome/DashboardTable.jsx     |   8 +-
 superset/assets/yarn.lock                          |  57 +++++-
 49 files changed, 1377 insertions(+), 614 deletions(-)

diff --git a/superset/assets/images/loading.gif b/superset/assets/images/loading.gif
index ae5cbdd..d82fc5d 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 84f0856..4b28480 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 1160d65..d11c37f 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 b153e1e..8e6f889 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 0000000..99e2282
--- /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 677c329..8d00c18 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 9d36d85..45924e3 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 4959921..67c8fd5 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';
@@ -238,7 +239,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 810c581..953e702 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 c4908b0..bd01146 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 c35a637..aafee5d 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 99e93aa..f069cd1 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 59a9152..9621a49 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 f5ca6e5..d85015e 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 5fa4afe..3b1b6b1 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 ed652c0..f237a06 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';
@@ -222,13 +223,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 0000000..5af0934
--- /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 91fc055..2c1128e 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 cb98a6f..a3b16aa 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 a49a893..bd639e7 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 4cba2e6..b91d808 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 813961d..01c0e60 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 29071cb..6f5b2e0 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 6c2f624..3d3e468 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 396a56c..51cd02a 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 6250243..e807bcc 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 73914fb..bc0005d 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 2f26d95..e923f8c 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 9403103..71b2176 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 2cfd929..87974d9 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 382417e..06d98e6 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 b1124da..cee524c 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 3db5cdc..92f1ff1 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 0a10c61..9b5ea89 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 9d09ac7..1e22e1d 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 0c70f58..3c790e4 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 328d8e3..a5f99f3 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 9e47bf2..bf26d54 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 69d2792..0000000
--- 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 0000000..38c7c5a
--- /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 0000000..ee3096d
--- /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 74dfcaa..dd4add9 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 7efa040..a37bd5f 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 8abc9b9..9054d44 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 0b78cea..334ec78 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/styles/hljs/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 404ba5e..d63d6fe 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 1e67426..462e009 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 78d4bdd..f7f3007 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 2add797..9d1a39b 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"


[incubator-superset] 11/26: Dashboard save button (#4979)

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

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

commit ae37277ddf56ee4b1fc6d6309a9c0499d0bf94e7
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue May 22 22:36:33 2018 -0700

    Dashboard save button (#4979)
    
    * save button
    
    * fix slices list height
    
    * save custom css
    
    * merge save-dash changes from dashboard v1
    https://github.com/apache/incubator-superset/pull/4900
    https://github.com/apache/incubator-superset/pull/5051
---
 .../assets/src/dashboard/actions/dashboardState.js | 43 +++++++++-
 .../assets/src/dashboard/components/Controls.jsx   | 38 ++++-----
 .../assets/src/dashboard/components/Header.jsx     | 98 ++++++++++++++++++----
 .../assets/src/dashboard/components/SaveModal.jsx  | 78 ++++++-----------
 .../assets/src/dashboard/components/SliceAdder.jsx | 11 ++-
 .../dashboard/components/SliceHeaderControls.jsx   | 18 +++-
 .../dashboard/components/gridComponents/Chart.jsx  |  6 ++
 superset/assets/src/dashboard/containers/Chart.jsx |  2 +
 .../src/dashboard/containers/DashboardHeader.jsx   |  7 +-
 .../src/dashboard/reducers/dashboardState.js       |  6 +-
 .../src/dashboard/reducers/getInitialState.js      |  8 +-
 .../assets/src/dashboard/stylesheets/builder.less  |  5 ++
 .../src/dashboard/stylesheets/dashboard.less       | 32 ++++++-
 superset/assets/src/dashboard/util/constants.js    |  4 +
 superset/assets/src/modules/utils.js               |  2 +-
 15 files changed, 246 insertions(+), 112 deletions(-)

diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 10c0a26..42f68ad 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -6,7 +6,15 @@ import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
 import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
 import { applyDefaultFormData } from '../../explore/stores/store';
-import { addWarningToast } from './messageToasts';
+import { getAjaxErrorMsg } from '../../modules/utils';
+import { SAVE_TYPE_OVERWRITE } from '../util/constants';
+import { t } from '../../locales';
+
+import {
+  addSuccessToast,
+  addWarningToast,
+  addDangerToast,
+} from './messageToasts';
 
 export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
 export function setUnsavedChanges(hasUnsavedChanges) {
@@ -66,6 +74,11 @@ export function toggleExpandSlice(sliceId) {
   return { type: TOGGLE_EXPAND_SLICE, sliceId };
 }
 
+export const UPDATE_CSS = 'UPDATE_CSS';
+export function updateCss(css) {
+  return { type: UPDATE_CSS, css };
+}
+
 export const SET_EDIT_MODE = 'SET_EDIT_MODE';
 export function setEditMode(editMode) {
   return { type: SET_EDIT_MODE, editMode };
@@ -81,7 +94,7 @@ export function onSave() {
   return { type: ON_SAVE };
 }
 
-export function saveDashboard() {
+export function saveDashboardRequestSuccess() {
   return dispatch => {
     dispatch(onSave());
     // clear layout undo history
@@ -89,6 +102,32 @@ export function saveDashboard() {
   };
 }
 
+export function saveDashboardRequest(data, id, saveType) {
+  const path = saveType === SAVE_TYPE_OVERWRITE ? 'save_dash' : 'copy_dash';
+  const url = `/superset/${path}/${id}/`;
+  return dispatch =>
+    $.ajax({
+      type: 'POST',
+      url,
+      data: {
+        data: JSON.stringify(data),
+      },
+      success: () => {
+        dispatch(saveDashboardRequestSuccess());
+        dispatch(addSuccessToast(t('This dashboard was saved successfully.')));
+      },
+      error: error => {
+        const errorMsg = getAjaxErrorMsg(error);
+        dispatch(
+          addDangerToast(
+            `${t('Sorry, there was an error saving this dashboard: ')}
+          ${errorMsg}`,
+          ),
+        );
+      },
+    });
+}
+
 export function fetchCharts(chartList = [], force = false, interval = 0) {
   return (dispatch, getState) => {
     const timeout = getState().dashboardInfo.common.conf
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index 07b6c33..9d54b09 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
 import $ from 'jquery';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
 
+import CssEditor from './CssEditor';
 import RefreshIntervalModal from './RefreshIntervalModal';
-import SaveModal from './SaveModal';
 import { t } from '../../locales';
 
 function updateDom(css) {
@@ -31,12 +31,10 @@ const propTypes = {
   addDangerToast: PropTypes.func.isRequired,
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
-  layout: PropTypes.object.isRequired,
-  filters: PropTypes.object.isRequired,
-  expandedSlices: PropTypes.object.isRequired,
+  css: PropTypes.string.isRequired,
   slices: PropTypes.array,
-  onSave: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
+  updateCss: PropTypes.func.isRequired,
   forceRefreshAllCharts: PropTypes.func.isRequired,
   startPeriodicRender: PropTypes.func.isRequired,
   editMode: PropTypes.bool,
@@ -51,9 +49,11 @@ class Controls extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      css: '',
+      css: props.css,
       cssTemplates: [],
     };
+
+    this.changeCss = this.changeCss.bind(this);
   }
 
   componentWillMount() {
@@ -74,17 +74,14 @@ class Controls extends React.PureComponent {
       updateDom(css);
     });
     this.props.onChange();
+    this.props.updateCss(css);
   }
 
   render() {
     const {
       dashboardTitle,
-      layout,
-      filters,
-      expandedSlices,
       startPeriodicRender,
       forceRefreshAllCharts,
-      onSave,
       editMode,
     } = this.props;
 
@@ -110,19 +107,6 @@ class Controls extends React.PureComponent {
             }
             triggerNode={<span>{t('Set auto-refresh interval')}</span>}
           />
-          <SaveModal
-            addSuccessToast={this.props.addSuccessToast}
-            addDangerToast={this.props.addDangerToast}
-            dashboardId={this.props.dashboardInfo.id}
-            dashboardTitle={dashboardTitle}
-            layout={layout}
-            filters={filters}
-            expandedSlices={expandedSlices}
-            onSave={onSave}
-            css={this.state.css}
-            triggerNode={<span>{editMode ? t('Save') : t('Save as')}</span>}
-            isMenuItem
-          />
           {editMode && (
             <MenuItem
               target="_blank"
@@ -134,6 +118,14 @@ class Controls extends React.PureComponent {
           {editMode && (
             <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
           )}
+          {editMode && (
+            <CssEditor
+              triggerNode={<span>{t('Edit CSS')}</span>}
+              initialCss={this.state.css}
+              templates={this.state.cssTemplates}
+              onChange={this.changeCss}
+            />
+          )}
         </DropdownButton>
       </span>
     );
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 21b01db..31bd08c 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,6 +1,12 @@
+/* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
+import {
+  DropdownButton,
+  MenuItem,
+  ButtonGroup,
+  ButtonToolbar,
+} from 'react-bootstrap';
 
 import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
@@ -9,7 +15,11 @@ import FaveStar from '../../components/FaveStar';
 import SaveModal from './SaveModal';
 import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
-import { UNDO_LIMIT } from '../util/constants';
+import {
+  UNDO_LIMIT,
+  SAVE_TYPE_NEWDASHBOARD,
+  SAVE_TYPE_OVERWRITE,
+} from '../util/constants';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -20,6 +30,7 @@ const propTypes = {
   layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
   expandedSlices: PropTypes.object.isRequired,
+  css: PropTypes.string.isRequired,
   isStarred: PropTypes.bool.isRequired,
   onSave: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
@@ -32,6 +43,7 @@ const propTypes = {
   setEditMode: PropTypes.func.isRequired,
   showBuilderPane: PropTypes.bool.isRequired,
   toggleBuilderPane: PropTypes.func.isRequired,
+  updateCss: PropTypes.func.isRequired,
   hasUnsavedChanges: PropTypes.bool.isRequired,
   maxUndoHistoryExceeded: PropTypes.bool.isRequired,
 
@@ -45,6 +57,10 @@ const propTypes = {
 };
 
 class Header extends React.PureComponent {
+  static discardChanges() {
+    window.location.reload();
+  }
+
   constructor(props) {
     super(props);
     this.state = {
@@ -54,6 +70,7 @@ class Header extends React.PureComponent {
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
+    this.overwriteDashboard = this.overwriteDashboard.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -88,38 +105,62 @@ class Header extends React.PureComponent {
     this.props.setEditMode(!this.props.editMode);
   }
 
+  overwriteDashboard() {
+    const {
+      dashboardTitle,
+      layout: positions,
+      expandedSlices,
+      css,
+      filters,
+      dashboardInfo,
+    } = this.props;
+
+    const data = {
+      positions,
+      expanded_slices: expandedSlices,
+      css,
+      dashboard_title: dashboardTitle,
+      default_filters: JSON.stringify(filters),
+    };
+
+    this.props.onSave(data, dashboardInfo.id, SAVE_TYPE_OVERWRITE);
+  }
+
   render() {
     const {
       dashboardTitle,
       layout,
       filters,
       expandedSlices,
+      css,
       onUndo,
       onRedo,
       undoLength,
       redoLength,
       onChange,
       onSave,
+      updateCss,
       editMode,
       showBuilderPane,
       dashboardInfo,
       hasUnsavedChanges,
     } = this.props;
 
-    const userCanEdit = dashboardInfo.dash_save_perm;
+    const userCanEdit = dashboardInfo.dash_edit_perm;
+    const userCanSaveAs = dashboardInfo.dash_save_perm;
 
     return (
       <div className="dashboard-header">
         <div className="dashboard-component-header header-large">
           <EditableTitle
             title={dashboardTitle}
-            canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
+            canEdit={userCanEdit && editMode}
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
           />
           <span className="favstar m-l-5">
             <FaveStar
-              itemId={this.props.dashboardInfo.id}
+              itemId={dashboardInfo.id}
               fetchFaveStar={this.props.fetchFaveStar}
               saveFaveStar={this.props.saveFaveStar}
               isStarred={this.props.isStarred}
@@ -127,7 +168,7 @@ class Header extends React.PureComponent {
           </span>
         </div>
         <ButtonToolbar>
-          {userCanEdit && (
+          {userCanSaveAs && (
             <ButtonGroup>
               {editMode && (
                 <Button
@@ -161,44 +202,65 @@ class Header extends React.PureComponent {
                 <Button
                   bsSize="small"
                   onClick={this.toggleEditMode}
-                  bsStyle={editMode ? undefined : 'primary'}
+                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
+                  disabled={!userCanEdit}
                 >
-                  {editMode ? t('Switch to View Mode') : t('Edit Dashboard')}
+                  {editMode ? t('Switch to view mode') : t('Edit dashboard')}
                 </Button>
               ) : (
+                <Button
+                  bsSize="small"
+                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
+                  onClick={this.overwriteDashboard}
+                >
+                  {t('Save changes')}
+                </Button>
+              )}
+              <DropdownButton
+                title=""
+                id="save-dash-split-button"
+                bsStyle={hasUnsavedChanges ? 'primary' : undefined}
+                bsSize="small"
+                pullRight
+              >
                 <SaveModal
                   addSuccessToast={this.props.addSuccessToast}
                   addDangerToast={this.props.addDangerToast}
-                  dashboardId={this.props.dashboardInfo.id}
+                  dashboardId={dashboardInfo.id}
                   dashboardTitle={dashboardTitle}
+                  saveType={SAVE_TYPE_NEWDASHBOARD}
                   layout={layout}
                   filters={filters}
                   expandedSlices={expandedSlices}
+                  css={css}
                   onSave={onSave}
-                  // @TODO need to figure out css
-                  css=""
-                  triggerNode={
-                    <Button bsStyle="primary" bsSize="small">
-                      {t('Save changes')}
-                    </Button>
-                  }
+                  isMenuItem
+                  triggerNode={<span>{t('Save as')}</span>}
+                  canOverwrite={userCanEdit}
                 />
-              )}
+                {hasUnsavedChanges && (
+                  <MenuItem eventKey="discard" onSelect={Header.discardChanges}>
+                    {t('Discard changes')}
+                  </MenuItem>
+                )}
+              </DropdownButton>
             </ButtonGroup>
           )}
 
           <Controls
             addSuccessToast={this.props.addSuccessToast}
             addDangerToast={this.props.addDangerToast}
-            dashboardInfo={this.props.dashboardInfo}
+            dashboardInfo={dashboardInfo}
             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}
           />
         </ButtonToolbar>
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 1d287d6..9d63331 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,13 +1,12 @@
 /* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import $ from 'jquery';
 
 import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
-import { getAjaxErrorMsg } from '../../modules/utils';
 import ModalTrigger from '../../components/ModalTrigger';
 import { t } from '../../locales';
 import Checkbox from '../../components/Checkbox';
+import { SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -16,21 +15,25 @@ const propTypes = {
   dashboardTitle: PropTypes.string.isRequired,
   expandedSlices: PropTypes.object.isRequired,
   layout: PropTypes.object.isRequired,
+  saveType: PropTypes.oneOf([SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD]),
   triggerNode: PropTypes.node.isRequired,
   filters: PropTypes.object.isRequired,
+  css: PropTypes.string.isRequired,
   onSave: PropTypes.func.isRequired,
   isMenuItem: PropTypes.bool,
+  canOverwrite: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
   isMenuItem: false,
+  saveType: SAVE_TYPE_OVERWRITE,
 };
 
 class SaveModal extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      saveType: 'overwrite',
+      saveType: props.saveType,
       newDashName: `${props.dashboardTitle} [copy]`,
       duplicateSlices: false,
     };
@@ -40,6 +43,7 @@ class SaveModal extends React.PureComponent {
     this.saveDashboard = this.saveDashboard.bind(this);
     this.setModalRef = this.setModalRef.bind(this);
     this.toggleDuplicateSlices = this.toggleDuplicateSlices.bind(this);
+    this.onSave = this.props.onSave.bind(this);
   }
 
   setModalRef(ref) {
@@ -59,37 +63,7 @@ class SaveModal extends React.PureComponent {
   handleNameChange(event) {
     this.setState({
       newDashName: event.target.value,
-      saveType: 'newDashboard',
-    });
-  }
-
-  // @TODO this should all be moved to actions
-  saveDashboardRequest(data, url, saveType) {
-    $.ajax({
-      type: 'POST',
-      url,
-      data: {
-        data: JSON.stringify(data),
-      },
-      success: resp => {
-        this.modal.close();
-        this.props.onSave();
-        if (saveType === 'newDashboard') {
-          window.location = `/superset/dashboard/${resp.id}/`;
-        } else {
-          this.props.addSuccessToast(
-            t('This dashboard was saved successfully.'),
-          );
-        }
-      },
-      error: error => {
-        this.modal.close();
-        const errorMsg = getAjaxErrorMsg(error);
-        this.props.addDangerToast(
-          `${t('Sorry, there was an error saving this dashboard: ')}
-          ${errorMsg}`,
-        );
-      },
+      saveType: SAVE_TYPE_NEWDASHBOARD,
     });
   }
 
@@ -98,6 +72,7 @@ class SaveModal extends React.PureComponent {
     const {
       dashboardTitle,
       layout: positions,
+      css,
       expandedSlices,
       filters,
       dashboardId,
@@ -105,26 +80,24 @@ class SaveModal extends React.PureComponent {
 
     const data = {
       positions,
+      css,
       expanded_slices: expandedSlices,
       dashboard_title: dashboardTitle,
       default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
 
-    let url = null;
-    if (saveType === 'overwrite') {
-      url = `/superset/save_dash/${dashboardId}/`;
-      this.saveDashboardRequest(data, url, saveType);
-    } else if (saveType === 'newDashboard') {
-      if (!newDashName) {
-        this.props.addDangerToast(
-          t('You must pick a name for the new dashboard'),
-        );
-      } else {
-        data.dashboard_title = newDashName;
-        url = `/superset/copy_dash/${dashboardId}/`;
-        this.saveDashboardRequest(data, url, saveType);
-      }
+    if (saveType === SAVE_TYPE_NEWDASHBOARD && !newDashName) {
+      this.props.addDangerToast(
+        t('You must pick a name for the new dashboard'),
+      );
+    } else {
+      this.onSave(data, dashboardId, saveType).done(resp => {
+        if (saveType === SAVE_TYPE_NEWDASHBOARD) {
+          window.location = `/superset/dashboard/${resp.id}/`;
+        }
+      });
+      this.modal.close();
     }
   }
 
@@ -138,17 +111,18 @@ class SaveModal extends React.PureComponent {
         modalBody={
           <FormGroup>
             <Radio
-              value="overwrite"
+              value={SAVE_TYPE_OVERWRITE}
               onChange={this.handleSaveTypeChange}
-              checked={this.state.saveType === 'overwrite'}
+              checked={this.state.saveType === SAVE_TYPE_OVERWRITE}
+              disabled={!this.props.canOverwrite}
             >
               {t('Overwrite Dashboard [%s]', this.props.dashboardTitle)}
             </Radio>
             <hr />
             <Radio
-              value="newDashboard"
+              value={SAVE_TYPE_NEWDASHBOARD}
               onChange={this.handleSaveTypeChange}
-              checked={this.state.saveType === 'newDashboard'}
+              checked={this.state.saveType === SAVE_TYPE_NEWDASHBOARD}
             >
               {t('Save as:')}
             </Radio>
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 05c4270..47451c4 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -39,6 +39,10 @@ const KEYS_TO_SORT = [
   { key: 'changed_on', label: 'Recent' },
 ];
 
+const MARGIN_BOTTOM = 16;
+const SIDEPANE_HEADER_HEIGHT = 55;
+const SLICE_ADDER_CONTROL_HEIGHT = 64;
+
 class SliceAdder extends React.Component {
   static sortByComparator(attr) {
     const desc = attr === 'changed_on' ? -1 : 1;
@@ -166,6 +170,11 @@ class SliceAdder extends React.Component {
   }
 
   render() {
+    const slicesListHeight =
+      this.props.height -
+      SIDEPANE_HEADER_HEIGHT -
+      SLICE_ADDER_CONTROL_HEIGHT -
+      MARGIN_BOTTOM;
     return (
       <div className="slice-adder-container">
         <div className="controls">
@@ -202,7 +211,7 @@ class SliceAdder extends React.Component {
           this.state.filteredSlices.length > 0 && (
             <List
               width={376}
-              height={this.props.height}
+              height={slicesListHeight}
               rowCount={this.state.filteredSlices.length}
               rowHeight={136}
               rowRenderer={this.rowRenderer}
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index e793bc2..6729e57 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -11,6 +11,8 @@ const propTypes = {
   isCached: PropTypes.bool,
   isExpanded: PropTypes.bool,
   cachedDttm: PropTypes.string,
+  supersetCanExplore: PropTypes.bool,
+  sliceCanEdit: PropTypes.bool,
   toggleExpandSlice: PropTypes.func,
   forceRefresh: PropTypes.func,
   exploreChart: PropTypes.func,
@@ -25,6 +27,8 @@ const defaultProps = {
   cachedDttm: null,
   isCached: false,
   isExpanded: false,
+  supersetCanExplore: false,
+  sliceCanEdit: false,
 };
 
 const VerticalDotsTrigger = () => (
@@ -96,13 +100,19 @@ class SliceHeaderControls extends React.PureComponent {
             </MenuItem>
           )}
 
-          <MenuItem href={slice.edit_url} target="_blank">
-            {t('Edit chart metadata')}
-          </MenuItem>
+          {this.props.sliceCanEdit && (
+            <MenuItem href={slice.edit_url} target="_blank">
+              {t('Edit chart metadata')}
+            </MenuItem>
+          )}
 
           <MenuItem onClick={this.exportCSV}>{t('Export CSV')}</MenuItem>
 
-          <MenuItem onClick={this.exploreChart}>{t('Explore chart')}</MenuItem>
+          {this.props.supersetCanExplore && (
+            <MenuItem onClick={this.exploreChart}>
+              {t('Explore chart')}
+            </MenuItem>
+          )}
         </Dropdown.Menu>
       </Dropdown>
     );
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 4742d71..9f8d723 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -29,6 +29,8 @@ const propTypes = {
   removeFilter: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
   isExpanded: PropTypes.bool.isRequired,
+  supersetCanExplore: PropTypes.bool.isRequired,
+  sliceCanEdit: PropTypes.bool.isRequired,
 };
 
 // we use state + shouldComponentUpdate() logic to prevent perf-wrecking
@@ -155,6 +157,8 @@ class Chart extends React.Component {
       sliceName,
       toggleExpandSlice,
       timeout,
+      supersetCanExplore,
+      sliceCanEdit,
     } = this.props;
 
     const { width } = this.state;
@@ -179,6 +183,8 @@ class Chart extends React.Component {
           exportCSV={this.exportCSV}
           updateSliceName={updateSliceName}
           sliceName={sliceName}
+          supersetCanExplore={supersetCanExplore}
+          sliceCanEdit={sliceCanEdit}
         />
 
         {/*
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 61627d2..107e6c7 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -40,6 +40,8 @@ function mapStateToProps(
     }),
     editMode: dashboardState.editMode,
     isExpanded: !!dashboardState.expandedSlices[id],
+    supersetCanExplore: !!dashboardInfo.superset_can_explore,
+    sliceCanEdit: !!dashboardInfo.slice_can_edit,
   };
 }
 
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index fe7e7bb..19be06c 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -10,8 +10,9 @@ import {
   saveFaveStar,
   fetchCharts,
   startPeriodicRender,
+  updateCss,
   onChange,
-  saveDashboard,
+  saveDashboardRequest,
   setMaxUndoHistoryExceeded,
   maxUndoHistoryToast,
 } from '../actions/dashboardState';
@@ -42,6 +43,7 @@ function mapStateToProps({
       (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
     ).text,
     expandedSlices: dashboard.expandedSlices,
+    css: dashboard.css,
     charts,
     userId: dashboardInfo.userId,
     isStarred: !!dashboard.isStarred,
@@ -66,8 +68,9 @@ function mapDispatchToProps(dispatch) {
       fetchCharts,
       startPeriodicRender,
       updateDashboardTitle,
+      updateCss,
       onChange,
-      onSave: saveDashboard,
+      onSave: saveDashboardRequest,
       setMaxUndoHistoryExceeded,
       maxUndoHistoryToast,
     },
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 2d44399..2523494 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -14,13 +14,13 @@ import {
   TOGGLE_BUILDER_PANE,
   TOGGLE_EXPAND_SLICE,
   TOGGLE_FAVE_STAR,
-  UPDATE_DASHBOARD_TITLE,
+  UPDATE_CSS,
 } from '../actions/dashboardState';
 
 export default function dashboardStateReducer(state = {}, action) {
   const actionHandlers = {
-    [UPDATE_DASHBOARD_TITLE]() {
-      return { ...state, title: action.title };
+    [UPDATE_CSS]() {
+      return { ...state, css: action.css };
     },
     [ADD_SLICE]() {
       const updatedSliceIds = new Set(state.sliceIds);
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index b209043..f129bf7 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -58,10 +58,7 @@ export default function(bootstrapData) {
     future: [],
   };
 
-  delete dashboard.position_json;
-  delete dashboard.css;
-
-  // creat a lookup to sync layout names with slice names
+  // create a lookup to sync layout names with slice names
   const chartIdToLayoutId = {};
   Object.values(layout).forEach(layoutComponent => {
     if (layoutComponent.type === CHART_TYPE) {
@@ -124,6 +121,8 @@ export default function(bootstrapData) {
       userId: user_id,
       dash_edit_perm: dashboard.dash_edit_perm,
       dash_save_perm: dashboard.dash_save_perm,
+      superset_can_explore: dashboard.superset_can_explore,
+      slice_can_edit: dashboard.slice_can_edit,
       common,
     },
     dashboardState: {
@@ -131,6 +130,7 @@ export default function(bootstrapData) {
       refresh: false,
       filters,
       expandedSlices: dashboard.metadata.expanded_slices || {},
+      css: dashboard.css || '',
       editMode: false,
       showBuilderPane: false,
       hasUnsavedChanges: false,
diff --git a/superset/assets/src/dashboard/stylesheets/builder.less b/superset/assets/src/dashboard/stylesheets/builder.less
index 7c14056..ecf192e 100644
--- a/superset/assets/src/dashboard/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/stylesheets/builder.less
@@ -46,9 +46,14 @@
 /* @TODO remove upon new theme */
 .btn.btn-primary {
   background: @almost-black !important;
+  border-color: @almost-black;
   color: white !important;
 }
 
+.dropdown-toggle.btn.btn-primary .caret {
+  color: white;
+}
+
 .background--transparent {
   background-color: transparent;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 8d8c8be..5756786 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -38,6 +38,29 @@
   }
 }
 
+.dashboard .dashboard-header {
+  #save-dash-split-button {
+    border-radius: 0;
+    margin-left: -8px;
+    height: 30px;
+    width: 30px;
+
+    &.btn.btn-primary {
+      border-left-color: white;
+    }
+
+    .caret {
+      position: absolute;
+      top: 24px;
+      left: 3px;
+    }
+
+    & + .dropdown-menu.dropdown-menu-right {
+      min-width: unset;
+    }
+  }
+}
+
 .dashboard .chart-header,
 .dashboard .dashboard-header {
   .dropdown-menu {
@@ -63,7 +86,7 @@
   padding: 0 16px;
   position: absolute;
   top: 0;
-  right: -22px;
+  right: -16px; //increase the click-able area for the button
 
   &:hover {
     cursor: pointer;
@@ -80,12 +103,17 @@
 
   .is-cached & {
     background-color: @pink;
-    margin-right: 6px;
   }
 
   .vertical-dots-container & {
     display: block;
   }
+
+  a[role="menuitem"] & {
+    width: 8px;
+    height: 8px;
+    margin-right: 8px;
+  }
 }
 
 
diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index d682687..ef2c8bb 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -41,3 +41,7 @@ export const DANGER_TOAST = 'DANGER_TOAST';
 
 // undo-redo
 export const UNDO_LIMIT = 50;
+
+// save dash options
+export const SAVE_TYPE_OVERWRITE = 'overwrite';
+export const SAVE_TYPE_NEWDASHBOARD = 'newDashboard';
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index eb937bb..c5d4e75 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -198,7 +198,7 @@ export function slugify(string) {
 
 export function getAjaxErrorMsg(error) {
   const respJSON = error.responseJSON;
-  return (respJSON && respJSON.message) ? respJSON.message :
+  return (respJSON && respJSON.error) ? respJSON.error :
           error.responseText;
 }
 


[incubator-superset] 07/26: [dashboard builder] improve perf (#4855)

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

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

commit 22052040dc588739b460f4d07760d2a875171c56
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Tue Apr 24 11:07:23 2018 -0700

    [dashboard builder] improve perf (#4855)
    
    * address major perf + css issues
    
    [dashboard builder] fix dashboard filters and some css
    
    [dashboard builder] use VIZ_TYPES, move stricter .eslintrc to dashboard/, more css fixes
    
    [builder] delete GridCell and GridLayout, remove some unused css. fix broken tabs.
    
    * [builder] fix errors post-rebase
    
    * [builder] add support for custom DragDroppable drag layer and add AddSliceDragPreview
    
    * [AddSliceDragPreview] fix type check
    
    * [dashboard builder] add prettier and update all files
    
    * [dashboard builder] merge v2/ directory int dashboard/
    
    * [dashboard builder] move component/*Container => containers/*
---
 superset/assets/images/loading.gif                 |  Bin 16671 -> 1945878 bytes
 superset/assets/package.json                       |    5 +-
 superset/assets/src/chart/Chart.jsx                |   80 +-
 superset/assets/src/chart/ChartContainer.jsx       |   22 +-
 superset/assets/src/chart/chartReducer.js          |    5 +-
 superset/assets/src/components/Loading.jsx         |    3 +
 superset/assets/src/dashboard/{v2 => }/.eslintrc   |    6 +-
 superset/assets/src/dashboard/.prettierrc          |    4 +
 .../dashboard/{v2 => }/actions/dashboardLayout.js  |   59 +-
 .../assets/src/dashboard/actions/dashboardState.js |   58 +-
 .../assets/src/dashboard/actions/datasources.js    |    3 +-
 .../dashboard/{v2 => }/actions/messageToasts.js    |   13 +-
 .../assets/src/dashboard/actions/sliceEntities.js  |   15 +-
 .../src/dashboard/components/ActionMenuItem.jsx    |   40 +-
 .../src/dashboard/components/AddSliceCard.jsx      |   59 +
 .../{v2 => }/components/BuilderComponentPane.jsx   |   15 +-
 .../assets/src/dashboard/components/CodeModal.jsx  |   10 +-
 .../assets/src/dashboard/components/Controls.jsx   |  123 +-
 .../assets/src/dashboard/components/CssEditor.jsx  |   11 +-
 .../assets/src/dashboard/components/Dashboard.jsx  |  240 +-
 .../{v2 => }/components/DashboardBuilder.jsx       |   39 +-
 .../dashboard/components/DashboardContainer.jsx    |   51 -
 .../{v2 => }/components/DashboardGrid.jsx          |  106 +-
 .../{v2 => }/components/DeleteComponentButton.jsx  |    7 +-
 .../assets/src/dashboard/components/GridCell.jsx   |  158 -
 .../assets/src/dashboard/components/GridLayout.jsx |  156 -
 .../assets/src/dashboard/components/Header.jsx     |  117 +-
 .../dashboard/{v2 => }/components/IconButton.jsx   |    0
 .../dashboard/components/RefreshIntervalModal.jsx  |    4 +-
 .../assets/src/dashboard/components/SaveModal.jsx  |   63 +-
 .../assets/src/dashboard/components/SliceAdder.jsx |  125 +-
 .../dashboard/components/SliceAdderContainer.jsx   |   25 -
 .../src/dashboard/components/SliceHeader.jsx       |  102 +-
 .../dashboard/components/SliceHeaderControls.jsx   |   29 +-
 .../src/dashboard/{v2 => }/components/Toast.jsx    |   11 +-
 .../{v2 => }/components/ToastPresenter.jsx         |    9 +-
 .../components/dnd/AddSliceDragPreview.jsx         |   70 +
 .../{v2 => }/components/dnd/DragDroppable.jsx      |   52 +-
 .../{v2 => }/components/dnd/DragHandle.jsx         |    8 +-
 .../{v2 => }/components/dnd/dragDroppableConfig.js |   12 +-
 .../{v2 => }/components/dnd/handleDrop.js          |   22 +-
 .../{v2 => }/components/dnd/handleHover.js         |    0
 .../dashboard/components/gridComponents/Chart.jsx  |  233 ++
 .../components/gridComponents/ChartHolder.jsx      |   63 +-
 .../{v2 => }/components/gridComponents/Column.jsx  |   55 +-
 .../{v2 => }/components/gridComponents/Divider.jsx |    5 +-
 .../{v2 => }/components/gridComponents/Header.jsx  |   18 +-
 .../{v2 => }/components/gridComponents/Row.jsx     |   48 +-
 .../{v2 => }/components/gridComponents/Tab.jsx     |   26 +-
 .../{v2 => }/components/gridComponents/Tabs.jsx    |   61 +-
 .../{v2 => }/components/gridComponents/index.js    |    2 -
 .../gridComponents/new/DraggableNewComponent.jsx   |    5 +-
 .../components/gridComponents/new/NewColumn.jsx    |   16 +
 .../components/gridComponents/new/NewDivider.jsx   |   16 +
 .../components/gridComponents/new/NewHeader.jsx    |   16 +
 .../components/gridComponents/new/NewRow.jsx       |   16 +
 .../components/gridComponents/new/NewTabs.jsx      |   16 +
 .../components/menu/BackgroundStyleDropdown.jsx    |    0
 .../{v2 => }/components/menu/HoverMenu.jsx         |    0
 .../{v2 => }/components/menu/PopoverDropdown.jsx   |    4 +-
 .../{v2 => }/components/menu/WithPopoverMenu.jsx   |   22 +-
 .../components/resizable/ResizableContainer.jsx    |   72 +-
 .../components/resizable/ResizableHandle.jsx       |   12 +-
 superset/assets/src/dashboard/containers/Chart.jsx |   59 +
 .../assets/src/dashboard/containers/Dashboard.jsx  |   49 +
 .../{v2 => }/containers/DashboardBuilder.jsx       |   18 +-
 .../{v2 => }/containers/DashboardComponent.jsx     |   44 +-
 .../{v2 => }/containers/DashboardGrid.jsx          |   16 +-
 .../{v2 => }/containers/DashboardHeader.jsx        |   48 +-
 .../{v2 => }/containers/ToastPresenter.jsx         |    0
 .../{v2 => }/fixtures/emptyDashboardLayout.js      |    4 +-
 superset/assets/src/dashboard/index.jsx            |    7 +-
 .../dashboard/{v2 => }/reducers/dashboardLayout.js |   80 +-
 .../src/dashboard/reducers/dashboardState.js       |   25 +-
 .../assets/src/dashboard/reducers/datasources.js   |    9 +-
 .../src/dashboard/reducers/getInitialState.js      |   26 +-
 superset/assets/src/dashboard/reducers/index.js    |   12 +-
 .../dashboard/{v2 => }/reducers/messageToasts.js   |    4 +-
 .../assets/src/dashboard/reducers/sliceEntities.js |   10 +-
 .../dashboard/reducers/undoableDashboardLayout.js  |   27 +
 .../{v2 => }/stylesheets/builder-sidepane.less     |   28 +-
 .../dashboard/{v2 => }/stylesheets/builder.less    |   23 +-
 .../dashboard/{v2 => }/stylesheets/buttons.less    |    0
 .../dashboard/stylesheets/components/chart.less    |   69 +
 .../{v2 => }/stylesheets/components/column.less    |   19 +-
 .../{v2 => }/stylesheets/components/divider.less   |    0
 .../{v2 => }/stylesheets/components/header.less    |    5 +-
 .../{v2 => }/stylesheets/components/index.less     |    0
 .../stylesheets/components/new-component.less      |    0
 .../{v2 => }/stylesheets/components/row.less       |   19 +-
 .../{v2 => }/stylesheets/components/tabs.less      |    0
 .../src/dashboard/stylesheets/dashboard.less       |  104 +
 .../src/dashboard/{v2 => }/stylesheets/dnd.less    |    2 +-
 .../src/dashboard/{v2 => }/stylesheets/grid.less   |   14 +-
 .../dashboard/{v2 => }/stylesheets/hover-menu.less |    0
 .../src/dashboard/{v2 => }/stylesheets/index.less  |    2 +
 .../{v2 => }/stylesheets/popover-menu.less         |    0
 .../dashboard/{v2 => }/stylesheets/resizable.less  |   26 +-
 .../src/dashboard/{v2 => }/stylesheets/toast.less  |    5 +-
 .../dashboard/{v2 => }/stylesheets/variables.less  |    0
 .../src/dashboard/util/backgroundStyleOptions.js   |   15 +
 .../util/charts/getEffectiveExtraFilters.js        |   42 +
 .../util/charts/getFormDataWithExtraFilters.js     |   42 +
 .../src/dashboard/util/componentIsResizable.js     |    5 +
 .../src/dashboard/{v2 => }/util/componentTypes.js  |    0
 .../src/dashboard/{v2 => }/util/constants.js       |    0
 .../assets/src/dashboard/util/dashboardHelper.js   |    9 -
 .../src/dashboard/util/dashboardLayoutConverter.js |   93 +-
 .../src/dashboard/{v2 => }/util/dnd-reorder.js     |   12 +-
 .../dashboard/{v2 => }/util/dropOverflowsParent.js |   26 +-
 .../src/dashboard/{v2 => }/util/findParentId.js    |    6 +-
 .../src/dashboard/util/getChartIdsFromLayout.js    |    8 +
 .../src/dashboard/{v2 => }/util/getChildWidth.js   |    6 +-
 .../src/dashboard/{v2 => }/util/getDropPosition.js |   26 +-
 .../dashboard/{v2 => }/util/headerStyleOptions.js  |    2 +-
 .../src/dashboard/{v2 => }/util/isValidChild.js    |   28 +-
 .../dashboard/{v2 => }/util/newComponentFactory.js |   10 +-
 .../dashboard/{v2 => }/util/newEntitiesFromDrop.js |   14 +-
 .../src/dashboard/{v2 => }/util/propShapes.jsx     |   20 +-
 .../src/dashboard/{v2 => }/util/resizableConfig.js |    0
 .../{v2 => }/util/shouldWrapChildInRow.js          |    0
 .../assets/src/dashboard/v2/actions/editMode.js    |    9 -
 .../src/dashboard/v2/components/Dashboard.jsx      |   30 -
 .../dashboard/v2/components/DashboardHeader.jsx    |   99 -
 .../dashboard/v2/components/StaticDashboard.jsx    |   19 -
 .../v2/components/gridComponents/new/NewChart.jsx  |   24 -
 .../v2/components/gridComponents/new/NewColumn.jsx |   24 -
 .../components/gridComponents/new/NewDivider.jsx   |   24 -
 .../v2/components/gridComponents/new/NewHeader.jsx |   24 -
 .../v2/components/gridComponents/new/NewRow.jsx    |   23 -
 .../v2/components/gridComponents/new/NewTabs.jsx   |   24 -
 .../assets/src/dashboard/v2/reducers/editMode.js   |   11 -
 superset/assets/src/dashboard/v2/reducers/index.js |    8 -
 .../dashboard/v2/stylesheets/components/chart.less |   20 -
 .../dashboard/v2/util/backgroundStyleOptions.js    |    7 -
 .../src/dashboard/v2/util/componentIsResizable.js  |   13 -
 .../src/explore/components/ExploreChartHeader.jsx  |    2 +-
 .../src/explore/components/ExploreChartPanel.jsx   |   15 +-
 .../explore/components/ExploreViewContainer.jsx    |    2 +-
 superset/assets/stylesheets/dashboard.less         |  156 -
 superset/assets/stylesheets/superset.less          |    2 +-
 superset/assets/stylesheets/welcome.css            |    2 +-
 superset/assets/yarn.lock                          | 3248 ++++----------------
 superset/templates/superset/dashboard.html         |    7 +-
 superset/views/core.py                             |    2 +-
 145 files changed, 2943 insertions(+), 4613 deletions(-)

diff --git a/superset/assets/images/loading.gif b/superset/assets/images/loading.gif
index 01ae393..ae5cbdd 100644
Binary files a/superset/assets/images/loading.gif and b/superset/assets/images/loading.gif differ
diff --git a/superset/assets/package.json b/superset/assets/package.json
index d20dad7..ab440f0 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -114,7 +114,7 @@
     "redux": "^3.5.2",
     "redux-localstorage": "^0.4.1",
     "redux-thunk": "^2.1.0",
-    "redux-undo": "^0.6.1",
+    "redux-undo": "^1.0.0-beta9-9-7",
     "shortid": "^2.2.6",
     "sprintf-js": "^1.1.1",
     "srcdoc-polyfill": "^1.0.0",
@@ -138,8 +138,10 @@
     "enzyme": "^2.0.0",
     "eslint": "^4.19.0",
     "eslint-config-airbnb": "^15.0.1",
+    "eslint-config-prettier": "^2.9.0",
     "eslint-plugin-import": "^2.2.0",
     "eslint-plugin-jsx-a11y": "^5.1.1",
+    "eslint-plugin-prettier": "^2.6.0",
     "eslint-plugin-react": "^7.0.1",
     "exports-loader": "^0.7.0",
     "extract-text-webpack-plugin": "3.0.2",
@@ -154,6 +156,7 @@
     "less-loader": "^4.0.3",
     "mocha": "^3.2.0",
     "npm-check-updates": "^2.14.0",
+    "prettier": "^1.12.1",
     "react-addons-test-utils": "^15.6.2",
     "react-test-renderer": "^15.6.2",
     "redux-mock-store": "^1.2.3",
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index e9f7c63..4a471e8 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -73,10 +73,16 @@ class Chart extends React.PureComponent {
 
   componentDidMount() {
     if (this.props.triggerQuery) {
-      this.props.actions.runQuery(this.props.formData, false,
+      const { formData } = this.props;
+      this.props.actions.runQuery(
+        formData,
+        false,
         this.props.timeout,
         this.props.chartId,
       );
+    } else {
+      // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data
+      this.renderViz();
     }
   }
 
@@ -89,14 +95,16 @@ class Chart extends React.PureComponent {
   }
 
   componentDidUpdate(prevProps) {
-    if (this.props.queryResponse &&
+    if (
+      this.props.queryResponse &&
       ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
       !this.props.queryResponse.error && (
-      prevProps.annotationData !== this.props.annotationData ||
-      prevProps.queryResponse !== this.props.queryResponse ||
-      prevProps.height !== this.props.height ||
-      prevProps.width !== this.props.width ||
-      prevProps.lastRendered !== this.props.lastRendered)
+        prevProps.annotationData !== this.props.annotationData ||
+        prevProps.queryResponse !== this.props.queryResponse ||
+        prevProps.height !== this.props.height ||
+        prevProps.width !== this.props.width ||
+        prevProps.lastRendered !== this.props.lastRendered
+      )
     ) {
       this.renderViz();
     }
@@ -123,7 +131,8 @@ class Chart extends React.PureComponent {
   }
 
   width() {
-    return this.props.width || this.container.el.offsetWidth;
+    return this.props.width ||
+      (this.container && this.container.el && this.container.el.offsetWidth);
   }
 
   headerHeight() {
@@ -131,7 +140,8 @@ class Chart extends React.PureComponent {
   }
 
   height() {
-    return this.props.height || this.container.el.offsetHeight;
+    return this.props.height
+      || (this.container && this.container.el && this.container.el.offsetHeight);
   }
 
   d3format(col, number) {
@@ -159,7 +169,6 @@ class Chart extends React.PureComponent {
 
   renderTooltip() {
     if (this.state.tooltip) {
-      /* eslint-disable react/no-danger */
       return (
         <Tooltip
           className="chart-tooltip"
@@ -169,52 +178,55 @@ class Chart extends React.PureComponent {
           positionLeft={this.state.tooltip.x + 30}
           arrowOffsetTop={10}
         >
-          <div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
+          <div // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }}
+          />
         </Tooltip>
       );
-      /* eslint-enable react/no-danger */
     }
     return null;
   }
 
   renderViz() {
-    const viz = visMap[this.props.vizType];
-    const fd = this.props.formData;
-    const qr = this.props.queryResponse;
+    const { vizType, formData, queryResponse, setControlValue, chartId, chartStatus } = this.props;
+    const visRenderer = visMap[vizType];
     const renderStart = Logger.getTimestamp();
     try {
       // Executing user-defined data mutator function
-      if (fd.js_data) {
-        qr.data = sandboxedEval(fd.js_data)(qr.data);
+      if (formData.js_data) {
+        queryResponse.data = sandboxedEval(formData.js_data)(queryResponse.data);
+      }
+      visRenderer(this, queryResponse, setControlValue);
+      if (chartStatus !== 'rendered') {
+        this.props.actions.chartRenderingSucceeded(chartId);
       }
-      // [re]rendering the visualization
-      viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_EVENT, {
-        label: 'slice_' + this.props.chartId,
-        vis_type: this.props.vizType,
+        label: 'slice_' + chartId,
+        vis_type: vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(this.props.chartId);
+      this.props.actions.chartRenderingSucceeded(chartId);
     } catch (e) {
-      this.props.actions.chartRenderingFailed(e, this.props.chartId);
+      console.error(e); // eslint-disable-line no-console
+      this.props.actions.chartRenderingFailed(e, chartId);
     }
   }
 
   render() {
     const isLoading = this.props.chartStatus === 'loading';
+
+    // this allows <Loading /> to be positioned in the middle of the chart
+    const containerStyles = isLoading ? { height: this.height(), width: this.width() } : null;
     return (
-      <div className={`${isLoading ? 'is-loading' : ''}`}>
+      <div className={`chart-container ${isLoading ? 'is-loading' : ''}`} style={containerStyles}>
         {this.renderTooltip()}
-        {isLoading &&
-          <Loading size={25} />
-        }
+        {isLoading && <Loading size={75} />}
         {this.props.chartAlert &&
-        <StackTraceMessage
-          message={this.props.chartAlert}
-          queryResponse={this.props.queryResponse}
-        />
-        }
+          <StackTraceMessage
+            message={this.props.chartAlert}
+            queryResponse={this.props.queryResponse}
+          />}
 
         {!isLoading &&
           !this.props.chartAlert &&
@@ -226,8 +238,8 @@ class Chart extends React.PureComponent {
             width={this.width()}
             onQuery={this.props.onQuery}
             onDismiss={this.props.onDismissRefreshOverlay}
-          />
-        }
+          />}
+
         {!isLoading && !this.props.chartAlert &&
           <ChartBody
             containerId={this.containerId}
diff --git a/superset/assets/src/chart/ChartContainer.jsx b/superset/assets/src/chart/ChartContainer.jsx
index e3cb1f9..b66fe5d 100644
--- a/superset/assets/src/chart/ChartContainer.jsx
+++ b/superset/assets/src/chart/ChartContainer.jsx
@@ -1,29 +1,13 @@
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
 
-import * as Actions from './chartAction';
+import * as actions from './chartAction';
 import Chart from './Chart';
 
-function mapStateToProps({ charts }, ownProps) {
-  const chart = charts[ownProps.chartId];
-  return {
-    annotationData: chart.annotationData,
-    chartAlert: chart.chartAlert,
-    chartStatus: chart.chartStatus,
-    chartUpdateEndTime: chart.chartUpdateEndTime,
-    chartUpdateStartTime: chart.chartUpdateStartTime,
-    latestQueryFormData: chart.latestQueryFormData,
-    lastRendered: chart.lastRendered,
-    queryResponse: chart.queryResponse,
-    queryRequest: chart.queryRequest,
-    triggerQuery: chart.triggerQuery,
-  };
-}
-
 function mapDispatchToProps(dispatch) {
   return {
-    actions: bindActionCreators(Actions, dispatch),
+    actions: bindActionCreators(actions, dispatch),
   };
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(Chart);
+export default connect(null, mapDispatchToProps)(Chart);
diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js
index d57959a..ea8de8b 100644
--- a/superset/assets/src/chart/chartReducer.js
+++ b/superset/assets/src/chart/chartReducer.js
@@ -142,7 +142,10 @@ export default function chartReducer(charts = {}, action) {
   }
 
   if (action.type in actionHandlers) {
-    return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
+    return {
+      ...charts,
+      [action.key]: actionHandlers[action.type](charts[action.key], action),
+    };
   }
 
   return charts;
diff --git a/superset/assets/src/components/Loading.jsx b/superset/assets/src/components/Loading.jsx
index 416e770..810c581 100644
--- a/superset/assets/src/components/Loading.jsx
+++ b/superset/assets/src/components/Loading.jsx
@@ -20,6 +20,9 @@ export default function Loading(props) {
         padding: 0,
         margin: 0,
         position: 'absolute',
+        left: '50%',
+        top: '50%',
+        transform: 'translate(-50%, -60%)',
       }}
     />
   );
diff --git a/superset/assets/src/dashboard/v2/.eslintrc b/superset/assets/src/dashboard/.eslintrc
similarity index 84%
rename from superset/assets/src/dashboard/v2/.eslintrc
rename to superset/assets/src/dashboard/.eslintrc
index 70efc15..a3f86e3 100644
--- a/superset/assets/src/dashboard/v2/.eslintrc
+++ b/superset/assets/src/dashboard/.eslintrc
@@ -1,4 +1,6 @@
 {
+  "extends": "prettier",
+  "plugins": ["prettier"],
   "rules": {
     "prefer-template": 2,
     "new-cap": 2,
@@ -12,7 +14,7 @@
     "jsx-a11y/anchor-has-content": 2,
     "react/require-default-props": 2,
     "no-plusplus": 2,
-    "no-mixed-operators": 2,
+    "no-mixed-operators": 0,
     "no-continue": 2,
     "no-bitwise": 2,
     "no-undef": 2,
@@ -25,5 +27,7 @@
     "import/prefer-default-export": 2,
     "react/no-unescaped-entities": 2,
     "react/no-string-refs": 2,
+    "react/jsx-indent": 0,
+    "prettier/prettier": "error"
   }
 }
diff --git a/superset/assets/src/dashboard/.prettierrc b/superset/assets/src/dashboard/.prettierrc
new file mode 100644
index 0000000..a20502b
--- /dev/null
+++ b/superset/assets/src/dashboard/.prettierrc
@@ -0,0 +1,4 @@
+{
+  "singleQuote": true,
+  "trailingComma": "all"
+}
diff --git a/superset/assets/src/dashboard/v2/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
similarity index 73%
rename from superset/assets/src/dashboard/v2/actions/dashboardLayout.js
rename to superset/assets/src/dashboard/actions/dashboardLayout.js
index b6d41c4..5a04de5 100644
--- a/superset/assets/src/dashboard/v2/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -1,6 +1,11 @@
 import { addInfoToast } from './messageToasts';
+import { setUnsavedChanges } from './dashboardState';
 import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
-import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import {
+  DASHBOARD_ROOT_ID,
+  NEW_COMPONENTS_SOURCE_ID,
+  GRID_MIN_COLUMN_COUNT,
+} from '../util/constants';
 import dropOverflowsParent from '../util/dropOverflowsParent';
 import findParentId from '../util/findParentId';
 
@@ -62,12 +67,10 @@ export function resizeComponent({ id, width, height }) {
     const { dashboardLayout: undoableLayout } = getState();
     const { present: dashboard } = undoableLayout;
     const component = dashboard[id];
-
-    if (
-      component &&
-      (component.meta.width !== width || component.meta.height !== height)
-    ) {
-      // update the size of this component + any resizable children
+    const widthChanged = width && component.meta.width !== width;
+    const heightChanged = height && component.meta.height !== height;
+    if (component && (widthChanged || heightChanged)) {
+      // update the size of this component
       const updatedComponents = {
         [id]: {
           ...component,
@@ -79,14 +82,16 @@ export function resizeComponent({ id, width, height }) {
         },
       };
 
-      component.children.forEach((childId) => {
+      // 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: width || child.meta.width,
+              width: GRID_MIN_COLUMN_COUNT,
               height: height || child.meta.height,
             },
           };
@@ -94,6 +99,7 @@ export function resizeComponent({ id, width, height }) {
       });
 
       dispatch(updateComponents(updatedComponents));
+      dispatch(setUnsavedChanges(true));
     }
   };
 }
@@ -112,13 +118,18 @@ export function moveComponent(dropResult) {
 export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
 export function handleComponentDrop(dropResult) {
   return (dispatch, getState) => {
-    const overflowsParent = dropOverflowsParent(dropResult, getState().dashboardLayout.present);
+    const overflowsParent = dropOverflowsParent(
+      dropResult,
+      getState().dashboardLayout.present,
+    );
 
     if (overflowsParent) {
-      return dispatch(addInfoToast(
-        `Parent does not have enough space for this component.
+      return dispatch(
+        addInfoToast(
+          `Parent does not have enough space for this component.
          Try decreasing its width or add it to a new row.`,
-      ));
+        ),
+      );
     }
 
     const { source, destination } = dropResult;
@@ -130,12 +141,10 @@ export function handleComponentDrop(dropResult) {
     } else if (destination && isNewComponent) {
       dispatch(createComponent(dropResult));
     } else if (
-      destination
-      && source
-      && !( // ensure it has moved
-        destination.id === source.id
-        && destination.index === source.index
-      )
+      destination &&
+      source &&
+      !// ensure it has moved
+      (destination.id === source.id && destination.index === source.index)
     ) {
       dispatch(moveComponent(dropResult));
     }
@@ -146,12 +155,20 @@ export function handleComponentDrop(dropResult) {
       const { present: layout } = undoableLayout;
       const sourceComponent = layout[source.id];
 
-      if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
-        const parentId = findParentId({ childId: source.id, components: layout });
+      if (
+        sourceComponent.type === TABS_TYPE &&
+        sourceComponent.children.length === 0
+      ) {
+        const parentId = findParentId({
+          childId: source.id,
+          components: layout,
+        });
         dispatch(deleteComponent(source.id, parentId));
       }
     }
 
+    dispatch(setUnsavedChanges(true));
+
     return null;
   };
 }
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 2262729..d80ec83 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -6,6 +6,11 @@ import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
 import { applyDefaultFormData } from '../../explore/stores/store';
 
+export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
+export function setUnsavedChanges(hasUnsavedChanges) {
+  return { type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges } };
+}
+
 export const ADD_FILTER = 'ADD_FILTER';
 export function addFilter(chart, col, vals, merge = true, refresh = true) {
   return { type: ADD_FILTER, chart, col, vals, merge, refresh };
@@ -39,20 +44,19 @@ export function toggleFaveStar(isStarred) {
 
 export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
 export function fetchFaveStar(id) {
-  return function (dispatch) {
+  return function fetchFaveStarThunk(dispatch) {
     const url = `${FAVESTAR_BASE_URL}/${id}/count`;
-    return $.get(url)
-      .done((data) => {
-        if (data.count > 0) {
-          dispatch(toggleFaveStar(true));
-        }
-      });
+    return $.get(url).done(data => {
+      if (data.count > 0) {
+        dispatch(toggleFaveStar(true));
+      }
+    });
   };
 }
 
 export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
 export function saveFaveStar(id, isStarred) {
-  return function (dispatch) {
+  return function saveFaveStarThunk(dispatch) {
     const urlSuffix = isStarred ? 'unselect' : 'select';
     const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
     $.get(url);
@@ -82,21 +86,29 @@ export function onSave() {
 
 export function fetchCharts(chartList = [], force = false, interval = 0) {
   return (dispatch, getState) => {
-    const timeout = getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
+    const timeout = getState().dashboardInfo.common.conf
+      .SUPERSET_WEBSERVER_TIMEOUT;
     if (!interval) {
-      chartList.forEach(chart => (dispatch(refreshChart(chart, force, timeout))));
+      chartList.forEach(chart => dispatch(refreshChart(chart, force, timeout)));
       return;
     }
 
     const { metadata: meta } = getState().dashboardInfo;
     const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
     if (typeof meta.stagger_refresh !== 'boolean') {
-      meta.stagger_refresh = meta.stagger_refresh === undefined ?
-        true : meta.stagger_refresh === 'true';
+      meta.stagger_refresh =
+        meta.stagger_refresh === undefined
+          ? true
+          : meta.stagger_refresh === 'true';
     }
-    const delay = meta.stagger_refresh ? refreshTime / (chartList.length - 1) : 0;
+    const delay = meta.stagger_refresh
+      ? refreshTime / (chartList.length - 1)
+      : 0;
     chartList.forEach((chart, i) => {
-      setTimeout(() => dispatch(refreshChart(chart, force, timeout)), delay * i);
+      setTimeout(
+        () => dispatch(refreshChart(chart, force, timeout)),
+        delay * i,
+      );
     });
   };
 }
@@ -116,9 +128,9 @@ export function startPeriodicRender(interval) {
     const { metadata } = getState().dashboardInfo;
     const immune = metadata.timed_refresh_immune_slices || [];
     const refreshAll = () => {
-      const affected =
-        Object.values(getState().charts)
-          .filter(chart => immune.indexOf(chart.id) === -1);
+      const affected = Object.values(getState().charts).filter(
+        chart => immune.indexOf(chart.id) === -1,
+      );
       return dispatch(fetchCharts(affected, true, interval * 0.2));
     };
     const fetchAndRender = () => {
@@ -149,17 +161,15 @@ export function addSliceToDashboard(id) {
       formData: applyDefaultFormData(form_data),
     };
 
-    return Promise
-      .all([
-        dispatch(addChart(newChart, id)),
-        dispatch(fetchDatasourceMetadata(form_data.datasource)),
-      ])
-      .then(() => dispatch(addSlice(selectedSlice)));
+    return Promise.all([
+      dispatch(addChart(newChart, id)),
+      dispatch(fetchDatasourceMetadata(form_data.datasource)),
+    ]).then(() => dispatch(addSlice(selectedSlice)));
   };
 }
 
 export function removeSliceFromDashboard(chart) {
-  return (dispatch) => {
+  return dispatch => {
     dispatch(removeSlice(chart.id));
     dispatch(removeChart(chart.id));
   };
diff --git a/superset/assets/src/dashboard/actions/datasources.js b/superset/assets/src/dashboard/actions/datasources.js
index a00bb17..d97296e 100644
--- a/superset/assets/src/dashboard/actions/datasources.js
+++ b/superset/assets/src/dashboard/actions/datasources.js
@@ -29,7 +29,8 @@ export function fetchDatasourceMetadata(key) {
       type: 'GET',
       url,
       success: data => dispatch(setDatasource(data, key)),
-      error: error => dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
+      error: error =>
+        dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
     });
   };
 }
diff --git a/superset/assets/src/dashboard/v2/actions/messageToasts.js b/superset/assets/src/dashboard/actions/messageToasts.js
similarity index 85%
rename from superset/assets/src/dashboard/v2/actions/messageToasts.js
rename to superset/assets/src/dashboard/actions/messageToasts.js
index 2ebc06c..367b36f 100644
--- a/superset/assets/src/dashboard/v2/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/actions/messageToasts.js
@@ -1,7 +1,16 @@
-import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+import {
+  INFO_TOAST,
+  SUCCESS_TOAST,
+  WARNING_TOAST,
+  DANGER_TOAST,
+} from '../util/constants';
 
 function getToastUuid(type) {
-  return `${Math.random().toString(16).slice(2)}-${type}-${Math.random().toString(16).slice(2)}`;
+  return `${Math.random()
+    .toString(16)
+    .slice(2)}-${type}-${Math.random()
+    .toString(16)
+    .slice(2)}`;
 }
 
 export const ADD_TOAST = 'ADD_TOAST';
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index 3a1b1dc..6922753 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -9,16 +9,15 @@ export function updateSliceName(key, sliceName) {
 
 export function saveSliceName(slice, sliceName) {
   const oldName = slice.slice_name;
-  return (dispatch) => {
+  return dispatch => {
     const sliceParams = {};
     sliceParams.slice_id = slice.slice_id;
     sliceParams.action = 'overwrite';
     sliceParams.slice_name = sliceName;
 
-    const url = slice.slice_url + '&' +
-      Object.keys(sliceParams)
-      .map(key => (key + '=' + sliceParams[key]))
-      .join('&');
+    const url = `${slice.slice_url}&${Object.keys(sliceParams)
+      .map(key => `${key}=${sliceParams[key]}`)
+      .join('&')}`;
     const key = slice.slice_id;
     return $.ajax({
       url,
@@ -54,7 +53,7 @@ export function fetchAllSlicesFailed(error) {
 
 export function fetchAllSlices(userId) {
   return (dispatch, getState) => {
-    const { sliceEntities }  = getState();
+    const { sliceEntities } = getState();
     if (sliceEntities.lastUpdated === 0) {
       dispatch(fetchAllSlicesStarted());
 
@@ -62,9 +61,9 @@ export function fetchAllSlices(userId) {
       return $.ajax({
         url: uri,
         type: 'GET',
-        success: (response) => {
+        success: response => {
           const slices = {};
-          response.result.forEach((slice) => {
+          response.result.forEach(slice => {
             const form_data = JSON.parse(slice.params);
             slices[slice.id] = {
               slice_id: slice.id,
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
index aaae4df..a0ecb78 100644
--- a/superset/assets/src/dashboard/components/ActionMenuItem.jsx
+++ b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
@@ -7,9 +7,7 @@ import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
 export function MenuItemContent({ faIcon, text, tooltip, children }) {
   return (
     <span>
-      {faIcon &&
-        <i className={`fa fa-${faIcon}`}>&nbsp;</i>
-      }
+      {faIcon && <i className={`fa fa-${faIcon}`}>&nbsp;</i>}
       {text} {''}
       <InfoTooltipWithTrigger
         tooltip={tooltip}
@@ -20,6 +18,7 @@ export function MenuItemContent({ faIcon, text, tooltip, children }) {
     </span>
   );
 }
+
 MenuItemContent.propTypes = {
   faIcon: PropTypes.string,
   text: PropTypes.string,
@@ -27,19 +26,40 @@ MenuItemContent.propTypes = {
   children: PropTypes.node,
 };
 
-export function ActionMenuItem(props) {
+MenuItemContent.defaultProps = {
+  faIcon: '',
+  text: '',
+  tooltip: null,
+  children: null,
+};
+
+export function ActionMenuItem({
+  onClick,
+  href,
+  target,
+  text,
+  tooltip,
+  children,
+  faIcon,
+}) {
   return (
-    <MenuItem
-      onClick={props.onClick}
-      href={props.href}
-      target={props.target}
-    >
-      <MenuItemContent {...props} />
+    <MenuItem onClick={onClick} href={href} target={target}>
+      <MenuItemContent faIcon={faIcon} text={text} tooltip={tooltip}>
+        {children}
+      </MenuItemContent>
     </MenuItem>
   );
 }
+
 ActionMenuItem.propTypes = {
   onClick: PropTypes.func,
   href: PropTypes.string,
   target: PropTypes.string,
+  ...MenuItemContent.propTypes,
+};
+
+ActionMenuItem.defaultProps = {
+  onClick() {},
+  href: null,
+  target: null,
 };
diff --git a/superset/assets/src/dashboard/components/AddSliceCard.jsx b/superset/assets/src/dashboard/components/AddSliceCard.jsx
new file mode 100644
index 0000000..7fd9ba4
--- /dev/null
+++ b/superset/assets/src/dashboard/components/AddSliceCard.jsx
@@ -0,0 +1,59 @@
+import cx from 'classnames';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+  datasourceLink: PropTypes.string,
+  innerRef: PropTypes.func,
+  isSelected: PropTypes.bool,
+  lastModified: PropTypes.string.isRequired,
+  sliceName: PropTypes.string.isRequired,
+  style: PropTypes.object,
+  visType: PropTypes.string.isRequired,
+};
+
+const defaultProps = {
+  datasourceLink: '—',
+  innerRef: null,
+  isSelected: false,
+  style: null,
+};
+
+function AddSliceCard({
+  datasourceLink,
+  innerRef,
+  isSelected,
+  lastModified,
+  sliceName,
+  style,
+  visType,
+}) {
+  return (
+    <div ref={innerRef} className="chart-card-container" style={style}>
+      <div className={cx('chart-card', isSelected && 'is-selected')}>
+        <div className="card-title">{sliceName}</div>
+        <div className="card-body">
+          <div className="item">
+            <span>Modified </span>
+            <span>{lastModified}</span>
+          </div>
+          <div className="item">
+            <span>Visualization </span>
+            <span>{visType}</span>
+          </div>
+          <div className="item">
+            <span>Data source </span>
+            <span // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: datasourceLink }}
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+AddSliceCard.propTypes = propTypes;
+AddSliceCard.defaultProps = defaultProps;
+
+export default AddSliceCard;
diff --git a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
similarity index 85%
rename from superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
rename to superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index f9a37cc..e5bc74c 100644
--- a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -6,9 +6,7 @@ import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
 import NewTabs from './gridComponents/new/NewTabs';
-import SliceAdderContainer from '../../../dashboard/components/SliceAdderContainer';
-
-import '../stylesheets/builder-sidepane.less';
+import SliceAdderContainer from '../containers/SliceAdder';
 
 class BuilderComponentPane extends React.PureComponent {
   constructor(props) {
@@ -32,9 +30,13 @@ class BuilderComponentPane extends React.PureComponent {
       <div className="dashboard-builder-sidepane">
         <div className="dashboard-builder-sidepane-header">
           Insert components
-          {this.state.showSlices &&
-            <i className="fa fa-times close trigger" onClick={this.closeSlicesPane} role="none" />
-          }
+          {this.state.showSlices && (
+            <i
+              className="fa fa-times close trigger"
+              onClick={this.closeSlicesPane}
+              role="none"
+            />
+          )}
         </div>
 
         <div className="component-layer">
@@ -52,7 +54,6 @@ class BuilderComponentPane extends React.PureComponent {
 
           <NewHeader />
           <NewDivider />
-
           <NewTabs />
           <NewRow />
           <NewColumn />
diff --git a/superset/assets/src/dashboard/components/CodeModal.jsx b/superset/assets/src/dashboard/components/CodeModal.jsx
index 0e84ad1..cc0c9f2 100644
--- a/superset/assets/src/dashboard/components/CodeModal.jsx
+++ b/superset/assets/src/dashboard/components/CodeModal.jsx
@@ -12,13 +12,16 @@ const propTypes = {
 
 const defaultProps = {
   codeCallback: () => {},
+  code: '',
 };
 
 export default class CodeModal extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = { code: props.code };
+    this.beforeOpen = this.beforeOpen.bind(this);
   }
+
   beforeOpen() {
     let code = this.props.code;
     if (!code && this.props.codeCallback) {
@@ -26,18 +29,17 @@ export default class CodeModal extends React.PureComponent {
     }
     this.setState({ code });
   }
+
   render() {
     return (
       <ModalTrigger
         triggerNode={this.props.triggerNode}
         isButton
-        beforeOpen={this.beforeOpen.bind(this)}
+        beforeOpen={this.beforeOpen}
         modalTitle={t('Active Dashboard Filters')}
         modalBody={
           <div className="CodeModal">
-            <pre>
-              {this.state.code}
-            </pre>
+            <pre>{this.state.code}</pre>
           </div>
         }
       />
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index 8755e8f..06b4f7f 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -1,3 +1,4 @@
+/* global window */
 import React from 'react';
 import PropTypes from 'prop-types';
 import $ from 'jquery';
@@ -8,6 +9,24 @@ import SaveModal from './SaveModal';
 import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
 import { t } from '../../locales';
 
+function updateDom(css) {
+  const className = 'CssEditor-css';
+  const head = document.head || document.getElementsByTagName('head')[0];
+  let style = document.querySelector(`.${className}`);
+
+  if (!style) {
+    style = document.createElement('style');
+    style.className = className;
+    style.type = 'text/css';
+    head.appendChild(style);
+  }
+  if (style.styleSheet) {
+    style.styleSheet.cssText = css;
+  } else {
+    style.innerHTML = css;
+  }
+}
+
 const propTypes = {
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
@@ -15,13 +34,18 @@ const propTypes = {
   filters: PropTypes.object.isRequired,
   expandedSlices: PropTypes.object.isRequired,
   slices: PropTypes.array,
-  onSave: PropTypes.func,
-  onChange: PropTypes.func,
-  forceRefreshAllCharts: PropTypes.func,
-  startPeriodicRender: PropTypes.func,
+  onSave: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  forceRefreshAllCharts: PropTypes.func.isRequired,
+  startPeriodicRender: PropTypes.func.isRequired,
   editMode: PropTypes.bool,
 };
 
+const defaultProps = {
+  editMode: false,
+  slices: [],
+};
+
 class Controls extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -29,13 +53,12 @@ class Controls extends React.PureComponent {
       css: '',
       cssTemplates: [],
     };
-    this.toggleModal = this.toggleModal.bind(this);
-    this.updateDom = this.updateDom.bind(this);
   }
+
   componentWillMount() {
-    this.updateDom(this.state.css);
+    updateDom(this.state.css);
 
-    $.get('/csstemplateasyncmodelview/api/read', (data) => {
+    $.get('/csstemplateasyncmodelview/api/read', data => {
       const cssTemplates = data.result.map(row => ({
         value: row.template_name,
         css: row.css,
@@ -44,57 +67,48 @@ class Controls extends React.PureComponent {
       this.setState({ cssTemplates });
     });
   }
-  toggleModal(modal) {
-    let currentModal;
-    if (modal !== this.state.currentModal) {
-      currentModal = modal;
-    }
-    this.setState({ currentModal });
-  }
+
   changeCss(css) {
     this.setState({ css }, () => {
-      this.updateDom(css);
+      updateDom(css);
     });
     this.props.onChange();
   }
-  updateDom(css) {
-    const className = 'CssEditor-css';
-    const head = document.head || document.getElementsByTagName('head')[0];
-    let style = document.querySelector('.' + className);
 
-    if (!style) {
-      style = document.createElement('style');
-      style.className = className;
-      style.type = 'text/css';
-      head.appendChild(style);
-    }
-    if (style.styleSheet) {
-      style.styleSheet.cssText = css;
-    } else {
-      style.innerHTML = css;
-    }
-  }
   render() {
-    const { dashboardTitle, layout, filters, expandedSlices,
-      startPeriodicRender, forceRefreshAllCharts, onSave,
-      editMode } = this.props;
+    const {
+      dashboardTitle,
+      layout,
+      filters,
+      expandedSlices,
+      startPeriodicRender,
+      forceRefreshAllCharts,
+      onSave,
+      editMode,
+    } = this.props;
+
     const emailBody = t('Checkout this dashboard: %s', window.location.href);
-    const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
-      + `${dashboardTitle}&Body=${emailBody}`;
-    let saveText = t('Save as');
-    if (editMode) {
-      saveText = t('Save');
-    }
+    const emailLink =
+      'mailto:?Subject=Superset%20Dashboard%20' +
+      `${dashboardTitle}&Body=${emailBody}`;
+
     return (
       <span>
-        <DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
+        <DropdownButton
+          title="Actions"
+          bsSize="small"
+          id="bg-nested-dropdown"
+          pullRight
+        >
           <ActionMenuItem
             text={t('Force Refresh')}
             tooltip={t('Force refresh the whole dashboard')}
             onClick={forceRefreshAllCharts}
           />
           <RefreshIntervalModal
-            onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
+            onChange={refreshInterval =>
+              startPeriodicRender(refreshInterval * 1000)
+            }
             triggerNode={
               <MenuItemContent
                 text={t('Set autorefresh')}
@@ -112,30 +126,39 @@ class Controls extends React.PureComponent {
             css={this.state.css}
             triggerNode={
               <MenuItemContent
-                text={saveText}
+                text={editMode ? t('Save') : t('Save as')}
                 tooltip={t('Save the dashboard')}
               />
             }
+            isMenuItem
           />
-          {editMode &&
+          {editMode && (
             <ActionMenuItem
               text={t('Edit properties')}
               tooltip={t("Edit the dashboards's properties")}
-              onClick={() => { window.location = `/dashboardmodelview/edit/${this.props.dashboardInfo.id}`; }}
+              onClick={() => {
+                window.location = `/dashboardmodelview/edit/${
+                  this.props.dashboardInfo.id
+                }`;
+              }}
             />
-          }
-          {editMode &&
+          )}
+          {editMode && (
             <ActionMenuItem
               text={t('Email')}
               tooltip={t('Email a link to this dashboard')}
-              onClick={() => { window.location = emailLink; }}
+              onClick={() => {
+                window.location = emailLink;
+              }}
             />
-          }
+          )}
         </DropdownButton>
       </span>
     );
   }
 }
+
 Controls.propTypes = propTypes;
+Controls.defaultProps = defaultProps;
 
 export default Controls;
diff --git a/superset/assets/src/dashboard/components/CssEditor.jsx b/superset/assets/src/dashboard/components/CssEditor.jsx
index 5abf5f8..45ef86d 100644
--- a/superset/assets/src/dashboard/components/CssEditor.jsx
+++ b/superset/assets/src/dashboard/components/CssEditor.jsx
@@ -29,15 +29,20 @@ class CssEditor extends React.PureComponent {
       css: props.initialCss,
       cssTemplateOptions: [],
     };
+    this.changeCss = this.changeCss.bind(this);
+    this.changeCssTemplate = this.changeCssTemplate.bind(this);
   }
+
   changeCss(css) {
     this.setState({ css }, () => {
       this.props.onChange(css);
     });
   }
+
   changeCssTemplate(opt) {
     this.changeCss(opt.css);
   }
+
   renderTemplateSelector() {
     if (this.props.templates) {
       return (
@@ -46,13 +51,14 @@ class CssEditor extends React.PureComponent {
           <Select
             options={this.props.templates}
             placeholder={t('Load a CSS template')}
-            onChange={this.changeCssTemplate.bind(this)}
+            onChange={this.changeCssTemplate}
           />
         </div>
       );
     }
     return null;
   }
+
   render() {
     return (
       <ModalTrigger
@@ -70,7 +76,7 @@ class CssEditor extends React.PureComponent {
                   theme="github"
                   minLines={8}
                   maxLines={30}
-                  onChange={this.changeCss.bind(this)}
+                  onChange={this.changeCss}
                   height="200px"
                   width="100%"
                   editorProps={{ $blockScrolling: true }}
@@ -85,6 +91,7 @@ class CssEditor extends React.PureComponent {
     );
   }
 }
+
 CssEditor.propTypes = propTypes;
 CssEditor.defaultProps = defaultProps;
 
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 939476c..2d85ebf 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -1,26 +1,36 @@
+/* global window */
 import React from 'react';
 import PropTypes from 'prop-types';
 
 import AlertsWrapper from '../../components/AlertsWrapper';
-import GridLayout from './GridLayout';
+import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
+import DashboardBuilder from '../containers/DashboardBuilder';
 import {
   chartPropShape,
   slicePropShape,
   dashboardInfoPropShape,
   dashboardStatePropShape,
-} from '../v2/util/propShapes';
-import { exportChart } from '../../explore/exploreUtils';
+} from '../util/propShapes';
 import { areObjectsEqual } from '../../reduxUtils';
-import { getChartIdsFromLayout } from '../util/dashboardHelper';
-import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
-  LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
+import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
+import {
+  Logger,
+  ActionLog,
+  LOG_ACTIONS_PAGE_LOAD,
+  LOG_ACTIONS_LOAD_EVENT,
+  LOG_ACTIONS_RENDER_EVENT,
+} from '../../logger';
 import { t } from '../../locales';
 
-import '../../../stylesheets/dashboard.less';
-import '../v2/stylesheets/index.less';
+import '../stylesheets/index.less';
 
 const propTypes = {
-  actions: PropTypes.object.isRequired,
+  actions: PropTypes.shape({
+    addSliceToDashboard: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    removeSliceFromDashboard: PropTypes.func.isRequired,
+    runQuery: PropTypes.func.isRequired,
+  }).isRequired,
   dashboardInfo: dashboardInfoPropShape.isRequired,
   dashboardState: dashboardStatePropShape.isRequired,
   charts: PropTypes.objectOf(chartPropShape).isRequired,
@@ -40,6 +50,20 @@ const defaultProps = {
 };
 
 class Dashboard extends React.PureComponent {
+  static onBeforeUnload(hasChanged) {
+    if (hasChanged) {
+      window.addEventListener('beforeunload', Dashboard.unload);
+    } else {
+      window.removeEventListener('beforeunload', Dashboard.unload);
+    }
+  }
+
+  static unload() {
+    const message = t('You have unsaved changes.');
+    window.event.returnValue = message; // Gecko + IE
+    return message; // Gecko + Webkit, Safari, Chrome etc.
+  }
+
   constructor(props) {
     super(props);
 
@@ -52,31 +76,15 @@ class Dashboard extends React.PureComponent {
       eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
     });
     Logger.start(this.loadingLog);
-
-    this.rerenderCharts = this.rerenderCharts.bind(this);
-    this.getFilters = this.getFilters.bind(this);
-    this.refreshExcept = this.refreshExcept.bind(this);
-    this.getFormDataExtra = this.getFormDataExtra.bind(this);
-    this.exploreChart = this.exploreChart.bind(this);
-    this.exportCSV = this.exportCSV.bind(this);
-
-    this.props.actions.saveSliceName = this.props.actions.saveSliceName.bind(this);
-    this.props.actions.removeSliceFromDashboard =
-      this.props.actions.removeSliceFromDashboard.bind(this);
-    this.props.actions.toggleExpandSlice =
-      this.props.actions.toggleExpandSlice.bind(this);
-    this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
-    this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
-  }
-
-  componentDidMount() {
-    window.addEventListener('resize', this.rerenderCharts);
   }
 
   componentWillReceiveProps(nextProps) {
-    if (this.firstLoad &&
-      Object.values(nextProps.charts)
-        .every(chart => (['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1))
+    if (
+      this.firstLoad &&
+      Object.values(nextProps.charts).every(
+        chart =>
+          ['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1,
+      )
     ) {
       Logger.end(this.loadingLog);
       this.firstLoad = false;
@@ -86,13 +94,19 @@ class Dashboard extends React.PureComponent {
     const nextChartIds = getChartIdsFromLayout(nextProps.layout);
     if (currentChartIds.length < nextChartIds.length) {
       // adding new chart
-      const newChartId = nextChartIds.find(key => (currentChartIds.indexOf(key) === -1));
+      const newChartId = nextChartIds.find(
+        key => currentChartIds.indexOf(key) === -1,
+      );
       this.props.actions.addSliceToDashboard(newChartId);
       this.props.actions.onChange();
     } else if (currentChartIds.length > nextChartIds.length) {
       // remove chart
-      const removedChartId = currentChartIds.find(key => (nextChartIds.indexOf(key) === -1));
-      this.props.actions.removeSliceFromDashboard(this.props.charts[removedChartId]);
+      const removedChartId = currentChartIds.find(
+        key => nextChartIds.indexOf(key) === -1,
+      );
+      this.props.actions.removeSliceFromDashboard(
+        this.props.charts[removedChartId],
+      );
       this.props.actions.onChange();
     }
   }
@@ -101,11 +115,15 @@ class Dashboard extends React.PureComponent {
     const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
     if (refresh) {
       let changedFilterKey;
-      const prevFiltersKeySet = new Set(Object.keys(prevProps.dashboardState.filters));
-      Object.keys(filters).some((key) => {
+      const prevFiltersKeySet = new Set(
+        Object.keys(prevProps.dashboardState.filters),
+      );
+      Object.keys(filters).some(key => {
         prevFiltersKeySet.delete(key);
-        if (prevProps.dashboardState.filters[key] === undefined ||
-          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])) {
+        if (
+          prevProps.dashboardState.filters[key] === undefined ||
+          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])
+        ) {
           changedFilterKey = key;
           return true;
         }
@@ -118,21 +136,9 @@ class Dashboard extends React.PureComponent {
     }
 
     if (hasUnsavedChanges) {
-      this.onBeforeUnload(true);
-    } else {
-      this.onBeforeUnload(false);
-    }
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('resize', this.rerenderCharts);
-  }
-
-  onBeforeUnload(hasChanged) {
-    if (hasChanged) {
-      window.addEventListener('beforeunload', this.unload);
+      Dashboard.onBeforeUnload(true);
     } else {
-      window.removeEventListener('beforeunload', this.unload);
+      Dashboard.onBeforeUnload(false);
     }
   }
 
@@ -141,131 +147,37 @@ class Dashboard extends React.PureComponent {
     return Object.values(this.props.charts);
   }
 
-  getFormDataExtra(chart) {
-    const extraFilters = this.effectiveExtraFilters(chart.id);
-    const formDataExtra = {
-      ...chart.formData,
-      extra_filters: extraFilters,
-    };
-    return formDataExtra;
-  }
-
-  getFilters(sliceId) {
-    return this.props.dashboardState.filters[sliceId];
-  }
-
-  unload() {
-    const message = t('You have unsaved changes.');
-    window.event.returnValue = message; // Gecko + IE
-    return message; // Gecko + Webkit, Safari, Chrome etc.
-  }
-
-  effectiveExtraFilters(sliceId) {
-    const metadata = this.props.dashboardInfo.metadata;
-    const filters = this.props.dashboardState.filters;
-    const f = [];
-    const immuneSlices = metadata.filter_immune_slices || [];
-    if (sliceId && immuneSlices.includes(sliceId)) {
-      // The slice is immune to dashboard filters
-      return f;
-    }
-
-    // Building a list of fields the slice is immune to filters on
-    let immuneToFields = [];
-    if (
-      sliceId &&
-      metadata.filter_immune_slice_fields &&
-      metadata.filter_immune_slice_fields[sliceId]) {
-      immuneToFields = metadata.filter_immune_slice_fields[sliceId];
-    }
-    for (const filteringSliceId in filters) {
-      if (filteringSliceId === sliceId.toString()) {
-        // Filters applied by the slice don't apply to itself
-        continue;
-      }
-      for (const field in filters[filteringSliceId]) {
-        if (!immuneToFields.includes(field)) {
-          f.push({
-            col: field,
-            op: 'in',
-            val: filters[filteringSliceId][field],
-          });
-        }
-      }
-    }
-    return f;
-  }
-
   refreshExcept(filterKey) {
     const immune = this.props.dashboardInfo.metadata.filter_immune_slices || [];
     let charts = this.getAllCharts();
     if (filterKey) {
       charts = charts.filter(
-        chart => (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1),
+        chart =>
+          String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1,
       );
     }
-    charts.forEach((chart) => {
-      const updatedFormData = this.getFormDataExtra(chart);
-      this.props.actions.runQuery(updatedFormData, false, this.props.timeout, chart.id);
-    });
-  }
-
-  exploreChart(chartId) {
-    const chart = this.props.charts[chartId];
-    const formData = this.getFormDataExtra(chart);
-    exportChart(formData);
-  }
-
-  exportCSV(chartId) {
-    const chart = this.props.charts[chartId];
-    const formData = this.getFormDataExtra(chart);
-    exportChart(formData, 'csv');
-  }
+    charts.forEach(chart => {
+      const updatedFormData = getFormDataWithExtraFilters({
+        chart,
+        dashboardMetadata: this.props.dashboardInfo.metadata,
+        filters: this.props.dashboardState.filters,
+        sliceId: chart.id,
+      });
 
-  // re-render chart without fetch
-  rerenderCharts() {
-    this.getAllCharts().forEach((chart) => {
-      setTimeout(() => {
-        this.props.actions.renderTriggered(new Date().getTime(), chart.id);
-      }, 50);
+      this.props.actions.runQuery(
+        updatedFormData,
+        false,
+        this.props.timeout,
+        chart.id,
+      );
     });
   }
 
   render() {
-    const {
-      expandedSlices = {}, filters, sliceIds,
-      editMode, showBuilderPane,
-    } = this.props.dashboardState;
-
     return (
-      <div id="dashboard-container">
-        <div>
-          <AlertsWrapper initMessages={this.props.initMessages} />
-        </div>
-        <GridLayout
-          dashboardInfo={this.props.dashboardInfo}
-          layout={this.props.layout}
-          datasources={this.props.datasources}
-          slices={this.props.slices}
-          sliceIds={sliceIds}
-          expandedSlices={expandedSlices}
-          filters={filters}
-          charts={this.props.charts}
-          timeout={this.props.timeout}
-          onChange={this.onChange}
-          rerenderCharts={this.rerenderCharts}
-          getFormDataExtra={this.getFormDataExtra}
-          exploreChart={this.exploreChart}
-          exportCSV={this.exportCSV}
-          refreshChart={this.props.actions.refreshChart}
-          saveSliceName={this.props.actions.saveSliceName}
-          toggleExpandSlice={this.props.actions.toggleExpandSlice}
-          addFilter={this.props.actions.addFilter}
-          getFilters={this.getFilters}
-          removeFilter={this.props.actions.removeFilter}
-          editMode={editMode}
-          showBuilderPane={showBuilderPane}
-        />
+      <div>
+        <AlertsWrapper initMessages={this.props.initMessages} />
+        <DashboardBuilder />
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
similarity index 81%
rename from superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
rename to superset/assets/src/dashboard/components/DashboardBuilder.jsx
index f3f5867..79eb35d 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -20,8 +20,6 @@ import {
 } from '../util/constants';
 
 const propTypes = {
-  cells: PropTypes.object.isRequired,
-
   // redux
   dashboardLayout: PropTypes.object.isRequired,
   deleteTopLevelTabs: PropTypes.func.isRequired,
@@ -37,8 +35,10 @@ const defaultProps = {
 class DashboardBuilder extends React.Component {
   static shouldFocusTabs(event, container) {
     // don't focus the tabs when we click on a tab
-    return event.target.tagName === 'UL' || (
-      /icon-button/.test(event.target.className) && container.contains(event.target)
+    return (
+      event.target.tagName === 'UL' ||
+      (/icon-button/.test(event.target.className) &&
+        container.contains(event.target))
     );
   }
 
@@ -56,19 +56,27 @@ class DashboardBuilder extends React.Component {
 
   render() {
     const { tabIndex } = this.state;
-    const { handleComponentDrop, dashboardLayout, deleteTopLevelTabs, editMode } = this.props;
+    const {
+      handleComponentDrop,
+      dashboardLayout,
+      deleteTopLevelTabs,
+      editMode,
+    } = this.props;
     const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
-    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
+    const topLevelTabs =
+      rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
 
     const gridComponentId = topLevelTabs
-      ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
+      ? topLevelTabs.children[
+          Math.min(topLevelTabs.children.length - 1, tabIndex)
+        ]
       : DASHBOARD_GRID_ID;
 
     const gridComponent = dashboardLayout[gridComponentId];
 
     return (
-      <div className={cx('dashboard-v2', editMode && 'dashboard-v2--editing')}>
+      <div className={cx('dashboard', editMode && 'dashboard--editing')}>
         {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
           <DashboardHeader />
         ) : (
@@ -87,9 +95,10 @@ class DashboardBuilder extends React.Component {
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
             )}
-          </DragDroppable>)}
+          </DragDroppable>
+        )}
 
-        {topLevelTabs &&
+        {topLevelTabs && (
           <WithPopoverMenu
             shouldFocus={DashboardBuilder.shouldFocusTabs}
             menuItems={[
@@ -108,19 +117,17 @@ class DashboardBuilder extends React.Component {
               index={0}
               renderTabContent={false}
               onChangeTab={this.handleChangeTab}
-              cells={this.props.cells}
             />
-          </WithPopoverMenu>}
+          </WithPopoverMenu>
+        )}
 
         <div className="dashboard-content">
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
-            cells={this.props.cells}
           />
-          {this.props.editMode && this.props.showBuilderPane &&
-            <BuilderComponentPane />
-          }
+          {this.props.editMode &&
+            this.props.showBuilderPane && <BuilderComponentPane />}
         </div>
         <ToastPresenter />
       </div>
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
deleted file mode 100644
index 31fe035..0000000
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-
-import {
-  toggleExpandSlice,
-  addFilter,
-  removeFilter,
-  addSliceToDashboard,
-  removeSliceFromDashboard,
-  onChange,
-} from '../actions/dashboardState';
-import { saveSliceName } from '../actions/sliceEntities';
-import { refreshChart, runQuery, renderTriggered } from '../../chart/chartAction';
-import Dashboard from './Dashboard';
-
-function mapStateToProps({ datasources, sliceEntities, charts,
-                           dashboardInfo, dashboardState,
-                           dashboardLayout, impressionId }) {
-  return {
-    initMessages: dashboardInfo.common.flash_messages,
-    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    userId: dashboardInfo.userId,
-    dashboardInfo,
-    dashboardState,
-    charts,
-    datasources,
-    slices: sliceEntities.slices,
-    layout: dashboardLayout.present,
-    impressionId,
-  };
-}
-
-function mapDispatchToProps(dispatch) {
-  const actions = {
-    refreshChart,
-    runQuery,
-    renderTriggered,
-    saveSliceName,
-    toggleExpandSlice,
-    addFilter,
-    removeFilter,
-    addSliceToDashboard,
-    removeSliceFromDashboard,
-    onChange,
-  };
-  return {
-    actions: bindActionCreators(actions, dispatch),
-  };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
similarity index 57%
rename from superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
rename to superset/assets/src/dashboard/components/DashboardGrid.jsx
index 2aa82af..3e6fc0c 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -1,15 +1,14 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+// ParentSize uses resize observer so the dashboard will update size
+// when its container size changes, due to e.g., builder side panel opening
 import ParentSize from '@vx/responsive/build/components/ParentSize';
 
 import { componentShape } from '../util/propShapes';
 import DashboardComponent from '../containers/DashboardComponent';
 import DragDroppable from './dnd/DragDroppable';
 
-import {
-  GRID_GUTTER_SIZE,
-  GRID_COLUMN_COUNT,
-} from '../util/constants';
+import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT } from '../util/constants';
 
 const propTypes = {
   depth: PropTypes.number.isRequired,
@@ -19,8 +18,7 @@ const propTypes = {
   resizeComponent: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class DashboardGrid extends React.PureComponent {
   constructor(props) {
@@ -34,15 +32,24 @@ class DashboardGrid extends React.PureComponent {
     this.handleResize = this.handleResize.bind(this);
     this.handleResizeStop = this.handleResizeStop.bind(this);
     this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
+    this.setGridRef = this.setGridRef.bind(this);
   }
 
   getRowGuidePosition(resizeRef) {
     if (resizeRef && this.grid) {
-      return resizeRef.getBoundingClientRect().bottom - this.grid.getBoundingClientRect().top - 1;
+      return (
+        resizeRef.getBoundingClientRect().bottom -
+        this.grid.getBoundingClientRect().top -
+        1
+      );
     }
     return null;
   }
 
+  setGridRef(ref) {
+    this.grid = ref;
+  }
+
   handleResizeStart({ ref, direction }) {
     let rowGuideTop = null;
     if (direction === 'bottom' || direction === 'bottomRight') {
@@ -71,19 +78,36 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { gridComponent, handleComponentDrop, depth, editMode, cells } = this.props;
+    const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
     const { isResizing, rowGuideTop } = this.state;
 
     return (
-      <div className="grid-container" ref={(ref) => { this.grid = ref; }}>
+      <div className="grid-container" ref={this.setGridRef}>
         <ParentSize>
           {({ width }) => {
-            // account for (COLUMN_COUNT - 1) gutters
-            const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
+            const columnPlusGutterWidth =
+              (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
             const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
-
             return width < 50 ? null : (
               <div className="grid-content">
+                {editMode && (
+                  <DragDroppable
+                    component={gridComponent}
+                    depth={depth}
+                    parentComponent={null}
+                    index={0}
+                    orientation="column"
+                    onDrop={handleComponentDrop}
+                    editMode
+                  >
+                    {({ dropIndicatorProps }) =>
+                      dropIndicatorProps && (
+                        <div className="drop-indicator drop-indicator--bottom" />
+                      )
+                    }
+                  </DragDroppable>
+                )}
+
                 {gridComponent.children.map((id, index) => (
                   <DashboardComponent
                     key={id}
@@ -93,7 +117,6 @@ class DashboardGrid extends React.PureComponent {
                     index={index}
                     availableColumnCount={GRID_COLUMN_COUNT}
                     columnWidth={columnWidth}
-                    cells={cells}
                     onResizeStart={this.handleResizeStart}
                     onResize={this.handleResize}
                     onResizeStop={this.handleResizeStop}
@@ -101,7 +124,7 @@ class DashboardGrid extends React.PureComponent {
                 ))}
 
                 {/* render an empty drop target */}
-                {editMode &&
+                {editMode && (
                   <DragDroppable
                     component={gridComponent}
                     depth={depth}
@@ -112,29 +135,38 @@ class DashboardGrid extends React.PureComponent {
                     className="empty-grid-droptarget"
                     editMode
                   >
-                    {({ dropIndicatorProps }) => dropIndicatorProps &&
-                      <div className="drop-indicator drop-indicator--top" />}
-                  </DragDroppable>}
-
-                {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
-                  <div
-                    key={`grid-column-${i}`}
-                    className="grid-column-guide"
-                    style={{
-                      left: (i * GRID_GUTTER_SIZE) + (i * columnWidth),
-                      width: columnWidth,
-                    }}
-                  />
-                ))}
-
-                {isResizing && rowGuideTop &&
-                  <div
-                    className="grid-row-guide"
-                    style={{
-                      top: rowGuideTop,
-                      width,
-                    }}
-                  />}
+                    {({ dropIndicatorProps }) =>
+                      dropIndicatorProps && (
+                        <div className="drop-indicator drop-indicator--top" />
+                      )
+                    }
+                  </DragDroppable>
+                )}
+
+                {isResizing &&
+                  Array(GRID_COLUMN_COUNT)
+                    .fill(null)
+                    .map((_, i) => (
+                      <div
+                        key={`grid-column-${i}`}
+                        className="grid-column-guide"
+                        style={{
+                          left: i * GRID_GUTTER_SIZE + i * columnWidth,
+                          width: columnWidth,
+                        }}
+                      />
+                    ))}
+
+                {isResizing &&
+                  rowGuideTop && (
+                    <div
+                      className="grid-row-guide"
+                      style={{
+                        top: rowGuideTop,
+                        width,
+                      }}
+                    />
+                  )}
               </div>
             );
           }}
diff --git a/superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/src/dashboard/components/DeleteComponentButton.jsx
similarity index 78%
rename from superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx
rename to superset/assets/src/dashboard/components/DeleteComponentButton.jsx
index 18efff4..8470947 100644
--- a/superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx
+++ b/superset/assets/src/dashboard/components/DeleteComponentButton.jsx
@@ -7,15 +7,12 @@ const propTypes = {
   onDelete: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 export default class DeleteComponentButton extends React.PureComponent {
   render() {
     const { onDelete } = this.props;
-    return (
-      <IconButton onClick={onDelete} className="fa fa-trash" />
-    );
+    return <IconButton onClick={onDelete} className="fa fa-trash" />;
   }
 }
 
diff --git a/superset/assets/src/dashboard/components/GridCell.jsx b/superset/assets/src/dashboard/components/GridCell.jsx
deleted file mode 100644
index 3273272..0000000
--- a/superset/assets/src/dashboard/components/GridCell.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import SliceHeader from './SliceHeader';
-import ChartContainer from '../../chart/ChartContainer';
-import { chartPropShape, slicePropShape } from '../v2/util/propShapes';
-
-const propTypes = {
-  timeout: PropTypes.number,
-  datasource: PropTypes.object,
-  isLoading: PropTypes.bool,
-  isCached: PropTypes.bool,
-  cachedDttm: PropTypes.string,
-  isExpanded: PropTypes.bool,
-  widgetHeight: PropTypes.number,
-  widgetWidth: PropTypes.number,
-  slice: slicePropShape.isRequired,
-  chart: chartPropShape.isRequired,
-  formData: PropTypes.object,
-  filters: PropTypes.object,
-  refreshChart: PropTypes.func,
-  updateSliceName: PropTypes.func,
-  toggleExpandSlice: PropTypes.func,
-  exploreChart: PropTypes.func,
-  exportCSV: PropTypes.func,
-  addFilter: PropTypes.func,
-  getFilters: PropTypes.func,
-  removeFilter: PropTypes.func,
-  editMode: PropTypes.bool,
-  annotationQuery: PropTypes.object,
-};
-
-const defaultProps = {
-  refreshChart: () => ({}),
-  updateSliceName: () => ({}),
-  toggleExpandSlice: () => ({}),
-  exploreChart: () => ({}),
-  exportCSV: () => ({}),
-  addFilter: () => ({}),
-  getFilters: () => ({}),
-  removeFilter: () => ({}),
-  editMode: false,
-};
-
-class GridCell extends React.PureComponent {
-  constructor(props) {
-    super(props);
-
-    const sliceId = this.props.slice.slice_id;
-    this.forceRefresh = this.forceRefresh.bind(this);
-    this.addFilter = this.props.addFilter.bind(this, this.props.chart);
-    this.getFilters = this.props.getFilters.bind(this, sliceId);
-    this.removeFilter = this.props.removeFilter.bind(this, sliceId);
-  }
-
-  getDescriptionId(slice) {
-    return 'description_' + slice.slice_id;
-  }
-
-  getHeaderId(slice) {
-    return 'header_' + slice.slice_id;
-  }
-
-  width() {
-    return this.props.widgetWidth - 32;
-  }
-
-  height(slice) {
-    const widgetHeight = this.props.widgetHeight;
-    const headerHeight = this.headerHeight(slice);
-    const descriptionId = this.getDescriptionId(slice);
-    let descriptionHeight = 0;
-    if (this.props.isExpanded && this.refs[descriptionId]) {
-      descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
-    }
-
-    return widgetHeight - headerHeight - descriptionHeight - 32;
-  }
-
-  headerHeight(slice) {
-    const headerId = this.getHeaderId(slice);
-    return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
-  }
-
-  forceRefresh() {
-    return this.props.refreshChart(this.props.chart, true, this.props.timeout);
-  }
-
-  render() {
-    const {
-      isExpanded, isLoading, isCached, cachedDttm,
-      updateSliceName, toggleExpandSlice,
-      chart, slice, datasource, formData, timeout, annotationQuery,
-      exploreChart, exportCSV, editMode,
-    } = this.props;
-
-    return (
-      <div
-        className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
-        id={`${slice.slice_id}-cell`}
-      >
-        <div ref={this.getHeaderId(slice)}>
-          <SliceHeader
-            slice={slice}
-            isExpanded={isExpanded}
-            isCached={isCached}
-            cachedDttm={cachedDttm}
-            updateSliceName={updateSliceName}
-            toggleExpandSlice={toggleExpandSlice}
-            forceRefresh={this.forceRefresh}
-            editMode={editMode}
-            annotationQuery={annotationQuery}
-            exploreChart={exploreChart}
-            exportCSV={exportCSV}
-          />
-        </div>
-        {
-        /* This usage of dangerouslySetInnerHTML is safe since it is being used to render
-           markdown that is sanitized with bleach. See:
-             https://github.com/apache/incubator-superset/pull/4390
-           and
-             https://github.com/apache/incubator-superset/commit/b6fcc22d5a2cb7a5e92599ed5795a0169385a825 */}
-        <div
-          className="slice_description bs-callout bs-callout-default"
-          style={isExpanded ? {} : { display: 'none' }}
-          ref={this.getDescriptionId(slice)}
-          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
-        />
-        <div
-          className="chart-container"
-          style={{ width: this.width(), height: this.height(slice) }}
-        >
-          <input type="hidden" value="false" />
-          <ChartContainer
-            containerId={`slice-container-${slice.slice_id}`}
-            chartId={chart.id}
-            datasource={datasource}
-            formData={formData}
-            headerHeight={this.headerHeight(slice)}
-            height={this.height(slice)}
-            width={this.width()}
-            timeout={timeout}
-            vizType={slice.viz_type}
-            addFilter={this.addFilter}
-            getFilters={this.getFilters}
-            removeFilter={this.removeFilter}
-          />
-        </div>
-      </div>
-    );
-  }
-}
-
-GridCell.propTypes = propTypes;
-GridCell.defaultProps = defaultProps;
-
-export default GridCell;
diff --git a/superset/assets/src/dashboard/components/GridLayout.jsx b/superset/assets/src/dashboard/components/GridLayout.jsx
deleted file mode 100644
index fd561e2..0000000
--- a/superset/assets/src/dashboard/components/GridLayout.jsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cx from 'classnames';
-
-import GridCell from './GridCell';
-import { slicePropShape, chartPropShape } from '../v2/util/propShapes';
-import DashboardBuilder from '../v2/containers/DashboardBuilder';
-
-const propTypes = {
-  dashboardInfo: PropTypes.shape().isRequired,
-  layout: PropTypes.object.isRequired,
-  datasources: PropTypes.object,
-  charts: PropTypes.objectOf(chartPropShape).isRequired,
-  slices: PropTypes.objectOf(slicePropShape).isRequired,
-  expandedSlices: PropTypes.object.isRequired,
-  sliceIds: PropTypes.object.isRequired,
-  filters: PropTypes.object,
-  timeout: PropTypes.number,
-  onChange: PropTypes.func,
-  rerenderCharts: PropTypes.func,
-  getFormDataExtra: PropTypes.func,
-  exploreChart: PropTypes.func,
-  exportCSV: PropTypes.func,
-  refreshChart: PropTypes.func,
-  saveSliceName: PropTypes.func,
-  toggleExpandSlice: PropTypes.func,
-  addFilter: PropTypes.func,
-  getFilters: PropTypes.func,
-  removeFilter: PropTypes.func,
-  editMode: PropTypes.bool.isRequired,
-  showBuilderPane: PropTypes.bool.isRequired,
-};
-
-const defaultProps = {
-  expandedSlices: {},
-  filters: {},
-  timeout: 60,
-  onChange: () => ({}),
-  getFormDataExtra: () => ({}),
-  exploreChart: () => ({}),
-  exportCSV: () => ({}),
-  refreshChart: () => ({}),
-  saveSliceName: () => ({}),
-  toggleExpandSlice: () => ({}),
-  addFilter: () => ({}),
-  getFilters: () => ({}),
-  removeFilter: () => ({}),
-};
-
-class GridLayout extends React.Component {
-  constructor(props) {
-    super(props);
-
-    this.updateSliceName = this.props.dashboardInfo.dash_edit_perm ?
-      this.updateSliceName.bind(this) : null;
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.editMode !== this.props.editMode ||
-      prevProps.showBuilderPane !== this.props.showBuilderPane) {
-      this.props.rerenderCharts();
-    }
-  }
-
-  getWidgetId(sliceId) {
-    return 'widget_' + sliceId;
-  }
-
-  getWidgetHeight(sliceId) {
-    const widgetId = this.getWidgetId(sliceId);
-    if (!widgetId || !this.refs[widgetId]) {
-      return 400;
-    }
-    return this.refs[widgetId].parentNode.clientHeight;
-  }
-
-  getWidgetWidth(sliceId) {
-    const widgetId = this.getWidgetId(sliceId);
-    if (!widgetId || !this.refs[widgetId]) {
-      return 400;
-    }
-    return this.refs[widgetId].parentNode.clientWidth;
-  }
-
-  updateSliceName(sliceId, sliceName) {
-    const key = sliceId;
-    const currentSlice = this.props.slices[key];
-    if (!currentSlice || currentSlice.slice_name === sliceName) {
-      return;
-    }
-
-    this.props.saveSliceName(currentSlice, sliceName);
-  }
-
-  isExpanded(sliceId) {
-    return this.props.expandedSlices[sliceId];
-  }
-
-  render() {
-    const cells = {};
-    this.props.sliceIds.forEach((sliceId) => {
-      const key = sliceId;
-      const currentChart = this.props.charts[key];
-      const currentSlice = this.props.slices[key];
-      if (currentChart) {
-        const currentDatasource = this.props.datasources[currentChart.form_data.datasource];
-        const queryResponse = currentChart.queryResponse || {};
-        cells[key] = (
-          <div
-            id={key}
-            key={sliceId}
-            className={cx('widget', `${currentSlice.viz_type}`, { 'is-edit': this.props.editMode })}
-            ref={this.getWidgetId(sliceId)}
-          >
-            <GridCell
-              slice={currentSlice}
-              chart={currentChart}
-              datasource={currentDatasource}
-              filters={this.props.filters}
-              formData={this.props.getFormDataExtra(currentChart)}
-              timeout={this.props.timeout}
-              widgetHeight={this.getWidgetHeight(sliceId)}
-              widgetWidth={this.getWidgetWidth(sliceId)}
-              exploreChart={this.props.exploreChart}
-              exportCSV={this.props.exportCSV}
-              isExpanded={!!this.isExpanded(sliceId)}
-              isLoading={currentChart.chartStatus === 'loading'}
-              isCached={queryResponse.is_cached}
-              cachedDttm={queryResponse.cached_dttm}
-              toggleExpandSlice={this.props.toggleExpandSlice}
-              refreshChart={this.props.refreshChart}
-              updateSliceName={this.updateSliceName}
-              addFilter={this.props.addFilter}
-              getFilters={this.props.getFilters}
-              removeFilter={this.props.removeFilter}
-              editMode={this.props.editMode}
-              annotationQuery={currentChart.annotationQuery}
-              annotationError={currentChart.annotationError}
-            />
-          </div>
-        );
-      }
-    });
-
-    return (
-      <DashboardBuilder
-        cells={cells}
-      />
-    );
-  }
-}
-
-GridLayout.propTypes = propTypes;
-GridLayout.defaultProps = defaultProps;
-
-export default GridLayout;
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index f533506..242102e 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -6,8 +6,9 @@ import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
-import { chartPropShape } from '../v2/util/propShapes';
+// import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import SaveModal from './SaveModal';
+import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
 
 const propTypes = {
@@ -20,9 +21,9 @@ const propTypes = {
   isStarred: PropTypes.bool.isRequired,
   onSave: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
-  fetchFaveStar: PropTypes.func,
+  fetchFaveStar: PropTypes.func.isRequired,
   fetchCharts: PropTypes.func.isRequired,
-  saveFaveStar: PropTypes.func,
+  saveFaveStar: PropTypes.func.isRequired,
   startPeriodicRender: PropTypes.func.isRequired,
   updateDashboardTitle: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
@@ -41,13 +42,16 @@ const propTypes = {
 class Header extends React.PureComponent {
   constructor(props) {
     super(props);
+
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
   }
+
   forceRefresh() {
     return this.props.fetchCharts(Object.values(this.props.charts), true);
   }
+
   handleChangeText(nextText) {
     const { updateDashboardTitle, onChange } = this.props;
     if (nextText && this.props.dashboardTitle !== nextText) {
@@ -55,45 +59,11 @@ class Header extends React.PureComponent {
       onChange();
     }
   }
+
   toggleEditMode() {
     this.props.setEditMode(!this.props.editMode);
   }
-  renderUnsaved() {
-    if (!this.props.hasUnsavedChanges) {
-      return null;
-    }
-    return (
-      <InfoTooltipWithTrigger
-        label="unsaved"
-        tooltip={t('Unsaved changes')}
-        icon="exclamation-triangle"
-        className="text-danger m-r-5"
-        placement="top"
-      />
-    );
-  }
-  renderInsertButton() {
-    if (!this.props.editMode) {
-      return null;
-    }
-    const btnText = this.props.showBuilderPane ? t('Hide builder pane') : t('Insert components');
-    return (
-      <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
-        {btnText}
-      </Button>
-    );
-  }
-  renderEditButton() {
-    if (!this.props.dashboardInfo.dash_save_perm) {
-      return null;
-    }
-    const btnText = this.props.editMode ? t('Switch to View Mode') : t('Edit Dashboard');
-    return (
-      <Button bsSize="small" onClick={this.toggleEditMode}>
-        {btnText}
-      </Button>
-    );
-  }
+
   render() {
     const {
       dashboardTitle,
@@ -107,8 +77,13 @@ class Header extends React.PureComponent {
       onChange,
       onSave,
       editMode,
+      showBuilderPane,
+      dashboardInfo,
+      hasUnsavedChanges,
     } = this.props;
 
+    const userCanEdit = dashboardInfo.dash_save_perm;
+
     return (
       <div className="dashboard-header">
         <div className="dashboard-component-header header-large">
@@ -126,19 +101,58 @@ class Header extends React.PureComponent {
               isStarred={this.props.isStarred}
             />
           </span>
-          {this.renderUnsaved()}
         </div>
         <ButtonToolbar>
-          <ButtonGroup>
-            <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
-              Undo
-            </Button>
-            <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
-              Redo
-            </Button>
-            {this.renderInsertButton()}
-            {this.renderEditButton()}
-          </ButtonGroup>
+          {userCanEdit && (
+            <ButtonGroup>
+              {editMode && (
+                <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
+                  Undo
+                </Button>
+              )}
+
+              {editMode && (
+                <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
+                  Redo
+                </Button>
+              )}
+
+              {editMode && (
+                <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+                  {showBuilderPane
+                    ? t('Hide builder pane')
+                    : t('Insert components')}
+                </Button>
+              )}
+
+              {!hasUnsavedChanges ? (
+                <Button
+                  bsSize="small"
+                  onClick={this.toggleEditMode}
+                  bsStyle={editMode ? undefined : 'primary'}
+                >
+                  {editMode ? t('Switch to View Mode') : t('Edit Dashboard')}
+                </Button>
+              ) : (
+                <SaveModal
+                  dashboardId={this.props.dashboardInfo.id}
+                  dashboardTitle={dashboardTitle}
+                  layout={layout}
+                  filters={filters}
+                  expandedSlices={expandedSlices}
+                  onSave={onSave}
+                  // @TODO need to figure out css
+                  css=""
+                  triggerNode={
+                    <Button bsStyle="primary" bsSize="small">
+                      {t('Save changes')}
+                    </Button>
+                  }
+                />
+              )}
+            </ButtonGroup>
+          )}
+
           <Controls
             dashboardInfo={this.props.dashboardInfo}
             dashboardTitle={dashboardTitle}
@@ -156,6 +170,7 @@ class Header extends React.PureComponent {
     );
   }
 }
+
 Header.propTypes = propTypes;
 
 export default Header;
diff --git a/superset/assets/src/dashboard/v2/components/IconButton.jsx b/superset/assets/src/dashboard/components/IconButton.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/IconButton.jsx
rename to superset/assets/src/dashboard/components/IconButton.jsx
diff --git a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
index c2a5637..1ed4f82 100644
--- a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
@@ -16,7 +16,7 @@ const defaultProps = {
 };
 
 const options = [
-  [0, t('Don\'t refresh')],
+  [0, t("Don't refresh")],
   [10, t('10 seconds')],
   [30, t('30 seconds')],
   [60, t('1 minute')],
@@ -47,7 +47,7 @@ class RefreshIntervalModal extends React.PureComponent {
             <Select
               options={options}
               value={this.state.refreshFrequency}
-              onChange={(opt) => {
+              onChange={opt => {
                 const value = opt ? opt.value : options[0].value;
                 this.setState({
                   refreshFrequency: value,
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 2e76bf4..41c6364 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,4 +1,4 @@
-/* global notify */
+/* global notify, window */
 import React from 'react';
 import PropTypes from 'prop-types';
 import $ from 'jquery';
@@ -17,6 +17,11 @@ const propTypes = {
   triggerNode: PropTypes.node.isRequired,
   filters: PropTypes.object.isRequired,
   onSave: PropTypes.func.isRequired,
+  isMenuItem: PropTypes.bool,
+};
+
+const defaultProps = {
+  isMenuItem: false,
 };
 
 class SaveModal extends React.PureComponent {
@@ -24,28 +29,38 @@ class SaveModal extends React.PureComponent {
     super(props);
     this.state = {
       saveType: 'overwrite',
-      newDashName: props.dashboardTitle + ' [copy]',
+      newDashName: `${props.dashboardTitle} [copy]`,
       duplicateSlices: false,
     };
     this.modal = null;
     this.handleSaveTypeChange = this.handleSaveTypeChange.bind(this);
     this.handleNameChange = this.handleNameChange.bind(this);
     this.saveDashboard = this.saveDashboard.bind(this);
+    this.setModalRef = this.setModalRef.bind(this);
+    this.toggleDuplicateSlices = this.toggleDuplicateSlices.bind(this);
+  }
+
+  setModalRef(ref) {
+    this.modal = ref;
   }
+
   toggleDuplicateSlices() {
     this.setState({ duplicateSlices: !this.state.duplicateSlices });
   }
+
   handleSaveTypeChange(event) {
     this.setState({
       saveType: event.target.value,
     });
   }
+
   handleNameChange(event) {
     this.setState({
       newDashName: event.target.value,
       saveType: 'newDashboard',
     });
   }
+
   saveDashboardRequest(data, url, saveType) {
     const saveModal = this.modal;
     const onSaveDashboard = this.props.onSave;
@@ -67,12 +82,25 @@ class SaveModal extends React.PureComponent {
       error(error) {
         saveModal.close();
         const errorMsg = getAjaxErrorMsg(error);
-        notify.error(t('Sorry, there was an error saving this dashboard: ') + errorMsg);
+        notify.error(
+          `${t(
+            'Sorry, there was an error saving this dashboard: ',
+          )} ${errorMsg}`,
+        );
       },
     });
   }
-  saveDashboard(saveType, newDashboardTitle) {
-    const { dashboardTitle, layout: positions, expandedSlices, filters, dashboardId } = this.props;
+
+  saveDashboard() {
+    const { saveType, newDashName } = this.state;
+    const {
+      dashboardTitle,
+      layout: positions,
+      expandedSlices,
+      filters,
+      dashboardId,
+    } = this.props;
+
     const data = {
       positions,
       expanded_slices: expandedSlices,
@@ -80,29 +108,27 @@ class SaveModal extends React.PureComponent {
       default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
+
     let url = null;
     if (saveType === 'overwrite') {
       url = `/superset/save_dash/${dashboardId}/`;
       this.saveDashboardRequest(data, url, saveType);
     } else if (saveType === 'newDashboard') {
-      if (!newDashboardTitle) {
-        this.modal.close();
-        showModal({
-          title: t('Error'),
-          body: t('You must pick a name for the new dashboard'),
-        });
+      if (!newDashName) {
+        notify.error('You must pick a name for the new dashboard');
       } else {
-        data.dashboard_title = newDashboardTitle;
+        data.dashboard_title = newDashName;
         url = `/superset/copy_dash/${dashboardId}/`;
         this.saveDashboardRequest(data, url, saveType);
       }
     }
   }
+
   render() {
     return (
       <ModalTrigger
-        ref={(modal) => { this.modal = modal; }}
-        isMenuItem
+        ref={this.setModalRef}
+        isMenuItem={this.props.isMenuItem}
         triggerNode={this.props.triggerNode}
         modalTitle={t('Save Dashboard')}
         modalBody={
@@ -132,7 +158,7 @@ class SaveModal extends React.PureComponent {
             <div className="m-l-25 m-t-5">
               <Checkbox
                 checked={this.state.duplicateSlices}
-                onChange={this.toggleDuplicateSlices.bind(this)}
+                onChange={this.toggleDuplicateSlices}
               />
               <span className="m-l-5">also copy (duplicate) charts</span>
             </div>
@@ -140,10 +166,7 @@ class SaveModal extends React.PureComponent {
         }
         modalFooter={
           <div>
-            <Button
-              bsStyle="primary"
-              onClick={() => { this.saveDashboard(this.state.saveType, this.state.newDashName); }}
-            >
+            <Button bsStyle="primary" onClick={this.saveDashboard}>
               {t('Save')}
             </Button>
           </div>
@@ -152,6 +175,8 @@ class SaveModal extends React.PureComponent {
     );
   }
 }
+
 SaveModal.propTypes = propTypes;
+SaveModal.defaultProps = defaultProps;
 
 export default SaveModal;
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 6477fc4..37ce21f 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,14 +1,15 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import cx from 'classnames';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
 import { List } from 'react-virtualized';
 import SearchInput, { createFilter } from 'react-search-input';
 
-import DragDroppable from '../v2/components/dnd/DragDroppable';
-import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../v2/util/componentTypes';
-import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../v2/util/constants';
-import { slicePropShape } from '../v2/util/propShapes';
+import AddSliceCard from './AddSliceCard';
+import AddSliceDragPreview from './dnd/AddSliceDragPreview';
+import DragDroppable from './dnd/DragDroppable';
+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';
 
 const propTypes = {
   fetchAllSlices: PropTypes.func.isRequired,
@@ -24,6 +25,7 @@ const propTypes = {
 const defaultProps = {
   selectedSliceIds: new Set(),
   editMode: false,
+  errorMessage: '',
 };
 
 const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
@@ -35,12 +37,25 @@ const KEYS_TO_SORT = [
 ];
 
 class SliceAdder extends React.Component {
+  static sortByComparator(attr) {
+    const desc = attr === 'changed_on' ? -1 : 1;
+
+    return (a, b) => {
+      if (a[attr] < b[attr]) {
+        return -1 * desc;
+      } else if (a[attr] > b[attr]) {
+        return 1 * desc;
+      }
+      return 0;
+    };
+  }
+
   constructor(props) {
     super(props);
     this.state = {
       filteredSlices: [],
       searchTerm: '',
-      sortBy: KEYS_TO_SORT.findIndex(item => (item.key === 'changed_on')),
+      sortBy: KEYS_TO_SORT.findIndex(item => item.key === 'changed_on'),
     };
 
     this.rowRenderer = this.rowRenderer.bind(this);
@@ -58,7 +73,9 @@ class SliceAdder extends React.Component {
       this.setState({
         filteredSlices: Object.values(nextProps.slices)
           .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
-          .sort(this.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key)),
+          .sort(
+            SliceAdder.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key),
+          ),
       });
     }
   }
@@ -72,20 +89,7 @@ class SliceAdder extends React.Component {
   getFilteredSortedSlices(searchTerm, sortBy) {
     return Object.values(this.props.slices)
       .filter(createFilter(searchTerm, KEYS_TO_FILTERS))
-      .sort(this.sortByComparator(KEYS_TO_SORT[sortBy].key));
-  }
-
-  sortByComparator(attr) {
-    const desc = (attr === 'changed_on') ? -1 : 1;
-
-    return (a, b) => {
-      if (a[attr] < b[attr]) {
-        return -1 * desc;
-      } else if (a[attr] > b[attr]) {
-        return 1 * desc;
-      }
-      return 0;
-    };
+      .sort(SliceAdder.sortByComparator(KEYS_TO_SORT[sortBy].key));
   }
 
   handleKeyPress(ev) {
@@ -99,20 +103,25 @@ class SliceAdder extends React.Component {
   searchUpdated(searchTerm) {
     this.setState({
       searchTerm,
-      filteredSlices: this.getFilteredSortedSlices(searchTerm, this.state.sortBy),
+      filteredSlices: this.getFilteredSortedSlices(
+        searchTerm,
+        this.state.sortBy,
+      ),
     });
   }
 
   handleSelect(sortBy) {
     this.setState({
       sortBy,
-      filteredSlices: this.getFilteredSortedSlices(this.state.searchTerm, sortBy),
+      filteredSlices: this.getFilteredSortedSlices(
+        this.state.searchTerm,
+        sortBy,
+      ),
     });
   }
 
   rowRenderer({ key, index, style }) {
     const cellData = this.state.filteredSlices[index];
-    const duration = cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : '';
     const isSelected = this.props.selectedSliceIds.has(cellData.slice_id);
     const type = CHART_TYPE;
     const id = NEW_CHART_ID;
@@ -122,38 +131,32 @@ class SliceAdder extends React.Component {
 
     return (
       <DragDroppable
+        key={key}
         component={{ type, id, meta }}
-        parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
-        index={0}
+        parentComponent={{
+          id: NEW_COMPONENTS_SOURCE_ID,
+          type: NEW_COMPONENT_SOURCE_TYPE,
+        }}
+        index={index}
         depth={0}
         disableDragDrop={isSelected}
         editMode={this.props.editMode}
+        // we must use a custom drag preview within the List because
+        // it does not seem to work within a fixed-position container
+        useEmptyDragPreview
       >
         {({ dragSourceRef }) => (
-          <div
-            ref={dragSourceRef}
-            className="chart-card-container"
-            key={key}
+          <AddSliceCard
+            innerRef={dragSourceRef}
             style={style}
-          >
-            <div className={cx('chart-card', { 'is-selected': isSelected })}>
-              <div className="card-title">{cellData.slice_name}</div>
-              <div className="card-body">
-                <div className="item">
-                  <span>Modified </span>
-                  <span>{duration}</span>
-                </div>
-                <div className="item">
-                  <span>Visualization </span>
-                  <span>{cellData.viz_type}</span>
-                </div>
-                <div className="item">
-                  <span>Data source </span>
-                  <span dangerouslySetInnerHTML={{ __html: cellData.datasource_link }} />
-                </div>
-              </div>
-            </div>
-          </div>
+            sliceName={cellData.slice_name}
+            lastModified={
+              cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : ''
+            }
+            visType={cellData.viz_type}
+            datasourceLink={cellData.datasource_link}
+            isSelected={isSelected}
+          />
         )}
       </DragDroppable>
     );
@@ -169,7 +172,9 @@ class SliceAdder extends React.Component {
             id="slice-adder-sortby"
           >
             {KEYS_TO_SORT.map((item, index) => (
-              <MenuItem key={item.key} eventKey={index}>{item.label}</MenuItem>
+              <MenuItem key={item.key} eventKey={index}>
+                {item.label}
+              </MenuItem>
             ))}
           </DropdownButton>
 
@@ -179,18 +184,18 @@ class SliceAdder extends React.Component {
           />
         </div>
 
-        {this.props.isLoading &&
+        {this.props.isLoading && (
           <img
             src="/static/assets/images/loading.gif"
             className="loading"
             alt="loading"
           />
-        }
-        <div className={this.props.errorMessage ? '' : 'hidden'}>
-          {this.props.errorMessage}
-        </div>
-        <div className={!this.props.isLoading ? '' : 'hidden'}>
-          {this.state.filteredSlices.length > 0 &&
+        )}
+
+        {this.props.errorMessage && <div>{this.props.errorMessage}</div>}
+
+        {!this.props.isLoading &&
+          this.state.filteredSlices.length > 0 && (
             <List
               width={376}
               height={500}
@@ -201,8 +206,10 @@ class SliceAdder extends React.Component {
               sortBy={this.state.sortBy}
               selectedSliceIds={this.props.selectedSliceIds}
             />
-          }
-        </div>
+          )}
+
+        {/* Drag preview is just a single fixed-position element */}
+        <AddSliceDragPreview slices={this.state.filteredSlices} />
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
deleted file mode 100644
index b4f10d9..0000000
--- a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-
-import { fetchAllSlices } from '../actions/sliceEntities';
-import SliceAdder from './SliceAdder';
-
-function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
-  return {
-    userId: dashboardInfo.userId,
-    selectedSliceIds: dashboardState.sliceIds,
-    slices: sliceEntities.slices,
-    isLoading: sliceEntities.isLoading,
-    errorMessage: sliceEntities.errorMessage,
-    lastUpdated: sliceEntities.lastUpdated,
-    editMode: dashboardState.editMode,
-  };
-}
-
-function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    fetchAllSlices
-  }, dispatch);
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index f126949..bcdaedf 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -7,9 +7,8 @@ import TooltipWrapper from '../../components/TooltipWrapper';
 import SliceHeaderControls from './SliceHeaderControls';
 
 const propTypes = {
+  innerRef: PropTypes.func,
   slice: PropTypes.object.isRequired,
-  supersetCanExplore: PropTypes.bool,
-  sliceCanEdit: PropTypes.bool,
   isExpanded: PropTypes.bool,
   isCached: PropTypes.bool,
   cachedDttm: PropTypes.string,
@@ -24,6 +23,7 @@ const propTypes = {
 };
 
 const defaultProps = {
+  innerRef: null,
   forceRefresh: () => ({}),
   removeSlice: () => ({}),
   updateSliceName: () => ({}),
@@ -31,6 +31,11 @@ const defaultProps = {
   exploreChart: () => ({}),
   exportCSV: () => ({}),
   editMode: false,
+  annotationQuery: {},
+  annotationError: {},
+  cachedDttm: null,
+  isCached: false,
+  isExpanded: false,
 };
 
 class SliceHeader extends React.PureComponent {
@@ -48,55 +53,62 @@ class SliceHeader extends React.PureComponent {
 
   render() {
     const {
-      slice, isExpanded, isCached, cachedDttm,
-      toggleExpandSlice, forceRefresh,
-      exploreChart, exportCSV,
+      slice,
+      isExpanded,
+      isCached,
+      cachedDttm,
+      toggleExpandSlice,
+      forceRefresh,
+      exploreChart,
+      exportCSV,
+      innerRef,
     } = this.props;
+
     const annoationsLoading = t('Annotation layers are still loading.');
     const annoationsError = t('One ore more annotation layers failed loading.');
 
     return (
-      <div className="row chart-header">
-        <div className="col-md-12">
-          <div className="header">
-            <EditableTitle
-              title={slice.slice_name}
-              canEdit={!!this.props.updateSliceName && this.props.editMode}
-              onSaveTitle={this.onSaveTitle}
-              showTooltip={this.props.editMode}
-              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
-            />
-            {!!Object.values(this.props.annotationQuery || {}).length &&
-              <TooltipWrapper
-                label="annotations-loading"
-                placement="top"
-                tooltip={annoationsLoading}
-              >
-                <i className="fa fa-refresh warning" />
-              </TooltipWrapper>
-            }
-            {!!Object.values(this.props.annotationError || {}).length &&
-              <TooltipWrapper
-                label="annoation-errors"
-                placement="top"
-                tooltip={annoationsError}
-              >
-                <i className="fa fa-exclamation-circle danger" />
-              </TooltipWrapper>
+      <div className="chart-header" ref={innerRef}>
+        <div className="header">
+          <EditableTitle
+            title={slice.slice_name}
+            canEdit={!!this.props.updateSliceName && this.props.editMode}
+            onSaveTitle={this.onSaveTitle}
+            noPermitTooltip={
+              "You don't have the rights to alter this dashboard."
             }
-            {!this.props.editMode &&
-              <SliceHeaderControls
-                slice={slice}
-                isCached={isCached}
-                isExpanded={isExpanded}
-                cachedDttm={cachedDttm}
-                toggleExpandSlice={toggleExpandSlice}
-                forceRefresh={forceRefresh}
-                exploreChart={exploreChart}
-                exportCSV={exportCSV}
-              />
-            }
-          </div>
+            showTooltip={!!this.props.updateSliceName && this.props.editMode}
+          />
+          {!!Object.values(this.props.annotationQuery).length && (
+            <TooltipWrapper
+              label="annotations-loading"
+              placement="top"
+              tooltip={annoationsLoading}
+            >
+              <i className="fa fa-refresh warning" />
+            </TooltipWrapper>
+          )}
+          {!!Object.values(this.props.annotationError).length && (
+            <TooltipWrapper
+              label="annoation-errors"
+              placement="top"
+              tooltip={annoationsError}
+            >
+              <i className="fa fa-exclamation-circle danger" />
+            </TooltipWrapper>
+          )}
+          {!this.props.editMode && (
+            <SliceHeaderControls
+              slice={slice}
+              isCached={isCached}
+              isExpanded={isExpanded}
+              cachedDttm={cachedDttm}
+              toggleExpandSlice={toggleExpandSlice}
+              forceRefresh={forceRefresh}
+              exploreChart={exploreChart}
+              exportCSV={exportCSV}
+            />
+          )}
         </div>
       </div>
     );
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index f61e59b..0dae6f8 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -23,14 +23,23 @@ const defaultProps = {
   toggleExpandSlice: () => ({}),
   exploreChart: () => ({}),
   exportCSV: () => ({}),
+  cachedDttm: null,
+  isCached: false,
+  isExpanded: false,
 };
 
 class SliceHeaderControls extends React.PureComponent {
   constructor(props) {
     super(props);
     this.exportCSV = this.props.exportCSV.bind(this, this.props.slice.slice_id);
-    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice.slice_id);
-    this.toggleExpandSlice = this.props.toggleExpandSlice.bind(this, this.props.slice.slice_id);
+    this.exploreChart = this.props.exploreChart.bind(
+      this,
+      this.props.slice.slice_id,
+    );
+    this.toggleExpandSlice = this.props.toggleExpandSlice.bind(
+      this,
+      this.props.slice.slice_id,
+    );
     this.toggleControls = this.toggleControls.bind(this);
 
     this.state = {
@@ -48,9 +57,9 @@ class SliceHeaderControls extends React.PureComponent {
     const slice = this.props.slice;
     const isCached = this.props.isCached;
     const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
-    const refreshTooltip = isCached ?
-      t('Served from data cached %s . Click to force refresh.', cachedWhen) :
-      t('Force refresh data');
+    const refreshTooltip = isCached
+      ? t('Served from data cached %s . Click to force refresh.', cachedWhen)
+      : t('Force refresh data');
 
     // @TODO account for
     //  dashboard.dashboard.superset_can_explore
@@ -59,7 +68,9 @@ class SliceHeaderControls extends React.PureComponent {
       <DropdownButton
         title=""
         id={`slice_${slice.slice_id}-controls`}
-        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', { 'is-cached': isCached })}
+        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', {
+          'is-cached': isCached,
+        })}
         pullRight
         noCaret
       >
@@ -69,17 +80,17 @@ class SliceHeaderControls extends React.PureComponent {
           onClick={this.props.forceRefresh}
         />
 
-        {slice.description &&
+        {slice.description && (
           <ActionMenuItem
             text={t('Toggle chart description')}
             tooltip={t('Toggle chart description')}
             onClick={this.toggleExpandSlice}
           />
-        }
+        )}
 
         <ActionMenuItem
           text={t('Edit chart')}
-          tooltip={t('Edit the chart\'s properties')}
+          tooltip={t("Edit the chart's properties")}
           href={slice.edit_url}
           target="_blank"
         />
diff --git a/superset/assets/src/dashboard/v2/components/Toast.jsx b/superset/assets/src/dashboard/components/Toast.jsx
similarity index 93%
rename from superset/assets/src/dashboard/v2/components/Toast.jsx
rename to superset/assets/src/dashboard/components/Toast.jsx
index 537388d..3c5a3ca 100644
--- a/superset/assets/src/dashboard/v2/components/Toast.jsx
+++ b/superset/assets/src/dashboard/components/Toast.jsx
@@ -4,7 +4,12 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import { toastShape } from '../util/propShapes';
-import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+import {
+  INFO_TOAST,
+  SUCCESS_TOAST,
+  WARNING_TOAST,
+  DANGER_TOAST,
+} from '../util/constants';
 
 const propTypes = {
   toast: toastShape.isRequired,
@@ -60,7 +65,9 @@ class Toast extends React.Component {
 
   render() {
     const { visible } = this.state;
-    const { toast: { toastType, text } } = this.props;
+    const {
+      toast: { toastType, text },
+    } = this.props;
 
     return (
       <Alert
diff --git a/superset/assets/src/dashboard/v2/components/ToastPresenter.jsx b/superset/assets/src/dashboard/components/ToastPresenter.jsx
similarity index 81%
rename from superset/assets/src/dashboard/v2/components/ToastPresenter.jsx
rename to superset/assets/src/dashboard/components/ToastPresenter.jsx
index 95a0251..19d44b0 100644
--- a/superset/assets/src/dashboard/v2/components/ToastPresenter.jsx
+++ b/superset/assets/src/dashboard/components/ToastPresenter.jsx
@@ -19,16 +19,13 @@ class ToastPresenter extends React.Component {
     const { toasts, removeToast } = this.props;
 
     return (
-      toasts.length > 0 &&
+      toasts.length > 0 && (
         <div className="toast-presenter">
           {toasts.map(toast => (
-            <Toast
-              key={toast.id}
-              toast={toast}
-              onCloseToast={removeToast}
-            />
+            <Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
           ))}
         </div>
+      )
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
new file mode 100644
index 0000000..94cab42
--- /dev/null
+++ b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DragLayer } from 'react-dnd';
+
+import AddSliceCard from '../AddSliceCard';
+import { slicePropShape } from '../../util/propShapes';
+import {
+  NEW_COMPONENT_SOURCE_TYPE,
+  CHART_TYPE,
+} from '../../util/componentTypes';
+
+const propTypes = {
+  dragItem: PropTypes.shape({
+    index: PropTypes.number.isRequired,
+  }),
+  slices: PropTypes.arrayOf(slicePropShape),
+  isDragging: PropTypes.bool.isRequired,
+  currentOffset: PropTypes.shape({
+    x: PropTypes.number.isRequired,
+    y: PropTypes.number.isRequired,
+  }),
+};
+
+const defaultProps = {
+  currentOffset: null,
+  dragItem: null,
+  slices: null,
+};
+
+function AddSliceDragPreview({ dragItem, slices, isDragging, currentOffset }) {
+  if (!isDragging || !currentOffset || !dragItem || !slices) return null;
+
+  const slice = slices[dragItem.index];
+
+  // make sure it's a new component and a chart
+  const shouldRender =
+    slice &&
+    dragItem.parentType === NEW_COMPONENT_SOURCE_TYPE &&
+    dragItem.type === CHART_TYPE;
+
+  return !shouldRender ? null : (
+    <AddSliceCard
+      style={{
+        position: 'fixed',
+        background: 'white',
+        pointerEvents: 'none',
+        top: 0,
+        left: 0,
+        zIndex: 100,
+        transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
+      }}
+      sliceName={slice.slice_name}
+      lastModified={
+        slice.modified ? slice.modified.replace(/<[^>]*>/g, '') : ''
+      }
+      visType={slice.viz_type}
+      datasourceLink={slice.datasource_link}
+    />
+  );
+}
+
+AddSliceDragPreview.propTypes = propTypes;
+AddSliceDragPreview.defaultProps = defaultProps;
+
+// This injects these props into the component
+export default DragLayer(monitor => ({
+  dragItem: monitor.getItem(),
+  currentOffset: monitor.getSourceClientOffset(),
+  isDragging: monitor.isDragging(),
+}))(AddSliceDragPreview);
diff --git a/superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
similarity index 65%
rename from superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx
rename to superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
index 775e092..bfe4973 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
@@ -1,3 +1,4 @@
+import { getEmptyImage } from 'react-dnd-html5-backend';
 import React from 'react';
 import PropTypes from 'prop-types';
 import { DragSource, DropTarget } from 'react-dnd';
@@ -5,7 +6,12 @@ import cx from 'classnames';
 
 import { componentShape } from '../../util/propShapes';
 import { dragConfig, dropConfig } from './dragDroppableConfig';
-import { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+import {
+  DROP_TOP,
+  DROP_RIGHT,
+  DROP_BOTTOM,
+  DROP_LEFT,
+} from '../../util/getDropPosition';
 
 const propTypes = {
   children: PropTypes.func,
@@ -19,6 +25,7 @@ const propTypes = {
   style: PropTypes.object,
   onDrop: PropTypes.func,
   editMode: PropTypes.bool.isRequired,
+  useEmptyDragPreview: PropTypes.bool,
 
   // from react-dnd
   isDragging: PropTypes.bool.isRequired,
@@ -37,6 +44,7 @@ const defaultProps = {
   children() {},
   onDrop() {},
   orientation: 'row',
+  useEmptyDragPreview: false,
 };
 
 class DragDroppable extends React.Component {
@@ -58,7 +66,16 @@ class DragDroppable extends React.Component {
 
   setRef(ref) {
     this.ref = ref;
-    this.props.dragPreviewRef(ref);
+    // this is needed for a custom drag preview
+    if (this.props.useEmptyDragPreview) {
+      this.props.dragPreviewRef(getEmptyImage(), {
+        // IE fallback: specify that we'd rather screenshot the node
+        // when it already knows it's being dragged so we can hide it with CSS.
+        captureDraggingState: true,
+      });
+    } else {
+      this.props.dragPreviewRef(ref);
+    }
     this.props.droppableRef(ref);
   }
 
@@ -74,8 +91,6 @@ class DragDroppable extends React.Component {
       editMode,
     } = this.props;
 
-    if (!editMode) return children({});
-
     const { dropIndicator } = this.state;
 
     return (
@@ -90,18 +105,23 @@ class DragDroppable extends React.Component {
           className,
         )}
       >
-        {children({
-          dragSourceRef,
-          dropIndicatorProps: isDraggingOver && dropIndicator && {
-            className: cx(
-              'drop-indicator',
-              dropIndicator === DROP_TOP && 'drop-indicator--top',
-              dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
-              dropIndicator === DROP_LEFT && 'drop-indicator--left',
-              dropIndicator === DROP_RIGHT && 'drop-indicator--right',
-            ),
-          },
-        })}
+        {children(
+          !editMode
+            ? {}
+            : {
+                dragSourceRef,
+                dropIndicatorProps: isDraggingOver &&
+                  dropIndicator && {
+                    className: cx(
+                      'drop-indicator',
+                      dropIndicator === DROP_TOP && 'drop-indicator--top',
+                      dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
+                      dropIndicator === DROP_LEFT && 'drop-indicator--left',
+                      dropIndicator === DROP_RIGHT && 'drop-indicator--right',
+                    ),
+                  },
+              },
+        )}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/src/dashboard/components/dnd/DragHandle.jsx
similarity index 82%
rename from superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx
rename to superset/assets/src/dashboard/components/dnd/DragHandle.jsx
index 36d1e6b..23d7d11 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx
+++ b/superset/assets/src/dashboard/components/dnd/DragHandle.jsx
@@ -26,9 +26,11 @@ export default class DragHandle extends React.PureComponent {
           position === 'top' && 'drag-handle--top',
         )}
       >
-        {Array(dotCount).fill(null).map((_, i) => (
-          <div key={`handle-dot-${i}`} className="drag-handle-dot" />
-        ))}
+        {Array(dotCount)
+          .fill(null)
+          .map((_, i) => (
+            <div key={`handle-dot-${i}`} className="drag-handle-dot" />
+          ))}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/src/dashboard/components/dnd/dragDroppableConfig.js
similarity index 88%
rename from superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
rename to superset/assets/src/dashboard/components/dnd/dragDroppableConfig.js
index 54ce67e..36c34a0 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/src/dashboard/components/dnd/dragDroppableConfig.js
@@ -38,15 +38,11 @@ export const dropConfig = [
   {
     hover(props, monitor, component) {
       if (
-        component
-        && component.decoratedComponentInstance
-        && component.decoratedComponentInstance.mounted
+        component &&
+        component.decoratedComponentInstance &&
+        component.decoratedComponentInstance.mounted
       ) {
-        handleHover(
-          props,
-          monitor,
-          component.decoratedComponentInstance,
-        );
+        handleHover(props, monitor, component.decoratedComponentInstance);
       }
     },
     // note:
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/src/dashboard/components/dnd/handleDrop.js
similarity index 73%
rename from superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
rename to superset/assets/src/dashboard/components/dnd/handleDrop.js
index 7cb630d..3739b18 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/components/dnd/handleDrop.js
@@ -1,4 +1,9 @@
-import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+import getDropPosition, {
+  DROP_TOP,
+  DROP_RIGHT,
+  DROP_BOTTOM,
+  DROP_LEFT,
+} from '../../util/getDropPosition';
 
 export default function handleDrop(props, monitor, Component) {
   // this may happen due to throttling
@@ -22,9 +27,12 @@ export default function handleDrop(props, monitor, Component) {
   const draggingItem = monitor.getItem();
 
   const dropAsChildOrSibling =
-    (orientation === 'row' && (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
-    (orientation === 'column' && (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
-    ? 'sibling' : 'child';
+    (orientation === 'row' &&
+      (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
+    (orientation === 'column' &&
+      (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
+      ? 'sibling'
+      : 'child';
 
   const dropResult = {
     source: {
@@ -49,8 +57,10 @@ export default function handleDrop(props, monitor, Component) {
   } else {
     // if the item is in the same list with a smaller index, you must account for the
     // "missing" index upon movement within the list
-    const sameParent = parentComponent && draggingItem.parentId === parentComponent.id;
-    const sameParentLowerIndex = sameParent && draggingItem.index < componentIndex;
+    const sameParent =
+      parentComponent && draggingItem.parentId === parentComponent.id;
+    const sameParentLowerIndex =
+      sameParent && draggingItem.index < componentIndex;
 
     let nextIndex = sameParentLowerIndex ? componentIndex - 1 : componentIndex;
     if (dropPosition === DROP_BOTTOM || dropPosition === DROP_RIGHT) {
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/dnd/handleHover.js
rename to superset/assets/src/dashboard/components/dnd/handleHover.js
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
new file mode 100644
index 0000000..54e1536
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -0,0 +1,233 @@
+import cx from 'classnames';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { exportChart } from '../../../explore/exploreUtils';
+import SliceHeader from '../SliceHeader';
+import ChartContainer from '../../../chart/ChartContainer';
+import { chartPropType } from '../../../chart/chartReducer';
+import { slicePropShape } from '../../util/propShapes';
+import { VIZ_TYPES } from '../../../visualizations/main';
+
+const propTypes = {
+  id: PropTypes.number.isRequired,
+  width: PropTypes.number.isRequired,
+  height: PropTypes.number.isRequired,
+
+  // from redux
+  chart: PropTypes.shape(chartPropType).isRequired,
+  formData: PropTypes.object.isRequired,
+  datasource: PropTypes.object.isRequired,
+  slice: slicePropShape.isRequired,
+  timeout: PropTypes.number.isRequired,
+  filters: PropTypes.object.isRequired,
+  refreshChart: PropTypes.func.isRequired,
+  saveSliceName: PropTypes.func.isRequired,
+  toggleExpandSlice: PropTypes.func.isRequired,
+  addFilter: PropTypes.func.isRequired,
+  removeFilter: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
+  isExpanded: PropTypes.bool.isRequired,
+};
+
+// we use state + shouldComponentUpdate() logic to prevent perf-wrecking
+// resizing across all slices on a dashboard on every update
+const RESIZE_TIMEOUT = 350;
+const SHOULD_UPDATE_ON_PROP_CHANGES = Object.keys(propTypes).filter(
+  prop => prop !== 'width' && prop !== 'height',
+);
+const OVERFLOWABLE_VIZ_TYPES = new Set([VIZ_TYPES.filter_box]);
+
+class Chart extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      width: props.width,
+      height: props.height,
+    };
+
+    this.addFilter = this.addFilter.bind(this);
+    this.exploreChart = this.exploreChart.bind(this);
+    this.exportCSV = this.exportCSV.bind(this);
+    this.forceRefresh = this.forceRefresh.bind(this);
+    this.getFilters = this.getFilters.bind(this);
+    this.removeFilter = this.removeFilter.bind(this);
+    this.resize = this.resize.bind(this);
+    this.setDescriptionRef = this.setDescriptionRef.bind(this);
+    this.setHeaderRef = this.setHeaderRef.bind(this);
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    // this logic mostly pertains to chart resizing. we keep a copy of the dimensions in
+    // state so that we can buffer component size updates and only update on the final call
+    // which improves performance significantly
+    if (
+      nextState.width !== this.state.width ||
+      nextState.height !== this.state.height
+    ) {
+      return true;
+    }
+
+    for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
+      const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
+      if (nextProps[prop] !== this.props[prop]) {
+        return true;
+      }
+    }
+
+    if (
+      nextProps.width !== this.props.width ||
+      nextProps.height !== this.props.height
+    ) {
+      clearTimeout(this.resizeTimeout);
+      this.resizeTimeout = setTimeout(this.resize, RESIZE_TIMEOUT);
+    }
+
+    return false;
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this.resizeTimeout);
+  }
+
+  getFilters() {
+    return this.props.filters;
+  }
+
+  getChartHeight() {
+    const headerHeight = this.getHeaderHeight();
+    const descriptionHeight =
+      this.props.isExpanded && this.descriptionRef
+        ? this.descriptionRef.offsetHeight
+        : 0;
+
+    return this.state.height - headerHeight - descriptionHeight;
+  }
+
+  getHeaderHeight() {
+    return (this.headerRef && this.headerRef.offsetHeight) || 30;
+  }
+
+  setDescriptionRef(ref) {
+    this.descriptionRef = ref;
+  }
+
+  setHeaderRef(ref) {
+    this.headerRef = ref;
+  }
+
+  resize() {
+    const { width, height } = this.props;
+    this.setState(() => ({ width, height }));
+  }
+
+  addFilter(...args) {
+    this.props.addFilter(this.props.chart, ...args);
+  }
+
+  exploreChart() {
+    exportChart(this.props.formData);
+  }
+
+  exportCSV() {
+    exportChart(this.props.formData, 'csv');
+  }
+
+  forceRefresh() {
+    return this.props.refreshChart(this.props.chart, true, this.props.timeout);
+  }
+
+  removeFilter(args) {
+    this.props.removeFilter(this.props.id, ...args);
+  }
+
+  render() {
+    const {
+      id,
+      chart,
+      slice,
+      datasource,
+      isExpanded,
+      editMode,
+      formData,
+      toggleExpandSlice,
+      timeout,
+    } = this.props;
+
+    const { width } = this.state;
+    const { queryResponse } = chart;
+    const isCached = queryResponse && queryResponse.is_cached;
+    const cachedDttm = queryResponse && queryResponse.cached_dttm;
+    const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice && slice.viz_type);
+
+    return (
+      <div
+        className={cx(
+          'dashboard-chart',
+          isOverflowable && 'dashboard-chart--overflowable',
+        )}
+      >
+        <SliceHeader
+          innerRef={this.setHeaderRef}
+          slice={slice}
+          isExpanded={!!isExpanded}
+          isCached={isCached}
+          cachedDttm={cachedDttm}
+          updateSliceName={this.updateSliceName}
+          toggleExpandSlice={toggleExpandSlice}
+          forceRefresh={this.forceRefresh}
+          editMode={editMode}
+          annotationQuery={chart.annotationQuery}
+          exploreChart={this.exploreChart}
+          exportCSV={this.exportCSV}
+        />
+
+        {/*
+          This usage of dangerouslySetInnerHTML is safe since it is being used to render
+          markdown that is sanitized with bleach. See:
+             https://github.com/apache/incubator-superset/pull/4390
+          and
+             https://github.com/apache/incubator-superset/commit/b6fcc22d5a2cb7a5e92599ed5795a0169385a825
+        */}
+        {isExpanded &&
+          slice.description_markeddown && (
+            <div
+              className="slice_description bs-callout bs-callout-default"
+              ref={this.setDescriptionRef}
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+            />
+          )}
+
+        <ChartContainer
+          containerId={`slice-container-${id}`}
+          chartId={id}
+          datasource={datasource}
+          formData={formData}
+          headerHeight={this.getHeaderHeight()}
+          height={this.getChartHeight()}
+          width={width}
+          timeout={timeout}
+          vizType={slice.viz_type}
+          addFilter={this.addFilter}
+          getFilters={this.getFilters}
+          removeFilter={this.removeFilter}
+          annotationData={chart.annotationData}
+          chartAlert={chart.chartAlert}
+          chartStatus={chart.chartStatus}
+          chartUpdateEndTime={chart.chartUpdateEndTime}
+          chartUpdateStartTime={chart.chartUpdateStartTime}
+          latestQueryFormData={chart.latestQueryFormData}
+          lastRendered={chart.lastRendered}
+          queryResponse={chart.queryResponse}
+          queryRequest={chart.queryRequest}
+          triggerQuery={chart.triggerQuery}
+        />
+      </div>
+    );
+  }
+}
+
+Chart.propTypes = propTypes;
+
+export default Chart;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
similarity index 68%
rename from superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
rename to superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index 2aed4b2..a684230 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -1,15 +1,21 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import Chart from '../../containers/Chart';
 import DeleteComponentButton from '../DeleteComponentButton';
 import DragDroppable from '../dnd/DragDroppable';
 import DragHandle from '../dnd/DragHandle';
 import HoverMenu from '../menu/HoverMenu';
 import ResizableContainer from '../resizable/ResizableContainer';
-import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
-import { ROW_TYPE } from '../../util/componentTypes';
-import { GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS } from '../../util/constants';
+import { ROW_TYPE, COLUMN_TYPE } from '../../util/componentTypes';
+import {
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+  GRID_BASE_UNIT,
+} from '../../util/constants';
+
+const CHART_MARGIN = 32;
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -19,7 +25,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  chart: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -33,8 +38,7 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class ChartHolder extends React.Component {
   constructor(props) {
@@ -73,6 +77,12 @@ class ChartHolder extends React.Component {
       editMode,
     } = this.props;
 
+    // inherit the size of parent columns
+    const widthMultiple =
+      parentComponent.type === COLUMN_TYPE
+        ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
+        : component.meta.width || GRID_MIN_COLUMN_COUNT;
+
     return (
       <DragDroppable
         component={component}
@@ -90,34 +100,37 @@ class ChartHolder extends React.Component {
             adjustableWidth={parentComponent.type === ROW_TYPE}
             adjustableHeight
             widthStep={columnWidth}
-            widthMultiple={component.meta.width}
+            widthMultiple={widthMultiple}
+            heightStep={GRID_BASE_UNIT}
             heightMultiple={component.meta.height}
             minWidthMultiple={GRID_MIN_COLUMN_COUNT}
             minHeightMultiple={GRID_MIN_ROW_UNITS}
-            maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
+            maxWidthMultiple={availableColumnCount + widthMultiple}
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
             editMode={editMode}
           >
-            {editMode &&
-              <HoverMenu innerRef={dragSourceRef} position="top">
-                <DragHandle position="top" />
-              </HoverMenu>}
-
-            <WithPopoverMenu
-              onChangeFocus={this.handleChangeFocus}
-              menuItems={[
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
-              ]}
-              editMode={editMode}
+            <div
+              ref={dragSourceRef}
+              className="dashboard-component dashboard-component-chart-holder"
             >
-              <div className="dashboard-component dashboard-component-chart">
-                {this.props.chart}
-              </div>
-
-              {dropIndicatorProps && <div {...dropIndicatorProps} />}
-            </WithPopoverMenu>
+              <Chart
+                id={component.meta.chartId}
+                width={widthMultiple * columnWidth}
+                height={component.meta.height * GRID_BASE_UNIT - CHART_MARGIN}
+              />
+              {editMode && (
+                <HoverMenu position="top">
+                  <DragHandle position="top" />
+                  <DeleteComponentButton
+                    onDelete={this.handleDeleteComponent}
+                  />
+                </HoverMenu>
+              )}
+            </div>
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
           </ResizableContainer>
         )}
       </DragDroppable>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
similarity index 82%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Column.jsx
index 490d7bd..a71d732 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
@@ -25,7 +25,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -41,8 +40,7 @@ const propTypes = {
   updateComponents: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class Column extends React.PureComponent {
   constructor(props) {
@@ -50,7 +48,10 @@ class Column extends React.PureComponent {
     this.state = {
       isFocused: false,
     };
-    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(
+      this,
+      'background',
+    );
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
   }
@@ -93,12 +94,13 @@ class Column extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
-      cells,
     } = this.props;
 
     const columnItems = columnComponent.children || [];
     const backgroundStyle = backgroundStyleOptions.find(
-      opt => opt.value === (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
+      opt =>
+        opt.value ===
+        (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -119,7 +121,9 @@ class Column extends React.PureComponent {
             widthStep={columnWidth}
             widthMultiple={columnComponent.meta.width}
             minWidthMultiple={minColumnWidth}
-            maxWidthMultiple={availableColumnCount + (columnComponent.meta.width || 0)}
+            maxWidthMultiple={
+              availableColumnCount + (columnComponent.meta.width || 0)
+            }
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
@@ -145,31 +149,33 @@ class Column extends React.PureComponent {
                   backgroundStyle.className,
                 )}
               >
-                {editMode &&
+                {editMode && (
                   <HoverMenu innerRef={dragSourceRef} position="top">
                     <DragHandle position="top" />
-                    <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                    <DeleteComponentButton
+                      onDelete={this.handleDeleteComponent}
+                    />
                     <IconButton
                       onClick={this.handleChangeFocus}
                       className="fa fa-cog"
                     />
-                  </HoverMenu>}
+                  </HoverMenu>
+                )}
 
                 {columnItems.map((componentId, itemIndex) => (
-                    <DashboardComponent
-                      key={componentId}
-                      id={componentId}
-                      parentId={columnComponent.id}
-                      depth={depth + 1}
-                      index={itemIndex }
-                      availableColumnCount={columnComponent.meta.width}
-                      columnWidth={columnWidth}
-                      cells={cells}
-                      onResizeStart={onResizeStart}
-                      onResize={onResize}
-                      onResizeStop={onResizeStop}
-                    />
-                  ))}
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={columnComponent.id}
+                    depth={depth + 1}
+                    index={itemIndex}
+                    availableColumnCount={columnComponent.meta.width}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                  />
+                ))}
 
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
@@ -177,7 +183,6 @@ class Column extends React.PureComponent {
           </ResizableContainer>
         )}
       </DragDroppable>
-
     );
   }
 }
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/src/dashboard/components/gridComponents/Divider.jsx
similarity index 96%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Divider.jsx
index b3010e9..7c7936d 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Divider.jsx
@@ -51,10 +51,11 @@ class Divider extends React.PureComponent {
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            {editMode &&
+            {editMode && (
               <HoverMenu position="left">
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-              </HoverMenu>}
+              </HoverMenu>
+            )}
 
             <div className="dashboard-component dashboard-component-divider" />
 
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
similarity index 93%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Header.jsx
index 97945a9..5114a77 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
@@ -4,7 +4,7 @@ import cx from 'classnames';
 
 import DragDroppable from '../dnd/DragDroppable';
 import DragHandle from '../dnd/DragHandle';
-import EditableTitle from '../../../../components/EditableTitle';
+import EditableTitle from '../../../components/EditableTitle';
 import HoverMenu from '../menu/HoverMenu';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
@@ -30,8 +30,7 @@ const propTypes = {
   updateComponents: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class Header extends React.PureComponent {
   constructor(props) {
@@ -43,7 +42,10 @@ class Header extends React.PureComponent {
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
     this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize');
-    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(
+      this,
+      'background',
+    );
     this.handleChangeText = this.handleUpdateMeta.bind(this, 'text');
   }
 
@@ -88,7 +90,8 @@ class Header extends React.PureComponent {
     );
 
     const rowStyle = backgroundStyleOptions.find(
-      opt => opt.value === (component.meta.background || BACKGROUND_TRANSPARENT),
+      opt =>
+        opt.value === (component.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -104,10 +107,11 @@ class Header extends React.PureComponent {
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            {editMode &&
+            {editMode && (
               <HoverMenu position="left">
                 <DragHandle position="left" />
-              </HoverMenu>}
+              </HoverMenu>
+            )}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
similarity index 82%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Row.jsx
index 8faaee1..91f200d 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
@@ -23,7 +23,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -51,7 +50,10 @@ class Row extends React.PureComponent {
     };
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
     this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
-    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(
+      this,
+      'background',
+    );
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
   }
 
@@ -93,13 +95,13 @@ class Row extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
-      cells,
     } = this.props;
 
     const rowItems = rowComponent.children || [];
 
     const backgroundStyle = backgroundStyleOptions.find(
-      opt => opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
+      opt =>
+        opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -133,31 +135,35 @@ class Row extends React.PureComponent {
                 backgroundStyle.className,
               )}
             >
-              {editMode &&
+              {editMode && (
                 <HoverMenu innerRef={dragSourceRef} position="left">
                   <DragHandle position="left" />
-                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                  <DeleteComponentButton
+                    onDelete={this.handleDeleteComponent}
+                  />
                   <IconButton
                     onClick={this.handleChangeFocus}
                     className="fa fa-cog"
                   />
-                </HoverMenu>}
+                </HoverMenu>
+              )}
 
               {rowItems.map((componentId, itemIndex) => (
-
-                  <DashboardComponent
-                    key={componentId}
-                    id={componentId}
-                    parentId={rowComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex }
-                    availableColumnCount={availableColumnCount - occupiedColumnCount}
-                    columnWidth={columnWidth}
-                    cells={cells}onResizeStart={onResizeStart}
-                    onResize={onResize}
-                    onResizeStop={onResizeStop}
-                  />
-                ))}
+                <DashboardComponent
+                  key={componentId}
+                  id={componentId}
+                  parentId={rowComponent.id}
+                  depth={depth + 1}
+                  index={itemIndex}
+                  availableColumnCount={
+                    availableColumnCount - occupiedColumnCount
+                  }
+                  columnWidth={columnWidth}
+                  onResizeStart={onResizeStart}
+                  onResize={onResize}
+                  onResizeStop={onResizeStop}
+                />
+              ))}
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
             </div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
similarity index 90%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 218c4e7..d73bc0c 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import DashboardComponent from '../../containers/DashboardComponent';
 import DragDroppable from '../dnd/DragDroppable';
-import EditableTitle from '../../../../components/EditableTitle';
+import EditableTitle from '../../../components/EditableTitle';
 import DeleteComponentButton from '../DeleteComponentButton';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
@@ -123,13 +123,7 @@ export default class Tab extends React.PureComponent {
 
   renderTab() {
     const { isFocused } = this.state;
-    const {
-      component,
-      parentComponent,
-      index,
-      depth,
-      editMode,
-    } = this.props;
+    const { component, parentComponent, index, depth, editMode } = this.props;
 
     return (
       <DragDroppable
@@ -149,9 +143,15 @@ export default class Tab extends React.PureComponent {
           <div className="dragdroppable-tab" ref={dragSourceRef}>
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
-              menuItems={parentComponent.children.length <= 1 ? [] : [
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
-              ]}
+              menuItems={
+                parentComponent.children.length <= 1
+                  ? []
+                  : [
+                      <DeleteComponentButton
+                        onDelete={this.handleDeleteComponent}
+                      />,
+                    ]
+              }
               editMode={editMode}
             >
               <EditableTitle
@@ -171,7 +171,9 @@ export default class Tab extends React.PureComponent {
 
   render() {
     const { renderType } = this.props;
-    return renderType === RENDER_TAB ? this.renderTab() : this.renderTabContent();
+    return renderType === RENDER_TAB
+      ? this.renderTab()
+      : this.renderTabContent();
   }
 }
 
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
similarity index 81%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 1f5f0c6..585041f 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -106,9 +106,10 @@ class Tabs extends React.PureComponent {
     // Ensure dropped tab is visible
     const { destination } = dropResult;
     if (destination) {
-      const dropTabIndex = destination.id === component.id
-        ? destination.index // dropped ON tabs
-        : component.children.indexOf(destination.id); // dropped IN tab
+      const dropTabIndex =
+        destination.id === component.id
+          ? destination.index // dropped ON tabs
+          : component.children.indexOf(destination.id); // dropped IN tab
 
       if (dropTabIndex > -1) {
         setTimeout(() => {
@@ -147,13 +148,17 @@ class Tabs extends React.PureComponent {
         onDrop={handleComponentDrop}
         editMode={editMode}
       >
-        {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
+        {({
+          dropIndicatorProps: tabsDropIndicatorProps,
+          dragSourceRef: tabsDragSourceRef,
+        }) => (
           <div className="dashboard-component dashboard-component-tabs">
-            {editMode &&
+            {editMode && (
               <HoverMenu innerRef={tabsDragSourceRef} position="left">
                 <DragHandle position="left" />
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-              </HoverMenu>}
+              </HoverMenu>
+            )}
 
             <BootstrapTabs
               id={tabsComponent.id}
@@ -187,37 +192,39 @@ class Tabs extends React.PureComponent {
                     render potentially-expensive charts (this also enables lazy loading
                     their content)
                   */}
-                  {tabIndex === selectedTabIndex && renderTabContent &&
-                    <DashboardComponent
-                      id={tabId}
-                      parentId={tabsComponent.id}
-                      depth={depth} // see isValidChild.js for why tabs don't increment child depth
-                      index={tabIndex}
-                      renderType={RENDER_TAB_CONTENT}
-                      availableColumnCount={availableColumnCount}
-                      columnWidth={columnWidth}
-                      onResizeStart={onResizeStart}
-                      onResize={onResize}
-                      onResizeStop={onResizeStop}
-                      onDropOnTab={this.handleDropOnTab}
-                    />}
+                  {tabIndex === selectedTabIndex &&
+                    renderTabContent && (
+                      <DashboardComponent
+                        id={tabId}
+                        parentId={tabsComponent.id}
+                        depth={depth} // see isValidChild.js for why tabs don't increment child depth
+                        index={tabIndex}
+                        renderType={RENDER_TAB_CONTENT}
+                        availableColumnCount={availableColumnCount}
+                        columnWidth={columnWidth}
+                        onResizeStart={onResizeStart}
+                        onResize={onResize}
+                        onResizeStop={onResizeStop}
+                        onDropOnTab={this.handleDropOnTab}
+                      />
+                    )}
                 </BootstrapTab>
               ))}
 
               {editMode &&
-                tabIds.length < MAX_TAB_COUNT &&
+                tabIds.length < MAX_TAB_COUNT && (
                   <BootstrapTab
                     eventKey={NEW_TAB_INDEX}
                     title={<div className="fa fa-plus" />}
-                  />}
-
+                  />
+                )}
             </BootstrapTabs>
 
             {/* don't indicate that a drop on root is allowed when tabs already exist */}
-            {tabsDropIndicatorProps
-              && parentComponent.id !== DASHBOARD_ROOT_ID
-              && <div {...tabsDropIndicatorProps} />}
-
+            {tabsDropIndicatorProps &&
+              parentComponent.id !== DASHBOARD_ROOT_ID && (
+                <div {...tabsDropIndicatorProps} />
+              )}
           </div>
         )}
       </DragDroppable>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/index.js b/superset/assets/src/dashboard/components/gridComponents/index.js
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/gridComponents/index.js
rename to superset/assets/src/dashboard/components/gridComponents/index.js
index ef6d13f..016ab03 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/components/gridComponents/index.js
@@ -3,7 +3,6 @@ import {
   COLUMN_TYPE,
   DIVIDER_TYPE,
   HEADER_TYPE,
-  INVISIBLE_ROW_TYPE,
   ROW_TYPE,
   TAB_TYPE,
   TABS_TYPE,
@@ -30,7 +29,6 @@ export default {
   [COLUMN_TYPE]: Column,
   [DIVIDER_TYPE]: Divider,
   [HEADER_TYPE]: Header,
-  [INVISIBLE_ROW_TYPE]: Row,
   [ROW_TYPE]: Row,
   [TAB_TYPE]: Tab,
   [TABS_TYPE]: Tabs,
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/src/dashboard/components/gridComponents/new/DraggableNewComponent.jsx
similarity index 90%
rename from superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
rename to superset/assets/src/dashboard/components/gridComponents/new/DraggableNewComponent.jsx
index eebd6e0..d579dc1 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/new/DraggableNewComponent.jsx
@@ -23,7 +23,10 @@ export default class DraggableNewComponent extends React.PureComponent {
     return (
       <DragDroppable
         component={{ type, id }}
-        parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
+        parentComponent={{
+          id: NEW_COMPONENTS_SOURCE_ID,
+          type: NEW_COMPONENT_SOURCE_TYPE,
+        }}
         index={0}
         depth={0}
         editMode
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewColumn.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewColumn.jsx
new file mode 100644
index 0000000..f624e58
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewColumn.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { COLUMN_TYPE } from '../../../util/componentTypes';
+import { NEW_COLUMN_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewColumn() {
+  return (
+    <DraggableNewComponent
+      id={NEW_COLUMN_ID}
+      type={COLUMN_TYPE}
+      label="Column"
+      className="fa fa-long-arrow-down"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewDivider.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewDivider.jsx
new file mode 100644
index 0000000..de07a24
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewDivider.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { DIVIDER_TYPE } from '../../../util/componentTypes';
+import { NEW_DIVIDER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewDivider() {
+  return (
+    <DraggableNewComponent
+      id={NEW_DIVIDER_ID}
+      type={DIVIDER_TYPE}
+      label="Divider"
+      className="divider-placeholder"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewHeader.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewHeader.jsx
new file mode 100644
index 0000000..50bd600
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewHeader.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { HEADER_TYPE } from '../../../util/componentTypes';
+import { NEW_HEADER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewHeader() {
+  return (
+    <DraggableNewComponent
+      id={NEW_HEADER_ID}
+      type={HEADER_TYPE}
+      label="Header"
+      className="fa fa-header"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewRow.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewRow.jsx
new file mode 100644
index 0000000..81bdc93
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewRow.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { ROW_TYPE } from '../../../util/componentTypes';
+import { NEW_ROW_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewRow() {
+  return (
+    <DraggableNewComponent
+      id={NEW_ROW_ID}
+      type={ROW_TYPE}
+      label="Row"
+      className="fa fa-long-arrow-right"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewTabs.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewTabs.jsx
new file mode 100644
index 0000000..fd9366b
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewTabs.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { TABS_TYPE } from '../../../util/componentTypes';
+import { NEW_TABS_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewTabs() {
+  return (
+    <DraggableNewComponent
+      id={NEW_TABS_ID}
+      type={TABS_TYPE}
+      label="Tabs"
+      className="fa fa-window-restore"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx b/superset/assets/src/dashboard/components/menu/BackgroundStyleDropdown.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
rename to superset/assets/src/dashboard/components/menu/BackgroundStyleDropdown.jsx
diff --git a/superset/assets/src/dashboard/v2/components/menu/HoverMenu.jsx b/superset/assets/src/dashboard/components/menu/HoverMenu.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/menu/HoverMenu.jsx
rename to superset/assets/src/dashboard/components/menu/HoverMenu.jsx
diff --git a/superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx b/superset/assets/src/dashboard/components/menu/PopoverDropdown.jsx
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx
rename to superset/assets/src/dashboard/components/menu/PopoverDropdown.jsx
index 6a56eab..4971793 100644
--- a/superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx
+++ b/superset/assets/src/dashboard/components/menu/PopoverDropdown.jsx
@@ -19,7 +19,9 @@ const propTypes = {
 
 const defaultProps = {
   renderButton: option => option.label,
-  renderOption: option => <div className={option.className}>{option.label}</div>,
+  renderOption: option => (
+    <div className={option.className}>{option.label}</div>
+  ),
 };
 
 class PopoverDropdown extends React.PureComponent {
diff --git a/superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
similarity index 91%
rename from superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx
rename to superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
index f213442..8a87fca 100644
--- a/superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
@@ -54,12 +54,15 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   handleClick(event) {
-    const { onChangeFocus, shouldFocus: shouldFocusFunc, disableClick, editMode } = this.props;
-    const shouldFocus = shouldFocusFunc(event, this.container);
-
-    if (!editMode) {
+    if (!this.props.editMode) {
       return;
     }
+    const {
+      onChangeFocus,
+      shouldFocus: shouldFocusFunc,
+      disableClick,
+    } = this.props;
+    const shouldFocus = shouldFocusFunc(event, this.container);
 
     if (!disableClick && shouldFocus && !this.state.isFocused) {
       // if not focused, set focus and add a window event listener to capture outside clicks
@@ -97,12 +100,15 @@ class WithPopoverMenu extends React.PureComponent {
         {children}
         {editMode &&
           isFocused &&
-          menuItems.length > 0 &&
-            <div className="popover-menu" >
+          menuItems.length > 0 && (
+            <div className="popover-menu">
               {menuItems.map((node, i) => (
-                <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
+                <div className="menu-item" key={`menu-item-${i}`}>
+                  {node}
+                </div>
               ))}
-            </div>}
+            </div>
+          )}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/src/dashboard/components/resizable/ResizableContainer.jsx
similarity index 78%
rename from superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx
rename to superset/assets/src/dashboard/components/resizable/ResizableContainer.jsx
index a532ff0..7e09e73 100644
--- a/superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/src/dashboard/components/resizable/ResizableContainer.jsx
@@ -56,7 +56,10 @@ const defaultProps = {
 // because columns are not multiples of a single variable (width = n*cols + (n-1) * gutters)
 // we snap to the base unit and then snap to _actual_ column multiples on stop
 const SNAP_TO_GRID = [GRID_BASE_UNIT, GRID_BASE_UNIT];
-
+const HANDLE_CLASSES = {
+  right: 'resizable-container-handle--right',
+  bottom: 'resizable-container-handle--bottom',
+};
 class ResizableContainer extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -139,29 +142,26 @@ class ResizableContainer extends React.PureComponent {
 
     const size = {
       width: adjustableWidth
-        ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth
-        : (staticWidthMultiple && staticWidthMultiple * widthStep)
-          || staticWidth
-          || undefined,
+        ? (widthStep + gutterWidth) * widthMultiple - gutterWidth
+        : (staticWidthMultiple && staticWidthMultiple * widthStep) ||
+          staticWidth ||
+          undefined,
       height: adjustableHeight
         ? heightStep * heightMultiple
-        : (staticHeightMultiple && staticHeightMultiple * heightStep)
-          || staticHeight
-          || undefined,
+        : (staticHeightMultiple && staticHeightMultiple * heightStep) ||
+          staticHeight ||
+          undefined,
     };
 
-    if (!editMode) {
-      return (
-        <div style={{ ...size }}>
-          {children}
-        </div>
-      );
-    }
-
     let enableConfig = resizableConfig.notAdjustable;
-    if (adjustableWidth && adjustableHeight) enableConfig = resizableConfig.widthAndHeight;
-    else if (adjustableWidth) enableConfig = resizableConfig.widthOnly;
-    else if (adjustableHeight) enableConfig = resizableConfig.heightOnly;
+
+    if (editMode && adjustableWidth && adjustableHeight) {
+      enableConfig = resizableConfig.widthAndHeight;
+    } else if (editMode && adjustableWidth) {
+      enableConfig = resizableConfig.widthOnly;
+    } else if (editMode && adjustableHeight) {
+      enableConfig = resizableConfig.heightOnly;
+    }
 
     const { isResizing } = this.state;
 
@@ -169,18 +169,27 @@ class ResizableContainer extends React.PureComponent {
       <Resizable
         enable={enableConfig}
         grid={SNAP_TO_GRID}
-        minWidth={adjustableWidth
-          ? (minWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
-          : undefined}
-        minHeight={adjustableHeight
-          ? (minHeightMultiple * heightStep)
-          : undefined}
-        maxWidth={adjustableWidth
-          ? Math.max(size.width, (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth)
-          : undefined}
-        maxHeight={adjustableHeight
-          ? Math.max(size.height, maxHeightMultiple * heightStep)
-          : undefined}
+        minWidth={
+          adjustableWidth
+            ? minWidthMultiple * (widthStep + gutterWidth) - gutterWidth
+            : undefined
+        }
+        minHeight={
+          adjustableHeight ? minHeightMultiple * heightStep : undefined
+        }
+        maxWidth={
+          adjustableWidth
+            ? Math.max(
+                size.width,
+                maxWidthMultiple * (widthStep + gutterWidth) - gutterWidth,
+              )
+            : undefined
+        }
+        maxHeight={
+          adjustableHeight
+            ? Math.max(size.height, maxHeightMultiple * heightStep)
+            : undefined
+        }
         size={size}
         onResizeStart={this.handleResizeStart}
         onResize={this.handleResize}
@@ -190,6 +199,7 @@ class ResizableContainer extends React.PureComponent {
           'resizable-container',
           isResizing && 'resizable-container--resizing',
         )}
+        handleClasses={HANDLE_CLASSES}
       >
         {children}
       </Resizable>
diff --git a/superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx b/superset/assets/src/dashboard/components/resizable/ResizableHandle.jsx
similarity index 54%
rename from superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx
rename to superset/assets/src/dashboard/components/resizable/ResizableHandle.jsx
index 9536f6b..b696b26 100644
--- a/superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx
+++ b/superset/assets/src/dashboard/components/resizable/ResizableHandle.jsx
@@ -1,21 +1,15 @@
 import React from 'react';
 
 export function BottomRightResizeHandle() {
-  return (
-    <div className="resize-handle resize-handle--bottom-right" />
-  );
+  return <div className="resize-handle resize-handle--bottom-right" />;
 }
 
 export function RightResizeHandle() {
-  return (
-    <div className="resize-handle resize-handle--right" />
-  );
+  return <div className="resize-handle resize-handle--right" />;
 }
 
 export function BottomResizeHandle() {
-  return (
-    <div className="resize-handle resize-handle--bottom" />
-  );
+  return <div className="resize-handle resize-handle--bottom" />;
 }
 
 export default {
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
new file mode 100644
index 0000000..470176b
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -0,0 +1,59 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import {
+  addFilter,
+  removeFilter,
+  toggleExpandSlice,
+} from '../actions/dashboardState';
+import { refreshChart } from '../../chart/chartAction';
+import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
+import { saveSliceName } from '../actions/sliceEntities';
+import Chart from '../components/gridComponents/Chart';
+
+function mapStateToProps(
+  {
+    charts: chartQueries,
+    dashboardInfo,
+    dashboardState,
+    datasources,
+    sliceEntities,
+  },
+  ownProps,
+) {
+  const { id } = ownProps;
+  const chart = chartQueries[id];
+  const { filters } = dashboardState;
+
+  return {
+    chart,
+    datasource: datasources[chart.form_data.datasource],
+    slice: sliceEntities.slices[id],
+    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    filters,
+    // note: this method caches filters if possible to prevent render cascades
+    formData: getFormDataWithExtraFilters({
+      chart,
+      dashboardMetadata: dashboardInfo.metadata,
+      filters,
+      sliceId: id,
+    }),
+    editMode: dashboardState.editMode,
+    isExpanded: !!dashboardState.expandedSlices[id],
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators(
+    {
+      saveSliceName,
+      toggleExpandSlice,
+      addFilter,
+      refreshChart,
+      removeFilter,
+    },
+    dispatch,
+  );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
new file mode 100644
index 0000000..9af0e81
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -0,0 +1,49 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import {
+  addSliceToDashboard,
+  removeSliceFromDashboard,
+  onChange,
+} from '../actions/dashboardState';
+import { runQuery } from '../../chart/chartAction';
+import Dashboard from '../components/Dashboard';
+
+function mapStateToProps({
+  datasources,
+  sliceEntities,
+  charts,
+  dashboardInfo,
+  dashboardState,
+  dashboardLayout,
+  impressionId,
+}) {
+  return {
+    initMessages: dashboardInfo.common.flash_messages,
+    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    userId: dashboardInfo.userId,
+    dashboardInfo,
+    dashboardState,
+    charts,
+    datasources,
+    slices: sliceEntities.slices,
+    layout: dashboardLayout.present,
+    impressionId,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    actions: bindActionCreators(
+      {
+        addSliceToDashboard,
+        onChange,
+        removeSliceFromDashboard,
+        runQuery,
+      },
+      dispatch,
+    ),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
similarity index 66%
rename from superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
rename to superset/assets/src/dashboard/containers/DashboardBuilder.jsx
index 62fc94a..6bece3d 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
@@ -7,20 +7,22 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
   return {
     dashboardLayout: undoableLayout.present,
-    cells: ownProps.cells,
-    editMode: dashboard.editMode,
-    showBuilderPane: dashboard.showBuilderPane,
+    editMode: dashboardState.editMode,
+    showBuilderPane: dashboardState.showBuilderPane,
   };
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    deleteTopLevelTabs,
-    handleComponentDrop,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      deleteTopLevelTabs,
+      handleComponentDrop,
+    },
+    dispatch,
+  );
 }
 
 export default connect(mapStateToProps, mapDispatchToProps)(DashboardBuilder);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
similarity index 73%
rename from superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
rename to superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 01f7805..650313e 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 import ComponentLookup from '../components/gridComponents';
 import getTotalChildWidth from '../util/getChildWidth';
 import { componentShape } from '../util/propShapes';
-import { CHART_TYPE, COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
 import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
 
 import {
@@ -25,24 +25,36 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
+function mapStateToProps(
+  {
+    dashboardLayout: undoableLayout,
+    dashboardState,
+    sliceEntities,
+    charts,
+    datasources,
+  },
+  ownProps,
+) {
   const dashboardLayout = undoableLayout.present;
-  const { id, parentId, cells } = ownProps;
+  const { id, parentId } = ownProps;
   const component = dashboardLayout[id];
   const props = {
     component,
     parentComponent: dashboardLayout[parentId],
-    editMode: dashboard.editMode,
+    editMode: dashboardState.editMode,
   };
 
   // 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({ id, components: dashboardLayout });
+    props.occupiedColumnCount = getTotalChildWidth({
+      id,
+      components: dashboardLayout,
+    });
   } else if (props.component.type === COLUMN_TYPE) {
     props.minColumnWidth = GRID_MIN_COLUMN_COUNT;
 
-    component.children.forEach((childId) => {
+    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(
@@ -51,23 +63,21 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dash
         );
       }
     });
-  } else if (props.component.type === CHART_TYPE) {
-    const chartId = props.component.meta && props.component.meta.chartId;
-    if (chartId) {
-      props.chart = cells[chartId];
-    }
   }
 
   return props;
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    createComponent,
-    deleteComponent,
-    updateComponents,
-    handleComponentDrop,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      createComponent,
+      deleteComponent,
+      updateComponents,
+      handleComponentDrop,
+    },
+    dispatch,
+  );
 }
 
 class DashboardComponent extends React.PureComponent {
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/src/dashboard/containers/DashboardGrid.jsx
similarity index 62%
rename from superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
rename to superset/assets/src/dashboard/containers/DashboardGrid.jsx
index 2adc390..718b543 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardGrid.jsx
@@ -7,18 +7,20 @@ import {
   resizeComponent,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardState: dashboard }, ownProps) {
+function mapStateToProps({ dashboardState }) {
   return {
-    editMode: dashboard.editMode,
-    cells: ownProps.cells,
+    editMode: dashboardState.editMode,
   };
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    handleComponentDrop,
-    resizeComponent,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      handleComponentDrop,
+      resizeComponent,
+    },
+    dispatch,
+  );
 }
 
 export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
similarity index 59%
rename from superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
rename to superset/assets/src/dashboard/containers/DashboardHeader.jsx
index cc8e944..2b3431a 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -2,7 +2,7 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-import DashboardHeader from '../../components/Header';
+import DashboardHeader from '../components/Header';
 import {
   setEditMode,
   toggleBuilderPane,
@@ -13,13 +13,15 @@ import {
   updateDashboardTitle,
   onChange,
   onSave,
-} from '../../actions/dashboardState';
-import {
-  handleComponentDrop,
-} from '../actions/dashboardLayout';
+} from '../actions/dashboardState';
+import { handleComponentDrop } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard,
-                           dashboardInfo, charts }) {
+function mapStateToProps({
+  dashboardLayout: undoableLayout,
+  dashboardState: dashboard,
+  dashboardInfo,
+  charts,
+}) {
   return {
     dashboardInfo,
     canUndo: undoableLayout.past.length > 0,
@@ -38,21 +40,23 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dash
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    handleComponentDrop,
-    onUndo: UndoActionCreators.undo,
-    onRedo: UndoActionCreators.redo,
-    setEditMode,
-    toggleBuilderPane,
-    fetchFaveStar,
-    saveFaveStar,
-    fetchCharts,
-    startPeriodicRender,
-    updateDashboardTitle,
-    onChange,
-    onSave,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      handleComponentDrop,
+      onUndo: UndoActionCreators.undo,
+      onRedo: UndoActionCreators.redo,
+      setEditMode,
+      toggleBuilderPane,
+      fetchFaveStar,
+      saveFaveStar,
+      fetchCharts,
+      startPeriodicRender,
+      updateDashboardTitle,
+      onChange,
+      onSave,
+    },
+    dispatch,
+  );
 }
 
-
 export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader);
diff --git a/superset/assets/src/dashboard/v2/containers/ToastPresenter.jsx b/superset/assets/src/dashboard/containers/ToastPresenter.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/containers/ToastPresenter.jsx
rename to superset/assets/src/dashboard/containers/ToastPresenter.jsx
diff --git a/superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
similarity index 92%
rename from superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js
rename to superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
index 7816cc2..cee948a 100644
--- a/superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js
+++ b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
@@ -14,9 +14,7 @@ export default {
   [DASHBOARD_ROOT_ID]: {
     type: DASHBOARD_ROOT_TYPE,
     id: DASHBOARD_ROOT_ID,
-    children: [
-      DASHBOARD_GRID_ID,
-    ],
+    children: [DASHBOARD_GRID_ID],
   },
 
   [DASHBOARD_GRID_ID]: {
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index 9c00f9e..846b82d 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -7,7 +7,7 @@ import thunk from 'redux-thunk';
 import { initEnhancer } from '../reduxUtils';
 import { appSetup } from '../common';
 import { initJQueryAjax } from '../modules/utils';
-import DashboardContainer from './components/DashboardContainer';
+import DashboardContainer from './containers/Dashboard';
 import getInitialState from './reducers/getInitialState';
 import rootReducer from './reducers/index';
 
@@ -19,7 +19,10 @@ const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
 const initState = getInitialState(bootstrapData);
 
 const store = createStore(
-  rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
+  rootReducer,
+  initState,
+  compose(applyMiddleware(thunk), initEnhancer(false)),
+);
 
 ReactDOM.render(
   <Provider store={store}>
diff --git a/superset/assets/src/dashboard/v2/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
similarity index 76%
rename from superset/assets/src/dashboard/v2/reducers/dashboardLayout.js
rename to superset/assets/src/dashboard/reducers/dashboardLayout.js
index 994ac47..573a143 100644
--- a/superset/assets/src/dashboard/v2/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -1,4 +1,9 @@
-import { DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import {
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_GRID_ID,
+  GRID_MIN_COLUMN_COUNT,
+  NEW_COMPONENTS_SOURCE_ID,
+} from '../util/constants';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
@@ -23,7 +28,9 @@ import {
 
 const actionHandlers = {
   [UPDATE_COMPONENTS](state, action) {
-    const { payload: { nextComponents } } = action;
+    const {
+      payload: { nextComponents },
+    } = action;
     return {
       ...state,
       ...nextComponents,
@@ -31,7 +38,9 @@ const actionHandlers = {
   },
 
   [DELETE_COMPONENT](state, action) {
-    const { payload: { id, parentId } } = action;
+    const {
+      payload: { id, parentId },
+    } = action;
 
     if (!parentId || !id || !state[id] || !state[parentId]) return state;
 
@@ -44,10 +53,13 @@ const actionHandlers = {
       delete nextComponents[componentId];
 
       const { children = [] } = component;
-      children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); });
+      children.forEach(childId => {
+        recursivelyDeleteChildren(childId, componentId);
+      });
 
       const parent = nextComponents[componentParentId];
-      if (parent) { // may have been deleted in another recursion
+      if (parent) {
+        // may have been deleted in another recursion
         const componentIndex = (parent.children || []).indexOf(componentId);
         if (componentIndex > -1) {
           const nextChildren = [...parent.children];
@@ -66,21 +78,28 @@ const actionHandlers = {
   },
 
   [CREATE_COMPONENT](state, action) {
-    const { payload: { dropResult } } = action;
+    const {
+      payload: { dropResult },
+    } = action;
     const { destination, dragging } = dropResult;
     const newEntities = newEntitiesFromDrop({ dropResult, components: state });
 
-    // inherit the width of a column parent
-    if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+    // if column is a parent, set any resizable children to have a minimum width so that
+    // the chances that they are validly movable to future containers is maximized
+    if (
+      destination.type === COLUMN_TYPE &&
+      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
+    ) {
       const newEntitiesArray = Object.values(newEntities);
-      const component = newEntitiesArray.find(entity => entity.type === dragging.type);
-      const parentColumn = newEntities[destination.id];
+      const component = newEntitiesArray.find(
+        entity => entity.type === dragging.type,
+      );
 
       newEntities[component.id] = {
         ...component,
         meta: {
           ...component.meta,
-          width: parentColumn.meta.width,
+          width: GRID_MIN_COLUMN_COUNT,
         },
       };
     }
@@ -92,7 +111,9 @@ const actionHandlers = {
   },
 
   [MOVE_COMPONENT](state, action) {
-    const { payload: { dropResult } } = action;
+    const {
+      payload: { dropResult },
+    } = action;
     const { source, destination, dragging } = dropResult;
 
     if (!source || !destination || !dragging) return state;
@@ -119,7 +140,10 @@ const actionHandlers = {
     }
 
     // inherit the width of a column parent
-    if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+    if (
+      destination.type === COLUMN_TYPE &&
+      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
+    ) {
       const component = nextEntities[dragging.id];
       const parentColumn = nextEntities[destination.id];
       nextEntities[dragging.id] = {
@@ -138,7 +162,9 @@ const actionHandlers = {
   },
 
   [CREATE_TOP_LEVEL_TABS](state, action) {
-    const { payload: { dropResult } } = action;
+    const {
+      payload: { dropResult },
+    } = action;
     const { source, dragging } = dropResult;
 
     // move children of current root to be children of the dragging tab
@@ -153,7 +179,9 @@ const actionHandlers = {
       const draggingTab = state[draggingTabId];
 
       // move all children except the one that is dragging
-      const childrenToMove = [...topLevelComponent.children].filter(id => id !== dragging.id);
+      const childrenToMove = [...topLevelComponent.children].filter(
+        id => id !== dragging.id,
+      );
 
       return {
         ...state,
@@ -167,10 +195,7 @@ const actionHandlers = {
         },
         [draggingTabId]: {
           ...draggingTab,
-          children: [
-            ...draggingTab.children,
-            ...childrenToMove,
-          ],
+          children: [...draggingTab.children, ...childrenToMove],
         },
       };
     }
@@ -178,12 +203,19 @@ const actionHandlers = {
     // create new component
     const newEntities = newEntitiesFromDrop({ dropResult, components: state });
     const newEntitiesArray = Object.values(newEntities);
-    const tabComponent = newEntitiesArray.find(component => component.type === TAB_TYPE);
-    const tabsComponent = newEntitiesArray.find(component => component.type === TABS_TYPE);
+    const tabComponent = newEntitiesArray.find(
+      component => component.type === TAB_TYPE,
+    );
+    const tabsComponent = newEntitiesArray.find(
+      component => component.type === TABS_TYPE,
+    );
 
     tabComponent.children = [...topLevelComponent.children];
     newEntities[topLevelId] = { ...topLevelComponent, children: [] };
-    newEntities[DASHBOARD_ROOT_ID] = { ...rootComponent, children: [tabsComponent.id] };
+    newEntities[DASHBOARD_ROOT_ID] = {
+      ...rootComponent,
+      children: [tabsComponent.id],
+    };
 
     return {
       ...state,
@@ -201,7 +233,7 @@ const actionHandlers = {
     let childrenToMove = [];
     const nextEntities = { ...state };
 
-    topLevelTabs.children.forEach((tabId) => {
+    topLevelTabs.children.forEach(tabId => {
       const tabComponent = state[tabId];
       childrenToMove = [...childrenToMove, ...tabComponent.children];
       delete nextEntities[tabId];
@@ -215,7 +247,7 @@ const actionHandlers = {
     };
 
     nextEntities[DASHBOARD_GRID_ID] = {
-      ...(state[DASHBOARD_GRID_ID]),
+      ...state[DASHBOARD_GRID_ID],
       children: childrenToMove,
     };
 
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 84ee58e..7b5a17a 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -9,13 +9,14 @@ import {
   REMOVE_SLICE,
   REMOVE_FILTER,
   SET_EDIT_MODE,
+  SET_UNSAVED_CHANGES,
   TOGGLE_BUILDER_PANE,
   TOGGLE_EXPAND_SLICE,
   TOGGLE_FAVE_STAR,
   UPDATE_DASHBOARD_TITLE,
 } from '../actions/dashboardState';
 
-export default function (state = {}, action) {
+export default function dashboardStateReducer(state = {}, action) {
   const actionHandlers = {
     [UPDATE_DASHBOARD_TITLE]() {
       return { ...state, title: action.title };
@@ -84,15 +85,23 @@ export default function (state = {}, action) {
       let filters = state.filters;
       const { chart, col, vals, merge, refresh } = action;
       const sliceId = chart.id;
-      const filterKeys = ['__from', '__to', '__time_col',
-        '__time_grain', '__time_origin', '__granularity'];
-      if (filterKeys.indexOf(col) >= 0 ||
-        action.chart.formData.groupby.indexOf(col) !== -1) {
+      const filterKeys = [
+        '__from',
+        '__to',
+        '__time_col',
+        '__time_grain',
+        '__time_origin',
+        '__granularity',
+      ];
+      if (
+        filterKeys.indexOf(col) >= 0 ||
+        action.chart.formData.groupby.indexOf(col) !== -1
+      ) {
         let newFilter = {};
         if (!(sliceId in filters)) {
           // Straight up set the filters if none existed for the slice
           newFilter = { [col]: vals };
-        } else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
+        } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
           newFilter = { ...filters[sliceId], [col]: vals };
           // d3.merge pass in array of arrays while some value form filter components
           // from and to filter box require string to be process and return
@@ -119,6 +128,10 @@ export default function (state = {}, action) {
       }
       return { ...state, filters, refresh };
     },
+    [SET_UNSAVED_CHANGES]() {
+      const { hasUnsavedChanges } = action.payload;
+      return { ...state, hasUnsavedChanges };
+    },
   };
 
   if (action.type in actionHandlers) {
diff --git a/superset/assets/src/dashboard/reducers/datasources.js b/superset/assets/src/dashboard/reducers/datasources.js
index 4df7507..87f6d09 100644
--- a/superset/assets/src/dashboard/reducers/datasources.js
+++ b/superset/assets/src/dashboard/reducers/datasources.js
@@ -1,8 +1,8 @@
-import * as actions from '../actions/datasources';
+import { SET_DATASOURCE } from '../actions/datasources';
 
 export default function datasourceReducer(datasources = {}, action) {
   const actionHandlers = {
-    [actions.SET_DATASOURCE]() {
+    [SET_DATASOURCE]() {
       return action.datasource;
     },
   };
@@ -10,7 +10,10 @@ export default function datasourceReducer(datasources = {}, action) {
   if (action.type in actionHandlers) {
     return {
       ...datasources,
-      [action.key]: actionHandlers[action.type](datasources[action.key], action),
+      [action.key]: actionHandlers[action.type](
+        datasources[action.key],
+        action,
+      ),
     };
   }
   return datasources;
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index 1129210..d0b4d7b 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -7,9 +7,9 @@ import { getParam } from '../../modules/utils';
 import { applyDefaultFormData } from '../../explore/stores/store';
 import { getColorFromScheme } from '../../modules/colors';
 import layoutConverter from '../util/dashboardLayoutConverter';
-import { DASHBOARD_ROOT_ID } from '../v2/util/constants';
+import { DASHBOARD_ROOT_ID } from '../util/constants';
 
-export default function (bootstrapData) {
+export default function(bootstrapData) {
   const { user_id, datasources, common } = bootstrapData;
   delete common.locale;
   delete common.language_pack;
@@ -18,7 +18,9 @@ export default function (bootstrapData) {
   let filters = {};
   try {
     // allow request parameter overwrite dashboard metadata
-    filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+    filters = JSON.parse(
+      getParam('preselect_filters') || dashboard.metadata.default_filters,
+    );
   } catch (e) {
     //
   }
@@ -27,9 +29,9 @@ export default function (bootstrapData) {
   // the dashboard's JSON metadata
   if (dashboard.metadata && dashboard.metadata.label_colors) {
     const colorMap = dashboard.metadata.label_colors;
-    for (const label in colorMap) {
+    Object.keys(colorMap).forEach(label => {
       getColorFromScheme(label, null, colorMap[label]);
-    }
+    });
   }
 
   // dashboard layout
@@ -52,9 +54,10 @@ export default function (bootstrapData) {
   const chartQueries = {};
   const slices = {};
   const sliceIds = new Set();
-  dashboard.slices.forEach((slice) => {
+  dashboard.slices.forEach(slice => {
     const key = slice.slice_id;
-    chartQueries[key] = { ...chart,
+    chartQueries[key] = {
+      ...chart,
       id: key,
       form_data: slice.form_data,
       formData: applyDefaultFormData(slice.form_data),
@@ -79,13 +82,16 @@ export default function (bootstrapData) {
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
     charts: chartQueries,
-    dashboardInfo: {  /* readOnly props */
+    dashboardInfo: {
+      // read-only data
       id: dashboard.id,
       slug: dashboard.slug,
       metadata: {
-        filter_immune_slice_fields: dashboard.metadata.filter_immune_slice_fields,
+        filter_immune_slice_fields:
+          dashboard.metadata.filter_immune_slice_fields,
         filter_immune_slices: dashboard.metadata.filter_immune_slices,
-        timed_refresh_immune_slices: dashboard.metadata.timed_refresh_immune_slices,
+        timed_refresh_immune_slices:
+          dashboard.metadata.timed_refresh_immune_slices,
       },
       userId: user_id,
       dash_edit_perm: dashboard.dash_edit_perm,
diff --git a/superset/assets/src/dashboard/reducers/index.js b/superset/assets/src/dashboard/reducers/index.js
index a2397e0..787cd5f 100644
--- a/superset/assets/src/dashboard/reducers/index.js
+++ b/superset/assets/src/dashboard/reducers/index.js
@@ -4,19 +4,19 @@ import charts from '../../chart/chartReducer';
 import dashboardState from './dashboardState';
 import datasources from './datasources';
 import sliceEntities from './sliceEntities';
-import dashboardLayout from '../v2/reducers/index';
-import messageToasts from '../v2/reducers/messageToasts';
+import dashboardLayout from '../reducers/undoableDashboardLayout';
+import messageToasts from '../reducers/messageToasts';
 
-const dashboardInfo = (state = {}) => (state);
-const impressionId = (state = '') => (state);
+const dashboardInfo = (state = {}) => state;
+const impressionId = (state = '') => state;
 
 export default combineReducers({
   charts,
   datasources,
-  sliceEntities,
   dashboardInfo,
   dashboardState,
   dashboardLayout,
-  messageToasts,
   impressionId,
+  messageToasts,
+  sliceEntities,
 });
diff --git a/superset/assets/src/dashboard/v2/reducers/messageToasts.js b/superset/assets/src/dashboard/reducers/messageToasts.js
similarity index 87%
rename from superset/assets/src/dashboard/v2/reducers/messageToasts.js
rename to superset/assets/src/dashboard/reducers/messageToasts.js
index 1f5728a..7383ab0 100644
--- a/superset/assets/src/dashboard/v2/reducers/messageToasts.js
+++ b/superset/assets/src/dashboard/reducers/messageToasts.js
@@ -8,7 +8,9 @@ export default function messageToastsReducer(toasts = [], action) {
     }
 
     case REMOVE_TOAST: {
-      const { payload: { id } } = action;
+      const {
+        payload: { id },
+      } = action;
       return [...toasts].filter(toast => toast.id !== id);
     }
 
diff --git a/superset/assets/src/dashboard/reducers/sliceEntities.js b/superset/assets/src/dashboard/reducers/sliceEntities.js
index 61a58f6..c1453f5 100644
--- a/superset/assets/src/dashboard/reducers/sliceEntities.js
+++ b/superset/assets/src/dashboard/reducers/sliceEntities.js
@@ -13,7 +13,10 @@ export const initSliceEntities = {
   lastUpdated: 0,
 };
 
-export default function (state = initSliceEntities, action) {
+export default function sliceEntitiesReducer(
+  state = initSliceEntities,
+  action,
+) {
   const actionHandlers = {
     [UPDATE_SLICE_NAME]() {
       const updatedSlice = {
@@ -44,8 +47,9 @@ export default function (state = initSliceEntities, action) {
       const respJSON = action.error.responseJSON;
       const errorMessage =
         t('Sorry, there was an error adding slices to this dashboard: ') +
-        (respJSON && respJSON.message) ? respJSON.message :
-          error.responseText;
+        (respJSON && respJSON.message)
+          ? respJSON.message
+          : action.error.responseText;
       return {
         ...state,
         isLoading: false,
diff --git a/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
new file mode 100644
index 0000000..b78c273
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
@@ -0,0 +1,27 @@
+import undoable, { includeAction } from 'redux-undo';
+import {
+  UPDATE_COMPONENTS,
+  DELETE_COMPONENT,
+  CREATE_COMPONENT,
+  CREATE_TOP_LEVEL_TABS,
+  DELETE_TOP_LEVEL_TABS,
+  RESIZE_COMPONENT,
+  MOVE_COMPONENT,
+  HANDLE_COMPONENT_DROP,
+} from '../actions/dashboardLayout';
+
+import dashboardLayout from './dashboardLayout';
+
+export default undoable(dashboardLayout, {
+  limit: 15,
+  filter: includeAction([
+    UPDATE_COMPONENTS,
+    DELETE_COMPONENT,
+    CREATE_COMPONENT,
+    CREATE_TOP_LEVEL_TABS,
+    DELETE_TOP_LEVEL_TABS,
+    RESIZE_COMPONENT,
+    MOVE_COMPONENT,
+    HANDLE_COMPONENT_DROP,
+  ]),
+});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
similarity index 76%
rename from superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
rename to superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index d9f1069..bdf342b 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -1,8 +1,21 @@
 .dashboard-builder-sidepane {
+  background: white;
+  flex: 0 0 376px;
+  border: 1px solid @gray-light;
+  z-index: 10;
+  position: relative;
+
+  .dashboard-builder-sidepane-header {
+    font-size: 15px;
+    font-weight: 700;
+    border-bottom: 1px solid @gray-light;
+    padding: 14px;
+  }
+
   .trigger {
     height: 25px;
     width: 25px;
-    color: #879399;
+    color: @gray;
     position: relative;
 
     &.close {
@@ -25,8 +38,8 @@
     position: absolute;
     width: 2px;
     top: 51px;
-    right: 1px;
-    background: #fff;
+    right: 0;
+    background: white;
     transition-property: width;
     transition-duration: 1s;
     transition-timing-function: ease;
@@ -39,18 +52,17 @@
 
   .chart-card-container {
     padding: 16px;
-    cursor: move;
 
     .chart-card {
-      border: 1px solid #ccc;
+      border: 1px solid @gray-light;
       height: 120px;
       padding: 16px;
-      pointer-events: unset;
+      cursor: move;
     }
 
     .chart-card.is-selected {
       opacity: 0.45;
-      pointer-events: none;
+      cursor: not-allowed;
     }
 
     .card-title {
@@ -88,7 +100,7 @@
       input {
         margin-left: 16px;
         width: 169px;
-        border: 1px solid #b3b3b3;
+        border: 1px solid @gray;
 
         &:focus {
           outline: none;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder.less b/superset/assets/src/dashboard/stylesheets/builder.less
similarity index 72%
rename from superset/assets/src/dashboard/v2/stylesheets/builder.less
rename to superset/assets/src/dashboard/stylesheets/builder.less
index 2ff99a4..7c14056 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/stylesheets/builder.less
@@ -1,7 +1,7 @@
-.dashboard-v2 {
-  //margin-top: -20px;
+.dashboard {
   position: relative;
   color: @almost-black;
+  margin-top: -20px;
 }
 
 .dashboard-header {
@@ -22,12 +22,12 @@
 }
 
 /* only top-level tabs have popover, give it more padding to match header + tabs */
-.dashboard-v2 > .with-popover-menu > .popover-menu {
+.dashboard > .with-popover-menu > .popover-menu {
   left: 24px;
 }
 
 /* drop shadow for top-level tabs only */
-.dashboard-v2 .dashboard-component-tabs {
+.dashboard .dashboard-component-tabs {
   box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
   padding-left: 8px; /* note this is added to tab-level padding, to match header */
 }
@@ -43,21 +43,6 @@
   position: relative;
 }
 
-.dashboard-builder-sidepane {
-  background: white;
-  flex: 0 0 376px;
-  border: 1px solid @gray-light;
-  z-index: 1;
-  position: relative;
-}
-
-.dashboard-builder-sidepane-header {
-  font-size: 15px;
-  font-weight: 700;
-  border-bottom: 1px solid @gray-light;
-  padding: 14px;
-}
-
 /* @TODO remove upon new theme */
 .btn.btn-primary {
   background: @almost-black !important;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/buttons.less b/superset/assets/src/dashboard/stylesheets/buttons.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/buttons.less
rename to superset/assets/src/dashboard/stylesheets/buttons.less
diff --git a/superset/assets/src/dashboard/stylesheets/components/chart.less b/superset/assets/src/dashboard/stylesheets/components/chart.less
new file mode 100644
index 0000000..dc366a1
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/components/chart.less
@@ -0,0 +1,69 @@
+.dashboard-component-chart-holder {
+  width: 100%;
+  height: 100%;
+  color: @gray-dark;
+  background-color: white;
+  position: relative;
+  padding: 16px;
+}
+
+.dashboard-chart {
+  overflow: hidden;
+}
+
+.dashboard-chart.dashboard-chart--overflowable {
+  overflow: visible;
+}
+
+.dashboard--editing .dashboard-component-chart-holder:after {
+  content: '';
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0px;
+  left: 0px;
+  z-index: 1;
+  pointer-events: none;
+  border: 1px solid transparent;
+}
+
+.dashboard--editing
+  .resizable-container:hover
+  > .dashboard-component-chart-holder:after,
+.dashboard--editing .dashboard-component-chart-holder:hover:after {
+  border: 1px solid @gray-light;
+}
+
+.dashboard--editing
+  .resizable-container.resizable-container--resizing:hover
+  > .dashboard-component-chart-holder:after {
+  border: 1px solid @indicator-color;
+}
+
+.dashboard--editing
+  .dashboard-component-chart-holder
+  .dashboard-chart
+  .chart-container {
+  cursor: move;
+  opacity: 0.2;
+}
+
+.dashboard--editing
+  .dashboard-component-chart-holder:hover
+  .dashboard-chart
+  .chart-container {
+  opacity: 0.7;
+}
+
+.dashboard--editing
+  .dashboard-component-chart-holder
+  .dashboard-chart
+  .slice_container {
+  /* disable chart interactions in edit mode */
+  pointer-events: none;
+}
+
+.dashboard-chart .chart-header {
+  font-size: 16px;
+  font-weight: bold;
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/column.less b/superset/assets/src/dashboard/stylesheets/components/column.less
similarity index 60%
rename from superset/assets/src/dashboard/v2/stylesheets/components/column.less
rename to superset/assets/src/dashboard/stylesheets/components/column.less
index 9565112..5fcb442 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/src/dashboard/stylesheets/components/column.less
@@ -1,5 +1,6 @@
 .grid-column {
   width: 100%;
+  position: relative;
 }
 
 /* gutters between elements in a column */
@@ -7,20 +8,24 @@
   margin-bottom: 16px;
 }
 
-.dashboard-v2--editing .grid-column:after {
-  border: 1px dashed transparent;
-  content: "";
+.dashboard--editing .grid-column:after {
+  border: 1px solid transparent;
+  content: '';
   position: absolute;
   width: 100%;
   height: 100%;
-  top: 1px;
+  top: 0;
   left: 0;
   z-index: 1;
   pointer-events: none;
 }
 
-.dashboard-v2--editing .grid-column:hover:after {
-  border: 1px solid @gray-light;
+.dashboard--editing
+  .resizable-container.resizable-container--resizing:hover
+  > .grid-column:after,
+.dashboard--editing .grid-column:hover:after {
+  border: 1px dashed @gray-light;
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
 .grid-column > .hover-menu--top {
@@ -32,7 +37,7 @@
 }
 
 .grid-column--empty:before {
-  content: "Empty column";
+  content: 'Empty column';
   position: absolute;
   top: 0;
   left: 0;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/divider.less b/superset/assets/src/dashboard/stylesheets/components/divider.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/divider.less
rename to superset/assets/src/dashboard/stylesheets/components/divider.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/header.less b/superset/assets/src/dashboard/stylesheets/components/header.less
similarity index 92%
rename from superset/assets/src/dashboard/v2/stylesheets/components/header.less
rename to superset/assets/src/dashboard/stylesheets/components/header.less
index 37c7598..8b93164 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/header.less
+++ b/superset/assets/src/dashboard/stylesheets/components/header.less
@@ -11,6 +11,10 @@
   width: auto;
 }
 
+.dashboard-header .btn-group button {
+  margin-right: 8px;
+}
+
 .dragdroppable-row .dashboard-component-header {
   cursor: move;
 }
@@ -40,7 +44,6 @@
  * grids add margin between items, so don't double pad within columns
  * we'll not worry about double padding on top as it can serve as a visual separator
  */
-// .grid-content > :not(:only-child):not(:last-child) .dashboard-component-header,
 .grid-column > :not(:only-child):not(:last-child) .dashboard-component-header {
   margin-bottom: -16px;
 }
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/index.less b/superset/assets/src/dashboard/stylesheets/components/index.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/index.less
rename to superset/assets/src/dashboard/stylesheets/components/index.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/src/dashboard/stylesheets/components/new-component.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/new-component.less
rename to superset/assets/src/dashboard/stylesheets/components/new-component.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/row.less b/superset/assets/src/dashboard/stylesheets/components/row.less
similarity index 66%
rename from superset/assets/src/dashboard/v2/stylesheets/components/row.less
rename to superset/assets/src/dashboard/stylesheets/components/row.less
index 956966d..7df5675 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/src/dashboard/stylesheets/components/row.less
@@ -1,7 +1,8 @@
 .grid-row {
+  position: relative;
   display: flex;
   flex-direction: row;
-  flex-wrap: wrap;
+  flex-wrap: nowrap;
   align-items: flex-start;
   width: 100%;
   height: fit-content;
@@ -13,20 +14,24 @@
 }
 
 /* hover indicator */
-.dashboard-v2--editing .grid-row:after {
+.dashboard--editing .grid-row:after {
   border: 1px dashed transparent;
-  content: "";
+  content: '';
   position: absolute;
   width: 100%;
   height: 100%;
-  top: 1px;
+  top: 0;
   left: 0;
   z-index: 1;
   pointer-events: none;
 }
 
-.dashboard-v2--editing .grid-row:hover:after {
-  border: 1px solid @gray-light;
+.dashboard--editing
+  .resizable-container.resizable-container--resizing:hover
+  > .grid-row:after,
+.dashboard--editing .grid-row:hover:after {
+  border: 1px dashed @gray-light;
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
 .grid-row.grid-row--empty {
@@ -38,7 +43,7 @@
   position: absolute;
   top: 0;
   left: 0;
-  content: "Empty row";
+  content: 'Empty row';
   display: flex;
   align-items: center;
   justify-content: center;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/tabs.less
rename to superset/assets/src/dashboard/stylesheets/components/tabs.less
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
new file mode 100644
index 0000000..03c804b
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -0,0 +1,104 @@
+// @import './less/cosmo/variables.less';
+
+.dashboard .chart-header {
+  position: relative;
+
+  .dropdown.btn-group {
+    position: absolute;
+    right: 0;
+  }
+
+  .dropdown-menu.dropdown-menu-right {
+    right: 7px;
+    top: -3px;
+  }
+}
+
+.slice-header-controls-trigger {
+  border: 0;
+  padding: 0 0 0 20px;
+  background: none;
+  outline: none;
+  box-shadow: none;
+  color: #263238;
+
+  &.is-cached {
+    color: red;
+  }
+
+  &:hover,
+  &:focus {
+    background: none;
+    cursor: pointer;
+  }
+
+  .controls-container.dropdown-menu {
+    top: 0;
+    left: unset;
+    right: 10px;
+
+    &.is-open {
+      display: block;
+    }
+
+    & li {
+      white-space: nowrap;
+    }
+  }
+}
+
+.modal img.loading {
+  width: 50px;
+  margin: 0;
+  position: relative;
+}
+
+.react-bs-container-body {
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.hidden,
+#pageDropDown {
+  display: none;
+}
+
+.separator .chart-container {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+
+.dashboard .title {
+  margin: 0 20px;
+}
+
+.dashboard .title .favstar {
+  font-size: 20px;
+  line-height: 1em;
+  position: relative;
+  top: -5px;
+}
+
+.ace_gutter {
+  z-index: 0;
+}
+.ace_content {
+  z-index: 0;
+}
+.ace_scrollbar {
+  z-index: 0;
+}
+.slice_container .alert {
+  margin: 10px;
+}
+
+i.danger {
+  color: red;
+}
+
+i.warning {
+  color: orange;
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/dnd.less b/superset/assets/src/dashboard/stylesheets/dnd.less
similarity index 98%
rename from superset/assets/src/dashboard/v2/stylesheets/dnd.less
rename to superset/assets/src/dashboard/stylesheets/dnd.less
index 45a9784..835b62b 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/dnd.less
+++ b/superset/assets/src/dashboard/stylesheets/dnd.less
@@ -12,7 +12,7 @@
 
 /* drop indicators */
 .drop-indicator {
-  margin: auto;
+  display: block;
   background-color: @indicator-color;
   position: absolute;
   z-index: 10;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/grid.less b/superset/assets/src/dashboard/stylesheets/grid.less
similarity index 68%
rename from superset/assets/src/dashboard/v2/stylesheets/grid.less
rename to superset/assets/src/dashboard/stylesheets/grid.less
index 45b8a42..a12ac97 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/src/dashboard/stylesheets/grid.less
@@ -1,12 +1,22 @@
 .grid-container {
+  min-height: 100%;
   position: relative;
   margin: 24px;
+  /* without this, the grid will not get smaller upon toggling the builder panel on */
+  min-width: 0;
+  width: 100%;
+}
+
+/* this is the ParentSize wrapper  */
+.grid-container > div:first-child {
+  height: inherit !important;
 }
 
 .grid-content {
-  height: 100%;
+  min-height: 100%;
   display: flex;
   flex-direction: column;
+  margin-bottom: 100px;
 }
 
 /* gutters between rows */
@@ -23,7 +33,7 @@
 .grid-column-guide {
   position: absolute;
   top: 0;
-  height: 100%;
+  min-height: 100%;
   background-color: rgba(68, 192, 255, 0.05);
   pointer-events: none;
   box-shadow: inset 0 0 0 1px rgba(68, 192, 255, 0.5);
diff --git a/superset/assets/src/dashboard/v2/stylesheets/hover-menu.less b/superset/assets/src/dashboard/stylesheets/hover-menu.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/hover-menu.less
rename to superset/assets/src/dashboard/stylesheets/hover-menu.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/index.less b/superset/assets/src/dashboard/stylesheets/index.less
similarity index 81%
rename from superset/assets/src/dashboard/v2/stylesheets/index.less
rename to superset/assets/src/dashboard/stylesheets/index.less
index 49ff5da..b69c7b0 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/src/dashboard/stylesheets/index.less
@@ -1,7 +1,9 @@
 @import './variables.less';
 
 @import './builder.less';
+@import './builder-sidepane.less';
 @import './buttons.less';
+@import './dashboard.less';
 @import './dnd.less';
 @import './grid.less';
 @import './hover-menu.less';
diff --git a/superset/assets/src/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/popover-menu.less
rename to superset/assets/src/dashboard/stylesheets/popover-menu.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/resizable.less b/superset/assets/src/dashboard/stylesheets/resizable.less
similarity index 69%
rename from superset/assets/src/dashboard/v2/stylesheets/resizable.less
rename to superset/assets/src/dashboard/stylesheets/resizable.less
index 7bdd5f8..973daab 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/resizable.less
+++ b/superset/assets/src/dashboard/stylesheets/resizable.less
@@ -16,6 +16,7 @@
 
 .resize-handle {
   opacity: 0;
+  z-index: 10;
 }
 
   .resizable-container:hover .resize-handle,
@@ -35,26 +36,43 @@
   height: 8px;
 }
 
+
 .resize-handle--right {
   width: 2px;
   height: 20px;
-  right: 2px;
-  top: ~"calc(50% - 9px)"; /* escape for .less */
+  right: 4px;
+  top: 50%;
+  transform: translate(0, -50%);
   position: absolute;
   border-left: 1px solid @gray;
   border-right: 1px solid @gray;
 }
 
+.dragdroppable-column .resizable-container-handle--right {
+  /* override the default because the inner column's handle's mouse target is very small */
+  right: -10px !important;
+}
+
+.dragdroppable-column .dragdroppable-column .resizable-container-handle--right {
+  /* override the default because the inner column's handle's mouse target is very small */
+  right: 0px !important;
+}
+
 .resize-handle--bottom {
   height: 2px;
   width: 20px;
-  bottom: 2px;
-  left: ~"calc(50% - 10px)"; /* escape for .less */
+  bottom: 4px;
+  left: 50%;
+  transform: translate(-50%);
   position: absolute;
   border-top: 1px solid @gray;
   border-bottom: 1px solid @gray;
 }
 
+.resizable-container-handle--bottom {
+  bottom: 0 !important;
+}
+
 .resizable-container--resizing > span .resize-handle {
   border-color: @indicator-color;
 }
diff --git a/superset/assets/src/dashboard/v2/stylesheets/toast.less b/superset/assets/src/dashboard/stylesheets/toast.less
similarity index 91%
rename from superset/assets/src/dashboard/v2/stylesheets/toast.less
rename to superset/assets/src/dashboard/stylesheets/toast.less
index a508637..1d1ebc5 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/toast.less
+++ b/superset/assets/src/dashboard/stylesheets/toast.less
@@ -13,8 +13,7 @@
   opacity: 0;
   position: relative;
   white-space: pre-line;
-  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
-  border-radius: 2px;
+  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.35);
   will-change: transform, opacity;
   transform: translateY(-100%);
   transition: transform .3s, opacity .3s;
@@ -38,7 +37,7 @@
   position: absolute;
   top: 0;
   left: 0;
-  width: 4px;
+  width: 6px;
   height: 100%;
 }
 
diff --git a/superset/assets/src/dashboard/v2/stylesheets/variables.less b/superset/assets/src/dashboard/stylesheets/variables.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/variables.less
rename to superset/assets/src/dashboard/stylesheets/variables.less
diff --git a/superset/assets/src/dashboard/util/backgroundStyleOptions.js b/superset/assets/src/dashboard/util/backgroundStyleOptions.js
new file mode 100644
index 0000000..926e7f1
--- /dev/null
+++ b/superset/assets/src/dashboard/util/backgroundStyleOptions.js
@@ -0,0 +1,15 @@
+import { t } from '../../locales';
+import { BACKGROUND_TRANSPARENT, BACKGROUND_WHITE } from './constants';
+
+export default [
+  {
+    value: BACKGROUND_TRANSPARENT,
+    label: t('Transparent'),
+    className: 'background--transparent',
+  },
+  {
+    value: BACKGROUND_WHITE,
+    label: t('White'),
+    className: 'background--white',
+  },
+];
diff --git a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
new file mode 100644
index 0000000..f48631f
--- /dev/null
+++ b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
@@ -0,0 +1,42 @@
+export default function getEffectiveExtraFilters({
+  dashboardMetadata,
+  filters,
+  sliceId,
+}) {
+  const immuneSlices = dashboardMetadata.filter_immune_slices || [];
+
+  const effectiveFilters = [];
+
+  if (sliceId && immuneSlices.includes(sliceId)) {
+    // The slice is immune to dashboard filters
+    return effectiveFilters;
+  }
+
+  // Build a list of fields the slice is immune to filters on
+  let immuneToFields = [];
+  if (
+    sliceId &&
+    dashboardMetadata.filter_immune_slice_fields &&
+    dashboardMetadata.filter_immune_slice_fields[sliceId]
+  ) {
+    immuneToFields = dashboardMetadata.filter_immune_slice_fields[sliceId];
+  }
+
+  Object.keys(filters).forEach(filteringSliceId => {
+    if (filteringSliceId === sliceId.toString()) {
+      // Filters applied by the slice don't apply to itself
+      return;
+    }
+    Object.keys(filters[filteringSliceId]).forEach(field => {
+      if (!immuneToFields.includes(field)) {
+        effectiveFilters.push({
+          col: field,
+          op: 'in',
+          val: filters[filteringSliceId][field],
+        });
+      }
+    });
+  });
+
+  return effectiveFilters;
+}
diff --git a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
new file mode 100644
index 0000000..031d90d
--- /dev/null
+++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
@@ -0,0 +1,42 @@
+import getEffectiveExtraFilters from './getEffectiveExtraFilters';
+
+// We cache formData objects so that our connected container components don't always trigger
+// render cascades. we cannot leverage the reselect library because our cache size is >1
+const cachedDashboardMetadataByChart = {};
+const cachedFiltersByChart = {};
+const cachedFormdataByChart = {};
+
+export default function getFormDataWithExtraFilters({
+  chart,
+  dashboardMetadata,
+  filters,
+  sliceId,
+}) {
+  // if dashboard metadata + filters have not changed, use cache if possible
+  if (
+    cachedDashboardMetadataByChart[sliceId] &&
+    cachedDashboardMetadataByChart[sliceId] === dashboardMetadata &&
+    cachedFiltersByChart[sliceId] &&
+    cachedFiltersByChart[sliceId] === filters &&
+    cachedFormdataByChart[sliceId]
+  ) {
+    return cachedFormdataByChart[sliceId];
+  }
+
+  const extraFilters = getEffectiveExtraFilters({
+    dashboardMetadata,
+    filters,
+    sliceId,
+  });
+
+  const formData = {
+    ...chart.formData,
+    extra_filters: [...chart.formData.filters, ...extraFilters],
+  };
+
+  cachedDashboardMetadataByChart[sliceId] = dashboardMetadata;
+  cachedFiltersByChart[sliceId] = filters;
+  cachedFormdataByChart[sliceId] = formData;
+
+  return formData;
+}
diff --git a/superset/assets/src/dashboard/util/componentIsResizable.js b/superset/assets/src/dashboard/util/componentIsResizable.js
new file mode 100644
index 0000000..45812d7
--- /dev/null
+++ b/superset/assets/src/dashboard/util/componentIsResizable.js
@@ -0,0 +1,5 @@
+import { COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE } from './componentTypes';
+
+export default function componentIsResizable(entity) {
+  return [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1;
+}
diff --git a/superset/assets/src/dashboard/v2/util/componentTypes.js b/superset/assets/src/dashboard/util/componentTypes.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/componentTypes.js
rename to superset/assets/src/dashboard/util/componentTypes.js
diff --git a/superset/assets/src/dashboard/v2/util/constants.js b/superset/assets/src/dashboard/util/constants.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/constants.js
rename to superset/assets/src/dashboard/util/constants.js
diff --git a/superset/assets/src/dashboard/util/dashboardHelper.js b/superset/assets/src/dashboard/util/dashboardHelper.js
deleted file mode 100644
index c9a6021..0000000
--- a/superset/assets/src/dashboard/util/dashboardHelper.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export function getChartIdsFromLayout(layout) {
-  return Object.values(layout)
-    .reduce((chartIds, value) => {
-      if (value && value.meta && value.meta.chartId) {
-        chartIds.push(value.meta.chartId);
-      }
-      return chartIds;
-    }, []);
-}
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index 854ca65..f04b50e 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -8,19 +8,23 @@ import {
   DASHBOARD_HEADER_TYPE,
   DASHBOARD_ROOT_TYPE,
   DASHBOARD_GRID_TYPE,
-} from '../v2/util/componentTypes';
+} from './componentTypes';
 import {
   DASHBOARD_GRID_ID,
   DASHBOARD_HEADER_ID,
   DASHBOARD_ROOT_ID,
-} from '../v2/util/constants';
+} from './constants';
 
 const MAX_RECURSIVE_LEVEL = 6;
 const GRID_RATIO = 4;
 const ROW_HEIGHT = 8;
 const generateId = (() => {
   let componentId = 1;
-  return () => (componentId++);
+  return () => {
+    const id = componentId;
+    componentId += 1;
+    return id;
+  };
 })();
 
 /**
@@ -33,7 +37,7 @@ function getBoundary(positions) {
   let bottom = 0;
   let left = Number.MAX_VALUE;
   let right = 1;
-  positions.forEach((item) => {
+  positions.forEach(item => {
     const { row, col, size_x, size_y } = item;
     if (row <= top) top = row;
     if (col <= left) left = col;
@@ -50,11 +54,10 @@ function getBoundary(positions) {
 }
 
 function getRowContainer() {
-  const id = 'DASHBOARD_ROW_TYPE-' + generateId();
   return {
     version: 'v2',
     type: ROW_TYPE,
-    id,
+    id: `DASHBOARD_ROW_TYPE-${generateId()}`,
     children: [],
     meta: {
       background: 'BACKGROUND_TRANSPARENT',
@@ -63,11 +66,10 @@ function getRowContainer() {
 }
 
 function getColContainer() {
-  const id = 'DASHBOARD_COLUMN_TYPE-' + generateId();
   return {
     version: 'v2',
     type: COLUMN_TYPE,
-    id,
+    id: `DASHBOARD_COLUMN_TYPE-${generateId()}`,
     children: [],
     meta: {
       background: 'BACKGROUND_TRANSPARENT',
@@ -88,7 +90,7 @@ function getChartHolder(item) {
   return {
     version: 'v2',
     type: CHART_TYPE,
-    id: 'DASHBOARD_CHART_TYPE-' + generateId(),
+    id: `DASHBOARD_CHART_TYPE-${generateId()}`,
     children: [],
     meta: {
       width: converted.size_x,
@@ -99,13 +101,31 @@ function getChartHolder(item) {
 }
 
 function getChildrenMax(items, attr, layout) {
-  return Math.max.apply(null, items.map(child => (layout[child].meta[attr])));
+  return Math.max.apply(null, items.map(child => layout[child].meta[attr]));
 }
 
 function getChildrenSum(items, attr, layout) {
-  return items.reduce((preValue, child) => (preValue + layout[child].meta[attr]), 0);
+  return items.reduce(
+    (preValue, child) => preValue + layout[child].meta[attr],
+    0,
+  );
 }
 
+// function getChildrenMax(items, attr, layout) {
+//   return Math.max.apply(null, items.map((childId) => {
+//     const child = layout[childId];
+//     if (child.type === ROW_TYPE && attr === 'width') {
+//       // rows don't have widths themselves
+//       return getChildrenSum(child.children, attr, layout);
+//     } else if (child.type === COLUMN_TYPE && attr === 'height') {
+//       // columns don't have heights themselves
+//       return getChildrenSum(child.children, attr, layout);
+//     }
+//
+//     return child.meta[attr];
+//   }));
+// }
+
 function sortByRowId(item1, item2) {
   return item1.row - item2.row;
 }
@@ -115,7 +135,8 @@ function sortByColId(item1, item2) {
 }
 
 function hasOverlap(positions, xAxis = true) {
-  return positions.slice()
+  return positions
+    .slice()
     .sort(xAxis ? sortByColId : sortByRowId)
     .some((item, index, arr) => {
       if (index === arr.length - 1) {
@@ -123,9 +144,9 @@ function hasOverlap(positions, xAxis = true) {
       }
 
       if (xAxis) {
-        return (item.col + item.size_x) > arr[index + 1].col;
+        return item.col + item.size_x > arr[index + 1].col;
       }
-      return (item.row + item.size_y) > arr[index + 1].row;
+      return item.row + item.size_y > arr[index + 1].row;
     });
 }
 
@@ -158,7 +179,7 @@ function doConvert(positions, level, parent, root) {
     const upper = [];
     const lower = [];
 
-    const isRowDivider = currentItems.every((item) => {
+    const isRowDivider = currentItems.every(item => {
       const { row, size_y } = item;
       if (row + size_y <= currentRow) {
         lower.push(item);
@@ -174,10 +195,10 @@ function doConvert(positions, level, parent, root) {
       currentItems = upper.slice();
       layers.push(lower);
     }
-    currentRow++;
+    currentRow += 1;
   }
 
-  layers.forEach((layer) => {
+  layers.forEach(layer => {
     if (layer.length === 0) {
       return;
     }
@@ -196,7 +217,7 @@ function doConvert(positions, level, parent, root) {
 
     currentItems = layer.slice();
     if (!hasOverlap(currentItems)) {
-      currentItems.sort(sortByColId).forEach((item) => {
+      currentItems.sort(sortByColId).forEach(item => {
         const chartHolder = getChartHolder(item);
         root[chartHolder.id] = chartHolder;
         rowContainer.children.push(chartHolder.id);
@@ -208,7 +229,7 @@ function doConvert(positions, level, parent, root) {
         const upper = [];
         const lower = [];
 
-        const isColDivider = currentItems.every((item) => {
+        const isColDivider = currentItems.every(item => {
           const { col, size_x } = item;
           if (col + size_x <= currentCol) {
             lower.push(item);
@@ -232,7 +253,7 @@ function doConvert(positions, level, parent, root) {
             rowContainer.children.push(colContainer.id);
 
             if (!hasOverlap(lower, false)) {
-              lower.sort(sortByRowId).forEach((item) => {
+              lower.sort(sortByRowId).forEach(item => {
                 const chartHolder = getChartHolder(item);
                 root[chartHolder.id] = chartHolder;
                 colContainer.children.push(chartHolder.id);
@@ -242,37 +263,47 @@ function doConvert(positions, level, parent, root) {
             }
 
             // add col meta
-            colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
+            colContainer.meta.width = getChildrenMax(
+              colContainer.children,
+              'width',
+              root,
+            );
           }
 
           currentItems = upper.slice();
         }
-        currentCol++;
+        currentCol += 1;
       }
     }
 
-    rowContainer.meta.width = getChildrenSum(rowContainer.children, 'width', root);
+    rowContainer.meta.width = getChildrenSum(
+      rowContainer.children,
+      'width',
+      root,
+    );
   });
 }
 
-export default function (dashboard) {
+export default function(dashboard) {
   const positions = [];
 
   // position data clean up. some dashboard didn't have position_json
   let { position_json } = dashboard;
   const posDict = {};
   if (Array.isArray(position_json)) {
-    position_json.forEach((position) => {
+    position_json.forEach(position => {
       posDict[position.slice_id] = position;
     });
   } else {
     position_json = [];
   }
 
-  const lastRowId = Math.max(0, Math.max.apply(null,
-    position_json.map(pos => (pos.row + pos.size_y))));
+  const lastRowId = Math.max(
+    0,
+    Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
+  );
   let newSliceCounter = 0;
-  dashboard.slices.forEach((slice) => {
+  dashboard.slices.forEach(slice => {
     const sliceId = slice.slice_id;
     let pos = posDict[sliceId];
     if (!pos) {
@@ -284,7 +315,7 @@ export default function (dashboard) {
         size_y: 16,
         slice_id: String(sliceId),
       };
-      newSliceCounter++;
+      newSliceCounter += 1;
     }
 
     positions.push(pos);
@@ -292,7 +323,6 @@ export default function (dashboard) {
 
   const root = {
     [DASHBOARD_ROOT_ID]: {
-      version: 'v2',
       type: DASHBOARD_ROOT_TYPE,
       id: DASHBOARD_ROOT_ID,
       children: [DASHBOARD_GRID_ID],
@@ -310,13 +340,12 @@ export default function (dashboard) {
   doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
 
   // remove row's width/height and col's height
-  Object.values(root).forEach((item) => {
+  Object.values(root).forEach(item => {
     if (ROW_TYPE === item.type) {
       const meta = item.meta;
       delete meta.width;
     }
   });
 
-  // console.log(JSON.stringify(root));
   return root;
 }
diff --git a/superset/assets/src/dashboard/v2/util/dnd-reorder.js b/superset/assets/src/dashboard/util/dnd-reorder.js
similarity index 84%
rename from superset/assets/src/dashboard/v2/util/dnd-reorder.js
rename to superset/assets/src/dashboard/util/dnd-reorder.js
index 9a0dedf..76fb56c 100644
--- a/superset/assets/src/dashboard/v2/util/dnd-reorder.js
+++ b/superset/assets/src/dashboard/util/dnd-reorder.js
@@ -6,22 +6,14 @@ export function reorder(list, startIndex, endIndex) {
   return result;
 }
 
-export default function reorderItem({
-  entitiesMap,
-  source,
-  destination,
-}) {
+export default function reorderItem({ entitiesMap, source, destination }) {
   const current = [...entitiesMap[source.id].children];
   const next = [...entitiesMap[destination.id].children];
   const target = current[source.index];
 
   // moving to same list
   if (source.id === destination.id) {
-    const reordered = reorder(
-      current,
-      source.index,
-      destination.index,
-    );
+    const reordered = reorder(current, source.index, destination.index);
 
     const result = {
       ...entitiesMap,
diff --git a/superset/assets/src/dashboard/v2/util/dropOverflowsParent.js b/superset/assets/src/dashboard/util/dropOverflowsParent.js
similarity index 55%
rename from superset/assets/src/dashboard/v2/util/dropOverflowsParent.js
rename to superset/assets/src/dashboard/util/dropOverflowsParent.js
index 0fd0c4e..bc7195f 100644
--- a/superset/assets/src/dashboard/v2/util/dropOverflowsParent.js
+++ b/superset/assets/src/dashboard/util/dropOverflowsParent.js
@@ -1,23 +1,37 @@
 import { COLUMN_TYPE } from '../util/componentTypes';
-import { GRID_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID } from './constants';
+import {
+  GRID_COLUMN_COUNT,
+  NEW_COMPONENTS_SOURCE_ID,
+  GRID_MIN_COLUMN_COUNT,
+} from './constants';
 import findParentId from './findParentId';
 import getChildWidth from './getChildWidth';
 import newComponentFactory from './newComponentFactory';
 
 export default function doesChildOverflowParent(dropResult, components) {
   const { source, destination, dragging } = dropResult;
-  const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
 
+  // 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, components });
 
-  const child = isNewComponent ? newComponentFactory(dragging.type) : components[dragging.id] || {};
+  const child = isNewComponent
+    ? newComponentFactory(dragging.type)
+    : components[dragging.id] || {};
   const parent = components[destination.id] || {};
   const grandparent = components[grandparentId] || {};
 
-  const grandparentWidth = (grandparent.meta && grandparent.meta.width) || GRID_COLUMN_COUNT;
+  const grandparentWidth =
+    (grandparent.meta && grandparent.meta.width) || GRID_COLUMN_COUNT;
   const parentWidth = (parent.meta && parent.meta.width) || grandparentWidth;
-  const parentChildWidth = parent.type === COLUMN_TYPE
-    ? 0 : getChildWidth({ id: destination.id, components });
+  const parentChildWidth =
+    parent.type === COLUMN_TYPE
+      ? (parent.meta && parent.meta.width) || GRID_MIN_COLUMN_COUNT
+      : getChildWidth({ id: destination.id, components });
   const childWidth = (child.meta && child.meta.width) || 0;
 
   return parentWidth - parentChildWidth < childWidth;
diff --git a/superset/assets/src/dashboard/v2/util/findParentId.js b/superset/assets/src/dashboard/util/findParentId.js
similarity index 73%
rename from superset/assets/src/dashboard/v2/util/findParentId.js
rename to superset/assets/src/dashboard/util/findParentId.js
index 0ca15a6..f84b0de 100644
--- a/superset/assets/src/dashboard/v2/util/findParentId.js
+++ b/superset/assets/src/dashboard/util/findParentId.js
@@ -5,7 +5,11 @@ export default function findParentId({ childId, components = {} }) {
   for (let i = 0; i < ids.length - 1; i += 1) {
     const id = ids[i];
     const component = components[id] || {};
-    if (id !== childId && component.children && component.children.includes(childId)) {
+    if (
+      id !== childId &&
+      component.children &&
+      component.children.includes(childId)
+    ) {
       parentId = id;
       break;
     }
diff --git a/superset/assets/src/dashboard/util/getChartIdsFromLayout.js b/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
new file mode 100644
index 0000000..f0963c1
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
@@ -0,0 +1,8 @@
+export default function getChartIdsFromLayout(layout) {
+  return Object.values(layout).reduce((chartIds, value) => {
+    if (value && value.meta && value.meta.chartId) {
+      chartIds.push(value.meta.chartId);
+    }
+    return chartIds;
+  }, []);
+}
diff --git a/superset/assets/src/dashboard/v2/util/getChildWidth.js b/superset/assets/src/dashboard/util/getChildWidth.js
similarity index 55%
rename from superset/assets/src/dashboard/v2/util/getChildWidth.js
rename to superset/assets/src/dashboard/util/getChildWidth.js
index aa32b96..69d2792 100644
--- a/superset/assets/src/dashboard/v2/util/getChildWidth.js
+++ b/superset/assets/src/dashboard/util/getChildWidth.js
@@ -4,9 +4,9 @@ export default function getTotalChildWidth({ id, components }) {
 
   let width = 0;
 
-  (component.children || []).forEach((childId) => {
-    const child = components[childId];
-    width += child.meta.width || 0;
+  (component.children || []).forEach(childId => {
+    const child = components[childId] || {};
+    width += (child.meta || {}).width || 0;
   });
 
   return width;
diff --git a/superset/assets/src/dashboard/v2/util/getDropPosition.js b/superset/assets/src/dashboard/util/getDropPosition.js
similarity index 80%
rename from superset/assets/src/dashboard/v2/util/getDropPosition.js
rename to superset/assets/src/dashboard/util/getDropPosition.js
index 9605db2..2a02702 100644
--- a/superset/assets/src/dashboard/v2/util/getDropPosition.js
+++ b/superset/assets/src/dashboard/util/getDropPosition.js
@@ -8,7 +8,7 @@ export const DROP_LEFT = 'DROP_LEFT';
 
 // this defines how close the mouse must be to the edge of a component to display
 // a sibling type drop indicator
-const SIBLING_DROP_THRESHOLD = 15;
+const SIBLING_DROP_THRESHOLD = 20;
 
 export default function getDropPosition(monitor, Component) {
   const {
@@ -22,7 +22,11 @@ export default function getDropPosition(monitor, Component) {
   const draggingItem = monitor.getItem();
 
   // if dropped self on self, do nothing
-  if (!draggingItem || draggingItem.id === component.id || !isDraggingOverShallow) {
+  if (
+    !draggingItem ||
+    draggingItem.id === component.id ||
+    !isDraggingOverShallow
+  ) {
     return null;
   }
 
@@ -34,7 +38,8 @@ export default function getDropPosition(monitor, Component) {
 
   const parentType = parentComponent && parentComponent.type;
   const parentDepth = // see isValidChild.js for why tabs don't increment child depth
-    componentDepth + (parentType === TAB_TYPE || parentType === TABS_TYPE ? 0 : -1);
+    componentDepth +
+    (parentType === TAB_TYPE || parentType === TABS_TYPE ? 0 : -1);
 
   const validSibling = isValidChild({
     parentType,
@@ -47,10 +52,13 @@ export default function getDropPosition(monitor, Component) {
   }
 
   const hasChildren = (component.children || []).length > 0;
-  const childDropOrientation = orientation === 'row' ? 'vertical' : 'horizontal';
-  const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical';
+  const childDropOrientation =
+    orientation === 'row' ? 'vertical' : 'horizontal';
+  const siblingDropOrientation =
+    orientation === 'row' ? 'horizontal' : 'vertical';
 
-  if (validChild && !validSibling) { // easiest case, insert as child
+  if (validChild && !validSibling) {
+    // easiest case, insert as child
     if (childDropOrientation === 'vertical') {
       return hasChildren ? DROP_RIGHT : DROP_LEFT;
     }
@@ -64,10 +72,12 @@ export default function getDropPosition(monitor, Component) {
   if (validSibling && !validChild) {
     if (siblingDropOrientation === 'vertical') {
       const refMiddleX =
-        refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2);
+        refBoundingRect.left +
+        (refBoundingRect.right - refBoundingRect.left) / 2;
       return clientOffset.x < refMiddleX ? DROP_LEFT : DROP_RIGHT;
     }
-    const refMiddleY = refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2);
+    const refMiddleY =
+      refBoundingRect.top + (refBoundingRect.bottom - refBoundingRect.top) / 2;
     return clientOffset.y < refMiddleY ? DROP_TOP : DROP_BOTTOM;
   }
 
diff --git a/superset/assets/src/dashboard/v2/util/headerStyleOptions.js b/superset/assets/src/dashboard/util/headerStyleOptions.js
similarity index 89%
rename from superset/assets/src/dashboard/v2/util/headerStyleOptions.js
rename to superset/assets/src/dashboard/util/headerStyleOptions.js
index 309d482..7efa040 100644
--- a/superset/assets/src/dashboard/v2/util/headerStyleOptions.js
+++ b/superset/assets/src/dashboard/util/headerStyleOptions.js
@@ -1,4 +1,4 @@
-import { t } from '../../../locales';
+import { t } from '../../locales';
 import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from './constants';
 
 export default [
diff --git a/superset/assets/src/dashboard/v2/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
similarity index 65%
rename from superset/assets/src/dashboard/v2/util/isValidChild.js
rename to superset/assets/src/dashboard/util/isValidChild.js
index 66942f0..d789f45 100644
--- a/superset/assets/src/dashboard/v2/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -1,19 +1,19 @@
 /* eslint max-len: 0 */
 /**
-  * When determining if a component is a valid child of another component we must consider both
-  *   - parent + child component types
-  *   - component depth, or depth of nesting of container components
-  *
-  * We consider types because some components aren't containers (e.g. a heading) and we consider
-  * depth to prevent infinite nesting of container components.
-  *
-  * The following example container nestings should be valid, which means that some containers
-  * don't increase the (depth) of their children, namely tabs and tab:
-  *   (a) root (0) > grid (1) >                         row (2) > column (3) > row (4) > non-container (5)
-  *   (b) root (0) > grid (1) >    tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
-  *   (c) root (0) > top-tab (1) >                      row (2) > column (3) > row (4) > non-container (5)
-  *   (d) root (0) > top-tab (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
-  */
+ * When determining if a component is a valid child of another component we must consider both
+ *   - parent + child component types
+ *   - component depth, or depth of nesting of container components
+ *
+ * We consider types because some components aren't containers (e.g. a heading) and we consider
+ * depth to prevent infinite nesting of container components.
+ *
+ * The following example container nestings should be valid, which means that some containers
+ * don't increase the (depth) of their children, namely tabs and tab:
+ *   (a) root (0) > grid (1) >                         row (2) > column (3) > row (4) > non-container (5)
+ *   (b) root (0) > grid (1) >    tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+ *   (c) root (0) > top-tab (1) >                      row (2) > column (3) > row (4) > non-container (5)
+ *   (d) root (0) > top-tab (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+ */
 import {
   CHART_TYPE,
   COLUMN_TYPE,
diff --git a/superset/assets/src/dashboard/v2/util/newComponentFactory.js b/superset/assets/src/dashboard/util/newComponentFactory.js
similarity index 81%
rename from superset/assets/src/dashboard/v2/util/newComponentFactory.js
rename to superset/assets/src/dashboard/util/newComponentFactory.js
index b428ddd..4e2de37 100644
--- a/superset/assets/src/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/util/newComponentFactory.js
@@ -1,3 +1,5 @@
+import shortid from 'shortid';
+
 import {
   CHART_TYPE,
   COLUMN_TYPE,
@@ -9,10 +11,7 @@ import {
   TAB_TYPE,
 } from './componentTypes';
 
-import {
-  MEDIUM_HEADER,
-  BACKGROUND_TRANSPARENT,
-} from './constants';
+import { MEDIUM_HEADER, BACKGROUND_TRANSPARENT } from './constants';
 
 const typeToDefaultMetaData = {
   [CHART_TYPE]: { width: 3, height: 30 },
@@ -29,9 +28,8 @@ const typeToDefaultMetaData = {
   [TAB_TYPE]: { text: 'New Tab' },
 };
 
-// @TODO this should be replaced by a more robust algorithm
 function uuid(type) {
-  return `${type}-${Math.random().toString(16)}`;
+  return `${type}-${shortid.generate()}`;
 }
 
 export default function entityFactory(type, meta) {
diff --git a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
similarity index 81%
rename from superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
rename to superset/assets/src/dashboard/util/newEntitiesFromDrop.js
index 7cccc5f..7fe7f4e 100644
--- a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
@@ -1,11 +1,7 @@
 import shouldWrapChildInRow from './shouldWrapChildInRow';
 import newComponentFactory from './newComponentFactory';
 
-import {
-  ROW_TYPE,
-  TABS_TYPE,
-  TAB_TYPE,
-} from './componentTypes';
+import { ROW_TYPE, TABS_TYPE, TAB_TYPE } from './componentTypes';
 
 export default function newEntitiesFromDrop({ dropResult, components }) {
   const { dragging, destination } = dropResult;
@@ -15,7 +11,10 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
   const dropEntity = components[destination.id];
   const dropType = dropEntity.type;
   let newDropChild = newComponentFactory(dragType, dragMeta);
-  const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
+  const wrapChildInRow = shouldWrapChildInRow({
+    parentType: dropType,
+    childType: dragType,
+  });
 
   const newEntities = {
     [newDropChild.id]: newDropChild,
@@ -26,7 +25,8 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
     rowWrapper.children = [newDropChild.id];
     newEntities[rowWrapper.id] = rowWrapper;
     newDropChild = rowWrapper;
-  } else if (dragType === TABS_TYPE) { // create a new tab component
+  } else if (dragType === TABS_TYPE) {
+    // create a new tab component
     const tabChild = newComponentFactory(TAB_TYPE);
     newDropChild.children = [tabChild.id];
     newEntities[tabChild.id] = tabChild;
diff --git a/superset/assets/src/dashboard/v2/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
similarity index 88%
rename from superset/assets/src/dashboard/v2/util/propShapes.jsx
rename to superset/assets/src/dashboard/util/propShapes.jsx
index 388c726..73a10b0 100644
--- a/superset/assets/src/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -2,13 +2,16 @@ import PropTypes from 'prop-types';
 import componentTypes from './componentTypes';
 import backgroundStyleOptions from './backgroundStyleOptions';
 import headerStyleOptions from './headerStyleOptions';
-import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from './constants';
+import {
+  INFO_TOAST,
+  SUCCESS_TOAST,
+  WARNING_TOAST,
+  DANGER_TOAST,
+} from './constants';
 
-export const componentShape = PropTypes.shape({ // eslint-disable-line
+export const componentShape = PropTypes.shape({
   id: PropTypes.string.isRequired,
-  type: PropTypes.oneOf(
-    Object.values(componentTypes),
-  ).isRequired,
+  type: PropTypes.oneOf(Object.values(componentTypes)).isRequired,
   children: PropTypes.arrayOf(PropTypes.string),
   meta: PropTypes.shape({
     // Dimensions
@@ -25,7 +28,12 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
 
 export const toastShape = PropTypes.shape({
   id: PropTypes.string.isRequired,
-  toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
+  toastType: PropTypes.oneOf([
+    INFO_TOAST,
+    SUCCESS_TOAST,
+    WARNING_TOAST,
+    DANGER_TOAST,
+  ]).isRequired,
   text: PropTypes.string.isRequired,
 });
 
diff --git a/superset/assets/src/dashboard/v2/util/resizableConfig.js b/superset/assets/src/dashboard/util/resizableConfig.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/resizableConfig.js
rename to superset/assets/src/dashboard/util/resizableConfig.js
diff --git a/superset/assets/src/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/src/dashboard/util/shouldWrapChildInRow.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/shouldWrapChildInRow.js
rename to superset/assets/src/dashboard/util/shouldWrapChildInRow.js
diff --git a/superset/assets/src/dashboard/v2/actions/editMode.js b/superset/assets/src/dashboard/v2/actions/editMode.js
deleted file mode 100644
index 0a849ea..0000000
--- a/superset/assets/src/dashboard/v2/actions/editMode.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-export function setEditMode(editMode) {
-  return {
-    type: SET_EDIT_MODE,
-    payload: {
-      editMode,
-    },
-  };
-}
diff --git a/superset/assets/src/dashboard/v2/components/Dashboard.jsx b/superset/assets/src/dashboard/v2/components/Dashboard.jsx
deleted file mode 100644
index ffd1280..0000000
--- a/superset/assets/src/dashboard/v2/components/Dashboard.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import DashboardBuilder from '../containers/DashboardBuilder';
-
-import '../stylesheets/index.less';
-
-const propTypes = {
-  actions: PropTypes.shape({
-    updateDashboardTitle: PropTypes.func.isRequired,
-    setEditMode: PropTypes.func.isRequired,
-  }),
-  editMode: PropTypes.bool,
-};
-
-const defaultProps = {
-  editMode: true,
-};
-
-class Dashboard extends React.Component {
-  render() {
-    // @TODO delete this component?
-    return <DashboardBuilder />;
-  }
-}
-
-Dashboard.propTypes = propTypes;
-Dashboard.defaultProps = defaultProps;
-
-export default Dashboard;
diff --git a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
deleted file mode 100644
index d3ec7ac..0000000
--- a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { ButtonGroup, ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
-
-import Button from '../../../components/Button';
-import { componentShape } from '../util/propShapes';
-import EditableTitle from '../../../components/EditableTitle';
-
-const propTypes = {
-  editMode: PropTypes.bool.isRequired,
-  component: componentShape.isRequired,
-
-  // redux
-  updateComponents: PropTypes.func.isRequired,
-  onUndo: PropTypes.func.isRequired,
-  onRedo: PropTypes.func.isRequired,
-  canUndo: PropTypes.bool.isRequired,
-  canRedo: PropTypes.bool.isRequired,
-  setEditMode: PropTypes.func.isRequired,
-};
-
-class DashboardHeader extends React.Component {
-  constructor(props) {
-    super(props);
-    this.handleChangeText = this.handleChangeText.bind(this);
-    this.toggleEditMode = this.toggleEditMode.bind(this);
-  }
-
-  toggleEditMode() {
-    this.props.setEditMode(!this.props.editMode);
-  }
-
-  handleChangeText(nextText) {
-    const { updateComponents, component } = this.props;
-    if (nextText && component.meta.text !== nextText) {
-      updateComponents({
-        [component.id]: {
-          ...component,
-          meta: {
-            ...component.meta,
-            text: nextText,
-          },
-        },
-      });
-    }
-  }
-
-  render() {
-    const { component, onUndo, onRedo, canUndo, canRedo, editMode } = this.props;
-
-    return (
-      <div className="dashboard-header">
-        <div className="dashboard-component-header header-large">
-          <EditableTitle
-            title={'Test title'}
-            onSaveTitle={this.handleChangeText}
-            showTooltip={false}
-            canEdit={editMode}
-          />
-        </div>
-        <ButtonToolbar>
-          <ButtonGroup>
-            <Button
-              bsSize="small"
-              onClick={onUndo}
-              disabled={!canUndo}
-            >
-              Undo
-            </Button>
-            <Button
-              bsSize="small"
-              onClick={onRedo}
-              disabled={!canRedo}
-            >
-              Redo
-            </Button>
-          </ButtonGroup>
-
-          <DropdownButton title="Actions" bsSize="small" id="btn-dashboard-actions">
-            <MenuItem>Action 1</MenuItem>
-            <MenuItem>Action 2</MenuItem>
-            <MenuItem>Action 3</MenuItem>
-          </DropdownButton>
-
-          <Button
-            bsStyle="primary"
-            onClick={this.toggleEditMode}
-          >
-            {editMode ? 'Save changes' : 'Edit dashboard'}
-          </Button>
-        </ButtonToolbar>
-      </div>
-    );
-  }
-}
-
-DashboardHeader.propTypes = propTypes;
-
-export default DashboardHeader;
diff --git a/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx b/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx
deleted file mode 100644
index 4fd2397..0000000
--- a/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-const propTypes = {
-};
-
-class StaticDashboard extends React.Component {
-  render() {
-    return (
-      <div>
-        Static dashboard ...
-      </div>
-    );
-  }
-}
-
-StaticDashboard.propTypes = propTypes;
-
-export default StaticDashboard;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx
deleted file mode 100644
index 0255755..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { CHART_TYPE } from '../../../util/componentTypes';
-import { NEW_CHART_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewChart extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_CHART_ID}
-        type={CHART_TYPE}
-        label="Chart"
-        className="fa fa-area-chart"
-      />
-    );
-  }
-}
-
-DraggableNewChart.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx
deleted file mode 100644
index 654c60b..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { COLUMN_TYPE } from '../../../util/componentTypes';
-import { NEW_COLUMN_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewColumn extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_COLUMN_ID}
-        type={COLUMN_TYPE}
-        label="Column"
-        className="fa fa-long-arrow-down"
-      />
-    );
-  }
-}
-
-DraggableNewColumn.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx
deleted file mode 100644
index 5d70041..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { DIVIDER_TYPE } from '../../../util/componentTypes';
-import { NEW_DIVIDER_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewDivider extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_DIVIDER_ID}
-        type={DIVIDER_TYPE}
-        label="Divider"
-        className="divider-placeholder"
-      />
-    );
-  }
-}
-
-DraggableNewDivider.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx
deleted file mode 100644
index d207a9c..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { HEADER_TYPE } from '../../../util/componentTypes';
-import { NEW_HEADER_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewHeader extends React.Component {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_HEADER_ID}
-        type={HEADER_TYPE}
-        label="Header"
-        className="fa fa-header"
-      />
-    );
-  }
-}
-
-DraggableNewHeader.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx
deleted file mode 100644
index 1d9ab10..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-
-import { ROW_TYPE } from '../../../util/componentTypes';
-import { NEW_ROW_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewRow extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_ROW_ID}
-        type={ROW_TYPE}
-        label="Row"
-        className="fa fa-long-arrow-right"
-      />
-    );
-  }
-}
-
-DraggableNewRow.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx
deleted file mode 100644
index a473281..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { TABS_TYPE } from '../../../util/componentTypes';
-import { NEW_TABS_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewTabs extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_TABS_ID}
-        type={TABS_TYPE}
-        label="Tabs"
-        className="fa fa-window-restore"
-      />
-    );
-  }
-}
-
-DraggableNewTabs.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/reducers/editMode.js b/superset/assets/src/dashboard/v2/reducers/editMode.js
deleted file mode 100644
index b1a1630..0000000
--- a/superset/assets/src/dashboard/v2/reducers/editMode.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { SET_EDIT_MODE } from '../actions/editMode';
-
-export default function editModeReducer(editMode = false, action) {
-  switch (action.type) {
-    case SET_EDIT_MODE:
-      return action.payload.editMode;
-
-    default:
-      return editMode;
-  }
-}
diff --git a/superset/assets/src/dashboard/v2/reducers/index.js b/superset/assets/src/dashboard/v2/reducers/index.js
deleted file mode 100644
index 061255d..0000000
--- a/superset/assets/src/dashboard/v2/reducers/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import undoable, { distinctState } from 'redux-undo';
-
-import dashboardLayout from './dashboardLayout';
-
-export default undoable(dashboardLayout, {
-  limit: 15,
-  filter: distinctState(),
-});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
deleted file mode 100644
index ce03797..0000000
--- a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
+++ /dev/null
@@ -1,20 +0,0 @@
-.dashboard-component-chart {
-  width: 100%;
-  height: 100%;
-  color: @gray-dark;
-  background-color: white;
-  padding: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  position: relative;
-}
-
-.dashboard-component-chart .fa {
-  //font-size: 100px;
-  opacity: 0.3;
-}
-
-.dashboard-v2--editing .dashboard-component-chart:hover {
-  box-shadow: inset 0 0 0 1px @gray-light;
-}
diff --git a/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js b/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js
deleted file mode 100644
index cda678f..0000000
--- a/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { t } from '../../../locales';
-import { BACKGROUND_TRANSPARENT, BACKGROUND_WHITE } from './constants';
-
-export default [
-  { value: BACKGROUND_TRANSPARENT, label: t('Transparent'), className: 'background--transparent' },
-  { value: BACKGROUND_WHITE, label: t('White'), className: 'background--white' },
-];
diff --git a/superset/assets/src/dashboard/v2/util/componentIsResizable.js b/superset/assets/src/dashboard/v2/util/componentIsResizable.js
deleted file mode 100644
index c0016f3..0000000
--- a/superset/assets/src/dashboard/v2/util/componentIsResizable.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
-  COLUMN_TYPE,
-  CHART_TYPE,
-  MARKDOWN_TYPE,
-} from './componentTypes';
-
-export default function componentIsResizable(entity) {
-  return [
-    COLUMN_TYPE,
-    CHART_TYPE,
-    MARKDOWN_TYPE,
-  ].indexOf(entity.type) > -1;
-}
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 19416b0..3825335 100644
--- a/superset/assets/src/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { chartPropShape } from '../../dashboard/v2/util/propShapes';
+import { chartPropShape } from '../../dashboard/util/propShapes';
 import ExploreActionButtons from './ExploreActionButtons';
 import RowCountLabel from './RowCountLabel';
 import EditableTitle from '../../components/EditableTitle';
diff --git a/superset/assets/src/explore/components/ExploreChartPanel.jsx b/superset/assets/src/explore/components/ExploreChartPanel.jsx
index 21c6a64..bcda75d 100644
--- a/superset/assets/src/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/src/explore/components/ExploreChartPanel.jsx
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Panel } from 'react-bootstrap';
 
-import { chartPropShape } from '../../dashboard/v2/util/propShapes';
+import { chartPropShape } from '../../dashboard/util/propShapes';
 import ChartContainer from '../../chart/ChartContainer';
 import ExploreChartHeader from './ExploreChartHeader';
 
@@ -38,14 +38,15 @@ class ExploreChartPanel extends React.PureComponent {
   }
 
   renderChart() {
+    const { chart } = this.props;
     return (
       <ChartContainer
+        chartId={chart.id}
         containerId={this.props.containerId}
         datasource={this.props.datasource}
         formData={this.props.form_data}
         height={this.getHeight()}
         slice={this.props.slice}
-        chartId={this.props.chart.id}
         setControlValue={this.props.actions.setControlValue}
         timeout={this.props.timeout}
         vizType={this.props.vizType}
@@ -53,6 +54,16 @@ class ExploreChartPanel extends React.PureComponent {
         errorMessage={this.props.errorMessage}
         onQuery={this.props.onQuery}
         onDismissRefreshOverlay={this.props.onDismissRefreshOverlay}
+        annotationData={chart.annotationData}
+        chartAlert={chart.chartAlert}
+        chartStatus={chart.chartStatus}
+        chartUpdateEndTime={chart.chartUpdateEndTime}
+        chartUpdateStartTime={chart.chartUpdateStartTime}
+        latestQueryFormData={chart.latestQueryFormData}
+        lastRendered={chart.lastRendered}
+        queryResponse={chart.queryResponse}
+        queryRequest={chart.queryRequest}
+        triggerQuery={chart.triggerQuery}
       />
     );
   }
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index 3e761eb..a648464 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -11,7 +11,7 @@ import QueryAndSaveBtns from './QueryAndSaveBtns';
 import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { getFormDataFromControls } from '../store';
-import { chartPropShape } from '../../dashboard/v2/util/propShapes';
+import { chartPropShape } from '../../dashboard/util/propShapes';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
 import * as chartActions from '../../chart/chartAction';
diff --git a/superset/assets/stylesheets/dashboard.less b/superset/assets/stylesheets/dashboard.less
deleted file mode 100644
index b812a42..0000000
--- a/superset/assets/stylesheets/dashboard.less
+++ /dev/null
@@ -1,156 +0,0 @@
-@import "./less/cosmo/variables.less";
-
-.dashboard a i {
-  cursor: pointer;
-}
-.dashboard i.drag {
-  cursor: move !important;
-}
-.dashboard .slice-grid .preview-holder {
-  z-index: 1;
-  position: absolute;
-  background-color: #AAA;
-  border-color: #AAA;
-  opacity: 0.3;
-}
-.dashboard .widget {
-  position: absolute;
-  top: 16px;
-  left: 16px;
-  box-shadow: none;
-  background-color: transparent;
-  overflow: visible;
-}
-.dashboard .chart-header {
-  .dropdown.btn-group {
-    position: absolute;
-    top: 0;
-    right: 0;
-  }
-
-  .dropdown-menu.dropdown-menu-right {
-    right: 7px;
-    top: -3px
-  }
-}
-
-.slice-header-controls-trigger {
-  border: 0;
-  padding: 0 0 0 20px;
-  background: none;
-  outline: none;
-  box-shadow: none;
-  color: #263238;
-
-  &.is-cached {
-    color: red;
-  }
-
-  &:hover, &:focus {
-    background: none;
-    cursor: pointer;
-  }
-
-  .controls-container.dropdown-menu {
-    top: 0;
-    left: unset;
-    right: 10px;
-
-    &.is-open {
-      display: block;
-    }
-
-    & li {
-      white-space: nowrap;
-    }
-  }
-}
-.slice-grid .slice_container {
-  background-color: #fff;
-}
-
-.dashboard .slice-grid .dragging,
-.dashboard .slice-grid .resizing {
-  opacity: 0.5;
-}
-.dashboard img.loading {
-  width: 20px;
-  margin: 5px;
-  position: absolute;
-}
-
-.dashboard .slice_title {
-  text-align: center;
-  font-weight: bold;
-  font-size: 14px;
-  padding: 5px;
-}
-.dashboard div.slice_content {
-  width: 100%;
-  height: 100%;
-}
-
-.modal img.loading {
-  width: 50px;
-  margin: 0;
-  position: relative;
-}
-
-.react-bs-container-body {
-  max-height: 400px;
-  overflow-y: auto;
-}
-
-.hidden, #pageDropDown {
-  display: none;
-}
-
-.slice-cell {
-  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
-  transition: box-shadow 1s ease-in;
-
-  .dropdown,
-  .dropdown-menu {
-    .fa {
-      font-size: 14px;
-    }
-  }
-}
-
-.slice-cell-highlight {
-  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0.2);
-  height: 100%;
-}
-
-.slice-cell .editable-title input[type="button"] {
-  font-weight: bold;
-}
-
-.chart-container {
-  box-sizing: border-box;
-}
-
-.chart-header .header {
-  font-size: 16px;
-  margin: 0 -10px;
-}
-.ace_gutter {
-    z-index: 0;
-}
-.ace_content {
-    z-index: 0;
-}
-.ace_scrollbar {
-    z-index: 0;
-}
-.slice_container .alert {
-    margin: 10px;
-}
-
-i.danger {
-  color: red;
-}
-
-i.warning {
-  color: orange;
-}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 6987544..d756551 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -141,7 +141,7 @@ div.navbar {
 img.loading {
   width: 40px;
   position: relative;
-  z-index: 10;
+  z-index: 1;
   margin: 10px;
 }
 img.viz-thumb-option {
diff --git a/superset/assets/stylesheets/welcome.css b/superset/assets/stylesheets/welcome.css
index 8e2496e..1f72852 100644
--- a/superset/assets/stylesheets/welcome.css
+++ b/superset/assets/stylesheets/welcome.css
@@ -3,7 +3,7 @@
 }
 
 img.loading {
-    width: 25px;
+  width: 25px;
 }
 
 .welcome table {
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index d3efc78..77ca84d 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -2,27 +2,27 @@
 # yarn lockfile v1
 
 
-"@data-ui/event-flow@^0.0.54":
-  version "0.0.54"
-  resolved "https://registry.yarnpkg.com/@data-ui/event-flow/-/event-flow-0.0.54.tgz#bb03e1fd2b5634248655b8df9d3c6c38a747e65e"
-  dependencies:
-    "@data-ui/forms" "0.0.50"
-    "@data-ui/radial-chart" "0.0.54"
-    "@data-ui/theme" "0.0.48"
-    "@vx/axis" "0.0.140"
-    "@vx/bounds" "0.0.140"
-    "@vx/clip-path" "0.0.140"
-    "@vx/glyph" "0.0.140"
-    "@vx/gradient" "0.0.140"
-    "@vx/grid" "0.0.140"
-    "@vx/group" "0.0.140"
-    "@vx/legend" "0.0.140"
-    "@vx/pattern" "0.0.140"
-    "@vx/point" "0.0.136"
-    "@vx/responsive" "0.0.140"
-    "@vx/scale" "0.0.140"
-    "@vx/shape" "0.0.140"
-    "@vx/tooltip" "0.0.140"
+"@data-ui/event-flow@^0.0.8":
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/@data-ui/event-flow/-/event-flow-0.0.8.tgz#237ef225e3085fae9e47bbb1ae11b6fab27eaede"
+  dependencies:
+    "@data-ui/forms" "0.0.4"
+    "@data-ui/radial-chart" "0.0.8"
+    "@data-ui/theme" "0.0.9"
+    "@vx/axis" "0.0.120"
+    "@vx/bounds" "0.0.129"
+    "@vx/clip-path" "0.0.126"
+    "@vx/glyph" "0.0.126"
+    "@vx/gradient" "0.0.120"
+    "@vx/grid" "0.0.120"
+    "@vx/group" "0.0.120"
+    "@vx/legend" "0.0.121"
+    "@vx/pattern" "0.0.120"
+    "@vx/point" "0.0.112"
+    "@vx/responsive" "0.0.120"
+    "@vx/scale" "0.0.121"
+    "@vx/shape" "0.0.120"
+    "@vx/tooltip" "0.0.126"
     aphrodite "^1.2.0"
     d3-array "^1.2.0"
     d3-format "^1.2.0"
@@ -35,29 +35,28 @@
     react-with-styles-interface-aphrodite "^1.2.0"
     recompose "^0.23.5"
 
-"@data-ui/forms@0.0.50":
-  version "0.0.50"
-  resolved "https://registry.yarnpkg.com/@data-ui/forms/-/forms-0.0.50.tgz#c55a699ee4b7cf44ff263d30784299c38d939932"
+"@data-ui/forms@0.0.4":
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/@data-ui/forms/-/forms-0.0.4.tgz#3c1efc55904289e4bffc6536c5f89b3b18f39e4d"
   dependencies:
     prop-types "^15.5.10"
     react-select "^1.0.0-rc.5"
 
-"@data-ui/radial-chart@0.0.54":
-  version "0.0.54"
-  resolved "https://registry.yarnpkg.com/@data-ui/radial-chart/-/radial-chart-0.0.54.tgz#0d28b07681d9b6027d9ac23b729241827d513001"
-  dependencies:
-    "@data-ui/shared" "0.0.54"
-    "@data-ui/theme" "0.0.48"
-    "@vx/event" "0.0.140"
-    "@vx/group" "0.0.140"
-    "@vx/scale" "0.0.140"
-    "@vx/shape" "0.0.140"
-    "@vx/tooltip" "0.0.140"
+"@data-ui/radial-chart@0.0.8":
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/@data-ui/radial-chart/-/radial-chart-0.0.8.tgz#44c0d789a3981c0e9e90f9dbb5e0cbaaafcea09d"
+  dependencies:
+    "@data-ui/theme" "0.0.9"
+    "@vx/event" "0.0.127"
+    "@vx/group" "0.0.127"
+    "@vx/scale" "0.0.127"
+    "@vx/shape" "0.0.131"
+    "@vx/tooltip" "0.0.134"
     prop-types "^15.5.10"
 
-"@data-ui/shared@0.0.54":
-  version "0.0.54"
-  resolved "https://registry.yarnpkg.com/@data-ui/shared/-/shared-0.0.54.tgz#2fb0d6dee90dac20bf8f3c2913c6850a8223d59b"
+"@data-ui/shared@0.0.49":
+  version "0.0.49"
+  resolved "https://registry.yarnpkg.com/@data-ui/shared/-/shared-0.0.49.tgz#cea1fff1434e54a2dcc2d5f9612300738a466a19"
   dependencies:
     "@data-ui/theme" "0.0.48"
     "@vx/event" "0.0.143"
@@ -67,11 +66,11 @@
     d3-array "^1.2.1"
     prop-types "^15.5.10"
 
-"@data-ui/sparkline@^0.0.54":
-  version "0.0.54"
-  resolved "https://registry.yarnpkg.com/@data-ui/sparkline/-/sparkline-0.0.54.tgz#ce3d166d9e0b239a0ba02f3894cb9e8c84171cef"
+"@data-ui/sparkline@^0.0.49":
+  version "0.0.49"
+  resolved "https://registry.yarnpkg.com/@data-ui/sparkline/-/sparkline-0.0.49.tgz#5ae28769289af7f8a1bffc6b2ac5299f1fb9e846"
   dependencies:
-    "@data-ui/shared" "0.0.54"
+    "@data-ui/shared" "0.0.49"
     "@data-ui/theme" "0.0.8"
     "@vx/axis" "0.0.140"
     "@vx/curve" "0.0.140"
@@ -95,6 +94,10 @@
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/@data-ui/theme/-/theme-0.0.8.tgz#3116723d04b99f65c7750f81a500e9608b4837c3"
 
+"@data-ui/theme@0.0.9":
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/@data-ui/theme/-/theme-0.0.9.tgz#a9d66b20d74018009c129eed4326ca4ef3b83127"
+
 "@mapbox/geojson-area@0.2.2":
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10"
@@ -105,13 +108,9 @@
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/@mapbox/gl-matrix/-/gl-matrix-0.0.1.tgz#e5126aab4d64c36b81c7a97d0ae0dddde5773d2b"
 
-"@mapbox/jsonlint-lines-primitives@^2.0.1":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.1.tgz#bc4c1593e2ec2371e2771c518068d6eab8eeae58"
-
-"@mapbox/mapbox-gl-supported@^1.3.1":
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.3.1.tgz#81bd3a5e3cfdc40047608656ee9b519e02216bf1"
+"@mapbox/mapbox-gl-supported@^1.3.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.3.0.tgz#89daee16845400ea1c76e084bdfab2971e552a9c"
 
 "@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0":
   version "0.1.0"
@@ -135,42 +134,22 @@
   dependencies:
     "@mapbox/point-geometry" "~0.1.0"
 
-"@mapbox/vector-tile@^1.3.1":
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666"
-  dependencies:
-    "@mapbox/point-geometry" "~0.1.0"
-
 "@mapbox/whoots-js@^3.0.0":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.0.0.tgz#c1de4293081424da3ac30c23afa850af1019bb54"
 
-"@mrmlnc/readdir-enhanced@^2.2.1":
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
-  dependencies:
-    call-me-maybe "^1.0.1"
-    glob-to-regexp "^0.3.0"
-
-"@nodelib/fs.stat@^1.0.1":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
-
-"@samverschueren/stream-to-observable@^0.3.0":
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
-  dependencies:
-    any-observable "^0.3.0"
-
-"@sindresorhus/is@^0.7.0":
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
+"@types/react@>=15":
+  version "16.0.12"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.12.tgz#63f460337e83b24549db744aa2f033121d6d55db"
 
-"@sinonjs/formatio@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
+"@vx/axis@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/axis/-/axis-0.0.120.tgz#5dd5841d03e992990491b20ce918b242f9061d4e"
   dependencies:
-    samsam "1.3.0"
+    "@vx/group" "0.0.120"
+    "@vx/point" "0.0.112"
+    "@vx/shape" "0.0.120"
+    classnames "^2.2.5"
 
 "@vx/axis@0.0.140":
   version "0.0.140"
@@ -182,11 +161,12 @@
     classnames "^2.2.5"
     prop-types "15.5.10"
 
-"@vx/bounds@0.0.140":
-  version "0.0.140"
-  resolved "https://registry.yarnpkg.com/@vx/bounds/-/bounds-0.0.140.tgz#4ede9766aabb41b791a4fbf4c27fcc19ed83f910"
+"@vx/bounds@0.0.129":
+  version "0.0.129"
+  resolved "https://registry.yarnpkg.com/@vx/bounds/-/bounds-0.0.129.tgz#7ecbdfaa3f5041e3f87c851bf4981547e2d19ffe"
   dependencies:
     prop-types "^15.5.10"
+    react-dom "^15.0.0 || 15.x"
 
 "@vx/bounds@0.0.147":
   version "0.0.147"
@@ -194,9 +174,21 @@
   dependencies:
     prop-types "^15.5.10"
 
-"@vx/clip-path@0.0.140":
-  version "0.0.140"
-  resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.140.tgz#b2623d004dd5c3c8a6afe8d060de59df51472d94"
+"@vx/clip-path@0.0.126":
+  version "0.0.126"
+  resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.126.tgz#58f595dbd5b7fbb27f6a98bac9885a3d701e976e"
+
+"@vx/curve@0.0.112":
+  version "0.0.112"
+  resolved "https://registry.yarnpkg.com/@vx/curve/-/curve-0.0.112.tgz#8e4880a7ae8902bbd5b337ad1b0719bdfb28df3d"
+  dependencies:
+    d3-shape "^1.0.6"
+
+"@vx/curve@0.0.127":
+  version "0.0.127"
+  resolved "https://registry.yarnpkg.com/@vx/curve/-/curve-0.0.127.tgz#f05ea9871a97e1e1d59129b847f216751ffb31c3"
+  dependencies:
+    d3-shape "^1.0.6"
 
 "@vx/curve@0.0.140":
   version "0.0.140"
@@ -210,11 +202,11 @@
   dependencies:
     d3-shape "^1.0.6"
 
-"@vx/event@0.0.140":
-  version "0.0.140"
-  resolved "https://registry.yarnpkg.com/@vx/event/-/event-0.0.140.tgz#658ec4de92cd61df40b883296168d4e0824015bf"
+"@vx/event@0.0.127":
+  version "0.0.127"
+  resolved "https://registry.yarnpkg.com/@vx/event/-/event-0.0.127.tgz#8b0079a63cf0aeefd884123dd99ee18efef0af69"
   dependencies:
-    "@vx/point" "0.0.136"
+    "@vx/point" "0.0.127"
 
 "@vx/event@0.0.143":
   version "0.0.143"
@@ -222,6 +214,14 @@
   dependencies:
     "@vx/point" "0.0.143"
 
+"@vx/glyph@0.0.126":
+  version "0.0.126"
+  resolved "https://registry.yarnpkg.com/@vx/glyph/-/glyph-0.0.126.tgz#9ebf0924aa15098b6e4b8c44bfbdfbb8c8a2c658"
+  dependencies:
+    "@vx/group" "0.0.126"
+    classnames "^2.2.5"
+    d3-shape "^1.2.0"
+
 "@vx/glyph@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/glyph/-/glyph-0.0.140.tgz#f8323f82aee22192b675bef25789bbb7d74691ba"
@@ -230,6 +230,12 @@
     classnames "^2.2.5"
     d3-shape "^1.2.0"
 
+"@vx/gradient@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/gradient/-/gradient-0.0.120.tgz#eeac03390d543bcba354d5d5de169ec5f1945c2d"
+  dependencies:
+    classnames "^2.2.5"
+
 "@vx/gradient@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/gradient/-/gradient-0.0.140.tgz#56b421016cbae0dcb00190cfffb9e860a28febf4"
@@ -237,13 +243,31 @@
     classnames "^2.2.5"
     prop-types "^15.5.7"
 
-"@vx/grid@0.0.140":
-  version "0.0.140"
-  resolved "https://registry.yarnpkg.com/@vx/grid/-/grid-0.0.140.tgz#9dfd3071bc5d90d4b457dd55d7f795699233b230"
+"@vx/grid@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/grid/-/grid-0.0.120.tgz#3471d5cc8a4bef32082ff476ef73fc04db29335e"
+  dependencies:
+    "@vx/group" "0.0.120"
+    "@vx/point" "0.0.112"
+    "@vx/shape" "0.0.120"
+    classnames "^2.2.5"
+
+"@vx/group@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/group/-/group-0.0.120.tgz#0cc8bc8f40e371585c8c523d98c41cc62684aab9"
+  dependencies:
+    classnames "^2.2.5"
+
+"@vx/group@0.0.126":
+  version "0.0.126"
+  resolved "https://registry.yarnpkg.com/@vx/group/-/group-0.0.126.tgz#5610ce74d0f118db28287806315ae479f313479c"
+  dependencies:
+    classnames "^2.2.5"
+
+"@vx/group@0.0.127":
+  version "0.0.127"
+  resolved "https://registry.yarnpkg.com/@vx/group/-/group-0.0.127.tgz#d607c957119cdb787aa709818532f40e7e04eb8f"
   dependencies:
-    "@vx/group" "0.0.140"
-    "@vx/point" "0.0.136"
-    "@vx/shape" "0.0.140"
     classnames "^2.2.5"
 
 "@vx/group@0.0.140":
@@ -258,11 +282,18 @@
   dependencies:
     classnames "^2.2.5"
 
-"@vx/legend@0.0.140":
-  version "0.0.140"
-  resolved "https://registry.yarnpkg.com/@vx/legend/-/legend-0.0.140.tgz#4062c27d6bc9c4d607309d77eff12b844727ae99"
+"@vx/legend@0.0.121":
+  version "0.0.121"
+  resolved "https://registry.yarnpkg.com/@vx/legend/-/legend-0.0.121.tgz#0c8cd6860f45af6cbc524ec6526256d2951cca34"
+  dependencies:
+    "@vx/group" "0.0.120"
+    classnames "^2.2.5"
+    prop-types "^15.5.10"
+
+"@vx/pattern@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/pattern/-/pattern-0.0.120.tgz#cff57b6279c9d6b097a20df3562b8b81c17bdb35"
   dependencies:
-    "@vx/group" "0.0.140"
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
@@ -273,6 +304,14 @@
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
+"@vx/point@0.0.112":
+  version "0.0.112"
+  resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.112.tgz#cd5b0740268bb432550902ad5e00261bad641cef"
+
+"@vx/point@0.0.127":
+  version "0.0.127"
+  resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.127.tgz#51ef7f648488feed758a6ec1cc8c71319602e3e7"
+
 "@vx/point@0.0.136":
   version "0.0.136"
   resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.136.tgz#93b325b4b95c9d5b96df740f4204017f57396559"
@@ -281,18 +320,65 @@
   version "0.0.143"
   resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.143.tgz#7b6dfa611175ee1b74e3c392072589a79dadf265"
 
+"@vx/responsive@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/responsive/-/responsive-0.0.120.tgz#31c021f213796570787cdd2d2ddb90c4c9444e39"
+  dependencies:
+    lodash "^4.0.8"
+
 "@vx/responsive@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/responsive/-/responsive-0.0.140.tgz#c73ec68b9e89a181605f1ac9ecc09f35216779a8"
   dependencies:
     lodash "^4.0.8"
 
+"@vx/responsive@0.0.153":
+  version "0.0.153"
+  resolved "https://registry.yarnpkg.com/@vx/responsive/-/responsive-0.0.153.tgz#2ce7e819341d2e59ff4151b40e5792aea460e202"
+  dependencies:
+    lodash "^4.0.8"
+    resize-observer-polyfill "1.5.0"
+
+"@vx/scale@0.0.121":
+  version "0.0.121"
+  resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.121.tgz#5f49ea2060469ded0bf0e3ef5a5bb1416b81180e"
+  dependencies:
+    d3-scale "^1.0.5"
+
+"@vx/scale@0.0.127":
+  version "0.0.127"
+  resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.127.tgz#2f81530c89b1ad837be387aaccebedd507f16549"
+  dependencies:
+    d3-scale "^1.0.5"
+
 "@vx/scale@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.140.tgz#1eb087d11d0000b250c2cdc4061b9e2212edb10d"
   dependencies:
     d3-scale "^1.0.5"
 
+"@vx/shape@0.0.120":
+  version "0.0.120"
+  resolved "https://registry.yarnpkg.com/@vx/shape/-/shape-0.0.120.tgz#53d5457ec58e298bed507baff015fed2507ac741"
+  dependencies:
+    "@vx/curve" "0.0.112"
+    "@vx/group" "0.0.120"
+    "@vx/point" "0.0.112"
+    classnames "^2.2.5"
+    d3-shape "^1.2.0"
+    prop-types "^15.5.10"
+
+"@vx/shape@0.0.131":
+  version "0.0.131"
+  resolved "https://registry.yarnpkg.com/@vx/shape/-/shape-0.0.131.tgz#38585e93319c9f958d317485b0b6520ff295f179"
+  dependencies:
+    "@vx/curve" "0.0.127"
+    "@vx/group" "0.0.127"
+    "@vx/point" "0.0.127"
+    classnames "^2.2.5"
+    d3-shape "^1.2.0"
+    prop-types "^15.5.10"
+
 "@vx/shape@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/shape/-/shape-0.0.140.tgz#6a282d5fdf3a5752b6e938bb3debe983e89ff6d3"
@@ -315,11 +401,19 @@
     d3-shape "^1.2.0"
     prop-types "^15.5.10"
 
-"@vx/tooltip@0.0.140":
-  version "0.0.140"
-  resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.140.tgz#c5c8306272877c1bbd4e8b478ea5291f1019ffe3"
+"@vx/tooltip@0.0.126":
+  version "0.0.126"
+  resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.126.tgz#bcbd48bddf614585b11dff4d5b75ba35a42df68c"
   dependencies:
-    "@vx/bounds" "0.0.140"
+    classnames "^2.2.5"
+    prop-types "^15.5.10"
+    recompose "^0.23.5"
+
+"@vx/tooltip@0.0.134":
+  version "0.0.134"
+  resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.134.tgz#8337f0876a98b3eec8e9636b4694126789eba60c"
+  dependencies:
+    "@vx/bounds" "0.0.129"
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
@@ -332,12 +426,16 @@
     prop-types "^15.5.10"
 
 JSONStream@^1.3.2:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.3.tgz#27b4b8fbbfeab4e71bcf551e7f27be8d952239bf"
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
   dependencies:
     jsonparse "^1.2.0"
     through ">=2.2.7 <3"
 
+"JSV@>= 4.0.x":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
+
 abab@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -390,26 +488,26 @@ acorn@^5.5.0:
   version "5.5.3"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
 
-agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
+agent-base@4, agent-base@^4.1.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce"
   dependencies:
     es6-promisify "^5.0.0"
 
-agentkeepalive@^3.3.0, agentkeepalive@^3.4.1:
+agentkeepalive@^3.3.0:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c"
   dependencies:
     humanize-ms "^1.2.1"
 
+ajv-keywords@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
+
 ajv-keywords@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
 
-ajv-keywords@^3.1.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
-
 ajv@^4.9.1:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
@@ -417,7 +515,7 @@ ajv@^4.9.1:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^5.0.0, ajv@^5.1.0:
+ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5:
   version "5.2.3"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
   dependencies:
@@ -435,15 +533,6 @@ ajv@^5.2.3, ajv@^5.3.0:
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
 
-ajv@^6.1.0:
-  version "6.5.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.0.tgz#4c8affdf80887d8f132c9c52ab8a2dc4d0b7b24c"
-  dependencies:
-    fast-deep-equal "^2.0.1"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.3.0"
-    uri-js "^4.2.1"
-
 align-text@^0.1.1, align-text@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -466,7 +555,7 @@ ansi-align@^2.0.0:
   dependencies:
     string-width "^2.0.0"
 
-ansi-escapes@^1.0.0, ansi-escapes@^1.1.0, ansi-escapes@^1.3.0:
+ansi-escapes@^1.1.0, ansi-escapes@^1.3.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
 
@@ -514,10 +603,6 @@ ansistyles@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
 
-any-observable@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
-
 anymatch@^1.3.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -525,13 +610,6 @@ anymatch@^1.3.0:
     micromatch "^2.1.5"
     normalize-path "^2.0.0"
 
-anymatch@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
-  dependencies:
-    micromatch "^3.1.4"
-    normalize-path "^2.1.1"
-
 aphrodite@^1.2.0:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/aphrodite/-/aphrodite-1.2.4.tgz#5dc1622aa6f1b02c775e1f1ed850df08839e203c"
@@ -594,22 +672,10 @@ arr-diff@^2.0.0:
   dependencies:
     arr-flatten "^1.0.1"
 
-arr-diff@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
-
-arr-flatten@^1.0.1, arr-flatten@^1.1.0:
+arr-flatten@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
 
-arr-union@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-
-array-differ@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
-
 array-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
@@ -646,15 +712,11 @@ array-unique@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
 
-array-unique@^0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
-
-arrify@^1.0.0, arrify@^1.0.1:
+arrify@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
 
-asap@^2.0.0, asap@^2.0.3, asap@~2.0.3, asap@~2.0.5:
+asap@^2.0.0, asap@^2.0.3, asap@^2.0.6, asap@~2.0.3, asap@~2.0.5:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
 
@@ -696,37 +758,19 @@ assertion-error@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
 
-assign-symbols@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
-
 ast-types-flow@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
 
-ast-types@0.10.1:
-  version "0.10.1"
-  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd"
-
-ast-types@0.11.3:
-  version "0.11.3"
-  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8"
-
 async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
 
-async@1.x, async@^1.4.0, async@^1.5.0, async@~1.5:
+async@1.x, async@^1.4.0, async@~1.5:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
-async@^2.1.2, async@^2.6.0:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
-  dependencies:
-    lodash "^4.17.10"
-
-async@^2.1.4, async@^2.4.1:
+async@^2.1.2, async@^2.1.4, async@^2.4.1:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
   dependencies:
@@ -744,10 +788,6 @@ asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
 
-atob@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
-
 autoprefixer@^6.3.1:
   version "6.7.7"
   resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
@@ -847,14 +887,6 @@ babel-generator@^6.18.0, babel-generator@^6.26.0:
     source-map "^0.5.6"
     trim-right "^1.0.1"
 
-babel-helper-bindify-decorators@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330"
-  dependencies:
-    babel-runtime "^6.22.0"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
 babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
@@ -897,15 +929,6 @@ babel-helper-explode-assignable-expression@^6.24.1:
     babel-traverse "^6.24.1"
     babel-types "^6.24.1"
 
-babel-helper-explode-class@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb"
-  dependencies:
-    babel-helper-bindify-decorators "^6.24.1"
-    babel-runtime "^6.22.0"
-    babel-traverse "^6.24.1"
-    babel-types "^6.24.1"
-
 babel-helper-function-name@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
@@ -1025,34 +1048,10 @@ babel-plugin-syntax-async-functions@^6.8.0:
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
 
-babel-plugin-syntax-async-generators@^6.5.0:
-  version "6.13.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
-
-babel-plugin-syntax-class-constructor-call@^6.18.0:
-  version "6.18.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
-
-babel-plugin-syntax-class-properties@^6.8.0:
-  version "6.13.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
-
-babel-plugin-syntax-decorators@^6.13.0:
-  version "6.13.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
-
-babel-plugin-syntax-dynamic-import@^6.18.0:
-  version "6.18.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
-
 babel-plugin-syntax-exponentiation-operator@^6.8.0:
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
 
-babel-plugin-syntax-export-extensions@^6.8.0:
-  version "6.13.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
-
 babel-plugin-syntax-flow@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
@@ -1069,15 +1068,7 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
 
-babel-plugin-transform-async-generator-functions@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db"
-  dependencies:
-    babel-helper-remap-async-to-generator "^6.24.1"
-    babel-plugin-syntax-async-generators "^6.5.0"
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1:
+babel-plugin-transform-async-to-generator@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
   dependencies:
@@ -1085,33 +1076,6 @@ babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-
     babel-plugin-syntax-async-functions "^6.8.0"
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-class-constructor-call@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
-  dependencies:
-    babel-plugin-syntax-class-constructor-call "^6.18.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-
-babel-plugin-transform-class-properties@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
-  dependencies:
-    babel-helper-function-name "^6.24.1"
-    babel-plugin-syntax-class-properties "^6.8.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-
-babel-plugin-transform-decorators@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
-  dependencies:
-    babel-helper-explode-class "^6.24.1"
-    babel-plugin-syntax-decorators "^6.13.0"
-    babel-runtime "^6.22.0"
-    babel-template "^6.24.1"
-    babel-types "^6.24.1"
-
 babel-plugin-transform-es2015-arrow-functions@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
@@ -1124,7 +1088,7 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.24.1:
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
   dependencies:
@@ -1134,7 +1098,7 @@ babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es20
     babel-types "^6.26.0"
     lodash "^4.17.4"
 
-babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-classes@^6.24.1:
+babel-plugin-transform-es2015-classes@^6.23.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
   dependencies:
@@ -1148,33 +1112,33 @@ babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-cla
     babel-traverse "^6.24.1"
     babel-types "^6.24.1"
 
-babel-plugin-transform-es2015-computed-properties@^6.22.0, babel-plugin-transform-es2015-computed-properties@^6.24.1:
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
   dependencies:
     babel-runtime "^6.22.0"
     babel-template "^6.24.1"
 
-babel-plugin-transform-es2015-destructuring@^6.22.0, babel-plugin-transform-es2015-destructuring@^6.23.0:
+babel-plugin-transform-es2015-destructuring@^6.23.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-duplicate-keys@^6.22.0, babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
   dependencies:
     babel-runtime "^6.22.0"
     babel-types "^6.24.1"
 
-babel-plugin-transform-es2015-for-of@^6.22.0, babel-plugin-transform-es2015-for-of@^6.23.0:
+babel-plugin-transform-es2015-for-of@^6.23.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-function-name@^6.22.0, babel-plugin-transform-es2015-function-name@^6.24.1:
+babel-plugin-transform-es2015-function-name@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
   dependencies:
@@ -1205,7 +1169,7 @@ babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-e
     babel-template "^6.26.0"
     babel-types "^6.26.0"
 
-babel-plugin-transform-es2015-modules-systemjs@^6.23.0, babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
   dependencies:
@@ -1213,7 +1177,7 @@ babel-plugin-transform-es2015-modules-systemjs@^6.23.0, babel-plugin-transform-e
     babel-runtime "^6.22.0"
     babel-template "^6.24.1"
 
-babel-plugin-transform-es2015-modules-umd@^6.23.0, babel-plugin-transform-es2015-modules-umd@^6.24.1:
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
   dependencies:
@@ -1221,14 +1185,14 @@ babel-plugin-transform-es2015-modules-umd@^6.23.0, babel-plugin-transform-es2015
     babel-runtime "^6.22.0"
     babel-template "^6.24.1"
 
-babel-plugin-transform-es2015-object-super@^6.22.0, babel-plugin-transform-es2015-object-super@^6.24.1:
+babel-plugin-transform-es2015-object-super@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
   dependencies:
     babel-helper-replace-supers "^6.24.1"
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-parameters@^6.24.1:
+babel-plugin-transform-es2015-parameters@^6.23.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
   dependencies:
@@ -1239,7 +1203,7 @@ babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-
     babel-traverse "^6.24.1"
     babel-types "^6.24.1"
 
-babel-plugin-transform-es2015-shorthand-properties@^6.22.0, babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
   dependencies:
@@ -1252,7 +1216,7 @@ babel-plugin-transform-es2015-spread@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-sticky-regex@^6.22.0, babel-plugin-transform-es2015-sticky-regex@^6.24.1:
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
   dependencies:
@@ -1266,13 +1230,13 @@ babel-plugin-transform-es2015-template-literals@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-typeof-symbol@^6.22.0, babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-unicode-regex@^6.22.0, babel-plugin-transform-es2015-unicode-regex@^6.24.1:
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
   dependencies:
@@ -1300,14 +1264,7 @@ babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-e
     babel-plugin-syntax-exponentiation-operator "^6.8.0"
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-export-extensions@^6.22.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
-  dependencies:
-    babel-plugin-syntax-export-extensions "^6.8.0"
-    babel-runtime "^6.22.0"
-
-babel-plugin-transform-flow-strip-types@^6.22.0, babel-plugin-transform-flow-strip-types@^6.8.0:
+babel-plugin-transform-flow-strip-types@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
   dependencies:
@@ -1320,7 +1277,7 @@ babel-plugin-transform-jscript@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-object-rest-spread@^6.22.0, babel-plugin-transform-object-rest-spread@^6.23.0:
+babel-plugin-transform-object-rest-spread@^6.23.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
   dependencies:
@@ -1355,7 +1312,7 @@ babel-plugin-transform-react-jsx@^6.24.1:
     babel-plugin-syntax-jsx "^6.8.0"
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-regenerator@^6.22.0, babel-plugin-transform-regenerator@^6.24.1:
+babel-plugin-transform-regenerator@^6.22.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
   dependencies:
@@ -1427,35 +1384,6 @@ babel-preset-env@^1.5.2:
     invariant "^2.2.2"
     semver "^5.3.0"
 
-babel-preset-es2015@^6.9.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
-  dependencies:
-    babel-plugin-check-es2015-constants "^6.22.0"
-    babel-plugin-transform-es2015-arrow-functions "^6.22.0"
-    babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
-    babel-plugin-transform-es2015-block-scoping "^6.24.1"
-    babel-plugin-transform-es2015-classes "^6.24.1"
-    babel-plugin-transform-es2015-computed-properties "^6.24.1"
-    babel-plugin-transform-es2015-destructuring "^6.22.0"
-    babel-plugin-transform-es2015-duplicate-keys "^6.24.1"
-    babel-plugin-transform-es2015-for-of "^6.22.0"
-    babel-plugin-transform-es2015-function-name "^6.24.1"
-    babel-plugin-transform-es2015-literals "^6.22.0"
-    babel-plugin-transform-es2015-modules-amd "^6.24.1"
-    babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
-    babel-plugin-transform-es2015-modules-systemjs "^6.24.1"
-    babel-plugin-transform-es2015-modules-umd "^6.24.1"
-    babel-plugin-transform-es2015-object-super "^6.24.1"
-    babel-plugin-transform-es2015-parameters "^6.24.1"
-    babel-plugin-transform-es2015-shorthand-properties "^6.24.1"
-    babel-plugin-transform-es2015-spread "^6.22.0"
-    babel-plugin-transform-es2015-sticky-regex "^6.24.1"
-    babel-plugin-transform-es2015-template-literals "^6.22.0"
-    babel-plugin-transform-es2015-typeof-symbol "^6.22.0"
-    babel-plugin-transform-es2015-unicode-regex "^6.24.1"
-    babel-plugin-transform-regenerator "^6.24.1"
-
 babel-preset-flow@^6.23.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d"
@@ -1473,34 +1401,7 @@ babel-preset-react@^6.24.1:
     babel-plugin-transform-react-jsx-source "^6.22.0"
     babel-preset-flow "^6.23.0"
 
-babel-preset-stage-1@^6.5.0:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
-  dependencies:
-    babel-plugin-transform-class-constructor-call "^6.24.1"
-    babel-plugin-transform-export-extensions "^6.22.0"
-    babel-preset-stage-2 "^6.24.1"
-
-babel-preset-stage-2@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
-  dependencies:
-    babel-plugin-syntax-dynamic-import "^6.18.0"
-    babel-plugin-transform-class-properties "^6.24.1"
-    babel-plugin-transform-decorators "^6.24.1"
-    babel-preset-stage-3 "^6.24.1"
-
-babel-preset-stage-3@^6.24.1:
-  version "6.24.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395"
-  dependencies:
-    babel-plugin-syntax-trailing-function-commas "^6.22.0"
-    babel-plugin-transform-async-generator-functions "^6.24.1"
-    babel-plugin-transform-async-to-generator "^6.24.1"
-    babel-plugin-transform-exponentiation-operator "^6.24.1"
-    babel-plugin-transform-object-rest-spread "^6.22.0"
-
-babel-register@^6.24.1, babel-register@^6.26.0, babel-register@^6.9.0:
+babel-register@^6.24.1, babel-register@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
   dependencies:
@@ -1552,14 +1453,10 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26
     lodash "^4.17.4"
     to-fast-properties "^1.0.3"
 
-babylon@^6.15.0, babylon@^6.17.3, babylon@^6.18.0:
+babylon@^6.15.0, babylon@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
 
-babylon@^7.0.0-beta.30:
-  version "7.0.0-beta.47"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.47.tgz#6d1fa44f0abec41ab7c780481e62fd9aafbdea80"
-
 balanced-match@^0.4.2:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -1576,18 +1473,6 @@ base64-js@^1.0.2:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
 
-base@^0.11.1:
-  version "0.11.2"
-  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
-  dependencies:
-    cache-base "^1.0.1"
-    class-utils "^0.3.5"
-    component-emitter "^1.2.1"
-    define-property "^1.0.0"
-    isobject "^3.0.1"
-    mixin-deep "^1.2.0"
-    pascalcase "^0.1.1"
-
 bcrypt-pbkdf@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
@@ -1598,7 +1483,7 @@ big.js@^3.1.3:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
 
-bin-links@^1.1.2:
+bin-links@^1.1.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.2.tgz#fb74bd54bae6b7befc6c6221f25322ac830d9757"
   dependencies:
@@ -1612,10 +1497,6 @@ binary-extensions@^1.0.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
 
-binaryextensions@2:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
-
 bl@~0.9.4:
   version "0.9.5"
   resolved "https://registry.yarnpkg.com/bl/-/bl-0.9.5.tgz#c06b797af085ea00bc527afc8efcf11de2232054"
@@ -1674,15 +1555,15 @@ boom@5.x.x:
   dependencies:
     hoek "4.x.x"
 
-bootstrap-slider@9.9.0:
-  version "9.9.0"
-  resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-9.9.0.tgz#4e14ecc6401901da1ddf7681aa24e33b00dadce8"
-
 bootstrap-slider@^10.0.0:
   version "10.0.0"
   resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-10.0.0.tgz#d4edd3a10af03197d020e3792d32ea6d37cb3b28"
 
-bootstrap@^3.3.6:
+bootstrap-slider@^9.8.0:
+  version "9.10.0"
+  resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-9.10.0.tgz#1103d6bc00cfbfa8cfc9a2599ab518c55643da3f"
+
+bootstrap@^3.3.6, bootstrap@^3.3.7:
   version "3.3.7"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
 
@@ -1740,10 +1621,6 @@ brace@^0.10.0:
   dependencies:
     w3c-blob "0.0.1"
 
-brace@^0.11.1:
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
-
 braces@^1.8.2:
   version "1.8.5"
   resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
@@ -1752,21 +1629,6 @@ braces@^1.8.2:
     preserve "^0.2.0"
     repeat-element "^1.1.2"
 
-braces@^2.3.0, braces@^2.3.1:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
-  dependencies:
-    arr-flatten "^1.1.0"
-    array-unique "^0.3.2"
-    extend-shallow "^2.0.1"
-    fill-range "^4.0.0"
-    isobject "^3.0.1"
-    repeat-element "^1.1.2"
-    snapdragon "^0.8.1"
-    snapdragon-node "^2.0.1"
-    split-string "^3.0.2"
-    to-regex "^3.0.1"
-
 brcast@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.2.tgz#2db16de44140e418dc37fab10beec0369e78dcef"
@@ -1780,15 +1642,6 @@ brfs@^1.3.0, brfs@^1.4.0, brfs@^1.4.3:
     static-module "^1.1.0"
     through2 "^2.0.0"
 
-brfs@^1.4.4:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3"
-  dependencies:
-    quote-stream "^1.0.1"
-    resolve "^1.1.5"
-    static-module "^2.2.0"
-    through2 "^2.0.0"
-
 brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -1930,10 +1783,6 @@ byline@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
 
-byte-size@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.3.tgz#b7c095efc68eadf82985fccd9a2df43a74fa2ccd"
-
 cacache@^10.0.0, cacache@^10.0.4:
   version "10.0.4"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
@@ -1970,51 +1819,6 @@ cacache@^10.0.1:
     unique-filename "^1.1.0"
     y18n "^3.2.1"
 
-cacache@^11.0.1, cacache@^11.0.2:
-  version "11.0.2"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.0.2.tgz#ff30541a05302200108a759e660e30786f788764"
-  dependencies:
-    bluebird "^3.5.1"
-    chownr "^1.0.1"
-    figgy-pudding "^3.1.0"
-    glob "^7.1.2"
-    graceful-fs "^4.1.11"
-    lru-cache "^4.1.2"
-    mississippi "^3.0.0"
-    mkdirp "^0.5.1"
-    move-concurrently "^1.0.1"
-    promise-inflight "^1.0.1"
-    rimraf "^2.6.2"
-    ssri "^6.0.0"
-    unique-filename "^1.1.0"
-    y18n "^4.0.0"
-
-cache-base@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
-  dependencies:
-    collection-visit "^1.0.0"
-    component-emitter "^1.2.1"
-    get-value "^2.0.6"
-    has-value "^1.0.0"
-    isobject "^3.0.1"
-    set-value "^2.0.0"
-    to-object-path "^0.3.0"
-    union-value "^1.0.0"
-    unset-value "^1.0.0"
-
-cacheable-request@^2.1.1:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
-  dependencies:
-    clone-response "1.0.2"
-    get-stream "3.0.0"
-    http-cache-semantics "3.8.1"
-    keyv "3.0.0"
-    lowercase-keys "1.0.0"
-    normalize-url "2.0.1"
-    responselike "1.0.2"
-
 call-limit@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea"
@@ -2028,10 +1832,6 @@ call-matcher@^1.0.1:
     espurify "^1.6.0"
     estraverse "^4.0.0"
 
-call-me-maybe@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
-
 caller-path@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -2138,14 +1938,6 @@ chalk@^2.1.0:
     escape-string-regexp "^1.0.5"
     supports-color "^4.0.0"
 
-chalk@^2.3.0, chalk@^2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
-  dependencies:
-    ansi-styles "^3.2.1"
-    escape-string-regexp "^1.0.5"
-    supports-color "^5.3.0"
-
 chalk@~0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
@@ -2191,7 +1983,7 @@ cheerio@^0.22.0:
     lodash.reject "^4.4.0"
     lodash.some "^4.4.0"
 
-chokidar@^1.6.1:
+chokidar@^1.6.1, chokidar@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
   dependencies:
@@ -2206,24 +1998,6 @@ chokidar@^1.6.1:
   optionalDependencies:
     fsevents "^1.0.0"
 
-chokidar@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
-  dependencies:
-    anymatch "^2.0.0"
-    async-each "^1.0.0"
-    braces "^2.3.0"
-    glob-parent "^3.1.0"
-    inherits "^2.0.1"
-    is-binary-path "^1.0.0"
-    is-glob "^4.0.0"
-    normalize-path "^2.1.1"
-    path-is-absolute "^1.0.0"
-    readdirp "^2.0.0"
-    upath "^1.0.0"
-  optionalDependencies:
-    fsevents "^1.1.2"
-
 chownr@^1.0.1, chownr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
@@ -2232,11 +2006,9 @@ ci-info@^1.0.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2"
 
-cidr-regex@^2.0.8:
-  version "2.0.8"
-  resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.8.tgz#c79bae6223d241c0860d93bfde1fb1c1c4fdcab6"
-  dependencies:
-    ip-regex "^2.1.0"
+cidr-regex@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
 
 cint@^8.2.1:
   version "8.2.1"
@@ -2259,16 +2031,7 @@ clap@^1.0.9:
   dependencies:
     chalk "^1.1.3"
 
-class-utils@^0.3.5:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
-  dependencies:
-    arr-union "^3.1.0"
-    define-property "^0.2.5"
-    isobject "^3.0.0"
-    static-extend "^0.1.1"
-
-classnames@2.x, classnames@^2.1.2, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
+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"
 
@@ -2280,16 +2043,9 @@ clean-webpack-plugin@^0.1.16:
 
 cli-boxes@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
-
-cli-columns@^3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e"
-  dependencies:
-    string-width "^2.0.0"
-    strip-ansi "^3.0.1"
+  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
 
-cli-cursor@^1.0.1, cli-cursor@^1.0.2:
+cli-cursor@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
   dependencies:
@@ -2301,11 +2057,7 @@ cli-cursor@^2.1.0:
   dependencies:
     restore-cursor "^2.0.0"
 
-cli-spinners@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
-
-cli-table2@^0.2.0, cli-table2@~0.2.0:
+cli-table2@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
   dependencies:
@@ -2320,25 +2072,10 @@ cli-table@^0.3.1:
   dependencies:
     colors "1.0.3"
 
-cli-truncate@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
-  dependencies:
-    slice-ansi "0.0.4"
-    string-width "^1.0.1"
-
 cli-width@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
 
-clipboard@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.1.tgz#a12481e1c13d8a50f5f036b0560fe5d16d74e46a"
-  dependencies:
-    good-listener "^1.2.2"
-    select "^1.1.2"
-    tiny-emitter "^2.0.0"
-
 clite@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/clite/-/clite-0.3.0.tgz#e7fcbc8cc5bd3e7f8b84ed48db12e9474cc73441"
@@ -2370,17 +2107,13 @@ cliui@^3.2.0:
     wrap-ansi "^2.0.0"
 
 cliui@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
   dependencies:
     string-width "^2.1.1"
     strip-ansi "^4.0.0"
     wrap-ansi "^2.0.0"
 
-clone-buffer@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
-
 clone-deep@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8"
@@ -2390,24 +2123,6 @@ clone-deep@^0.3.0:
     kind-of "^3.2.2"
     shallow-clone "^0.1.2"
 
-clone-response@1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
-  dependencies:
-    mimic-response "^1.0.0"
-
-clone-stats@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
-
-clone-stats@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
-
-clone@^1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
-
 clone@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
@@ -2416,14 +2131,6 @@ clone@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
 
-cloneable-readable@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.2.tgz#d591dee4a8f8bc15da43ce97dceeba13d43e2a65"
-  dependencies:
-    inherits "^2.0.1"
-    process-nextick-args "^2.0.0"
-    readable-stream "^2.3.5"
-
 cmd-shim@^2.0.2, cmd-shim@~2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
@@ -2445,13 +2152,6 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
-collection-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
-  dependencies:
-    map-visit "^1.0.0"
-    object-visit "^1.0.0"
-
 color-convert@^1.3.0, color-convert@^1.9.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
@@ -2493,8 +2193,8 @@ colors@1.0.3:
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
 colors@^1.1.2:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e"
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794"
 
 colors@~1.1.2:
   version "1.1.2"
@@ -2519,12 +2219,6 @@ combined-stream@~0.0.4:
   dependencies:
     delayed-stream "0.0.5"
 
-comma-separated-tokens@^1.0.0:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz#b13793131d9ea2d2431cf5b507ddec258f0ce0db"
-  dependencies:
-    trim "0.0.1"
-
 commander@2.9.0:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
@@ -2547,10 +2241,6 @@ complex.js@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373"
 
-component-emitter@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
-
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2629,7 +2319,7 @@ console-browserify@^1.1.0:
   dependencies:
     date-now "^0.1.4"
 
-console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
 
@@ -2649,10 +2339,6 @@ convert-source-map@^1.1.1, convert-source-map@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
 
-convert-source-map@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
-
 cookie-jar@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/cookie-jar/-/cookie-jar-0.3.0.tgz#bc9a27d4e2b97e186cd57c9e2063cb99fa68cccc"
@@ -2668,10 +2354,6 @@ copy-concurrently@^1.0.0:
     rimraf "^2.5.4"
     run-queue "^1.0.0"
 
-copy-descriptor@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-
 core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -2680,10 +2362,6 @@ core-js@^2.0.0, core-js@^2.4.0, core-js@^2.5.0:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
 
-core-js@^2.4.1:
-  version "2.5.7"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
-
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -2721,7 +2399,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
-create-react-class@^15.5.2, create-react-class@^15.6.0:
+create-react-class@^15.5.2, create-react-class@^15.5.x, create-react-class@^15.6.0:
   version "15.6.2"
   resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a"
   dependencies:
@@ -2737,16 +2415,6 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
-  dependencies:
-    nice-try "^1.0.4"
-    path-key "^2.0.1"
-    semver "^5.5.0"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
-
 crypt@~0.0.1:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
@@ -2935,7 +2603,7 @@ d3-cloud@^1.2.1:
   dependencies:
     d3-dispatch "1"
 
-d3-collection@1, d3-collection@^1.0.4:
+d3-collection@1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
 
@@ -3018,10 +2686,6 @@ d3-selection@1, d3-selection@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.1.0.tgz#1998684896488f839ca0372123da34f1d318809c"
 
-d3-selection@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
-
 d3-shape@^1.0.6, d3-shape@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
@@ -3046,12 +2710,11 @@ d3-timer@1:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
 
-d3-tip@^0.9.1:
-  version "0.9.1"
-  resolved "https://registry.yarnpkg.com/d3-tip/-/d3-tip-0.9.1.tgz#84e6d331c4e6650d80c5228a07e41820609ab64b"
+d3-tip@^0.6.7:
+  version "0.6.8"
+  resolved "https://registry.yarnpkg.com/d3-tip/-/d3-tip-0.6.8.tgz#e5b4491ae8983fde646ea49008ff542a033c0a2c"
   dependencies:
-    d3-collection "^1.0.4"
-    d3-selection "^1.3.0"
+    d3 "^3.5.5"
 
 d3-transition@1:
   version "1.1.0"
@@ -3074,7 +2737,7 @@ d3-zoom@^1.3.0:
     d3-selection "1"
     d3-transition "1"
 
-d3@3, d3@^3.5.17, d3@^3.5.6:
+d3@3, d3@^3.5.17, d3@^3.5.5, d3@^3.5.6:
   version "3.5.17"
   resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
 
@@ -3088,10 +2751,6 @@ damerau-levenshtein@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
 
-dargs@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829"
-
 dashdash@^1.12.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -3118,18 +2777,10 @@ datatables.net@1.10.16:
   dependencies:
     jquery ">=1.7"
 
-date-fns@^1.27.2:
-  version "1.29.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
-
 date-now@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
 
-dateformat@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
-
 debug@2.6.8:
   version "2.6.8"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
@@ -3142,7 +2793,7 @@ debug@3.1.0, debug@^3.1.0:
   dependencies:
     ms "2.0.0"
 
-debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.3, debug@^2.6.8:
+debug@^2.1.2, debug@^2.2.0, debug@^2.6.3, debug@^2.6.8:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -3178,12 +2829,6 @@ decode-uri-component@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
 
-decompress-response@^3.2.0, decompress-response@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
-  dependencies:
-    mimic-response "^1.0.0"
-
 deep-eql@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
@@ -3194,10 +2839,6 @@ deep-equal@^1.0.0, deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
 
-deep-extend@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
-
 deep-extend@~0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@@ -3229,25 +2870,6 @@ define-properties@^1.1.2:
     foreach "^2.0.5"
     object-keys "^1.0.8"
 
-define-property@^0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
-  dependencies:
-    is-descriptor "^0.1.0"
-
-define-property@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
-  dependencies:
-    is-descriptor "^1.0.0"
-
-define-property@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
-  dependencies:
-    is-descriptor "^1.0.2"
-    isobject "^3.0.1"
-
 defined@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -3272,10 +2894,6 @@ delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
 
-delegate@^3.1.2:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
-
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -3287,10 +2905,6 @@ des.js@^1.0.0:
     inherits "^2.0.1"
     minimalistic-assert "^1.0.0"
 
-detect-conflict@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e"
-
 detect-indent@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
@@ -3301,10 +2915,6 @@ detect-indent@~5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
 
-detect-libc@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
-
 detect-newline@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
@@ -3320,9 +2930,9 @@ diff@3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
 
-diff@^3.1.0, diff@^3.3.1, diff@^3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+diff@^3.1.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
 
 diffie-hellman@^5.0.0:
   version "5.0.2"
@@ -3332,12 +2942,9 @@ diffie-hellman@^5.0.0:
     miller-rabin "^4.0.0"
     randombytes "^2.0.0"
 
-dir-glob@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
-  dependencies:
-    arrify "^1.0.1"
-    path-type "^3.0.0"
+disposables@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.2.tgz#36c6a674475f55a2d6913567a601444e487b4b6e"
 
 distributions@^1.0.0:
   version "1.0.0"
@@ -3345,6 +2952,15 @@ distributions@^1.0.0:
   dependencies:
     mathfn "^1.0.0"
 
+dnd-core@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.6.0.tgz#12bad66d58742c6e5f7cf2943fb6859440f809c4"
+  dependencies:
+    asap "^2.0.6"
+    invariant "^2.0.0"
+    lodash "^4.2.0"
+    redux "^3.7.1"
+
 doctrine@1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -3428,7 +3044,7 @@ dotenv@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
 
-duplexer2@^0.1.4, duplexer2@~0.1.4:
+duplexer2@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
   dependencies:
@@ -3476,26 +3092,14 @@ ecc-jsbn@~0.1.1:
   dependencies:
     jsbn "~0.1.0"
 
-editions@^1.3.3:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
-
 editor@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742"
 
-ejs@^2.5.9:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
-
 electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18:
   version "1.3.24"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6"
 
-elegant-spinner@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
-
 elliptic@^6.0.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -3541,22 +3145,10 @@ enhanced-resolve@^3.4.0:
     object-assign "^4.0.1"
     tapable "^0.2.7"
 
-enhanced-resolve@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a"
-  dependencies:
-    graceful-fs "^4.1.2"
-    memory-fs "^0.4.0"
-    tapable "^1.0.0"
-
 entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
-envinfo@^5.7.0:
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-5.9.0.tgz#1e681b7400c384c679ffc886b6e3f811d7e687b5"
-
 enzyme@^2.0.0:
   version "2.9.1"
   resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.9.1.tgz#07d5ce691241240fb817bf2c4b18d6e530240df6"
@@ -3594,19 +3186,12 @@ errno@~0.1.7:
   dependencies:
     prr "~1.0.1"
 
-error-ex@^1.2.0, error-ex@^1.3.1:
+error-ex@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
   dependencies:
     is-arrayish "^0.2.1"
 
-error@^7.0.2:
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
-  dependencies:
-    string-template "~0.2.1"
-    xtend "~4.0.0"
-
 es-abstract@^1.6.1, es-abstract@^1.7.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
@@ -3632,15 +3217,7 @@ es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
     es6-iterator "2"
     es6-symbol "~3.1"
 
-es5-ext@^0.10.35:
-  version "0.10.44"
-  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.44.tgz#7a877e994bb190aebd4718aa2d62784a12e2ca20"
-  dependencies:
-    es6-iterator "~2.0.3"
-    es6-symbol "~3.1.1"
-    next-tick "1"
-
-es6-iterator@2:
+es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
   dependencies:
@@ -3648,14 +3225,6 @@ es6-iterator@2:
     es5-ext "^0.10.14"
     es6-symbol "^3.1"
 
-es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
-  dependencies:
-    d "1"
-    es5-ext "^0.10.35"
-    es6-symbol "^3.1.1"
-
 es6-map@^0.1.3:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
@@ -3707,10 +3276,6 @@ es6-weak-map@^2.0.1:
     es6-iterator "^2.0.1"
     es6-symbol "^3.1.1"
 
-es6bindall@^0.0.9:
-  version "0.0.9"
-  resolved "https://registry.yarnpkg.com/es6bindall/-/es6bindall-0.0.9.tgz#71e00afa69f8dd59ac5ac898a0d31c978df817d5"
-
 escape-latex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.0.0.tgz#74b9e94d8c178645704c33791e95a4155b59718f"
@@ -3741,17 +3306,6 @@ escodegen@^1.6.1:
   optionalDependencies:
     source-map "~0.5.6"
 
-escodegen@^1.8.1, escodegen@~1.9.0:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
-  dependencies:
-    esprima "^3.1.3"
-    estraverse "^4.2.0"
-    esutils "^2.0.2"
-    optionator "^0.8.1"
-  optionalDependencies:
-    source-map "~0.6.1"
-
 escodegen@~0.0.24:
   version "0.0.28"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-0.0.28.tgz#0e4ff1715f328775d6cab51ac44a406cd7abffd3"
@@ -3792,6 +3346,12 @@ eslint-config-airbnb@^15.0.1:
   dependencies:
     eslint-config-airbnb-base "^11.3.0"
 
+eslint-config-prettier@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz#5ecd65174d486c22dff389fe036febf502d468a3"
+  dependencies:
+    get-stdin "^5.0.1"
+
 eslint-import-resolver-node@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
@@ -3833,6 +3393,13 @@ eslint-plugin-jsx-a11y@^5.1.1:
     emoji-regex "^6.1.0"
     jsx-ast-utils "^1.4.0"
 
+eslint-plugin-prettier@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz#33e4e228bdb06142d03c560ce04ec23f6c767dd7"
+  dependencies:
+    fast-diff "^1.1.1"
+    jest-docblock "^21.0.0"
+
 eslint-plugin-react@^7.0.1:
   version "7.4.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz#300a95861b9729c087d362dd64abcc351a74364a"
@@ -3915,7 +3482,7 @@ esprima@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
 
-esprima@^4.0.0, esprima@~4.0.0:
+esprima@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
 
@@ -4000,10 +3567,6 @@ 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"
@@ -4014,40 +3577,22 @@ expand-brackets@^0.1.4:
   dependencies:
     is-posix-bracket "^0.1.0"
 
-expand-brackets@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
-  dependencies:
-    debug "^2.3.3"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    posix-character-classes "^0.1.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
 expand-range@^1.8.1:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
   dependencies:
     fill-range "^2.1.0"
 
-expand-tilde@^2.0.0, expand-tilde@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
-  dependencies:
-    homedir-polyfill "^1.0.1"
-
 expect.js@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1"
 
-exports-loader@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.7.0.tgz#84881c784dea6036b8e1cd1dac3da9b6409e21a5"
+exports-loader@^0.6.3:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886"
   dependencies:
-    loader-utils "^1.1.0"
-    source-map "0.5.0"
+    loader-utils "^1.0.2"
+    source-map "0.5.x"
 
 extend-shallow@^2.0.1:
   version "2.0.1"
@@ -4055,13 +3600,6 @@ extend-shallow@^2.0.1:
   dependencies:
     is-extendable "^0.1.0"
 
-extend-shallow@^3.0.0, extend-shallow@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
-  dependencies:
-    assign-symbols "^1.0.0"
-    is-extendable "^1.0.1"
-
 extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
@@ -4078,36 +3616,15 @@ external-editor@^2.0.4:
     iconv-lite "^0.4.17"
     tmp "^0.0.33"
 
-external-editor@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
-  dependencies:
-    chardet "^0.4.0"
-    iconv-lite "^0.4.17"
-    tmp "^0.0.33"
-
 extglob@^0.3.1:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
   dependencies:
     is-extglob "^1.0.0"
 
-extglob@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
-  dependencies:
-    array-unique "^0.3.2"
-    define-property "^1.0.0"
-    expand-brackets "^2.1.4"
-    extend-shallow "^2.0.1"
-    fragment-cache "^0.2.1"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-extract-text-webpack-plugin@3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7"
+extract-text-webpack-plugin@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612"
   dependencies:
     async "^2.4.1"
     loader-utils "^1.1.0"
@@ -4131,25 +3648,10 @@ fast-deep-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
 
-fast-deep-equal@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
-
-fast-diff@^1.0.1:
+fast-diff@^1.0.1, fast-diff@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
 
-fast-glob@^2.0.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.2.tgz#71723338ac9b4e0e2fff1d6748a2a13d5ed352bf"
-  dependencies:
-    "@mrmlnc/readdir-enhanced" "^2.2.1"
-    "@nodelib/fs.stat" "^1.0.1"
-    glob-parent "^3.1.0"
-    is-glob "^4.0.0"
-    merge2 "^1.2.1"
-    micromatch "^3.1.10"
-
 fast-json-stable-stringify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@@ -4184,11 +3686,7 @@ fbjs@^0.8.1, fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9:
     setimmediate "^1.0.5"
     ua-parser-js "^0.7.9"
 
-figgy-pudding@^3.0.0, figgy-pudding@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.1.0.tgz#a77ed2284175976c424b390b298569e9df86dd1e"
-
-figures@^1.3.5, figures@^1.7.0:
+figures@^1.3.5:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
   dependencies:
@@ -4235,15 +3733,6 @@ fill-range@^2.1.0:
     repeat-element "^1.1.2"
     repeat-string "^1.5.2"
 
-fill-range@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-    to-regex-range "^2.1.0"
-
 filled-array@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84"
@@ -4273,12 +3762,6 @@ find-up@^2.0.0, find-up@^2.1.0:
   dependencies:
     locate-path "^2.0.0"
 
-first-chunk-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
-  dependencies:
-    readable-stream "^2.0.2"
-
 flat-cache@^1.2.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
@@ -4292,10 +3775,6 @@ flatten@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
 
-flow-parser@^0.*:
-  version "0.73.0"
-  resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.73.0.tgz#525ac0776f743e16b6dca1a3dd6c602260b15773"
-
 flow-remove-types@^1.1.2:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-1.2.1.tgz#58e261bf8b842bd234c86cafb982a1213aff0edb"
@@ -4314,7 +3793,7 @@ for-in@^0.1.3:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
 
-for-in@^1.0.1, for-in@^1.0.2:
+for-in@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
 
@@ -4374,16 +3853,16 @@ form-data@~2.3.1:
     combined-stream "^1.0.5"
     mime-types "^2.1.12"
 
+formatio@1.2.0, formatio@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
+  dependencies:
+    samsam "1.x"
+
 fraction.js@4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.4.tgz#04e567110718adf7b52974a10434ab4c67a5183e"
 
-fragment-cache@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
-  dependencies:
-    map-cache "^0.2.2"
-
 from2@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/from2/-/from2-1.3.0.tgz#88413baaa5f9a597cfde9221d86986cd3c061dfd"
@@ -4391,7 +3870,7 @@ from2@^1.3.0:
     inherits "~2.0.1"
     readable-stream "~1.1.10"
 
-from2@^2.1.0, from2@^2.1.1:
+from2@^2.1.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
   dependencies:
@@ -4446,13 +3925,6 @@ fsevents@^1.0.0:
     nan "^2.3.0"
     node-pre-gyp "^0.6.36"
 
-fsevents@^1.1.2:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
-  dependencies:
-    nan "^2.9.2"
-    node-pre-gyp "^0.10.0"
-
 fstream-ignore@^1.0.0, fstream-ignore@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
@@ -4493,6 +3965,10 @@ functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
 
+fuse.js@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4"
+
 gauge@~2.6.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.6.0.tgz#d35301ad18e96902b4751dcbbe40f4218b942a46"
@@ -4607,9 +4083,9 @@ geojson-vt@^2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-2.4.0.tgz#3c1cf44493f35eb4d2c70c95da6550de66072c05"
 
-geojson-vt@^3.1.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.1.2.tgz#ea98849bd7717979db4c86fed8ef4e47475e4bab"
+geojson-vt@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.0.0.tgz#a24cae5488ab4897e86ca0e4bf0d9760d628ae0a"
 
 geolib@^2.0.24:
   version "2.0.24"
@@ -4627,14 +4103,10 @@ get-stdin@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
 
-get-stream@3.0.0, get-stream@^3.0.0:
+get-stream@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
 
-get-value@^2.0.3, get-value@^2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
-
 getpass@^0.1.1:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -4647,13 +4119,6 @@ gettext-parser@1.1.0:
   dependencies:
     encoding "^0.1.11"
 
-gh-got@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0"
-  dependencies:
-    got "^7.0.0"
-    is-plain-obj "^1.1.0"
-
 ghauth@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/ghauth/-/ghauth-3.0.0.tgz#8292a24ef47899f180a39c780c4809561294bdbc"
@@ -4688,12 +4153,6 @@ github-commit-stream@0.1.0:
     request "~2.22.0"
     through "~2.3.4"
 
-github-username@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
-  dependencies:
-    gh-got "^6.0.0"
-
 github@0.1.16:
   version "0.1.16"
   resolved "https://registry.yarnpkg.com/github/-/github-0.1.16.tgz#895d2a85b0feb7980d89ac0ce4f44dcaa03f17b5"
@@ -4746,13 +4205,6 @@ glamorous@^3.13.1:
     react-html-attributes "^1.3.0"
     svg-tag-names "^1.1.0"
 
-glob-all@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab"
-  dependencies:
-    glob "^7.0.5"
-    yargs "~1.2.6"
-
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -4766,13 +4218,6 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob-parent@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
-  dependencies:
-    is-glob "^3.1.0"
-    path-dirname "^1.0.0"
-
 glob-to-regexp@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
@@ -4798,7 +4243,7 @@ glob@7.1.1:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.0, glob@~7.1.2:
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.0, glob@~7.1.2:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
   dependencies:
@@ -4822,24 +4267,6 @@ global-dirs@^0.1.0:
   dependencies:
     ini "^1.3.4"
 
-global-modules@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
-  dependencies:
-    global-prefix "^1.0.1"
-    is-windows "^1.0.1"
-    resolve-dir "^1.0.0"
-
-global-prefix@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
-  dependencies:
-    expand-tilde "^2.0.2"
-    homedir-polyfill "^1.0.1"
-    ini "^1.3.4"
-    is-windows "^1.0.1"
-    which "^1.2.14"
-
 globals@^11.0.1:
   version "11.4.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.4.0.tgz#b85c793349561c16076a3c13549238a27945f1bc"
@@ -4859,24 +4286,6 @@ globby@^5.0.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-globby@^8.0.0, globby@^8.0.1:
-  version "8.0.1"
... 3609 lines suppressed ...


[incubator-superset] 18/26: add slice from explore view (#5141)

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

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

commit 7ad60ba84ba0bcaf0c34d8291d74bed97dbfe659
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Wed Jun 6 17:00:54 2018 -0700

    add slice from explore view (#5141)
---
 .../util/findFirstParentContainer_spec.js          | 116 +++++++++++++++++++++
 .../src/dashboard/reducers/getInitialState.js      |  65 ++++++++----
 .../src/dashboard/util/dashboardLayoutConverter.js |  37 ++-----
 .../src/dashboard/util/findFirstParentContainer.js |  19 ++++
 .../assets/src/dashboard/util/getEmptyLayout.js    |  23 ++++
 5 files changed, 215 insertions(+), 45 deletions(-)

diff --git a/superset/assets/spec/javascripts/dashboard/util/findFirstParentContainer_spec.js b/superset/assets/spec/javascripts/dashboard/util/findFirstParentContainer_spec.js
new file mode 100644
index 0000000..4ab29bd
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/findFirstParentContainer_spec.js
@@ -0,0 +1,116 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import findFirstParentContainerId from '../../../../src/dashboard/util/findFirstParentContainer';
+import {
+  DASHBOARD_GRID_ID,
+  DASHBOARD_ROOT_ID,
+} from '../../../../src/dashboard/util/constants';
+
+describe('findFirstParentContainer', () => {
+  const mockGridLayout = {
+    DASHBOARD_VERSION_KEY: 'v2',
+    DASHBOARD_ROOT_ID: {
+      type: 'DASHBOARD_ROOT_TYPE',
+      id: 'DASHBOARD_ROOT_ID',
+      children: ['DASHBOARD_GRID_ID'],
+    },
+    DASHBOARD_GRID_ID: {
+      type: 'DASHBOARD_GRID_TYPE',
+      id: 'DASHBOARD_GRID_ID',
+      children: ['DASHBOARD_ROW_TYPE-Bk45URrlQ'],
+    },
+    'DASHBOARD_ROW_TYPE-Bk45URrlQ': {
+      type: 'DASHBOARD_ROW_TYPE',
+      id: 'DASHBOARD_ROW_TYPE-Bk45URrlQ',
+      children: ['DASHBOARD_CHART_TYPE-ryxVc8RHlX'],
+    },
+    'DASHBOARD_CHART_TYPE-ryxVc8RHlX': {
+      type: 'DASHBOARD_CHART_TYPE',
+      id: 'DASHBOARD_CHART_TYPE-ryxVc8RHlX',
+      children: [],
+    },
+    DASHBOARD_HEADER_ID: {
+      id: 'DASHBOARD_HEADER_ID',
+      type: 'DASHBOARD_HEADER_TYPE',
+    },
+  };
+  const mockTabsLayout = {
+    'DASHBOARD_CHART_TYPE-S1gilYABe7': {
+      children: [],
+      id: 'DASHBOARD_CHART_TYPE-S1gilYABe7',
+      type: 'DASHBOARD_CHART_TYPE',
+    },
+    'DASHBOARD_CHART_TYPE-SJli5K0HlQ': {
+      children: [],
+      id: 'DASHBOARD_CHART_TYPE-SJli5K0HlQ',
+      type: 'DASHBOARD_CHART_TYPE',
+    },
+    DASHBOARD_GRID_ID: {
+      children: [],
+      id: 'DASHBOARD_GRID_ID',
+      type: 'DASHBOARD_GRID_TYPE',
+    },
+    DASHBOARD_HEADER_ID: {
+      id: 'DASHBOARD_HEADER_ID',
+      type: 'DASHBOARD_HEADER_TYPE',
+    },
+    DASHBOARD_ROOT_ID: {
+      children: ['DASHBOARD_TABS_TYPE-SkgJ5t0Bem'],
+      id: 'DASHBOARD_ROOT_ID',
+      type: 'DASHBOARD_ROOT_TYPE',
+    },
+    'DASHBOARD_ROW_TYPE-S1B8-JLgX': {
+      children: ['DASHBOARD_CHART_TYPE-SJli5K0HlQ'],
+      id: 'DASHBOARD_ROW_TYPE-S1B8-JLgX',
+      type: 'DASHBOARD_ROW_TYPE',
+    },
+    'DASHBOARD_ROW_TYPE-S1bUb1Ilm': {
+      children: ['DASHBOARD_CHART_TYPE-S1gilYABe7'],
+      id: 'DASHBOARD_ROW_TYPE-S1bUb1Ilm',
+      type: 'DASHBOARD_ROW_TYPE',
+    },
+    'DASHBOARD_TABS_TYPE-ByeLSWyLe7': {
+      children: ['DASHBOARD_TAB_TYPE-BJbLSZ1UeQ'],
+      id: 'DASHBOARD_TABS_TYPE-ByeLSWyLe7',
+      type: 'DASHBOARD_TABS_TYPE',
+    },
+    'DASHBOARD_TABS_TYPE-SkgJ5t0Bem': {
+      children: [
+        'DASHBOARD_TAB_TYPE-HkWJcFCHxQ',
+        'DASHBOARD_TAB_TYPE-ByDBbkLlQ',
+      ],
+      id: 'DASHBOARD_TABS_TYPE-SkgJ5t0Bem',
+      meta: {},
+      type: 'DASHBOARD_TABS_TYPE',
+    },
+    'DASHBOARD_TAB_TYPE-BJbLSZ1UeQ': {
+      children: ['DASHBOARD_ROW_TYPE-S1bUb1Ilm'],
+      id: 'DASHBOARD_TAB_TYPE-BJbLSZ1UeQ',
+      type: 'DASHBOARD_TAB_TYPE',
+    },
+    'DASHBOARD_TAB_TYPE-ByDBbkLlQ': {
+      children: ['DASHBOARD_ROW_TYPE-S1B8-JLgX'],
+      id: 'DASHBOARD_TAB_TYPE-ByDBbkLlQ',
+      type: 'DASHBOARD_TAB_TYPE',
+    },
+    'DASHBOARD_TAB_TYPE-HkWJcFCHxQ': {
+      children: ['DASHBOARD_TABS_TYPE-ByeLSWyLe7'],
+      id: 'DASHBOARD_TAB_TYPE-HkWJcFCHxQ',
+      type: 'DASHBOARD_TAB_TYPE',
+    },
+    DASHBOARD_VERSION_KEY: 'v2',
+  };
+
+  it('should return grid root', () => {
+    expect(findFirstParentContainerId(mockGridLayout)).to.equal(
+      DASHBOARD_GRID_ID,
+    );
+  });
+
+  it('should return first tab', () => {
+    const tabsId = mockTabsLayout[DASHBOARD_ROOT_ID].children[0];
+    const firstTabId = mockTabsLayout[tabsId].children[0];
+    expect(findFirstParentContainerId(mockTabsLayout)).to.equal(firstTabId);
+  });
+});
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index f4e091e..7378c7b 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -6,9 +6,16 @@ import { initSliceEntities } from './sliceEntities';
 import { getParam } from '../../modules/utils';
 import { applyDefaultFormData } from '../../explore/store';
 import { getColorFromScheme } from '../../modules/colors';
+import findFirstParentContainerId from '../util/findFirstParentContainer';
 import layoutConverter from '../util/dashboardLayoutConverter';
+import getEmptyLayout from '../util/getEmptyLayout';
+import newComponentFactory from '../util/newComponentFactory';
 import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
-import { DASHBOARD_HEADER_TYPE, CHART_TYPE } from '../util/componentTypes';
+import {
+  DASHBOARD_HEADER_TYPE,
+  CHART_TYPE,
+  ROW_TYPE,
+} from '../util/componentTypes';
 
 export default function(bootstrapData) {
   const {
@@ -48,22 +55,9 @@ export default function(bootstrapData) {
   const shouldConvertToV2 =
     !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2';
 
-  const layout = shouldConvertToV2 ? layoutConverter(dashboard) : positionJson;
-
-  // store the header as a layout component so we can undo/redo changes
-  layout[DASHBOARD_HEADER_ID] = {
-    id: DASHBOARD_HEADER_ID,
-    type: DASHBOARD_HEADER_TYPE,
-    meta: {
-      text: dashboard.dashboard_title,
-    },
-  };
-
-  const dashboardLayout = {
-    past: [],
-    present: layout,
-    future: [],
-  };
+  const layout = shouldConvertToV2
+    ? layoutConverter(dashboard)
+    : positionJson || getEmptyLayout();
 
   // create a lookup to sync layout names with slice names
   const chartIdToLayoutId = {};
@@ -73,6 +67,9 @@ export default function(bootstrapData) {
     }
   });
 
+  // find root level chart container node for newly-added slices
+  const parentId = findFirstParentContainerId(layout);
+  let hasUnsavedChanges = false;
   const chartQueries = {};
   const slices = {};
   const sliceIds = new Set();
@@ -99,6 +96,23 @@ export default function(bootstrapData) {
       };
 
       sliceIds.add(key);
+
+      // if chart is newly added from explore view, add a row in layout
+      if (!chartIdToLayoutId[key] && layout[parentId]) {
+        const parent = layout[parentId];
+        const rowContainer = newComponentFactory(ROW_TYPE);
+        layout[rowContainer.id] = rowContainer;
+        parent.children.push(rowContainer.id);
+
+        const chartHolder = newComponentFactory(CHART_TYPE, {
+          chartId: slice.slice_id,
+        });
+
+        layout[chartHolder.id] = chartHolder;
+        rowContainer.children.push(chartHolder.id);
+        chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id;
+        hasUnsavedChanges = true;
+      }
     }
 
     // sync layout names with current slice names in case a slice was edited
@@ -110,6 +124,21 @@ export default function(bootstrapData) {
     }
   });
 
+  // store the header as a layout component so we can undo/redo changes
+  layout[DASHBOARD_HEADER_ID] = {
+    id: DASHBOARD_HEADER_ID,
+    type: DASHBOARD_HEADER_TYPE,
+    meta: {
+      text: dashboard.dashboard_title,
+    },
+  };
+
+  const dashboardLayout = {
+    past: [],
+    present: layout,
+    future: [],
+  };
+
   return {
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
@@ -143,7 +172,7 @@ export default function(bootstrapData) {
       css: dashboard.css || '',
       editMode: dashboard.dash_edit_perm && editMode,
       showBuilderPane: dashboard.dash_edit_perm && editMode,
-      hasUnsavedChanges: false,
+      hasUnsavedChanges,
       maxUndoHistoryExceeded: false,
       isV2Preview: shouldConvertToV2,
     },
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index e28e3be..cf7a493 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -1,32 +1,23 @@
 /* eslint-disable no-param-reassign */
 /* eslint-disable camelcase */
 /* eslint-disable no-loop-func */
+import shortid from 'shortid';
+
+import getEmptyLayout from './getEmptyLayout';
+
 import {
   ROW_TYPE,
   COLUMN_TYPE,
   CHART_TYPE,
   MARKDOWN_TYPE,
-  DASHBOARD_ROOT_TYPE,
   DASHBOARD_GRID_TYPE,
 } from './componentTypes';
 
-import {
-  DASHBOARD_GRID_ID,
-  DASHBOARD_ROOT_ID,
-  DASHBOARD_VERSION_KEY,
-} from './constants';
+import { DASHBOARD_GRID_ID } from './constants';
 
 const MAX_RECURSIVE_LEVEL = 6;
 const GRID_RATIO = 4;
 const ROW_HEIGHT = 8;
-const generateId = (() => {
-  let componentId = 1;
-  return () => {
-    const id = componentId;
-    componentId += 1;
-    return id;
-  };
-})();
 
 /**
  *
@@ -54,6 +45,10 @@ function getBoundary(positions) {
   };
 }
 
+function generateId() {
+  return shortid.generate();
+}
+
 function getRowContainer() {
   return {
     type: ROW_TYPE,
@@ -275,19 +270,7 @@ function doConvert(positions, level, parent, root) {
 }
 
 export function convertToLayout(positions) {
-  const root = {
-    [DASHBOARD_VERSION_KEY]: 'v2',
-    [DASHBOARD_ROOT_ID]: {
-      type: DASHBOARD_ROOT_TYPE,
-      id: DASHBOARD_ROOT_ID,
-      children: [DASHBOARD_GRID_ID],
-    },
-    [DASHBOARD_GRID_ID]: {
-      type: DASHBOARD_GRID_TYPE,
-      id: DASHBOARD_GRID_ID,
-      children: [],
-    },
-  };
+  const root = getEmptyLayout();
 
   doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
 
diff --git a/superset/assets/src/dashboard/util/findFirstParentContainer.js b/superset/assets/src/dashboard/util/findFirstParentContainer.js
new file mode 100644
index 0000000..d4a75ce
--- /dev/null
+++ b/superset/assets/src/dashboard/util/findFirstParentContainer.js
@@ -0,0 +1,19 @@
+import { TABS_TYPE } from './componentTypes';
+import { DASHBOARD_ROOT_ID } from './constants';
+
+export default function(layout = {}) {
+  // DASHBOARD_GRID_TYPE or TABS_TYPE?
+  let parent = layout[DASHBOARD_ROOT_ID];
+  if (
+    parent &&
+    parent.children.length &&
+    layout[parent.children[0]].type === TABS_TYPE
+  ) {
+    const tabs = layout[parent.children[0]];
+    parent = layout[tabs.children[0]];
+  } else {
+    parent = layout[parent.children[0]];
+  }
+
+  return parent.id;
+}
diff --git a/superset/assets/src/dashboard/util/getEmptyLayout.js b/superset/assets/src/dashboard/util/getEmptyLayout.js
new file mode 100644
index 0000000..bc5fb71
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getEmptyLayout.js
@@ -0,0 +1,23 @@
+import { DASHBOARD_ROOT_TYPE, DASHBOARD_GRID_TYPE } from './componentTypes';
+
+import {
+  DASHBOARD_GRID_ID,
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_VERSION_KEY,
+} from './constants';
+
+export default function() {
+  return {
+    [DASHBOARD_VERSION_KEY]: 'v2',
+    [DASHBOARD_ROOT_ID]: {
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: [DASHBOARD_GRID_ID],
+    },
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: [],
+    },
+  };
+}


[incubator-superset] 13/26: [dashboard v2] fix bugs from rebase

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

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

commit 439fa1476ee9c3117997b05f25c3c3d8c54e9d91
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Wed May 23 17:12:51 2018 -0700

    [dashboard v2] fix bugs from rebase
---
 superset/assets/src/dashboard/actions/dashboardState.js    |  2 +-
 superset/assets/src/dashboard/components/SliceHeader.jsx   |  4 ++++
 .../src/dashboard/components/gridComponents/Chart.jsx      |  4 +++-
 superset/assets/src/dashboard/containers/Chart.jsx         |  2 +-
 superset/assets/src/dashboard/reducers/getInitialState.js  |  2 +-
 .../dashboard/util/charts/getFormDataWithExtraFilters.js   | 14 ++++++--------
 6 files changed, 16 insertions(+), 12 deletions(-)

diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 42f68ad..aac4f98 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -5,7 +5,7 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
 import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
-import { applyDefaultFormData } from '../../explore/stores/store';
+import { applyDefaultFormData } from '../../explore/store';
 import { getAjaxErrorMsg } from '../../modules/utils';
 import { SAVE_TYPE_OVERWRITE } from '../util/constants';
 import { t } from '../../locales';
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 0c572d8..3151841 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -21,6 +21,7 @@ const propTypes = {
   annotationQuery: PropTypes.object,
   annotationError: PropTypes.object,
   sliceName: PropTypes.string,
+  supersetCanExplore: PropTypes.bool,
 };
 
 const defaultProps = {
@@ -38,6 +39,7 @@ const defaultProps = {
   isCached: false,
   isExpanded: false,
   sliceName: '',
+  supersetCanExplore: false,
 };
 
 class SliceHeader extends React.PureComponent {
@@ -53,6 +55,7 @@ class SliceHeader extends React.PureComponent {
       exportCSV,
       innerRef,
       sliceName,
+      supersetCanExplore,
     } = this.props;
 
     const annoationsLoading = t('Annotation layers are still loading.');
@@ -95,6 +98,7 @@ class SliceHeader extends React.PureComponent {
               forceRefresh={forceRefresh}
               exploreChart={exploreChart}
               exportCSV={exportCSV}
+              supersetCanExplore={supersetCanExplore}
             />
           )}
         </div>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 9f8d723..2aedca7 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -7,7 +7,7 @@ import SliceHeader from '../SliceHeader';
 import ChartContainer from '../../../chart/ChartContainer';
 import { chartPropType } from '../../../chart/chartReducer';
 import { slicePropShape } from '../../util/propShapes';
-import { VIZ_TYPES } from '../../../visualizations/main';
+import { VIZ_TYPES } from '../../../visualizations';
 
 const propTypes = {
   id: PropTypes.number.isRequired,
@@ -161,6 +161,8 @@ class Chart extends React.Component {
       sliceCanEdit,
     } = this.props;
 
+    if (!chart || !slice) return null;
+
     const { width } = this.state;
     const { queryResponse } = chart;
     const isCached = queryResponse && queryResponse.is_cached;
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 107e6c7..2b38d8a 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -27,7 +27,7 @@ function mapStateToProps(
 
   return {
     chart,
-    datasource: datasources[chart.form_data.datasource],
+    datasource: chart && datasources[chart.form_data.datasource],
     slice: sliceEntities.slices[id],
     timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
     filters,
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index f129bf7..534b15d 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -4,7 +4,7 @@ import shortid from 'shortid';
 import { chart } from '../../chart/chartReducer';
 import { initSliceEntities } from './sliceEntities';
 import { getParam } from '../../modules/utils';
-import { applyDefaultFormData } from '../../explore/stores/store';
+import { applyDefaultFormData } from '../../explore/store';
 import { getColorFromScheme } from '../../modules/colors';
 import layoutConverter from '../util/dashboardLayoutConverter';
 import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
diff --git a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
index 031d90d..17f7bb3 100644
--- a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
+++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
@@ -7,7 +7,7 @@ const cachedFiltersByChart = {};
 const cachedFormdataByChart = {};
 
 export default function getFormDataWithExtraFilters({
-  chart,
+  chart = {},
   dashboardMetadata,
   filters,
   sliceId,
@@ -23,15 +23,13 @@ export default function getFormDataWithExtraFilters({
     return cachedFormdataByChart[sliceId];
   }
 
-  const extraFilters = getEffectiveExtraFilters({
-    dashboardMetadata,
-    filters,
-    sliceId,
-  });
-
   const formData = {
     ...chart.formData,
-    extra_filters: [...chart.formData.filters, ...extraFilters],
+    extra_filters: getEffectiveExtraFilters({
+      dashboardMetadata,
+      filters,
+      sliceId,
+    }),
   };
 
   cachedDashboardMetadataByChart[sliceId] = dashboardMetadata;


[incubator-superset] 23/26: [fix] new dashboard state (#5213)

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

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

commit da816742329305cab61d3d356467d5080359a66c
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Wed Jun 20 14:35:27 2018 -0700

    [fix] new dashboard state (#5213)
---
 .../src/dashboard/reducers/getInitialState.js      |  6 ++--
 .../assets/src/explore/components/SaveModal.jsx    |  2 +-
 superset/views/core.py                             | 37 +++++++++++++++-------
 3 files changed, 28 insertions(+), 17 deletions(-)

diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index e1cb6ba..e529bf4 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -53,7 +53,7 @@ export default function(bootstrapData) {
   // dashboard layout
   const { position_json: positionJson } = dashboard;
   const shouldConvertToV2 =
-    !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2';
+    positionJson && positionJson[DASHBOARD_VERSION_KEY] !== 'v2';
 
   const layout = shouldConvertToV2
     ? layoutConverter(dashboard)
@@ -69,7 +69,6 @@ export default function(bootstrapData) {
 
   // find root level chart container node for newly-added slices
   const parentId = findFirstParentContainerId(layout);
-  let hasUnsavedChanges = false;
   const chartQueries = {};
   const slices = {};
   const sliceIds = new Set();
@@ -112,7 +111,6 @@ export default function(bootstrapData) {
         layout[chartHolder.id] = chartHolder;
         rowContainer.children.push(chartHolder.id);
         chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id;
-        hasUnsavedChanges = true;
       }
     }
 
@@ -173,7 +171,7 @@ export default function(bootstrapData) {
       css: dashboard.css || '',
       editMode: dashboard.dash_edit_perm && editMode,
       showBuilderPane: dashboard.dash_edit_perm && editMode,
-      hasUnsavedChanges,
+      hasUnsavedChanges: false,
       maxUndoHistoryExceeded: false,
       isV2Preview: shouldConvertToV2,
     },
diff --git a/superset/assets/src/explore/components/SaveModal.jsx b/superset/assets/src/explore/components/SaveModal.jsx
index 86028c8..79880ea 100644
--- a/superset/assets/src/explore/components/SaveModal.jsx
+++ b/superset/assets/src/explore/components/SaveModal.jsx
@@ -108,7 +108,7 @@ class SaveModal extends React.Component {
       .then((data) => {
         // Go to new slice url or dashboard url
         if (gotodash) {
-          window.location = supersetURL(data.dashboard, { edit: 'true' });
+          window.location = supersetURL(data.dashboard);
         } else {
           window.location = data.slice.slice_url;
         }
diff --git a/superset/views/core.py b/superset/views/core.py
index 6bbd7ae..be951a1 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1550,6 +1550,8 @@ class Superset(BaseSupersetView):
 
         dash.owners = [g.user] if g.user else []
         dash.dashboard_title = data['dashboard_title']
+
+        is_v2_dash = Superset._is_v2_dash(data['positions'])
         if data['duplicate_slices']:
             # Duplicating slices as well, mapping old ids to new ones
             old_to_new_sliceids = {}
@@ -1559,17 +1561,24 @@ class Superset(BaseSupersetView):
                 session.add(new_slice)
                 session.flush()
                 new_slice.dashboards.append(dash)
-                old_to_new_sliceids[slc.id] = new_slice.id
+                old_to_new_sliceids['{}'.format(slc.id)] = \
+                    '{}'.format(new_slice.id)
 
             # update chartId of layout entities
-            for value in data['positions'].values():
-                if (
-                    isinstance(value, dict) and value.get('meta') and
-                    value.get('meta').get('chartId')
-                ):
-                    old_id = value.get('meta').get('chartId')
-                    new_id = old_to_new_sliceids[old_id]
-                    value['meta']['chartId'] = new_id
+            # in v2_dash positions json data, chartId should be integer,
+            # while in older version slice_id is string type
+            if is_v2_dash:
+                for value in data['positions'].values():
+                    if (
+                        isinstance(value, dict) and value.get('meta') and
+                        value.get('meta').get('chartId')
+                    ):
+                        old_id = '{}'.format(value.get('meta').get('chartId'))
+                        new_id = int(old_to_new_sliceids[old_id])
+                        value['meta']['chartId'] = new_id
+            else:
+                for d in data['positions']:
+                    d['slice_id'] = old_to_new_sliceids[d['slice_id']]
         else:
             dash.slices = original_dash.slices
         dash.params = original_dash.params
@@ -1599,13 +1608,17 @@ class Superset(BaseSupersetView):
         return 'SUCCESS'
 
     @staticmethod
-    def _set_dash_metadata(dashboard, data):
-        positions = data['positions']
-        is_v2_dash = (
+    def _is_v2_dash(positions):
+        return (
             isinstance(positions, dict) and
             positions.get('DASHBOARD_VERSION_KEY') == 'v2'
         )
 
+    @staticmethod
+    def _set_dash_metadata(dashboard, data):
+        positions = data['positions']
+        is_v2_dash = Superset._is_v2_dash(positions)
+
         # @TODO remove upon v1 deprecation
         if not is_v2_dash:
             positions = data['positions']


[incubator-superset] 04/26: [dashboard builder] static layout + toasts (#4763)

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

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

commit 67e78c385a5535655f958afca41afaa0a0babfce
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Wed Apr 4 17:34:11 2018 -0700

    [dashboard builder] static layout + toasts (#4763)
    
    * [dashboard-builder] remove spacer component
    
    * [dashboard-builder] better transparent indicator, better grid gutter logic, no dragging top-level tabs, headers are multiples of grid unit, fix row height granularity, update redux state key dashboard => dashboardLayout
    
    * [dashboard-builder] don't blast column child dimensions on resize
    
    * [dashboard-builder] ResizableContainer min size can't be smaller than size, fix row style, role=none on WithPopoverMenu container
    
    * [edit mode] add edit mode to redux and propogate to all <DashboardComponent />s
    
    * [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers
    
    * [dashboard-builder] add info toast when dropResult overflows parent
---
 .../v2/actions/{index.js => dashboardLayout.js}    |  35 +++---
 .../javascripts/dashboard/v2/actions/editMode.js   |   9 ++
 .../dashboard/v2/actions/messageToasts.js          |  49 ++++++++
 .../v2/components/BuilderComponentPane.jsx         |   2 -
 .../dashboard/v2/components/DashboardBuilder.jsx   |  28 +++--
 .../dashboard/v2/components/DashboardGrid.jsx      |  10 +-
 .../dashboard/v2/components/DashboardHeader.jsx    |  14 +--
 .../javascripts/dashboard/v2/components/Toast.jsx  |  87 ++++++++++++++
 .../dashboard/v2/components/ToastPresenter.jsx     |  39 +++++++
 .../dashboard/v2/components/dnd/DragDroppable.jsx  |   4 +
 .../dashboard/v2/components/dnd/handleDrop.js      |   2 +-
 .../v2/components/gridComponents/Chart.jsx         |  17 +--
 .../v2/components/gridComponents/Column.jsx        |  75 +++++-------
 .../v2/components/gridComponents/Divider.jsx       |  10 +-
 .../v2/components/gridComponents/Header.jsx        |  13 ++-
 .../dashboard/v2/components/gridComponents/Row.jsx |  69 +++++------
 .../v2/components/gridComponents/Spacer.jsx        | 106 -----------------
 .../dashboard/v2/components/gridComponents/Tab.jsx |  18 ++-
 .../v2/components/gridComponents/Tabs.jsx          |  39 ++++---
 .../v2/components/gridComponents/index.js          |   4 -
 .../gridComponents/new/DraggableNewComponent.jsx   |   1 +
 .../v2/components/gridComponents/new/NewSpacer.jsx |  24 ----
 .../v2/components/menu/WithPopoverMenu.jsx         |  40 ++++---
 .../v2/components/resizable/ResizableContainer.jsx |  32 ++++--
 .../dashboard/v2/containers/DashboardBuilder.jsx   |   7 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  17 +--
 .../dashboard/v2/containers/DashboardGrid.jsx      |   4 +-
 .../dashboard/v2/containers/DashboardHeader.jsx    |  16 ++-
 .../dashboard/v2/containers/ToastPresenter.jsx     |  10 ++
 .../reducers/{dashboard.js => dashboardLayout.js}  |   5 +-
 .../javascripts/dashboard/v2/reducers/editMode.js  |  11 ++
 .../javascripts/dashboard/v2/reducers/index.js     |  12 +-
 .../dashboard/v2/reducers/messageToasts.js         |  18 +++
 .../dashboard/v2/stylesheets/builder.less          |  14 ++-
 .../v2/stylesheets/components/DashboardBuilder.jsx | 127 ---------------------
 .../dashboard/v2/stylesheets/components/chart.less |   4 +-
 .../v2/stylesheets/components/column.less          |  30 +++--
 .../v2/stylesheets/components/divider.less         |   2 +-
 .../v2/stylesheets/components/header.less          |  28 ++++-
 .../dashboard/v2/stylesheets/components/index.less |   1 -
 .../v2/stylesheets/components/new-component.less   |  12 --
 .../dashboard/v2/stylesheets/components/row.less   |  23 ++--
 .../v2/stylesheets/components/spacer.less          |  13 ---
 .../javascripts/dashboard/v2/stylesheets/grid.less |  24 +---
 .../dashboard/v2/stylesheets/index.less            |   1 +
 .../dashboard/v2/stylesheets/popover-menu.less     |   4 +-
 .../dashboard/v2/stylesheets/resizable.less        |  16 +--
 .../dashboard/v2/stylesheets/toast.less            |  59 ++++++++++
 .../dashboard/v2/stylesheets/variables.less        |   8 ++
 .../dashboard/v2/util/componentIsResizable.js      |   2 -
 .../dashboard/v2/util/componentTypes.js            |   2 -
 .../javascripts/dashboard/v2/util/constants.js     |   8 +-
 .../dashboard/v2/util/dropOverflowsParent.js       |  24 ++++
 .../javascripts/dashboard/v2/util/getChildWidth.js |   5 +-
 .../dashboard/v2/util/getDropPosition.js           |   2 +
 .../javascripts/dashboard/v2/util/isValidChild.js  |  11 +-
 .../dashboard/v2/util/newComponentFactory.js       |   6 +-
 .../dashboard/v2/util/newComponentIdToType.js      |  35 ------
 .../javascripts/dashboard/v2/util/propShapes.jsx   |   7 ++
 superset/assets/src/components/EditableTitle.jsx   |  16 ++-
 superset/assets/src/dashboard/index.jsx            |   5 +-
 superset/assets/stylesheets/superset.less          |  29 +++--
 62 files changed, 715 insertions(+), 630 deletions(-)

diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
similarity index 79%
rename from superset/assets/javascripts/dashboard/v2/actions/index.js
rename to superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
index a6c7b77..b6d41c4 100644
--- a/superset/assets/javascripts/dashboard/v2/actions/index.js
+++ b/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
@@ -1,10 +1,8 @@
+import { addInfoToast } from './messageToasts';
+import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
 import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import dropOverflowsParent from '../util/dropOverflowsParent';
 import findParentId from '../util/findParentId';
-import {
-  CHART_TYPE,
-  MARKDOWN_TYPE,
-  TABS_TYPE,
-} from '../util/componentTypes';
 
 // Component CRUD -------------------------------------------------------------
 export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
@@ -61,8 +59,8 @@ export function deleteTopLevelTabs() {
 export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
 export function resizeComponent({ id, width, height }) {
   return (dispatch, getState) => {
-    const { dashboard: undoableDashboard } = getState();
-    const { present: dashboard } = undoableDashboard;
+    const { dashboardLayout: undoableLayout } = getState();
+    const { present: dashboard } = undoableLayout;
     const component = dashboard[id];
 
     if (
@@ -88,8 +86,8 @@ export function resizeComponent({ id, width, height }) {
             ...child,
             meta: {
               ...child.meta,
-              width: width || component.meta.width,
-              height: height || component.meta.height,
+              width: width || child.meta.width,
+              height: height || child.meta.height,
             },
           };
         }
@@ -114,6 +112,15 @@ export function moveComponent(dropResult) {
 export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
 export function handleComponentDrop(dropResult) {
   return (dispatch, getState) => {
+    const overflowsParent = dropOverflowsParent(dropResult, getState().dashboardLayout.present);
+
+    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.`,
+      ));
+    }
+
     const { source, destination } = dropResult;
     const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
     const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
@@ -133,14 +140,14 @@ export function handleComponentDrop(dropResult) {
       dispatch(moveComponent(dropResult));
     }
 
-    // if we moved a tab and the parent tabs no longer has children, delete it.
+    // if we moved a Tab and the parent Tabs no longer has children, delete it.
     if (!isNewComponent) {
-      const { dashboard: undoableDashboard } = getState();
-      const { present: dashboard } = undoableDashboard;
-      const sourceComponent = dashboard[source.id];
+      const { dashboardLayout: undoableLayout } = getState();
+      const { present: layout } = undoableLayout;
+      const sourceComponent = layout[source.id];
 
       if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
-        const parentId = findParentId({ childId: source.id, components: dashboard });
+        const parentId = findParentId({ childId: source.id, components: layout });
         dispatch(deleteComponent(source.id, parentId));
       }
     }
diff --git a/superset/assets/javascripts/dashboard/v2/actions/editMode.js b/superset/assets/javascripts/dashboard/v2/actions/editMode.js
new file mode 100644
index 0000000..0a849ea
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/actions/editMode.js
@@ -0,0 +1,9 @@
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export function setEditMode(editMode) {
+  return {
+    type: SET_EDIT_MODE,
+    payload: {
+      editMode,
+    },
+  };
+}
diff --git a/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js b/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
new file mode 100644
index 0000000..af10ead
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
@@ -0,0 +1,49 @@
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+
+function getToastUuid(type) {
+  return `${Math.random().toString(16).slice(2)}-${type}-${Math.random().toString(16).slice(2)}`;
+}
+
+export const ADD_TOAST = 'ADD_TOAST';
+export function addToast({ toastType, text }) {
+  debugger;
+  return {
+    type: ADD_TOAST,
+    payload: {
+      id: getToastUuid(toastType),
+      toastType,
+      text,
+    },
+  };
+}
+
+export const REMOVE_TOAST = 'REMOVE_TOAST';
+export function removeToast(id) {
+  return {
+    type: REMOVE_TOAST,
+    payload: {
+      id,
+    },
+  };
+}
+
+// Different types of toasts
+export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
+export function addInfoToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST }));
+}
+
+export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
+export function addSuccessToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST }));
+}
+
+export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
+export function addWarningToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST }));
+}
+
+export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
+export function addDangerToast(text) {
+  return dispatch => dispatch(addToast({ text, toastType: DANGER_TOAST }));
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
index 86f3788..efef5a5 100644
--- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
@@ -6,7 +6,6 @@ import NewColumn from './gridComponents/new/NewColumn';
 import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
-import NewSpacer from './gridComponents/new/NewSpacer';
 import NewTabs from './gridComponents/new/NewTabs';
 
 const propTypes = {
@@ -24,7 +23,6 @@ class BuilderComponentPane extends React.PureComponent {
         <NewHeader />
 
         <NewDivider />
-        <NewSpacer />
 
         <NewTabs />
         <NewRow />
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index f371718..8e2d985 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -1,3 +1,4 @@
+import cx from 'classnames';
 import React from 'react';
 import PropTypes from 'prop-types';
 import HTML5Backend from 'react-dnd-html5-backend';
@@ -9,6 +10,7 @@ import DashboardGrid from '../containers/DashboardGrid';
 import IconButton from './IconButton';
 import DragDroppable from './dnd/DragDroppable';
 import DashboardComponent from '../containers/DashboardComponent';
+import ToastPresenter from '../containers/ToastPresenter';
 import WithPopoverMenu from './menu/WithPopoverMenu';
 
 import {
@@ -18,11 +20,10 @@ import {
 } from '../util/constants';
 
 const propTypes = {
-  editMode: PropTypes.bool,
-
   // redux
-  dashboard: PropTypes.object.isRequired,
+  dashboardLayout: PropTypes.object.isRequired,
   deleteTopLevelTabs: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
@@ -52,20 +53,20 @@ class DashboardBuilder extends React.Component {
 
   render() {
     const { tabIndex } = this.state;
-    const { handleComponentDrop, dashboard, deleteTopLevelTabs } = this.props;
-    const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
+    const { handleComponentDrop, dashboardLayout, deleteTopLevelTabs, editMode } = this.props;
+    const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
-    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
+    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
 
     const gridComponentId = topLevelTabs
       ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
       : DASHBOARD_GRID_ID;
 
-    const gridComponent = dashboard[gridComponentId];
+    const gridComponent = dashboardLayout[gridComponentId];
 
     return (
-      <div className="dashboard-v2">
-        {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
+      <div className={cx('dashboard-v2', editMode && 'dashboard-v2--editing')}>
+        {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
           <DashboardHeader />
         ) : (
           <DragDroppable
@@ -74,7 +75,8 @@ class DashboardBuilder extends React.Component {
             depth={DASHBOARD_ROOT_DEPTH}
             index={0}
             orientation="column"
-            onDrop={topLevelTabs ? null : handleComponentDrop}
+            onDrop={handleComponentDrop}
+            editMode
           >
             {({ dropIndicatorProps }) => (
               <div>
@@ -94,6 +96,7 @@ class DashboardBuilder extends React.Component {
                 onClick={deleteTopLevelTabs}
               />,
             ]}
+            editMode={editMode}
           >
             <DashboardComponent
               id={topLevelTabs.id}
@@ -105,13 +108,14 @@ class DashboardBuilder extends React.Component {
             />
           </WithPopoverMenu>}
 
-        <div className="dashboard-builder">
+        <div className="dashboard-content">
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
           />
-          <BuilderComponentPane />
+          {editMode && <BuilderComponentPane />}
         </div>
+        <ToastPresenter />
       </div>
     );
   }
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
index cfe99c7..9f4cb93 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -13,6 +13,7 @@ import {
 
 const propTypes = {
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
   gridComponent: componentShape.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
   resizeComponent: PropTypes.func.isRequired,
@@ -70,7 +71,7 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { gridComponent, handleComponentDrop, depth } = this.props;
+    const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
     const { isResizing, rowGuideTop } = this.state;
 
     return (
@@ -99,18 +100,19 @@ class DashboardGrid extends React.PureComponent {
                 ))}
 
                 {/* render an empty drop target */}
-                {gridComponent.children.length === 0 &&
+                {editMode &&
                   <DragDroppable
                     component={gridComponent}
                     depth={depth}
                     parentComponent={null}
-                    index={0}
+                    index={gridComponent.children.length}
                     orientation="column"
                     onDrop={handleComponentDrop}
                     className="empty-grid-droptarget"
+                    editMode
                   >
                     {({ dropIndicatorProps }) => dropIndicatorProps &&
-                      <div {...dropIndicatorProps} />}
+                      <div className="drop-indicator drop-indicator--top" />}
                   </DragDroppable>}
 
                 {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
index e0d14c4..ca204e5 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
@@ -7,8 +7,7 @@ import { componentShape } from '../util/propShapes';
 import EditableTitle from '../../../components/EditableTitle';
 
 const propTypes = {
-  // editMode: PropTypes.bool.isRequired,
-  // setEditMode: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
   component: componentShape.isRequired,
 
   // redux
@@ -17,6 +16,7 @@ const propTypes = {
   onRedo: PropTypes.func.isRequired,
   canUndo: PropTypes.bool.isRequired,
   canRedo: PropTypes.bool.isRequired,
+  setEditMode: PropTypes.func.isRequired,
 };
 
 class DashboardHeader extends React.Component {
@@ -27,8 +27,7 @@ class DashboardHeader extends React.Component {
   }
 
   toggleEditMode() {
-    console.log('@TODO toggleEditMode');
-    // this.props.setEditMode(!this.props.editMode);
+    this.props.setEditMode(!this.props.editMode);
   }
 
   handleChangeText(nextText) {
@@ -47,19 +46,18 @@ class DashboardHeader extends React.Component {
   }
 
   render() {
-    const { component, onUndo, onRedo, canUndo, canRedo } = this.props;
-    const editMode = true;
+    const { component, onUndo, onRedo, canUndo, canRedo, editMode } = this.props;
 
     return (
       <div className="dashboard-header">
-        <h1>
+        <div className="dashboard-component-header header-large">
           <EditableTitle
             title={component.meta.text}
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
             canEdit={editMode}
           />
-        </h1>
+        </div>
         <ButtonToolbar>
           <ButtonGroup>
             <Button
diff --git a/superset/assets/javascripts/dashboard/v2/components/Toast.jsx b/superset/assets/javascripts/dashboard/v2/components/Toast.jsx
new file mode 100644
index 0000000..537388d
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/Toast.jsx
@@ -0,0 +1,87 @@
+import { Alert } from 'react-bootstrap';
+import cx from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { toastShape } from '../util/propShapes';
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+
+const propTypes = {
+  toast: toastShape.isRequired,
+  onCloseToast: PropTypes.func.isRequired,
+  delay: PropTypes.number,
+  duration: PropTypes.number, // if duration is >0, the toast will close on its own
+};
+
+const defaultProps = {
+  delay: 0,
+  duration: 0,
+};
+
+class Toast extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      visible: false,
+    };
+
+    this.showToast = this.showToast.bind(this);
+    this.handleClosePress = this.handleClosePress.bind(this);
+  }
+
+  componentDidMount() {
+    const { delay, duration } = this.props;
+
+    setTimeout(this.showToast, delay);
+
+    if (duration > 0) {
+      this.hideTimer = setTimeout(this.handleClosePress, delay + duration);
+    }
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this.hideTimer);
+  }
+
+  showToast() {
+    this.setState({ visible: true });
+  }
+
+  handleClosePress() {
+    clearTimeout(this.hideTimer);
+
+    this.setState({ visible: false }, () => {
+      // Wait for the transition
+      setTimeout(() => {
+        this.props.onCloseToast(this.props.toast.id);
+      }, 150);
+    });
+  }
+
+  render() {
+    const { visible } = this.state;
+    const { toast: { toastType, text } } = this.props;
+
+    return (
+      <Alert
+        onDismiss={this.handleClosePress}
+        bsClass={cx(
+          'alert',
+          'toast',
+          visible && 'toast--visible',
+          toastType === INFO_TOAST && 'toast--info',
+          toastType === SUCCESS_TOAST && 'toast--success',
+          toastType === WARNING_TOAST && 'toast--warning',
+          toastType === DANGER_TOAST && 'toast--danger',
+        )}
+      >
+        {text}
+      </Alert>
+    );
+  }
+}
+
+Toast.propTypes = propTypes;
+Toast.defaultProps = defaultProps;
+
+export default Toast;
diff --git a/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx b/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
new file mode 100644
index 0000000..95a0251
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
@@ -0,0 +1,39 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Toast from './Toast';
+import { toastShape } from '../util/propShapes';
+
+const propTypes = {
+  toasts: PropTypes.arrayOf(toastShape),
+  removeToast: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  toasts: [],
+};
+
+// eslint-disable-next-line react/prefer-stateless-function
+class ToastPresenter extends React.Component {
+  render() {
+    const { toasts, removeToast } = this.props;
+
+    return (
+      toasts.length > 0 &&
+        <div className="toast-presenter">
+          {toasts.map(toast => (
+            <Toast
+              key={toast.id}
+              toast={toast}
+              onCloseToast={removeToast}
+            />
+          ))}
+        </div>
+    );
+  }
+}
+
+ToastPresenter.propTypes = propTypes;
+ToastPresenter.defaultProps = defaultProps;
+
+export default ToastPresenter;
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
index 89664e5..775e092 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
@@ -18,6 +18,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   style: PropTypes.object,
   onDrop: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 
   // from react-dnd
   isDragging: PropTypes.bool.isRequired,
@@ -70,8 +71,11 @@ class DragDroppable extends React.Component {
       isDragging,
       isDraggingOver,
       style,
+      editMode,
     } = this.props;
 
+    if (!editMode) return children({});
+
     const { dropIndicator } = this.state;
 
     return (
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
index 2207ca6..f27b604 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
@@ -2,7 +2,7 @@ import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '.
 
 export default function handleDrop(props, monitor, Component) {
   // this may happen due to throttling
-  if (!Component.mounted || !Component.props.onDrop) return undefined;
+  if (!Component.mounted) return undefined;
 
   Component.setState(() => ({ dropIndicator: null }));
   const dropPosition = getDropPosition(monitor, Component);
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
index 7ca506d..668d268 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
@@ -9,10 +9,7 @@ import ResizableContainer from '../resizable/ResizableContainer';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
 import { ROW_TYPE } from '../../util/componentTypes';
-import {
-  GRID_MIN_COLUMN_COUNT,
-  GRID_MIN_ROW_UNITS,
-} from '../../util/constants';
+import { GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS } from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -21,6 +18,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -71,6 +69,7 @@ class Chart extends React.Component {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     return (
@@ -82,6 +81,7 @@ class Chart extends React.Component {
         depth={depth}
         onDrop={handleComponentDrop}
         disableDragDrop={isFocused}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <ResizableContainer
@@ -97,16 +97,19 @@ class Chart extends React.Component {
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
+            editMode={editMode}
           >
-            <HoverMenu innerRef={dragSourceRef} position="top">
-              <DragHandle position="top" />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu innerRef={dragSourceRef} position="top">
+                <DragHandle position="top" />
+              </HoverMenu>}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
               menuItems={[
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
+              editMode={editMode}
             >
               <div className="dashboard-component dashboard-component-chart">
                 <div className="fa fa-area-chart" />
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
index d51870d..fe5a721 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
@@ -15,12 +15,7 @@ import WithPopoverMenu from '../menu/WithPopoverMenu';
 import backgroundStyleOptions from '../../util/backgroundStyleOptions';
 import { componentShape } from '../../util/propShapes';
 
-import {
-  BACKGROUND_TRANSPARENT,
-  GRID_GUTTER_SIZE,
-} from '../../util/constants';
-
-const GUTTER = 'GUTTER';
+import { BACKGROUND_TRANSPARENT } from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -29,6 +24,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -95,23 +91,14 @@ class Column extends React.PureComponent {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
-    const columnItems = [];
-
-    (columnComponent.children || []).forEach((id, childIndex) => {
-      columnItems.push(id);
-      if (childIndex < columnComponent.children.length - 1) {
-        columnItems.push(GUTTER);
-      }
-    });
-
+    const columnItems = columnComponent.children || [];
     const backgroundStyle = backgroundStyleOptions.find(
       opt => opt.value === (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
     );
 
-    console.log('occupied/avail cols', columnComponent.meta.width, '/', availableColumnCount, 'min width', minColumnWidth)
-
     return (
       <DragDroppable
         component={columnComponent}
@@ -120,6 +107,7 @@ class Column extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <ResizableContainer
@@ -133,6 +121,7 @@ class Column extends React.PureComponent {
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
+            editMode={editMode}
           >
             <WithPopoverMenu
               isFocused={this.state.isFocused}
@@ -145,6 +134,7 @@ class Column extends React.PureComponent {
                   onChange={this.handleChangeBackground}
                 />,
               ]}
+              editMode={editMode}
             >
               <div
                 className={cx(
@@ -153,35 +143,30 @@ class Column extends React.PureComponent {
                   backgroundStyle.className,
                 )}
               >
-                <HoverMenu innerRef={dragSourceRef} position="top">
-                  <DragHandle position="top" />
-                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-                  <IconButton
-                    onClick={this.handleChangeFocus}
-                    className="fa fa-cog"
-                  />
-                </HoverMenu>
-
-                {columnItems.map((componentId, itemIndex) => {
-                  if (componentId === GUTTER) {
-                    return <div key={`gutter-${itemIndex}`} style={{ height: GRID_GUTTER_SIZE }} />;
-                  }
-
-                  return (
-                    <DashboardComponent
-                      key={componentId}
-                      id={componentId}
-                      parentId={columnComponent.id}
-                      depth={depth + 1}
-                      index={itemIndex / 2} // account for gutters!
-                      availableColumnCount={columnComponent.meta.width}
-                      columnWidth={columnWidth}
-                      onResizeStart={onResizeStart}
-                      onResize={onResize}
-                      onResizeStop={onResizeStop}
+                {editMode &&
+                  <HoverMenu innerRef={dragSourceRef} position="top">
+                    <DragHandle position="top" />
+                    <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                    <IconButton
+                      onClick={this.handleChangeFocus}
+                      className="fa fa-cog"
                     />
-                  );
-                })}
+                  </HoverMenu>}
+
+                {columnItems.map((componentId, itemIndex) => (
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={columnComponent.id}
+                    depth={depth + 1}
+                    index={itemIndex}
+                    availableColumnCount={columnComponent.meta.width}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                  />
+                ))}
 
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
index ff29c3f..b3010e9 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
@@ -13,6 +13,7 @@ const propTypes = {
   depth: PropTypes.number.isRequired,
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
   deleteComponent: PropTypes.func.isRequired,
 };
@@ -35,6 +36,7 @@ class Divider extends React.PureComponent {
       parentComponent,
       index,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     return (
@@ -45,12 +47,14 @@ class Divider extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            <HoverMenu position="left">
-              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu position="left">
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+              </HoverMenu>}
 
             <div className="dashboard-component dashboard-component-divider" />
 
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
index d8744d6..97945a9 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   depth: PropTypes.number.isRequired,
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // redux
   handleComponentDrop: PropTypes.func.isRequired,
@@ -79,6 +80,7 @@ class Header extends React.PureComponent {
       parentComponent,
       index,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
     const headerStyle = headerStyleOptions.find(
@@ -98,12 +100,14 @@ class Header extends React.PureComponent {
         depth={depth}
         onDrop={handleComponentDrop}
         disableDragDrop={isFocused}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            <HoverMenu position="left">
-              <DragHandle position="left" />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu position="left">
+                <DragHandle position="left" />
+              </HoverMenu>}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
@@ -122,6 +126,7 @@ class Header extends React.PureComponent {
                 />,
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
+              editMode={editMode}
             >
               <div
                 className={cx(
@@ -133,7 +138,7 @@ class Header extends React.PureComponent {
               >
                 <EditableTitle
                   title={component.meta.text}
-                  canEdit={isFocused}
+                  canEdit={editMode}
                   onSaveTitle={this.handleChangeText}
                   showTooltip={false}
                 />
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
index a60524f..9866bc8 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -13,9 +13,7 @@ import WithPopoverMenu from '../menu/WithPopoverMenu';
 
 import { componentShape } from '../../util/propShapes';
 import backgroundStyleOptions from '../../util/backgroundStyleOptions';
-import { GRID_GUTTER_SIZE, BACKGROUND_TRANSPARENT } from '../../util/constants';
-
-const GUTTER = 'GUTTER';
+import { BACKGROUND_TRANSPARENT } from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -24,6 +22,7 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -92,17 +91,10 @@ class Row extends React.PureComponent {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      editMode,
     } = this.props;
 
-    const rowItems = [];
-
-    // this adds a gutter between each child in the row.
-    (rowComponent.children || []).forEach((id, childIndex) => {
-      rowItems.push(id);
-      if (childIndex < rowComponent.children.length - 1) {
-        rowItems.push(GUTTER);
-      }
-    });
+    const rowItems = rowComponent.children || [];
 
     const backgroundStyle = backgroundStyleOptions.find(
       opt => opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
@@ -116,6 +108,7 @@ class Row extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <WithPopoverMenu
@@ -129,6 +122,7 @@ class Row extends React.PureComponent {
                 onChange={this.handleChangeBackground}
               />,
             ]}
+            editMode={editMode}
           >
             <div
               className={cx(
@@ -137,35 +131,30 @@ class Row extends React.PureComponent {
                 backgroundStyle.className,
               )}
             >
-              <HoverMenu innerRef={dragSourceRef} position="left">
-                <DragHandle position="left" />
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-                <IconButton
-                  onClick={this.handleChangeFocus}
-                  className="fa fa-cog"
-                />
-              </HoverMenu>
-
-              {rowItems.map((componentId, itemIndex) => {
-                if (componentId === GUTTER) {
-                  return <div key={`gutter-${itemIndex}`} style={{ width: GRID_GUTTER_SIZE }} />;
-                }
-
-                return (
-                  <DashboardComponent
-                    key={componentId}
-                    id={componentId}
-                    parentId={rowComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex / 2} // account for gutters!
-                    availableColumnCount={availableColumnCount - occupiedColumnCount}
-                    columnWidth={columnWidth}
-                    onResizeStart={onResizeStart}
-                    onResize={onResize}
-                    onResizeStop={onResizeStop}
+              {editMode &&
+                <HoverMenu innerRef={dragSourceRef} position="left">
+                  <DragHandle position="left" />
+                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                  <IconButton
+                    onClick={this.handleChangeFocus}
+                    className="fa fa-cog"
                   />
-                );
-              })}
+                </HoverMenu>}
+
+              {rowItems.map((componentId, itemIndex) => (
+                <DashboardComponent
+                  key={componentId}
+                  id={componentId}
+                  parentId={rowComponent.id}
+                  depth={depth + 1}
+                  index={itemIndex}
+                  availableColumnCount={availableColumnCount - occupiedColumnCount}
+                  columnWidth={columnWidth}
+                  onResizeStart={onResizeStart}
+                  onResize={onResize}
+                  onResizeStop={onResizeStop}
+                />
+              ))}
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
             </div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
deleted file mode 100644
index 7a287d8..0000000
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import DeleteComponentButton from '../DeleteComponentButton';
-import DragDroppable from '../dnd/DragDroppable';
-import HoverMenu from '../menu/HoverMenu';
-import ResizableContainer from '../resizable/ResizableContainer';
-import { componentShape } from '../../util/propShapes';
-
-const propTypes = {
-  id: PropTypes.string.isRequired,
-  parentId: PropTypes.string.isRequired,
-  component: componentShape.isRequired,
-  parentComponent: componentShape.isRequired,
-  index: PropTypes.number.isRequired,
-  depth: PropTypes.number.isRequired,
-
-  // grid related
-  availableColumnCount: PropTypes.number.isRequired,
-  columnWidth: PropTypes.number.isRequired,
-  onResizeStart: PropTypes.func.isRequired,
-  onResize: PropTypes.func.isRequired,
-  onResizeStop: PropTypes.func.isRequired,
-
-  // dnd
-  deleteComponent: PropTypes.func.isRequired,
-  handleComponentDrop: PropTypes.func.isRequired,
-};
-
-const defaultProps = {
-};
-
-class Spacer extends React.PureComponent {
-  constructor(props) {
-    super(props);
-    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
-  }
-
-  handleDeleteComponent() {
-    const { deleteComponent, id, parentId } = this.props;
-    deleteComponent(id, parentId);
-  }
-
-  render() {
-    const {
-      component,
-      parentComponent,
-      index,
-      depth,
-      availableColumnCount,
-      columnWidth,
-      onResizeStart,
-      onResize,
-      onResizeStop,
-      handleComponentDrop,
-    } = this.props;
-
-    const orientation = depth % 2 === 0 ? 'row' : 'column';
-    const hoverMenuPosition = orientation === 'row' ? 'left' : 'top';
-    const adjustableWidth = orientation === 'column';
-    const adjustableHeight = orientation === 'row';
-
-    console.log('spacer', availableColumnCount)
-
-    return (
-      <DragDroppable
-        component={component}
-        parentComponent={parentComponent}
-        orientation={orientation}
-        index={index}
-        depth={depth}
-        onDrop={handleComponentDrop}
-      >
-        {({ dropIndicatorProps, dragSourceRef }) => (
-          <ResizableContainer
-            id={component.id}
-            adjustableWidth={adjustableWidth}
-            adjustableHeight={adjustableHeight}
-            widthStep={columnWidth}
-            widthMultiple={component.meta.width || 1}
-            heightMultiple={adjustableHeight ? component.meta.height || 1 : undefined}
-            minWidthMultiple={1}
-            minHeightMultiple={1}
-            maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
-            onResizeStart={onResizeStart}
-            onResize={onResize}
-            onResizeStop={onResizeStop}
-          >
-            <HoverMenu position={hoverMenuPosition}>
-              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-            </HoverMenu>
-
-            <div ref={dragSourceRef} className="grid-spacer" />
-
-            {dropIndicatorProps && <div {...dropIndicatorProps} />}
-          </ResizableContainer>
-        )}
-      </DragDroppable>
-    );
-  }
-}
-
-Spacer.propTypes = propTypes;
-Spacer.defaultProps = defaultProps;
-
-export default Spacer;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
index 9548a4b..218c4e7 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
@@ -7,6 +7,7 @@ import EditableTitle from '../../../../components/EditableTitle';
 import DeleteComponentButton from '../DeleteComponentButton';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
+import { DASHBOARD_ROOT_DEPTH } from '../../util/constants';
 
 export const RENDER_TAB = 'RENDER_TAB';
 export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
@@ -21,6 +22,7 @@ const propTypes = {
   renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
   onDropOnTab: PropTypes.func,
   onDeleteTab: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -77,9 +79,9 @@ export default class Tab extends React.PureComponent {
   }
 
   handleDeleteComponent() {
-    const { onDeleteTab, index, deleteComponent, id, parentId } = this.props;
-    deleteComponent(id, parentId);
-    onDeleteTab(index);
+    const { index, id, parentId } = this.props;
+    this.props.deleteComponent(id, parentId);
+    this.props.onDeleteTab(index);
   }
 
   handleDrop(dropResult) {
@@ -126,6 +128,7 @@ export default class Tab extends React.PureComponent {
       parentComponent,
       index,
       depth,
+      editMode,
     } = this.props;
 
     return (
@@ -136,7 +139,11 @@ export default class Tab extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={this.handleDrop}
-        disableDragDrop={isFocused}
+        // disable drag drop of top-level Tab's to prevent invalid nesting of a child in
+        // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
+        // reusult in circular children
+        disableDragDrop={isFocused || depth === DASHBOARD_ROOT_DEPTH + 1}
+        editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div className="dragdroppable-tab" ref={dragSourceRef}>
@@ -145,10 +152,11 @@ export default class Tab extends React.PureComponent {
               menuItems={parentComponent.children.length <= 1 ? [] : [
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
+              editMode={editMode}
             >
               <EditableTitle
                 title={component.meta.text}
-                canEdit={isFocused}
+                canEdit={editMode && isFocused}
                 onSaveTitle={this.handleChangeText}
                 showTooltip={false}
               />
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
index cc5f637..1f5f0c6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
@@ -22,7 +22,8 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
-  renderTabContent: PropTypes.bool,
+  renderTabContent: PropTypes.bool, // whether to render tabs + content or just tabs
+  editMode: PropTypes.bool.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number,
@@ -40,11 +41,11 @@ const propTypes = {
 };
 
 const defaultProps = {
-  onChangeTab: null,
   children: null,
   renderTabContent: true,
   availableColumnCount: 0,
   columnWidth: 0,
+  onChangeTab() {},
   onResizeStart() {},
   onResize() {},
   onResizeStop() {},
@@ -70,14 +71,9 @@ class Tabs extends React.PureComponent {
   }
 
   handleClickTab(tabIndex) {
-    const { onChangeTab, component, createComponent } = this.props;
+    const { component, createComponent } = this.props;
 
-    if (tabIndex !== NEW_TAB_INDEX && tabIndex !== this.state.tabIndex) {
-      this.setState(() => ({ tabIndex }));
-      if (onChangeTab) {
-        onChangeTab({ tabIndex, tabId: component.children[tabIndex] });
-      }
-    } else if (tabIndex === NEW_TAB_INDEX) {
+    if (tabIndex === NEW_TAB_INDEX) {
       createComponent({
         destination: {
           id: component.id,
@@ -89,6 +85,9 @@ class Tabs extends React.PureComponent {
           type: TAB_TYPE,
         },
       });
+    } else if (tabIndex !== this.state.tabIndex) {
+      this.setState(() => ({ tabIndex }));
+      this.props.onChangeTab({ tabIndex, tabId: component.children[tabIndex] });
     }
   }
 
@@ -132,6 +131,7 @@ class Tabs extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       renderTabContent,
+      editMode,
     } = this.props;
 
     const { tabIndex: selectedTabIndex } = this.state;
@@ -145,13 +145,15 @@ class Tabs extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
+        editMode={editMode}
       >
         {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
           <div className="dashboard-component dashboard-component-tabs">
-            <HoverMenu innerRef={tabsDragSourceRef} position="left">
-              <DragHandle position="left" />
-              <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-            </HoverMenu>
+            {editMode &&
+              <HoverMenu innerRef={tabsDragSourceRef} position="left">
+                <DragHandle position="left" />
+                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+              </HoverMenu>}
 
             <BootstrapTabs
               id={tabsComponent.id}
@@ -202,11 +204,12 @@ class Tabs extends React.PureComponent {
                 </BootstrapTab>
               ))}
 
-              {tabIds.length < MAX_TAB_COUNT &&
-                <BootstrapTab
-                  eventKey={NEW_TAB_INDEX}
-                  title={<div className="fa fa-plus" />}
-                />}
+              {editMode &&
+                tabIds.length < MAX_TAB_COUNT &&
+                  <BootstrapTab
+                    eventKey={NEW_TAB_INDEX}
+                    title={<div className="fa fa-plus" />}
+                  />}
 
             </BootstrapTabs>
 
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
index 3a3fad5..96c9a19 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
@@ -5,7 +5,6 @@ import {
   HEADER_TYPE,
   INVISIBLE_ROW_TYPE,
   ROW_TYPE,
-  SPACER_TYPE,
   TAB_TYPE,
   TABS_TYPE,
 } from '../../util/componentTypes';
@@ -15,7 +14,6 @@ import Column from './Column';
 import Divider from './Divider';
 import Header from './Header';
 import Row from './Row';
-import Spacer from './Spacer';
 import Tab from './Tab';
 import Tabs from './Tabs';
 
@@ -24,7 +22,6 @@ export { default as Column } from './Column';
 export { default as Divider } from './Divider';
 export { default as Header } from './Header';
 export { default as Row } from './Row';
-export { default as Spacer } from './Spacer';
 export { default as Tab } from './Tab';
 export { default as Tabs } from './Tabs';
 
@@ -35,7 +32,6 @@ export default {
   [HEADER_TYPE]: Header,
   [INVISIBLE_ROW_TYPE]: Row,
   [ROW_TYPE]: Row,
-  [SPACER_TYPE]: Spacer,
   [TAB_TYPE]: Tab,
   [TABS_TYPE]: Tabs,
 };
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
index 778f58e..eebd6e0 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
@@ -26,6 +26,7 @@ export default class DraggableNewComponent extends React.PureComponent {
         parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
         index={0}
         depth={0}
+        editMode
       >
         {({ dragSourceRef }) => (
           <div ref={dragSourceRef} className="new-component">
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx
deleted file mode 100644
index 7287770..0000000
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewSpacer.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { SPACER_TYPE } from '../../../util/componentTypes';
-import { NEW_SPACER_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewChart extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_SPACER_ID}
-        type={SPACER_TYPE}
-        label="Spacer"
-        className="spacer-placeholder fa fa-arrows"
-      />
-    );
-  }
-}
-
-DraggableNewChart.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
index 2054090..f213442 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
@@ -9,6 +9,7 @@ const propTypes = {
   onChangeFocus: PropTypes.func,
   isFocused: PropTypes.bool,
   shouldFocus: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -32,10 +33,14 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   componentWillReceiveProps(nextProps) {
-    if (nextProps.isFocused && !this.state.isFocused) {
+    if (nextProps.editMode && nextProps.isFocused && !this.state.isFocused) {
       document.addEventListener('click', this.handleClick, true);
       document.addEventListener('drag', this.handleClick, true);
       this.setState({ isFocused: true });
+    } else if (this.state.isFocused && !nextProps.editMode) {
+      document.removeEventListener('click', this.handleClick, true);
+      document.removeEventListener('drag', this.handleClick, true);
+      this.setState({ isFocused: false });
     }
   }
 
@@ -49,10 +54,14 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   handleClick(event) {
-    const { onChangeFocus, shouldFocus: shouldFocusThunk } = this.props;
-    const shouldFocus = shouldFocusThunk(event, this.container);
+    const { onChangeFocus, shouldFocus: shouldFocusFunc, disableClick, editMode } = this.props;
+    const shouldFocus = shouldFocusFunc(event, this.container);
+
+    if (!editMode) {
+      return;
+    }
 
-    if (shouldFocus && !this.state.isFocused) {
+    if (!disableClick && shouldFocus && !this.state.isFocused) {
       // if not focused, set focus and add a window event listener to capture outside clicks
       // this enables us to not set a click listener for ever item on a dashboard
       document.addEventListener('click', this.handleClick, true);
@@ -72,27 +81,28 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   render() {
-    const { children, menuItems, disableClick } = this.props;
+    const { children, menuItems, editMode } = this.props;
     const { isFocused } = this.state;
 
     return (
       <div
         ref={this.setRef}
-        onClick={!disableClick && this.handleClick}
-        role="button" // @TODO consider others?
-        tabIndex="0"
+        onClick={this.handleClick}
+        role="none"
         className={cx(
           'with-popover-menu',
-          isFocused && 'with-popover-menu--focused',
+          editMode && isFocused && 'with-popover-menu--focused',
         )}
       >
         {children}
-        {isFocused && menuItems.length ?
-          <div className="popover-menu" >
-            {menuItems.map((node, i) => (
-              <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
-            ))}
-          </div> : null}
+        {editMode &&
+          isFocused &&
+          menuItems.length > 0 &&
+            <div className="popover-menu" >
+              {menuItems.map((node, i) => (
+                <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
+              ))}
+            </div>}
       </div>
     );
   }
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
index fbb7d1d..a532ff0 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -5,11 +5,7 @@ import cx from 'classnames';
 
 import ResizableHandle from './ResizableHandle';
 import resizableConfig from '../../util/resizableConfig';
-import {
-  GRID_BASE_UNIT,
-  GRID_ROW_HEIGHT_UNIT,
-  GRID_GUTTER_SIZE,
-} from '../../util/constants';
+import { GRID_BASE_UNIT, GRID_GUTTER_SIZE } from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -25,10 +21,14 @@ const propTypes = {
   maxWidthMultiple: PropTypes.number,
   minHeightMultiple: PropTypes.number,
   maxHeightMultiple: PropTypes.number,
+  staticHeight: PropTypes.number,
   staticHeightMultiple: PropTypes.number,
+  staticWidth: PropTypes.number,
+  staticWidthMultiple: PropTypes.number,
   onResizeStop: PropTypes.func,
   onResize: PropTypes.func,
   onResizeStart: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -37,14 +37,17 @@ const defaultProps = {
   adjustableHeight: true,
   gutterWidth: GRID_GUTTER_SIZE,
   widthStep: GRID_BASE_UNIT,
-  heightStep: GRID_ROW_HEIGHT_UNIT,
+  heightStep: GRID_BASE_UNIT,
   widthMultiple: null,
   heightMultiple: null,
   minWidthMultiple: 1,
   maxWidthMultiple: Infinity,
   minHeightMultiple: 1,
   maxHeightMultiple: Infinity,
+  staticHeight: null,
   staticHeightMultiple: null,
+  staticWidth: null,
+  staticWidthMultiple: null,
   onResizeStop: null,
   onResize: null,
   onResizeStart: null,
@@ -99,9 +102,9 @@ class ResizableContainer extends React.PureComponent {
 
     if (onResizeStop) {
       const nextWidthMultiple =
-        Math.round(widthMultiple + (delta.width / (widthStep + gutterWidth)));
+        widthMultiple + Math.round(delta.width / (widthStep + gutterWidth));
       const nextHeightMultiple =
-        Math.round(heightMultiple + (delta.height / heightStep));
+        heightMultiple + Math.round(delta.height / heightStep);
 
       onResizeStop({
         id,
@@ -131,6 +134,7 @@ class ResizableContainer extends React.PureComponent {
       minHeightMultiple,
       maxHeightMultiple,
       gutterWidth,
+      editMode,
     } = this.props;
 
     const size = {
@@ -146,6 +150,14 @@ class ResizableContainer extends React.PureComponent {
           || undefined,
     };
 
+    if (!editMode) {
+      return (
+        <div style={{ ...size }}>
+          {children}
+        </div>
+      );
+    }
+
     let enableConfig = resizableConfig.notAdjustable;
     if (adjustableWidth && adjustableHeight) enableConfig = resizableConfig.widthAndHeight;
     else if (adjustableWidth) enableConfig = resizableConfig.widthOnly;
@@ -164,10 +176,10 @@ class ResizableContainer extends React.PureComponent {
           ? (minHeightMultiple * heightStep)
           : undefined}
         maxWidth={adjustableWidth
-          ? (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
+          ? Math.max(size.width, (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth)
           : undefined}
         maxHeight={adjustableHeight
-          ? (maxHeightMultiple * heightStep)
+          ? Math.max(size.height, maxHeightMultiple * heightStep)
           : undefined}
         size={size}
         onResizeStart={this.handleResizeStart}
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
index 6bd8658..b8d717e 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
@@ -5,11 +5,12 @@ import DashboardBuilder from '../components/DashboardBuilder';
 import {
   deleteTopLevelTabs,
   handleComponentDrop,
-} from '../actions';
+} from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboard: undoableDashboard }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
   return {
-    dashboard: undoableDashboard.present,
+    dashboardLayout: undoableLayout.present,
+    editMode,
   };
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
index f7e86cc..add5a6d 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
@@ -14,7 +14,7 @@ import {
   deleteComponent,
   updateComponents,
   handleComponentDrop,
-} from '../actions';
+} from '../actions/dashboardLayout';
 
 const propTypes = {
   component: componentShape.isRequired,
@@ -25,28 +25,29 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboard: undoableDashboard }, ownProps) {
-  const components = undoableDashboard.present;
+function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps) {
+  const dashboardLayout = undoableLayout.present;
   const { id, parentId } = ownProps;
-  const component = components[id];
+  const component = dashboardLayout[id];
   const props = {
     component,
-    parentComponent: components[parentId],
+    parentComponent: dashboardLayout[parentId],
+    editMode,
   };
 
   // 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({ id, components });
+    props.occupiedColumnCount = getTotalChildWidth({ 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 (components[childId].type === ROW_TYPE) {
+      if (dashboardLayout[childId].type === ROW_TYPE) {
         props.minColumnWidth = Math.max(
           props.minColumnWidth,
-          getTotalChildWidth({ id: childId, components }),
+          getTotalChildWidth({ id: childId, components: dashboardLayout }),
         );
       }
     });
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
index eb01616..67b2396 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
@@ -5,7 +5,7 @@ import DashboardGrid from '../components/DashboardGrid';
 import {
   handleComponentDrop,
   resizeComponent,
-} from '../actions';
+} from '../actions/dashboardLayout';
 
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
@@ -14,4 +14,4 @@ function mapDispatchToProps(dispatch) {
   }, dispatch);
 }
 
-export default connect(null, mapDispatchToProps)(DashboardGrid);
+export default connect(({ editMode }) => ({ editMode }), mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
index 52e7e7a..8855d2c 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
@@ -1,4 +1,4 @@
-import { ActionCreators as UndoActionCreators } from 'redux-undo'
+import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
@@ -8,13 +8,16 @@ import { DASHBOARD_HEADER_ID } from '../util/constants';
 import {
   updateComponents,
   handleComponentDrop,
-} from '../actions';
+} from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboard: undoableDashboard }) {
+import { setEditMode } from '../actions/editMode';
+
+function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
   return {
-    component: undoableDashboard.present[DASHBOARD_HEADER_ID],
-    canUndo: undoableDashboard.past.length > 0,
-    canRedo: undoableDashboard.future.length > 0,
+    component: undoableLayout.present[DASHBOARD_HEADER_ID],
+    canUndo: undoableLayout.past.length > 0,
+    canRedo: undoableLayout.future.length > 0,
+    editMode,
   };
 }
 
@@ -24,6 +27,7 @@ function mapDispatchToProps(dispatch) {
     handleComponentDrop,
     onUndo: UndoActionCreators.undo,
     onRedo: UndoActionCreators.redo,
+    setEditMode,
   }, dispatch);
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx b/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
new file mode 100644
index 0000000..7e70abc
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
@@ -0,0 +1,10 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import ToastPresenter from '../components/ToastPresenter';
+
+import { removeToast } from '../actions/messageToasts';
+
+export default connect(
+  ({ messageToasts: toasts }) => ({ toasts }),
+  dispatch => bindActionCreators({ removeToast }, dispatch),
+)(ToastPresenter);
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
similarity index 98%
rename from superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
rename to superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
index 9b03861..994ac47 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
@@ -10,7 +10,6 @@ import {
   ROW_TYPE,
   TAB_TYPE,
   TABS_TYPE,
-
 } from '../util/componentTypes';
 
 import {
@@ -20,7 +19,7 @@ import {
   MOVE_COMPONENT,
   CREATE_TOP_LEVEL_TABS,
   DELETE_TOP_LEVEL_TABS,
-} from '../actions';
+} from '../actions/dashboardLayout';
 
 const actionHandlers = {
   [UPDATE_COMPONENTS](state, action) {
@@ -224,7 +223,7 @@ const actionHandlers = {
   },
 };
 
-export default function dashboardReducer(state = {}, action) {
+export default function layoutReducer(state = {}, action) {
   if (action.type in actionHandlers) {
     const handler = actionHandlers[action.type];
     return handler(state, action);
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/editMode.js b/superset/assets/javascripts/dashboard/v2/reducers/editMode.js
new file mode 100644
index 0000000..b1a1630
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/editMode.js
@@ -0,0 +1,11 @@
+import { SET_EDIT_MODE } from '../actions/editMode';
+
+export default function editModeReducer(editMode = false, action) {
+  switch (action.type) {
+    case SET_EDIT_MODE:
+      return action.payload.editMode;
+
+    default:
+      return editMode;
+  }
+}
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index 9c0575e..731734d 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -1,13 +1,17 @@
 import { combineReducers } from 'redux';
 import undoable, { distinctState } from 'redux-undo';
 
-import dashboard from './dashboard';
+import dashboardLayout from './dashboardLayout';
+import editMode from './editMode';
+import messageToasts from './messageToasts';
 
-const undoableDashboard = undoable(dashboard, {
-  limit: 10,
+const undoableLayout = undoable(dashboardLayout, {
+  limit: 15,
   filter: distinctState(),
 });
 
 export default combineReducers({
-  dashboard: undoableDashboard,
+  dashboardLayout: undoableLayout,
+  editMode,
+  messageToasts,
 });
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js b/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
new file mode 100644
index 0000000..1f5728a
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
@@ -0,0 +1,18 @@
+import { ADD_TOAST, REMOVE_TOAST } from '../actions/messageToasts';
+
+export default function messageToastsReducer(toasts = [], action) {
+  switch (action.type) {
+    case ADD_TOAST: {
+      const { payload: toast } = action;
+      return [toast, ...toasts];
+    }
+
+    case REMOVE_TOAST: {
+      const { payload: { id } } = action;
+      return [...toasts].filter(toast => toast.id !== id);
+    }
+
+    default:
+      return toasts;
+  }
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
index 5f1a5b0..3651c57 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
@@ -14,7 +14,7 @@
   box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1); /* @TODO color */
 }
 
-.dashboard-builder {
+.dashboard-content {
   display: flex;
   flex-direction: row;
   flex-wrap: nowrap;
@@ -32,12 +32,12 @@
   padding-left: 8px; /* note this is added to tab-level padding, to match header */
 }
 
-.dashboard-builder .grid-container .dashboard-component-tabs {
+.dashboard-content .grid-container .dashboard-component-tabs {
   box-shadow: none;
   padding-left: 0;
 }
 
-.dashboard-builder > div:first-child {
+.dashboard-content > div:first-child {
   width: 100%;
   flex-grow: 1;
   position: relative;
@@ -62,3 +62,11 @@
   background: @almost-black !important;
   color: white !important;
 }
+
+.background--transparent {
+  background-color: transparent;
+}
+
+.background--white {
+  background-color: white;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx
deleted file mode 100644
index e011ad4..0000000
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import HTML5Backend from 'react-dnd-html5-backend';
-import { DragDropContext } from 'react-dnd';
-
-import BuilderComponentPane from './BuilderComponentPane';
-import DashboardHeader from '../containers/DashboardHeader';
-import DashboardGrid from './DashboardGrid';
-import IconButton from './IconButton';
-import DragDroppable from './dnd/DragDroppable';
-import DashboardComponent from '../containers/DashboardComponent';
-import WithPopoverMenu from './menu/WithPopoverMenu';
-
-import {
-  DASHBOARD_GRID_ID,
-  DASHBOARD_ROOT_ID,
-  DASHBOARD_ROOT_DEPTH,
-} from '../util/constants';
-
-const propTypes = {
-  editMode: PropTypes.bool,
-
-  // redux
-  dashboard: PropTypes.object.isRequired,
-  deleteTopLevelTabs: PropTypes.func.isRequired,
-  updateComponents: PropTypes.func.isRequired,
-  handleComponentDrop: PropTypes.func.isRequired,
-};
-
-const defaultProps = {
-  editMode: true,
-};
-
-class DashboardBuilder extends React.Component {
-  static shouldFocusTabs(event, container) {
-    // don't focus the tabs when we click on a tab
-    return event.target.tagName === 'UL' || (
-      /icon-button/.test(event.target.className) && container.contains(event.target)
-    );
-  }
-
-  constructor(props) {
-    super(props);
-    this.state = {
-      tabIndex: 0, // top-level tabs
-    };
-    this.handleChangeTab = this.handleChangeTab.bind(this);
-  }
-
-  handleChangeTab({ tabIndex }) {
-    this.setState(() => ({ tabIndex }));
-  }
-
-  render() {
-    const { tabIndex } = this.state;
-    const { handleComponentDrop, updateComponents, dashboard, deleteTopLevelTabs } = this.props;
-    const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
-    const rootChildId = dashboardRoot.children[0];
-    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
-
-    const gridComponentId = topLevelTabs
-      ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
-      : DASHBOARD_GRID_ID;
-
-    const gridComponent = dashboard[gridComponentId];
-
-    return (
-      <div className="dashboard-v2">
-        {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
-          <DashboardHeader />
-        ) : (
-          <DragDroppable
-            component={dashboardRoot}
-            parentComponent={null}
-            depth={DASHBOARD_ROOT_DEPTH}
-            index={0}
-            orientation="column"
-            onDrop={topLevelTabs ? null : handleComponentDrop}
-          >
-            {({ dropIndicatorProps }) => (
-              <div>
-                <DashboardHeader />
-                {dropIndicatorProps && <div {...dropIndicatorProps} />}
-              </div>
-            )}
-          </DragDroppable>)}
-
-        {topLevelTabs &&
-          <WithPopoverMenu
-            shouldFocus={DashboardBuilder.shouldFocusTabs}
-            menuItems={[
-              <IconButton
-                className="fa fa-level-down"
-                label="Collapse tab content"
-                onClick={deleteTopLevelTabs}
-              />,
-            ]}
-          >
-            <DashboardComponent
-              id={topLevelTabs.id}
-              parentId={DASHBOARD_ROOT_ID}
-              depth={DASHBOARD_ROOT_DEPTH + 1}
-              index={0}
-              renderTabContent={false}
-              onChangeTab={this.handleChangeTab}
-            />
-          </WithPopoverMenu>}
-
-        <div className="dashboard-builder">
-          <DashboardGrid
-            gridComponent={gridComponent}
-            dashboard={dashboard}
-            handleComponentDrop={handleComponentDrop}
-            updateComponents={updateComponents}
-            depth={DASHBOARD_ROOT_DEPTH + 1}
-          />
-          <BuilderComponentPane />
-        </div>
-      </div>
-    );
-  }
-}
-
-DashboardBuilder.propTypes = propTypes;
-DashboardBuilder.defaultProps = defaultProps;
-
-export default DragDropContext(HTML5Backend)(DashboardBuilder);
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
index 2bdf3cc..141c3e9 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
@@ -14,8 +14,6 @@
   opacity: 0.3;
 }
 
-.grid-container--resizing .dashboard-component-chart,
-.dashboard-builder--dragging .dashboard-component-chart,
-.dashboard-component-chart:hover {
+.dashboard-v2--editing .dashboard-component-chart:hover {
   box-shadow: inset 0 0 0 1px @gray-light;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
index 31ae21d..9565112 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
@@ -1,18 +1,34 @@
 .grid-column {
   width: 100%;
-  min-height: 56px;
 }
 
-.grid-column > .hover-menu--top {
-  top: -20px;
+/* gutters between elements in a column */
+.grid-column > :not(:only-child):not(.hover-menu):not(:last-child) {
+  margin-bottom: 16px;
+}
+
+.dashboard-v2--editing .grid-column:after {
+  border: 1px dashed transparent;
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 1px;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
+}
+
+.dashboard-v2--editing .grid-column:hover:after {
+  border: 1px solid @gray-light;
 }
 
-.grid-column.background--transparent {
-  background-color: transparent;
+.grid-column > .hover-menu--top {
+  top: -20px;
 }
 
-.grid-column.background--white {
-  background-color: white;
+.grid-column--empty {
+  min-height: 72px;
 }
 
 .grid-column--empty:before {
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
index f1d3d86..e4625d3 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
@@ -1,6 +1,6 @@
 .dashboard-component-divider {
   width: 100%;
-  padding: 24px 0; /* this is padding not margin to enable a larger mouse target */
+  padding: 8px 0; /* this is padding not margin to enable a larger mouse target */
   background-color: transparent;
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
index 77066da..37c7598 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
@@ -2,25 +2,45 @@
   width: 100%;
   line-height: 1em;
   font-weight: 700;
-  background-color: inherit;
   padding: 16px 0;
   color: @almost-black;
 }
 
+.dashboard-header .dashboard-component-header {
+  font-weight: 300;
+  width: auto;
+}
+
+.dragdroppable-row .dashboard-component-header {
+  cursor: move;
+}
+
+/* note: sizes should be a multiple of the 8px grid unit so that rows in the grid align */
 .header-small {
   font-size: 16px;
 }
 
 .header-medium {
-  font-size: 22px;
+  font-size: 24px;
 }
 
 .header-large {
   font-size: 32px;
 }
 
-.dragdroppable-row .dragdroppable-row .dashboard-component-header,
-.dragdroppable-row .dragdroppable-row .dashboard-component-divider {
+.background--white .dashboard-component-header,
+.dashboard-component-header.background--white,
+.dashboard-component-tabs .dashboard-component-header,
+.dashboard-component-tabs .dashboard-component-divider {
   padding-left: 16px;
   padding-right: 16px;
 }
+
+/*
+ * grids add margin between items, so don't double pad within columns
+ * we'll not worry about double padding on top as it can serve as a visual separator
+ */
+// .grid-content > :not(:only-child):not(:last-child) .dashboard-component-header,
+.grid-column > :not(:only-child):not(:last-child) .dashboard-component-header {
+  margin-bottom: -16px;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less
index 5da54e5..5a1803e 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less
@@ -4,5 +4,4 @@
 @import './header.less';
 @import './new-component.less';
 @import './row.less';
-@import './spacer.less';
 @import './tabs.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
index e36fee2..decb1ad 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
@@ -22,18 +22,6 @@
   font-size: 1.5em;
 }
 
-.new-component-placeholder.spacer-placeholder {
-  font-size: 1em;
-}
-
 .new-component-placeholder.fa-window-restore {
   font-size: 1em;
 }
-
-.new-component-placeholder.spacer-placeholder:after {
-  content: "";
-  position: absolute;
-  height: 60%;
-  width: 60%;
-  border: 1px dashed @gray;
-}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
index 2036815..956966d 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
@@ -5,19 +5,28 @@
   align-items: flex-start;
   width: 100%;
   height: fit-content;
-  background-color: transparent;
 }
 
-.grid-row.background--transparent {
-  background-color: transparent;
+/* gutters between elements in a row */
+.grid-row > :not(:only-child):not(:last-child):not(.hover-menu) {
+  margin-right: 16px;
 }
 
-.grid-row.background--white {
-  background-color: white;
+/* hover indicator */
+.dashboard-v2--editing .grid-row:after {
+  border: 1px dashed transparent;
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 1px;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
 }
 
-.dashboard-component-header.grid-row--white {
-  padding-left: 16px;
+.dashboard-v2--editing .grid-row:hover:after {
+  border: 1px solid @gray-light;
 }
 
 .grid-row.grid-row--empty {
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/spacer.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/spacer.less
deleted file mode 100644
index 8716c21..0000000
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/spacer.less
+++ /dev/null
@@ -1,13 +0,0 @@
-.grid-spacer {
-  width: 100%;
-  height: 100%;
-  background-color: transparent;
-}
-
-.dragdroppable .grid-spacer {
-  cursor: move;
-}
-
-.dragdroppable:hover .grid-spacer {
-  box-shadow: inset 0 0 0 1px @gray-light;
-}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
index 7c55dee..45b8a42 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
@@ -9,6 +9,11 @@
   flex-direction: column;
 }
 
+/* gutters between rows */
+.grid-content > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget) {
+  margin-bottom: 16px;
+}
+
 .empty-grid-droptarget {
   width: 100%;
   height: 100%;
@@ -33,22 +38,3 @@
   pointer-events: none;
   z-index: 10;
 }
-
-
-.grid-container .grid-row:after,
-.grid-container .grid-column:after {
-  border: 1px dashed transparent;
-  content: "";
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  top: 1px;
-  left: 0;
-  z-index: 1;
-  pointer-events: none;
-}
-
-.grid-container .grid-row:hover:after,
-.grid-container .grid-column:hover:after {
-  border: 1px solid @gray-light;
-}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
index d2a41a8..49ff5da 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
@@ -8,3 +8,4 @@
 @import './popover-menu.less';
 @import './resizable.less';
 @import './components/index.less';
+@import './toast.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
index a36ab1c..848949b 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
@@ -124,5 +124,7 @@
 }
 
 .background-style-option.background--transparent:before {
-  background: @gray-light;
+  background-image: linear-gradient(45deg, @gray 25%, transparent 25%), linear-gradient(-45deg, @gray 25%, transparent 25%), linear-gradient(45deg, transparent 75%, @gray 75%), linear-gradient(-45deg, transparent 75%, @gray 75%);
+  background-size: 8px 8px;
+  background-position: 0 0, 0 4px, 4px -4px, -4px 0px
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
index 3ce5cfd..7bdd5f8 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
@@ -29,8 +29,8 @@
   border-width: 0 1.5px 1.5px 0;
   border-right-color: @gray;
   border-bottom-color: @gray;
-  right: 16;
-  bottom: 16;
+  right: 16px;
+  bottom: 16px;
   width: 8px;
   height: 8px;
 }
@@ -38,22 +38,18 @@
 .resize-handle--right {
   width: 2px;
   height: 20px;
-  right: -2px;
-  top: 47%;
+  right: 2px;
+  top: ~"calc(50% - 9px)"; /* escape for .less */
   position: absolute;
   border-left: 1px solid @gray;
   border-right: 1px solid @gray;
 }
 
-  .grid-spacer + span .resize-handle--right {
-    right: 3px;
-  }
-
 .resize-handle--bottom {
   height: 2px;
   width: 20px;
-  bottom: 10px;
-  left: 47%;
+  bottom: 2px;
+  left: ~"calc(50% - 10px)"; /* escape for .less */
   position: absolute;
   border-top: 1px solid @gray;
   border-bottom: 1px solid @gray;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less b/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
new file mode 100644
index 0000000..a508637
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
@@ -0,0 +1,59 @@
+.toast-presenter {
+  position: fixed;
+  bottom: 16px;
+  left: 50%;
+  transform: translate(-50%, 0);
+  width: 500px;
+  z-index: 3000; // top of the world
+}
+
+.toast {
+  background: white;
+  color: @almost-black;
+  opacity: 0;
+  position: relative;
+  white-space: pre-line;
+  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
+  border-radius: 2px;
+  will-change: transform, opacity;
+  transform: translateY(-100%);
+  transition: transform .3s, opacity .3s;
+}
+
+.toast > button {
+  color: @almost-black;
+}
+
+.toast > button:hover {
+  color: @gray-dark;
+}
+
+.toast--visible {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+.toast:after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 4px;
+  height: 100%;
+}
+
+.toast--info:after {
+  background: linear-gradient(to bottom, @pink, @purple);
+}
+
+.toast--success:after {
+  background: @success;
+}
+
+.toast--warning:after {
+  background: @warning;
+}
+
+.toast--danger:after {
+  background: @danger;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
index f3a61df..254af23 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
@@ -5,3 +5,11 @@
 @gray: #879399;
 @gray-light: #CFD8DC;
 @gray-bg: #f5f5f5;
+
+/* toasts */
+@pink: #E32364;
+@purple: #2C2261;
+
+@success: #00BFA5;
+@warning: #FFAB00;
+@danger: @pink;
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js
index ab701a7..c0016f3 100644
--- a/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js
+++ b/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js
@@ -1,5 +1,4 @@
 import {
-  SPACER_TYPE,
   COLUMN_TYPE,
   CHART_TYPE,
   MARKDOWN_TYPE,
@@ -7,7 +6,6 @@ import {
 
 export default function componentIsResizable(entity) {
   return [
-    SPACER_TYPE,
     COLUMN_TYPE,
     CHART_TYPE,
     MARKDOWN_TYPE,
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
index c667138..2866898 100644
--- a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
+++ b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
@@ -8,7 +8,6 @@ export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE';
 export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE';
 export const NEW_COMPONENT_SOURCE_TYPE = 'NEW_COMPONENT_SOURCE_TYPE';
 export const ROW_TYPE = 'DASHBOARD_ROW_TYPE';
-export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE';
 export const TABS_TYPE = 'DASHBOARD_TABS_TYPE';
 export const TAB_TYPE = 'DASHBOARD_TAB_TYPE';
 
@@ -23,7 +22,6 @@ export default {
   MARKDOWN_TYPE,
   NEW_COMPONENT_SOURCE_TYPE,
   ROW_TYPE,
-  SPACER_TYPE,
   TABS_TYPE,
   TAB_TYPE,
 };
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js
index e892456..36ef71b 100644
--- a/superset/assets/javascripts/dashboard/v2/util/constants.js
+++ b/superset/assets/javascripts/dashboard/v2/util/constants.js
@@ -10,7 +10,6 @@ export const NEW_DIVIDER_ID = 'NEW_DIVIDER_ID';
 export const NEW_HEADER_ID = 'NEW_HEADER_ID';
 export const NEW_MARKDOWN_ID = 'NEW_MARKDOWN_ID';
 export const NEW_ROW_ID = 'NEW_ROW_ID';
-export const NEW_SPACER_ID = 'NEW_SPACER_ID';
 export const NEW_TAB_ID = 'NEW_TAB_ID';
 export const NEW_TABS_ID = 'NEW_TABS_ID';
 
@@ -18,7 +17,6 @@ export const NEW_TABS_ID = 'NEW_TABS_ID';
 export const DASHBOARD_ROOT_DEPTH = 0;
 export const GRID_BASE_UNIT = 8;
 export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
-export const GRID_ROW_HEIGHT_UNIT = 2 * GRID_BASE_UNIT;
 export const GRID_COLUMN_COUNT = 12;
 export const GRID_MIN_COLUMN_COUNT = 3;
 export const GRID_MIN_ROW_UNITS = 5;
@@ -33,3 +31,9 @@ export const LARGE_HEADER = 'LARGE_HEADER';
 // Style types
 export const BACKGROUND_WHITE = 'BACKGROUND_WHITE';
 export const BACKGROUND_TRANSPARENT = 'BACKGROUND_TRANSPARENT';
+
+// Toast types
+export const INFO_TOAST = 'INFO_TOAST';
+export const SUCCESS_TOAST = 'SUCCESS_TOAST';
+export const WARNING_TOAST = 'WARNING_TOAST';
+export const DANGER_TOAST = 'DANGER_TOAST';
diff --git a/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js b/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js
new file mode 100644
index 0000000..0fd0c4e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js
@@ -0,0 +1,24 @@
+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';
+
+export default function doesChildOverflowParent(dropResult, components) {
+  const { source, destination, dragging } = dropResult;
+  const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
+
+  const grandparentId = findParentId({ childId: destination.id, components });
+
+  const child = isNewComponent ? newComponentFactory(dragging.type) : components[dragging.id] || {};
+  const parent = components[destination.id] || {};
+  const grandparent = components[grandparentId] || {};
+
+  const grandparentWidth = (grandparent.meta && grandparent.meta.width) || GRID_COLUMN_COUNT;
+  const parentWidth = (parent.meta && parent.meta.width) || grandparentWidth;
+  const parentChildWidth = parent.type === COLUMN_TYPE
+    ? 0 : getChildWidth({ id: destination.id, components });
+  const childWidth = (child.meta && child.meta.width) || 0;
+
+  return parentWidth - parentChildWidth < childWidth;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js b/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
index 516624d..aa32b96 100644
--- a/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
+++ b/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
@@ -1,4 +1,4 @@
-export default function getTotalChildWidth({ id, components, recurse = false }) {
+export default function getTotalChildWidth({ id, components }) {
   const component = components[id];
   if (!component) return 0;
 
@@ -7,9 +7,6 @@ export default function getTotalChildWidth({ id, components, recurse = false })
   (component.children || []).forEach((childId) => {
     const child = components[childId];
     width += child.meta.width || 0;
-    if (recurse) {
-      width += getTotalChildWidth({ id: childId, components, recurse }) || 0;
-    }
   });
 
   return width;
diff --git a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
index 6a3bd0e..9605db2 100644
--- a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
+++ b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
@@ -6,6 +6,8 @@ export const DROP_RIGHT = 'DROP_RIGHT';
 export const DROP_BOTTOM = 'DROP_BOTTOM';
 export const DROP_LEFT = 'DROP_LEFT';
 
+// this defines how close the mouse must be to the edge of a component to display
+// a sibling type drop indicator
 const SIBLING_DROP_THRESHOLD = 15;
 
 export default function getDropPosition(monitor, Component) {
diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
index 9c6ae8e..66942f0 100644
--- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
+++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
@@ -23,7 +23,6 @@ import {
   HEADER_TYPE,
   MARKDOWN_TYPE,
   ROW_TYPE,
-  SPACER_TYPE,
   TABS_TYPE,
   TAB_TYPE,
 } from './componentTypes';
@@ -48,7 +47,6 @@ const parentMaxDepthLookup = {
     [DIVIDER_TYPE]: depthOne,
     [HEADER_TYPE]: depthOne,
     [ROW_TYPE]: depthOne,
-    [SPACER_TYPE]: depthOne,
     [TABS_TYPE]: depthOne,
   },
 
@@ -56,7 +54,6 @@ const parentMaxDepthLookup = {
     [CHART_TYPE]: depthFour,
     [MARKDOWN_TYPE]: depthFour,
     [COLUMN_TYPE]: depthTwo,
-    [SPACER_TYPE]: depthFour,
   },
 
   [TABS_TYPE]: {
@@ -69,7 +66,6 @@ const parentMaxDepthLookup = {
     [DIVIDER_TYPE]: depthTwo,
     [HEADER_TYPE]: depthTwo,
     [ROW_TYPE]: depthTwo,
-    [SPACER_TYPE]: depthTwo,
     [TABS_TYPE]: depthTwo,
   },
 
@@ -78,7 +74,6 @@ const parentMaxDepthLookup = {
     [HEADER_TYPE]: depthThree,
     [MARKDOWN_TYPE]: depthThree,
     [ROW_TYPE]: depthThree,
-    [SPACER_TYPE]: depthThree,
   },
 
   // these have no valid children
@@ -86,11 +81,13 @@ const parentMaxDepthLookup = {
   [DIVIDER_TYPE]: {},
   [HEADER_TYPE]: {},
   [MARKDOWN_TYPE]: {},
-  [SPACER_TYPE]: {},
 };
 
 export default function isValidChild({ parentType, childType, parentDepth }) {
-  if (!parentType || !childType || typeof parentDepth !== 'number') return false;
+  if (!parentType || !childType || typeof parentDepth !== 'number') {
+    return false;
+  }
+
   const maxParentDepth = (parentMaxDepthLookup[parentType] || {})[childType];
 
   return typeof maxParentDepth === 'number' && parentDepth <= maxParentDepth;
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
index 9bc01a7..af69eb8 100644
--- a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
@@ -5,7 +5,6 @@ import {
   HEADER_TYPE,
   MARKDOWN_TYPE,
   ROW_TYPE,
-  SPACER_TYPE,
   TABS_TYPE,
   TAB_TYPE,
 } from './componentTypes';
@@ -16,7 +15,7 @@ import {
 } from './constants';
 
 const typeToDefaultMetaData = {
-  [CHART_TYPE]: { width: 3, height: 15 },
+  [CHART_TYPE]: { width: 3, height: 30 },
   [COLUMN_TYPE]: { width: 3, background: BACKGROUND_TRANSPARENT },
   [DIVIDER_TYPE]: null,
   [HEADER_TYPE]: {
@@ -24,9 +23,8 @@ const typeToDefaultMetaData = {
     headerSize: MEDIUM_HEADER,
     background: BACKGROUND_TRANSPARENT,
   },
-  [MARKDOWN_TYPE]: { width: 3, height: 15 },
+  [MARKDOWN_TYPE]: { width: 3, height: 30 },
   [ROW_TYPE]: { background: BACKGROUND_TRANSPARENT },
-  [SPACER_TYPE]: {},
   [TABS_TYPE]: null,
   [TAB_TYPE]: { text: 'New Tab' },
 };
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js b/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js
deleted file mode 100644
index 38d1c7c..0000000
--- a/superset/assets/javascripts/dashboard/v2/util/newComponentIdToType.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
-  CHART_TYPE,
-  COLUMN_TYPE,
-  DIVIDER_TYPE,
-  HEADER_TYPE,
-  MARKDOWN_TYPE,
-  ROW_TYPE,
-  SPACER_TYPE,
-  TABS_TYPE,
-  TAB_TYPE,
-} from './componentTypes';
-
-import {
-  NEW_CHART_ID,
-  NEW_COLUMN_ID,
-  NEW_DIVIDER_ID,
-  NEW_HEADER_ID,
-  NEW_MARKDOWN_ID,
-  NEW_ROW_ID,
-  NEW_SPACER_ID,
-  NEW_TABS_ID,
-  NEW_TAB_ID,
-} from './constants';
-
-export default {
-  [NEW_CHART_ID]: CHART_TYPE, // @TODO we will have to encode real chart ids => type in the future
-  [NEW_COLUMN_ID]: COLUMN_TYPE,
-  [NEW_DIVIDER_ID]: DIVIDER_TYPE,
-  [NEW_HEADER_ID]: HEADER_TYPE,
-  [NEW_MARKDOWN_ID]: MARKDOWN_TYPE,
-  [NEW_ROW_ID]: ROW_TYPE,
-  [NEW_SPACER_ID]: SPACER_TYPE,
-  [NEW_TABS_ID]: TABS_TYPE,
-  [NEW_TAB_ID]: TAB_TYPE,
-};
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
index d701cc2..8acc192 100644
--- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
 import componentTypes from './componentTypes';
 import backgroundStyleOptions from './backgroundStyleOptions';
 import headerStyleOptions from './headerStyleOptions';
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from './constants';
 
 export const componentShape = PropTypes.shape({ // eslint-disable-line
   id: PropTypes.string.isRequired,
@@ -22,3 +23,9 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
     background: PropTypes.oneOf(backgroundStyleOptions.map(opt => opt.value)),
   }),
 });
+
+export const toastShape = PropTypes.shape({
+  id: PropTypes.string.isRequired,
+  toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
+  text: PropTypes.string.isRequired,
+});
diff --git a/superset/assets/src/components/EditableTitle.jsx b/superset/assets/src/components/EditableTitle.jsx
index a7e3f17..45fea1d 100644
--- a/superset/assets/src/components/EditableTitle.jsx
+++ b/superset/assets/src/components/EditableTitle.jsx
@@ -116,7 +116,7 @@ class EditableTitle extends React.PureComponent {
   }
 
   render() {
-    let input = (
+    let content = (
       <input
         required
         type={this.state.isEditing ? 'text' : 'button'}
@@ -129,19 +129,25 @@ class EditableTitle extends React.PureComponent {
       />
     );
     if (this.props.showTooltip) {
-      input = (
+      content = (
         <TooltipWrapper
           label="title"
           tooltip={this.props.canEdit ? t('click to edit title') :
               this.props.noPermitTooltip || t('You don\'t have the rights to alter this title.')}
         >
-          {input}
+          {content}
         </TooltipWrapper>
       );
     }
     return (
-      <span className={cx('editable-title', this.props.canEdit && 'editable-title--editable')}>
-        {input}
+      <span
+        className={cx(
+          'editable-title',
+          this.props.canEdit && 'editable-title--editable',
+          this.state.isEditing && 'editable-title--editing',
+        )}
+      >
+        {content}
       </span>
     );
   }
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index bb21a43..1aadc58 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -19,12 +19,15 @@ initJQueryAjax();
 const appContainer = document.getElementById('app');
 // const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
 // const initState = Object.assign({}, getInitialState(bootstrapData));
+
 const initState = {
-  dashboard: {
+  dashboardLayout: {
     past: [],
     present: emptyDashboardLayout,
     future: [],
   },
+  editMode: true,
+  messageToasts: [],
 };
 
 const store = createStore(
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index e9f508b..743daa8 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -233,32 +233,37 @@ table.table-no-hover tr:hover {
   border: none;
   box-shadow: none;
   padding: 0;
+  cursor: initial;
 }
 
 .editable-title input[type="button"] {
-    border-color: transparent;
-    background: transparent;
-    font-size: inherit;
-    line-height: inherit;
-    white-space: normal;
-    text-align: left;
+  border-color: transparent;
+  background: transparent;
+  font-size: inherit;
+  line-height: inherit;
+  white-space: normal;
+  text-align: left;
 }
 
-.editable-title--editable input[type="button"]:hover {
-    cursor: text;
+.editable-title.editable-title--editable {
+  cursor: pointer;
+}
+
+.editable-title.editable-title--editing {
+  cursor: text;
 }
 
 .m-r-5 {
-    margin-right: 5px;
+  margin-right: 5px;
 }
 .m-r-3 {
-    margin-right: 3px;
+  margin-right: 3px;
 }
 .m-t-5 {
-    margin-top: 5px;
+  margin-top: 5px;
 }
 .m-t-10 {
-    margin-top: 10px;
+  margin-top: 10px;
 }
 .m-b-10 {
     margin-bottom: 10px;


[incubator-superset] 10/26: fix dashboard server-side unit tests (#5009)

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

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

commit 82ab75e39dbd290ea1195007833937fd3fa26786
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue May 22 16:42:02 2018 -0700

    fix dashboard server-side unit tests (#5009)
---
 superset/models/core.py  |   9 ++++
 superset/views/core.py   |  17 ++++---
 tests/dashboard_tests.py | 123 ++++++++++++++++++++++++++++++++++++-----------
 3 files changed, 112 insertions(+), 37 deletions(-)

diff --git a/superset/models/core.py b/superset/models/core.py
index b450be0..df0aafc 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -343,6 +343,15 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
 
     @property
     def url(self):
+        if self.json_metadata:
+            # add default_filters to the preselect_filters of dashboard
+            json_metadata = json.loads(self.json_metadata)
+            default_filters = json_metadata.get('default_filters')
+            # make sure default_filters is not empty
+            if json.loads(default_filters):
+                filters = parse.quote(default_filters.encode('utf8'))
+                return '/superset/dashboard/{}/?preselect_filters={}'.format(
+                    self.slug or self.id, filters)
         return '/superset/dashboard/{}/'.format(self.slug or self.id)
 
     @property
diff --git a/superset/views/core.py b/superset/views/core.py
index 1e0d78f..b14050a 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1563,9 +1563,10 @@ class Superset(BaseSupersetView):
 
             # update chartId of layout entities
             for value in data['positions'].values():
-                if isinstance(value, dict) and value.get('meta') \
-                    and value.get('meta').get('chartId'):
-
+                if (
+                    isinstance(value, dict) and value.get('meta') and
+                    value.get('meta').get('chartId')
+                ):
                     old_id = value.get('meta').get('chartId')
                     new_id = old_to_new_sliceids[old_id]
                     value['meta']['chartId'] = new_id
@@ -1591,7 +1592,6 @@ class Superset(BaseSupersetView):
                 .filter_by(id=dashboard_id).first())
         check_ownership(dash, raise_if_false=True)
         data = json.loads(request.form.get('data'))
-        original_slice_names = {(slc.id): slc.slice_name for slc in dash.slices}
         self._set_dash_metadata(dash, data)
         session.merge(dash)
         session.commit()
@@ -1605,9 +1605,10 @@ class Superset(BaseSupersetView):
         slice_ids = []
         slice_id_to_name = {}
         for value in positions.values():
-            if isinstance(value, dict) and value.get('meta') \
-                and value.get('meta').get('chartId'):
-
+            if (
+                isinstance(value, dict) and value.get('meta') and
+                value.get('meta').get('chartId')
+            ):
                 slice_id = value.get('meta').get('chartId')
                 slice_ids.append(slice_id)
                 slice_id_to_name[slice_id] = value.get('meta').get('chartName')
@@ -1639,7 +1640,7 @@ class Superset(BaseSupersetView):
         if 'filter_immune_slice_fields' not in md:
             md['filter_immune_slice_fields'] = {}
         md['expanded_slices'] = data['expanded_slices']
-        default_filters_data = json.loads(data.get('default_filters', ''))
+        default_filters_data = json.loads(data.get('default_filters', '{}'))
         for key in default_filters_data.keys():
             if int(key) not in slice_ids:
                 del default_filters_data[key]
diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py
index 60c749b..e9cd85b 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -48,7 +48,12 @@ class DashboardTests(SupersetTestCase):
             .filter_by(slug='births')
             .first()
         )
-        resp = self.get_resp(dash.url + '?edit=true&standalone=true')
+        url = dash.url
+        if dash.url.find('?') == -1:
+            url += '?'
+        else:
+            url += '&'
+        resp = self.get_resp(url + 'edit=true&standalone=true')
         self.assertIn('editMode&#34;: true', resp)
         self.assertIn('standalone_mode&#34;: true', resp)
 
@@ -56,15 +61,20 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = []
+        positions = {}
         for i, slc in enumerate(dash.slices):
+            id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
-                'col': 0,
-                'row': i * 4,
-                'size_x': 4,
-                'size_y': 4,
-                'slice_id': '{}'.format(slc.id)}
-            positions.append(d)
+                'type': 'DASHBOARD_CHART_TYPE',
+                'id': id,
+                'children': [],
+                'meta': {
+                    'width': 4,
+                    'height': 50,
+                    'chartId': slc.id,
+                },
+            }
+            positions[id] = d
         data = {
             'css': '',
             'expanded_slices': {},
@@ -79,15 +89,20 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='world_health').first()
-        positions = []
+        positions = {}
         for i, slc in enumerate(dash.slices):
+            id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
-                'col': 0,
-                'row': i * 4,
-                'size_x': 4,
-                'size_y': 4,
-                'slice_id': '{}'.format(slc.id)}
-            positions.append(d)
+                'type': 'DASHBOARD_CHART_TYPE',
+                'id': id,
+                'children': [],
+                'meta': {
+                    'width': 4,
+                    'height': 50,
+                    'chartId': slc.id,
+                },
+            }
+            positions[id] = d
 
         filters = {str(dash.slices[0].id): {'region': ['North America']}}
         default_filters = json.dumps(filters)
@@ -119,15 +134,20 @@ class DashboardTests(SupersetTestCase):
             .first()
         )
         origin_title = dash.dashboard_title
-        positions = []
+        positions = {}
         for i, slc in enumerate(dash.slices):
+            id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
-                'col': 0,
-                'row': i * 4,
-                'size_x': 4,
-                'size_y': 4,
-                'slice_id': '{}'.format(slc.id)}
-            positions.append(d)
+                'type': 'DASHBOARD_CHART_TYPE',
+                'id': id,
+                'children': [],
+                'meta': {
+                    'width': 4,
+                    'height': 50,
+                    'chartId': slc.id,
+                },
+            }
+            positions[id] = d
         data = {
             'css': '',
             'expanded_slices': {},
@@ -150,15 +170,20 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = []
+        positions = {}
         for i, slc in enumerate(dash.slices):
+            id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
-                'col': 0,
-                'row': i * 4,
-                'size_x': 4,
-                'size_y': 4,
-                'slice_id': '{}'.format(slc.id)}
-            positions.append(d)
+                'type': 'DASHBOARD_CHART_TYPE',
+                'id': id,
+                'children': [],
+                'meta': {
+                    'width': 4,
+                    'height': 50,
+                    'chartId': slc.id,
+                },
+            }
+            positions[id] = d
         data = {
             'css': '',
             'duplicate_slices': False,
@@ -213,6 +238,46 @@ class DashboardTests(SupersetTestCase):
             o for o in dash.slices if o.slice_name != 'Mapbox Long/Lat']
         db.session.commit()
 
+    def test_remove_slices(self, username='admin'):
+        self.login(username=username)
+        dash = db.session.query(models.Dashboard).filter_by(
+            slug='births').first()
+        positions = {}
+        origin_slices_length = len(dash.slices)
+        for i, slc in enumerate(dash.slices):
+            id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
+            d = {
+                'type': 'DASHBOARD_CHART_TYPE',
+                'id': id,
+                'children': [],
+                'meta': {
+                    'width': 4,
+                    'height': 50,
+                    'chartId': slc.id,
+                },
+            }
+            # remove last slice
+            if i < len(dash.slices) - 1:
+                positions[id] = d
+
+        data = {
+            'css': '',
+            'expanded_slices': {},
+            'positions': positions,
+            'dashboard_title': dash.dashboard_title,
+        }
+
+        # save dash
+        dash_id = dash.id
+        url = '/superset/save_dash/{}/'.format(dash_id)
+        self.client.post(url, data=dict(data=json.dumps(data)))
+        dash = db.session.query(models.Dashboard).filter_by(
+            id=dash_id).first()
+
+        # verify slices data
+        data = dash.data
+        self.assertEqual(len(data['slices']), origin_slices_length - 1)
+
     def test_public_user_dashboard_access(self):
         table = (
             db.session


[incubator-superset] 17/26: [dashboard v2] add v1 switch (#5126)

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

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

commit 0e0c76881dcc7ce6cadd23dfabaef01970e32379
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Wed Jun 6 14:10:37 2018 -0700

    [dashboard v2] add v1 switch (#5126)
    
    * [dashboard] copy all dashboard v1 into working v1 switch
    
    * [dashboard] add functional v1 <> v2 switch with messaging
    
    * [dashboard] add v2 logging to v1 dashboard, add read-v2-changes link, add client logging to track v1 <> v2 switches
    
    * [dashboard] Remove default values for feedback url + v2 auto convert date
    
    * [dashboard v2] fix misc UI/UX issues
    
    * [dashboard v2] fix Markdown persistance issues and css, fix copy dash title, don't enforce shallow hovering with drop indicator
    
    * [dashboard v2] improve non-shallow drop target UX, fix Markdown drop indicator, clarify slice adder filter/sort
    
    * [dashboard v2] delete empty rows on drag or delete events that leave them without children, add test
    
    * [dashboard v2] improve v1<>v2 switch modals, add convert to v2 badge in v1, fix unsaved changes issue in preview mode, don't auto convert column child widths for now
    
    * [dashboard v2][dnd] add drop position cache to fix non-shallow drops
    
    * [dashboard] fix test script with glob instead of recurse, fix tests, add temp fix for tab nesting, ignore v1 lint errors
    
    * [dashboard] v2 badge style tweaks, add back v1 _set_dash_metadata for v1 editing
    
    * [dashboard] fix python linting and tests
    
    * [dashboard] lint tests
---
 superset/assets/.eslintignore                      |   1 +
 superset/assets/package.json                       |   4 +-
 .../dashboard/actions/dashboardLayout_spec.js      |   5 +-
 .../dashboard/components/DashboardBuilder_spec.jsx |  26 +-
 .../dashboard/components/DashboardGrid_spec.jsx    |  11 +-
 .../dashboard/fixtures/mockDashboardState.js       |   2 +
 .../dashboard/reducers/dashboardLayout_spec.js     |  74 ++--
 .../dashboard/reducers/dashboardState_spec.js      |   4 +-
 .../dashboard/util/isValidChild_spec.js            |   2 +-
 superset/assets/src/chart/Chart.jsx                |   4 +-
 .../src/dashboard/actions/dashboardLayout.js       |  15 +-
 .../assets/src/dashboard/actions/messageToasts.js  |  12 +-
 .../src/dashboard/components/AddSliceCard.jsx      |   2 +
 .../assets/src/dashboard/components/Controls.jsx   | 138 -------
 .../assets/src/dashboard/components/Dashboard.jsx  |  22 +-
 .../src/dashboard/components/DashboardBuilder.jsx  |  40 +-
 .../src/dashboard/components/DashboardGrid.jsx     |  73 +---
 .../assets/src/dashboard/components/Header.jsx     | 177 +++++----
 .../dashboard/components/HeaderActionsDropdown.jsx | 163 ++++++++
 .../assets/src/dashboard/components/SaveModal.jsx  |  10 +-
 .../assets/src/dashboard/components/SliceAdder.jsx |  19 +-
 superset/assets/src/dashboard/components/Toast.jsx |  15 +-
 .../src/dashboard/components/dnd/handleDrop.js     |   2 +
 .../src/dashboard/components/dnd/handleHover.js    |   2 +-
 .../components/gridComponents/ChartHolder.jsx      |   2 +-
 .../components/gridComponents/Markdown.jsx         |  49 ++-
 .../dashboard/components/gridComponents/Tab.jsx    |  23 +-
 superset/assets/src/dashboard/containers/Chart.jsx |   2 +-
 .../src/dashboard/containers/DashboardHeader.jsx   |  19 +-
 .../deprecated/PromptV2ConversionModal.jsx         | 102 +++++
 .../src/dashboard/deprecated/V2PreviewModal.jsx    | 148 +++++++
 .../src/{ => dashboard/deprecated}/chart/Chart.jsx | 150 ++++----
 .../src/dashboard/deprecated/chart/ChartBody.jsx   |  55 +++
 .../dashboard/deprecated/chart/ChartContainer.jsx  |  29 ++
 .../src/dashboard/deprecated/chart/chart.css       |   4 +
 .../src/dashboard/deprecated/chart/chartAction.js  | 195 ++++++++++
 .../src/dashboard/deprecated/chart/chartReducer.js | 158 ++++++++
 .../assets/src/dashboard/deprecated/v1/actions.js  | 127 +++++++
 .../deprecated/v1/components/CodeModal.jsx         |  48 +++
 .../deprecated/v1/components/Controls.jsx          | 214 +++++++++++
 .../deprecated/v1/components/CssEditor.jsx         |  91 +++++
 .../deprecated/v1/components/Dashboard.jsx         | 423 +++++++++++++++++++++
 .../v1/components/DashboardContainer.jsx           |  31 ++
 .../deprecated/v1/components/GridCell.jsx          | 157 ++++++++
 .../deprecated/v1/components/GridLayout.jsx        | 198 ++++++++++
 .../dashboard/deprecated/v1/components/Header.jsx  | 184 +++++++++
 .../v1/components/RefreshIntervalModal.jsx         |  64 ++++
 .../deprecated/v1/components/SaveModal.jsx         | 161 ++++++++
 .../deprecated/v1/components/SliceAdder.jsx        | 219 +++++++++++
 .../deprecated/v1/components/SliceHeader.jsx       | 194 ++++++++++
 .../assets/src/dashboard/deprecated/v1/index.jsx   |  28 ++
 .../assets/src/dashboard/deprecated/v1/reducers.js | 272 +++++++++++++
 .../src/dashboard/reducers/dashboardLayout.js      |  58 +--
 .../src/dashboard/reducers/dashboardState.js       |   2 +
 .../src/dashboard/reducers/getInitialState.js      |  27 +-
 .../dashboard/stylesheets/builder-sidepane.less    |  41 +-
 .../assets/src/dashboard/stylesheets/builder.less  |   1 -
 .../dashboard/stylesheets/components/markdown.less |   7 +-
 .../stylesheets/components/new-component.less      |   6 +-
 .../src/dashboard/stylesheets/components/tabs.less |   4 +
 .../src/dashboard/stylesheets/dashboard.less       |  74 +++-
 .../src/dashboard/stylesheets/popover-menu.less    |   1 +
 .../assets/src/dashboard/util/getDropPosition.js   |  58 ++-
 .../assets/src/dashboard/util/injectCustomCss.js   |  17 +
 superset/assets/src/dashboard/util/isValidChild.js |   2 +-
 superset/assets/src/dashboard/util/propShapes.jsx  |   1 +
 superset/assets/src/logger.js                      |  13 +
 .../assets/stylesheets/dashboard_deprecated.css    | 181 +++++++++
 superset/assets/webpack.config.js                  |   1 +
 superset/config.py                                 |   8 +
 superset/connectors/sqla/models.py                 |   4 +-
 .../superset/dashboard_v1_deprecated.html          |  10 +
 superset/views/core.py                             | 103 ++++-
 tests/dashboard_tests.py                           |  20 +-
 74 files changed, 4213 insertions(+), 596 deletions(-)

diff --git a/superset/assets/.eslintignore b/superset/assets/.eslintignore
index 7479173..61262fc 100644
--- a/superset/assets/.eslintignore
+++ b/superset/assets/.eslintignore
@@ -8,3 +8,4 @@ node_modules*/*
 stylesheets/*
 vendor/*
 docs/*
+src/dashboard/deprecated/*
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 21abd17..c68e490 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -8,8 +8,8 @@
     "test": "spec"
   },
   "scripts": {
-    "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/**/*_spec.*",
-    "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js --recursive spec/**/*_spec.*",
+    "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js 'spec/**/*_spec.*'",
+    "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js 'spec/**/*_spec.*'",
     "dev": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool eval-cheap-source-map",
     "dev-slow": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool inline-source-map",
     "dev-fast": "echo 'dev-fast in now replaced by dev'",
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 0c4fe12..84f0856 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -138,7 +138,6 @@ describe('dashboardLayout actions', () => {
     });
   });
 
-  // describe('createComponent', () => {});
   describe('createTopLevelTabs', () => {
     it('should dispatch a createTopLevelTabs action', () => {
       const { getState, dispatch } = setup({
@@ -282,7 +281,7 @@ describe('dashboardLayout actions', () => {
 
     it('should move a component if the component is not new', () => {
       const { getState, dispatch } = setup({
-        dashboardLayout: { present: { id: { type: ROW_TYPE } } },
+        dashboardLayout: { present: { id: { type: ROW_TYPE, children: [] } } },
       });
       const dropResult = {
         source: { id: 'id', index: 0, type: ROW_TYPE },
@@ -324,7 +323,7 @@ describe('dashboardLayout actions', () => {
       );
     });
 
-    it('should delete the parent Tabs if the moved Tab was the only child', () => {
+    it('should delete a parent Row or Tabs if the moved child was the only child', () => {
       const { getState, dispatch } = setup({
         dashboardLayout: {
           present: {
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
index 6b5d051..4c3185f 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
@@ -13,8 +13,7 @@ import DashboardBuilder from '../../../../src/dashboard/components/DashboardBuil
 import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent';
 import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader';
 import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid';
-import DragDroppable from '../../../../src/dashboard/components/dnd/DragDroppable';
-
+import WithDragDropContext from '../helpers/WithDragDropContext';
 import {
   dashboardLayout as undoableDashboardLayout,
   dashboardLayoutWithTabs as undoableDashboardLayoutWithTabs,
@@ -32,12 +31,17 @@ describe('DashboardBuilder', () => {
     editMode: false,
     showBuilderPane: false,
     handleComponentDrop() {},
+    toggleBuilderPane() {},
   };
 
   function setup(overrideProps, useProvider = false, store = mockStore) {
     const builder = <DashboardBuilder {...props} {...overrideProps} />;
     return useProvider
-      ? mount(<Provider store={store}>{builder}</Provider>)
+      ? mount(
+          <Provider store={store}>
+            <WithDragDropContext>{builder}</WithDragDropContext>
+          </Provider>,
+        )
       : shallow(builder);
   }
 
@@ -56,23 +60,11 @@ describe('DashboardBuilder', () => {
     );
   });
 
-  it('should render a DashboardHeader', () => {
-    const wrapper = setup();
+  it('should render a DragDroppable DashboardHeader', () => {
+    const wrapper = setup(null, true);
     expect(wrapper.find(DashboardHeader)).to.have.length(1);
   });
 
-  it('should render a DragDroppable DashboardHeader if editMode=true and no top-level Tabs exist', () => {
-    const withoutTabs = setup();
-    const withoutTabsEditMode = setup({ editMode: true });
-    const withTabs = setup({
-      dashboardLayout: layoutWithTabs,
-    });
-
-    expect(withoutTabs.find(DragDroppable)).to.have.length(0);
-    expect(withoutTabsEditMode.find(DragDroppable)).to.have.length(1);
-    expect(withTabs.find(DragDroppable)).to.have.length(0);
-  });
-
   it('should render a Sticky top-level Tabs if the dashboard has tabs', () => {
     const wrapper = setup(
       { dashboardLayout: layoutWithTabs },
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
index 7e9de51..3121e7e 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
@@ -42,9 +42,14 @@ describe('DashboardGrid', () => {
     expect(wrapper.find(DashboardComponent)).to.have.length(2);
   });
 
-  it('should render two empty DragDroppables targets when editMode=true', () => {
-    const wrapper = setup({ editMode: true });
-    expect(wrapper.find(DragDroppable)).to.have.length(2);
+  it('should render an empty DragDroppables target when the gridComponent has no children', () => {
+    const withChildren = setup({ editMode: true });
+    const withoutChildren = setup({
+      editMode: true,
+      gridComponent: { ...props.gridComponent, children: [] },
+    });
+    expect(withChildren.find(DragDroppable)).to.have.length(0);
+    expect(withoutChildren.find(DragDroppable)).to.have.length(1);
   });
 
   it('should render grid column guides when resizing', () => {
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
index 9d05344..fd640d1 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
@@ -10,4 +10,6 @@ export default {
   hasUnsavedChanges: false,
   maxUndoHistoryExceeded: false,
   isStarred: true,
+  css: '',
+  isV2Preview: false, // @TODO remove upon v1 deprecation
 };
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
index cbe1729..dd933ac 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
@@ -14,7 +14,6 @@ import {
 
 import {
   CHART_TYPE,
-  COLUMN_TYPE,
   DASHBOARD_GRID_TYPE,
   DASHBOARD_ROOT_TYPE,
   ROW_TYPE,
@@ -25,7 +24,6 @@ import {
 import {
   DASHBOARD_ROOT_ID,
   DASHBOARD_GRID_ID,
-  GRID_MIN_COLUMN_COUNT,
   NEW_COMPONENTS_SOURCE_ID,
   NEW_TABS_ID,
   NEW_ROW_ID,
@@ -54,6 +52,7 @@ describe('dashboardLayout reducer', () => {
           },
           parentId: {
             id: 'parentId',
+            type: ROW_TYPE,
             children: ['toDelete', 'anotherId'],
           },
         },
@@ -66,6 +65,42 @@ describe('dashboardLayout reducer', () => {
       parentId: {
         id: 'parentId',
         children: ['anotherId'],
+        type: ROW_TYPE,
+      },
+    });
+  });
+
+  it('should delete a parent if the parent was a row and no longer has children', () => {
+    expect(
+      layoutReducer(
+        {
+          grandparentId: {
+            id: 'grandparentId',
+            children: ['parentId'],
+          },
+          parentId: {
+            id: 'parentId',
+            type: ROW_TYPE,
+            children: ['toDelete'],
+          },
+          toDelete: {
+            id: 'toDelete',
+            children: ['child1'],
+          },
+          child1: {
+            id: 'child1',
+            children: [],
+          },
+        },
+        {
+          type: DELETE_COMPONENT,
+          payload: { id: 'toDelete', parentId: 'parentId' },
+        },
+      ),
+    ).to.deep.equal({
+      grandparentId: {
+        id: 'grandparentId',
+        children: [],
       },
     });
   });
@@ -170,41 +205,6 @@ describe('dashboardLayout reducer', () => {
     });
   });
 
-  it('should set the width of a moved component with column type parent to the minimum width', () => {
-    const layout = {
-      source: {
-        id: 'source',
-        type: ROW_TYPE,
-        children: ['dontMove', 'toMove'],
-      },
-      destination: {
-        id: 'destination',
-        type: COLUMN_TYPE,
-        children: [],
-        meta: { width: 100 },
-      },
-      toMove: {
-        id: 'toMove',
-        type: CHART_TYPE,
-        children: [],
-        meta: { width: 1001 },
-      },
-    };
-
-    const dropResult = {
-      source: { id: 'source', type: ROW_TYPE, index: 1 },
-      destination: { id: 'destination', type: COLUMN_TYPE, index: 0 },
-      dragging: { id: 'toMove', type: CHART_TYPE },
-    };
-
-    const result = layoutReducer(layout, {
-      type: MOVE_COMPONENT,
-      payload: { dropResult },
-    });
-
-    expect(result.toMove.meta.width).to.equal(GRID_MIN_COLUMN_COUNT);
-  });
-
   it('should wrap a moved component in a row if need be', () => {
     const layout = {
       source: {
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
index 89c4ffe..f8095cd 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
@@ -128,12 +128,14 @@ describe('dashboardState reducer', () => {
     });
   });
 
-  it('should set unsaved changes and max undo history to false on save', () => {
+  it('should set unsaved changes, max undo history, and editMode to false on save', () => {
     expect(
       dashboardStateReducer({ hasUnsavedChanges: true }, { type: ON_SAVE }),
     ).to.deep.equal({
       hasUnsavedChanges: false,
       maxUndoHistoryExceeded: false,
+      editMode: false,
+      isV2Preview: false, // @TODO remove upon v1 deprecation
     });
   });
 
diff --git a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
index ec57494..3563059 100644
--- a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
@@ -108,7 +108,7 @@ describe('isValidChild', () => {
       [ROOT, [MARKDOWN]],
       [ROOT, GRID, [TAB]],
       [ROOT, GRID, TABS, [ROW]],
-      [ROOT, GRID, TABS, TAB, [TABS]],
+      // [ROOT, GRID, TABS, TAB, [TABS]], // @TODO this needs to be fixed
       [ROOT, GRID, ROW, [TABS]],
       [ROOT, GRID, ROW, [TAB]],
       [ROOT, GRID, ROW, [DIVIDER]],
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index 060249f..1718fc7 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -190,8 +190,8 @@ class Chart extends React.PureComponent {
         this.props.actions.chartRenderingSucceeded(chartId);
       }
       Logger.append(LOG_ACTIONS_RENDER_CHART, {
-        label: 'slice_' + chartId,
-        vis_type: vizType,
+        slice_id: 'slice_' + chartId,
+        viz_type: vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index d210ee6..c4908b0 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -2,7 +2,12 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 
 import { addInfoToast } from './messageToasts';
 import { setUnsavedChanges } from './dashboardState';
-import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
+import {
+  CHART_TYPE,
+  MARKDOWN_TYPE,
+  TABS_TYPE,
+  ROW_TYPE,
+} from '../util/componentTypes';
 import {
   DASHBOARD_ROOT_ID,
   NEW_COMPONENTS_SOURCE_ID,
@@ -155,8 +160,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.`,
+          `Parent does not have enough space for this component. Try decreasing its width or add it to a new row.`,
         ),
       );
     }
@@ -180,12 +184,13 @@ export function handleComponentDrop(dropResult) {
 
     const { dashboardLayout: undoableLayout } = getState();
 
-    // if we moved a Tab and the parent Tabs no longer has children, delete it.
+    // if we moved a child from a Tab or Row parent and it was the only child, delete the parent.
     if (!isNewComponent) {
       const { present: layout } = undoableLayout;
       const sourceComponent = layout[source.id];
       if (
-        sourceComponent.type === TABS_TYPE &&
+        (sourceComponent.type === TABS_TYPE ||
+          sourceComponent.type === ROW_TYPE) &&
         sourceComponent.children.length === 0
       ) {
         const parentId = findParentId({
diff --git a/superset/assets/src/dashboard/actions/messageToasts.js b/superset/assets/src/dashboard/actions/messageToasts.js
index fde02c4..e5c04e6 100644
--- a/superset/assets/src/dashboard/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/actions/messageToasts.js
@@ -12,13 +12,14 @@ function getToastUuid(type) {
 }
 
 export const ADD_TOAST = 'ADD_TOAST';
-export function addToast({ toastType, text }) {
+export function addToast({ toastType, text, duration }) {
   return {
     type: ADD_TOAST,
     payload: {
       id: getToastUuid(toastType),
       toastType,
       text,
+      duration,
     },
   };
 }
@@ -36,17 +37,20 @@ export function removeToast(id) {
 // Different types of toasts
 export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
 export function addInfoToast(text) {
-  return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST }));
+  return dispatch =>
+    dispatch(addToast({ text, toastType: INFO_TOAST, duration: 4000 }));
 }
 
 export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
 export function addSuccessToast(text) {
-  return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST }));
+  return dispatch =>
+    dispatch(addToast({ text, toastType: SUCCESS_TOAST, duration: 4000 }));
 }
 
 export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
 export function addWarningToast(text) {
-  return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST }));
+  return dispatch =>
+    dispatch(addToast({ text, toastType: WARNING_TOAST, duration: 4000 }));
 }
 
 export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
diff --git a/superset/assets/src/dashboard/components/AddSliceCard.jsx b/superset/assets/src/dashboard/components/AddSliceCard.jsx
index 7fd9ba4..c8266ad 100644
--- a/superset/assets/src/dashboard/components/AddSliceCard.jsx
+++ b/superset/assets/src/dashboard/components/AddSliceCard.jsx
@@ -1,6 +1,7 @@
 import cx from 'classnames';
 import React from 'react';
 import PropTypes from 'prop-types';
+import { t } from '../../locales';
 
 const propTypes = {
   datasourceLink: PropTypes.string,
@@ -49,6 +50,7 @@ function AddSliceCard({
           </div>
         </div>
       </div>
+      {isSelected && <div className="is-added-label">{t('Added')}</div>}
     </div>
   );
 }
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
deleted file mode 100644
index 9d54b09..0000000
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-/* global window */
-import React from 'react';
-import PropTypes from 'prop-types';
-import $ from 'jquery';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
-
-import CssEditor from './CssEditor';
-import RefreshIntervalModal from './RefreshIntervalModal';
-import { t } from '../../locales';
-
-function updateDom(css) {
-  const className = 'CssEditor-css';
-  const head = document.head || document.getElementsByTagName('head')[0];
-  let style = document.querySelector(`.${className}`);
-
-  if (!style) {
-    style = document.createElement('style');
-    style.className = className;
-    style.type = 'text/css';
-    head.appendChild(style);
-  }
-  if (style.styleSheet) {
-    style.styleSheet.cssText = css;
-  } else {
-    style.innerHTML = css;
-  }
-}
-
-const propTypes = {
-  addSuccessToast: PropTypes.func.isRequired,
-  addDangerToast: PropTypes.func.isRequired,
-  dashboardInfo: PropTypes.object.isRequired,
-  dashboardTitle: PropTypes.string.isRequired,
-  css: PropTypes.string.isRequired,
-  slices: PropTypes.array,
-  onChange: PropTypes.func.isRequired,
-  updateCss: PropTypes.func.isRequired,
-  forceRefreshAllCharts: PropTypes.func.isRequired,
-  startPeriodicRender: PropTypes.func.isRequired,
-  editMode: PropTypes.bool,
-};
-
-const defaultProps = {
-  editMode: false,
-  slices: [],
-};
-
-class Controls extends React.PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      css: props.css,
-      cssTemplates: [],
-    };
-
-    this.changeCss = this.changeCss.bind(this);
-  }
-
-  componentWillMount() {
-    updateDom(this.state.css);
-
-    $.get('/csstemplateasyncmodelview/api/read', data => {
-      const cssTemplates = data.result.map(row => ({
-        value: row.template_name,
-        css: row.css,
-        label: row.template_name,
-      }));
-      this.setState({ cssTemplates });
-    });
-  }
-
-  changeCss(css) {
-    this.setState({ css }, () => {
-      updateDom(css);
-    });
-    this.props.onChange();
-    this.props.updateCss(css);
-  }
-
-  render() {
-    const {
-      dashboardTitle,
-      startPeriodicRender,
-      forceRefreshAllCharts,
-      editMode,
-    } = this.props;
-
-    const emailBody = t('Checkout this dashboard: %s', window.location.href);
-    const emailLink =
-      'mailto:?Subject=Superset%20Dashboard%20' +
-      `${dashboardTitle}&Body=${emailBody}`;
-
-    return (
-      <span>
-        <DropdownButton
-          title="Actions"
-          bsSize="small"
-          id="bg-nested-dropdown"
-          pullRight
-        >
-          <MenuItem onClick={forceRefreshAllCharts}>
-            {t('Force refresh dashboard')}
-          </MenuItem>
-          <RefreshIntervalModal
-            onChange={refreshInterval =>
-              startPeriodicRender(refreshInterval * 1000)
-            }
-            triggerNode={<span>{t('Set auto-refresh interval')}</span>}
-          />
-          {editMode && (
-            <MenuItem
-              target="_blank"
-              href={`/dashboardmodelview/edit/${this.props.dashboardInfo.id}`}
-            >
-              {t('Edit dashboard metadata')}
-            </MenuItem>
-          )}
-          {editMode && (
-            <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
-          )}
-          {editMode && (
-            <CssEditor
-              triggerNode={<span>{t('Edit CSS')}</span>}
-              initialCss={this.state.css}
-              templates={this.state.cssTemplates}
-              onChange={this.changeCss}
-            />
-          )}
-        </DropdownButton>
-      </span>
-    );
-  }
-}
-
-Controls.propTypes = propTypes;
-Controls.defaultProps = defaultProps;
-
-export default Controls;
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 62bcbb5..99e93aa 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -86,6 +86,9 @@ class Dashboard extends React.PureComponent {
 
   componentWillReceiveProps(nextProps) {
     if (!nextProps.dashboardState.editMode) {
+      const version = nextProps.dashboardState.isV2Preview
+        ? 'v2-preview'
+        : 'v2';
       // log pane loads
       const loadedPaneIds = [];
       const allPanesDidLoad = Object.entries(nextProps.loadStats).every(
@@ -101,6 +104,7 @@ class Dashboard extends React.PureComponent {
             Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, {
               ...restStats,
               duration,
+              version,
             });
 
             if (!this.isFirstLoad) {
@@ -118,6 +122,7 @@ class Dashboard extends React.PureComponent {
         Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
           pane_ids: loadedPaneIds,
           duration: new Date().getTime() - this.ts_mount,
+          version,
         });
         Logger.send(this.actionLog);
         this.isFirstLoad = false;
@@ -128,25 +133,20 @@ class Dashboard extends React.PureComponent {
     const nextChartIds = getChartIdsFromLayout(nextProps.layout);
 
     if (currentChartIds.length < nextChartIds.length) {
-      // adding new chart
       const newChartIds = nextChartIds.filter(
         key => currentChartIds.indexOf(key) === -1,
       );
-      if (newChartIds.length) {
-        newChartIds.forEach(newChartId =>
-          this.props.actions.addSliceToDashboard(newChartId),
-        );
-      }
+      newChartIds.forEach(newChartId =>
+        this.props.actions.addSliceToDashboard(newChartId),
+      );
     } else if (currentChartIds.length > nextChartIds.length) {
       // remove chart
       const removedChartIds = currentChartIds.filter(
         key => nextChartIds.indexOf(key) === -1,
       );
-      if (removedChartIds.length) {
-        removedChartIds.forEach(removedChartId =>
-          this.props.actions.removeSliceFromDashboard(removedChartId),
-        );
-      }
+      removedChartIds.forEach(removedChartId =>
+        this.props.actions.removeSliceFromDashboard(removedChartId),
+      );
     }
   }
 
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 30e2e78..2156ed3 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -96,26 +96,24 @@ class DashboardBuilder extends React.Component {
       <StickyContainer
         className={cx('dashboard', editMode && 'dashboard--editing')}
       >
-        {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
-          <DashboardHeader />
-        ) : (
-          <DragDroppable
-            component={dashboardRoot}
-            parentComponent={null}
-            depth={DASHBOARD_ROOT_DEPTH}
-            index={0}
-            orientation="column"
-            onDrop={handleComponentDrop}
-            editMode
-          >
-            {({ dropIndicatorProps }) => (
-              <div>
-                <DashboardHeader />
-                {dropIndicatorProps && <div {...dropIndicatorProps} />}
-              </div>
-            )}
-          </DragDroppable>
-        )}
+        <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>
+          )}
+        </DragDroppable>
 
         {topLevelTabs && (
           <Sticky topOffset={50}>
@@ -175,7 +173,7 @@ class DashboardBuilder extends React.Component {
                         <DashboardGrid
                           gridComponent={dashboardLayout[id]}
                           // see isValidChild for why tabs do not increment the depth of their children
-                          depth={DASHBOARD_ROOT_DEPTH + (topLevelTabs ? 0 : 1)}
+                          depth={DASHBOARD_ROOT_DEPTH + 1} // (topLevelTabs ? 0 : 1)}
                           width={width}
                         />
                       </TabPane>
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index 77503bb..4689051 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -26,7 +26,6 @@ class DashboardGrid extends React.PureComponent {
       rowGuideTop: null,
     };
 
-    this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
     this.handleResizeStart = this.handleResizeStart.bind(this);
     this.handleResize = this.handleResize.bind(this);
     this.handleResizeStop = this.handleResizeStop.bind(this);
@@ -76,19 +75,6 @@ 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,
@@ -107,26 +93,6 @@ class DashboardGrid extends React.PureComponent {
     return width < 100 ? null : (
       <div className="dashboard-grid" ref={this.setGridRef}>
         <div className="grid-content">
-          {/* empty drop target makes top droppable */}
-          {editMode && (
-            <DragDroppable
-              component={gridComponent}
-              depth={depth}
-              parentComponent={null}
-              index={0}
-              orientation="column"
-              onDrop={this.handleTopDropTargetDrop}
-              className="empty-grid-droptarget--top"
-              editMode
-            >
-              {({ dropIndicatorProps }) =>
-                dropIndicatorProps && (
-                  <div className="drop-indicator drop-indicator--bottom" />
-                )
-              }
-            </DragDroppable>
-          )}
-
           {gridComponent.children.map((id, index) => (
             <DashboardComponent
               key={id}
@@ -142,25 +108,26 @@ class DashboardGrid extends React.PureComponent {
             />
           ))}
 
-          {/* empty drop target makes bottom droppable */}
-          {editMode && (
-            <DragDroppable
-              component={gridComponent}
-              depth={depth}
-              parentComponent={null}
-              index={gridComponent.children.length}
-              orientation="column"
-              onDrop={handleComponentDrop}
-              className="empty-grid-droptarget--bottom"
-              editMode
-            >
-              {({ dropIndicatorProps }) =>
-                dropIndicatorProps && (
-                  <div className="drop-indicator drop-indicator--top" />
-                )
-              }
-            </DragDroppable>
-          )}
+          {/* make the grid droppable in the case that there are no children */}
+          {editMode &&
+            gridComponent.children.length === 0 && (
+              <DragDroppable
+                component={gridComponent}
+                depth={depth}
+                parentComponent={null}
+                index={gridComponent.children.length}
+                orientation="column"
+                onDrop={handleComponentDrop}
+                className="empty-grid-droptarget--bottom"
+                editMode
+              >
+                {({ dropIndicatorProps }) =>
+                  dropIndicatorProps && (
+                    <div className="drop-indicator drop-indicator--top" />
+                  )
+                }
+              </DragDroppable>
+            )}
 
           {isResizing &&
             Array(GRID_COLUMN_COUNT)
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 31bd08c..5fa4afe 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,25 +1,17 @@
 /* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import {
-  DropdownButton,
-  MenuItem,
-  ButtonGroup,
-  ButtonToolbar,
-} from 'react-bootstrap';
+import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
 
-import Controls from './Controls';
+import HeaderActionsDropdown from './HeaderActionsDropdown';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-import SaveModal from './SaveModal';
+import V2PreviewModal from '../deprecated/V2PreviewModal';
+
 import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
-import {
-  UNDO_LIMIT,
-  SAVE_TYPE_NEWDASHBOARD,
-  SAVE_TYPE_OVERWRITE,
-} from '../util/constants';
+import { UNDO_LIMIT, SAVE_TYPE_OVERWRITE } from '../util/constants';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -40,6 +32,7 @@ const propTypes = {
   startPeriodicRender: PropTypes.func.isRequired,
   updateDashboardTitle: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
+  isV2Preview: PropTypes.bool.isRequired,
   setEditMode: PropTypes.func.isRequired,
   showBuilderPane: PropTypes.bool.isRequired,
   toggleBuilderPane: PropTypes.func.isRequired,
@@ -65,12 +58,14 @@ class Header extends React.PureComponent {
     super(props);
     this.state = {
       didNotifyMaxUndoHistoryToast: false,
+      showV2PreviewModal: props.isV2Preview,
     };
 
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
     this.overwriteDashboard = this.overwriteDashboard.bind(this);
+    this.toggleShowV2PreviewModal = this.toggleShowV2PreviewModal.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -105,6 +100,10 @@ class Header extends React.PureComponent {
     this.props.setEditMode(!this.props.editMode);
   }
 
+  toggleShowV2PreviewModal() {
+    this.setState({ showV2PreviewModal: !this.state.showV2PreviewModal });
+  }
+
   overwriteDashboard() {
     const {
       dashboardTitle,
@@ -133,6 +132,7 @@ class Header extends React.PureComponent {
       filters,
       expandedSlices,
       css,
+      isV2Preview,
       onUndo,
       onRedo,
       undoLength,
@@ -148,6 +148,7 @@ class Header extends React.PureComponent {
 
     const userCanEdit = dashboardInfo.dash_edit_perm;
     const userCanSaveAs = dashboardInfo.dash_save_perm;
+    const popButton = hasUnsavedChanges || isV2Preview;
 
     return (
       <div className="dashboard-header">
@@ -158,7 +159,7 @@ class Header extends React.PureComponent {
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
           />
-          <span className="favstar m-l-5">
+          <span className="favstar">
             <FaveStar
               itemId={dashboardInfo.id}
               fetchFaveStar={this.props.fetchFaveStar}
@@ -166,7 +167,22 @@ class Header extends React.PureComponent {
               isStarred={this.props.isStarred}
             />
           </span>
+          {isV2Preview && (
+            <div
+              role="none"
+              className="v2-preview-badge"
+              onClick={this.toggleShowV2PreviewModal}
+            >
+              {t('v2 Preview')}
+              <span className="fa fa-info-circle m-l-5" />
+            </div>
+          )}
+          {isV2Preview &&
+            this.state.showV2PreviewModal && (
+              <V2PreviewModal onClose={this.toggleShowV2PreviewModal} />
+            )}
         </div>
+
         <ButtonToolbar>
           {userCanSaveAs && (
             <ButtonGroup>
@@ -193,76 +209,83 @@ class Header extends React.PureComponent {
               {editMode && (
                 <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
                   {showBuilderPane
-                    ? t('Hide builder pane')
+                    ? t('Hide components')
                     : t('Insert components')}
                 </Button>
               )}
 
-              {!hasUnsavedChanges ? (
-                <Button
-                  bsSize="small"
-                  onClick={this.toggleEditMode}
-                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
-                  disabled={!userCanEdit}
-                >
-                  {editMode ? t('Switch to view mode') : t('Edit dashboard')}
-                </Button>
-              ) : (
-                <Button
-                  bsSize="small"
-                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
-                  onClick={this.overwriteDashboard}
-                >
-                  {t('Save changes')}
-                </Button>
-              )}
-              <DropdownButton
-                title=""
-                id="save-dash-split-button"
-                bsStyle={hasUnsavedChanges ? 'primary' : undefined}
-                bsSize="small"
-                pullRight
-              >
-                <SaveModal
-                  addSuccessToast={this.props.addSuccessToast}
-                  addDangerToast={this.props.addDangerToast}
-                  dashboardId={dashboardInfo.id}
-                  dashboardTitle={dashboardTitle}
-                  saveType={SAVE_TYPE_NEWDASHBOARD}
-                  layout={layout}
-                  filters={filters}
-                  expandedSlices={expandedSlices}
-                  css={css}
-                  onSave={onSave}
-                  isMenuItem
-                  triggerNode={<span>{t('Save as')}</span>}
-                  canOverwrite={userCanEdit}
-                />
-                {hasUnsavedChanges && (
-                  <MenuItem eventKey="discard" onSelect={Header.discardChanges}>
-                    {t('Discard changes')}
-                  </MenuItem>
+              {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>
                 )}
-              </DropdownButton>
+
+              {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}
+              />
             </ButtonGroup>
           )}
-
-          <Controls
-            addSuccessToast={this.props.addSuccessToast}
-            addDangerToast={this.props.addDangerToast}
-            dashboardInfo={dashboardInfo}
-            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}
-          />
         </ButtonToolbar>
       </div>
     );
diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
new file mode 100644
index 0000000..7b8a245
--- /dev/null
+++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -0,0 +1,163 @@
+/* global window */
+import React from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+
+import CssEditor from './CssEditor';
+import RefreshIntervalModal from './RefreshIntervalModal';
+import SaveModal from './SaveModal';
+import injectCustomCss from '../util/injectCustomCss';
+import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
+import { t } from '../../locales';
+
+const propTypes = {
+  addSuccessToast: PropTypes.func.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
+  dashboardId: PropTypes.number.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  hasUnsavedChanges: PropTypes.bool.isRequired,
+  css: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
+  updateCss: PropTypes.func.isRequired,
+  forceRefreshAllCharts: PropTypes.func.isRequired,
+  startPeriodicRender: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
+  userCanEdit: PropTypes.bool.isRequired,
+  layout: PropTypes.object.isRequired,
+  filters: PropTypes.object.isRequired,
+  expandedSlices: PropTypes.object.isRequired,
+  onSave: PropTypes.func.isRequired,
+  isV2Preview: PropTypes.bool.isRequired,
+};
+
+const defaultProps = {};
+
+class HeaderActionsDropdown extends React.PureComponent {
+  static discardChanges() {
+    window.location.reload();
+  }
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      css: props.css,
+      cssTemplates: [],
+    };
+
+    this.changeCss = this.changeCss.bind(this);
+  }
+
+  componentWillMount() {
+    injectCustomCss(this.state.css);
+
+    $.get('/csstemplateasyncmodelview/api/read', data => {
+      const cssTemplates = data.result.map(row => ({
+        value: row.template_name,
+        css: row.css,
+        label: row.template_name,
+      }));
+      this.setState({ cssTemplates });
+    });
+  }
+
+  changeCss(css) {
+    this.setState({ css }, () => {
+      injectCustomCss(css);
+    });
+    this.props.onChange();
+    this.props.updateCss(css);
+  }
+
+  render() {
+    const {
+      dashboardTitle,
+      dashboardId,
+      startPeriodicRender,
+      forceRefreshAllCharts,
+      editMode,
+      css,
+      hasUnsavedChanges,
+      layout,
+      filters,
+      expandedSlices,
+      onSave,
+      userCanEdit,
+      isV2Preview,
+    } = this.props;
+
+    const emailBody = t('Check out this dashboard: %s', window.location.href);
+    const emailLink = `mailto:?Subject=Superset%20Dashboard%20${dashboardTitle}&Body=${emailBody}`;
+
+    return (
+      <DropdownButton
+        title=""
+        id="save-dash-split-button"
+        bsStyle={hasUnsavedChanges || isV2Preview ? 'primary' : undefined}
+        bsSize="small"
+        pullRight
+      >
+        <SaveModal
+          addSuccessToast={this.props.addSuccessToast}
+          addDangerToast={this.props.addDangerToast}
+          dashboardId={dashboardId}
+          dashboardTitle={dashboardTitle}
+          saveType={SAVE_TYPE_NEWDASHBOARD}
+          layout={layout}
+          filters={filters}
+          expandedSlices={expandedSlices}
+          css={css}
+          onSave={onSave}
+          isMenuItem
+          triggerNode={<span>{t('Save as')}</span>}
+          canOverwrite={userCanEdit}
+          isV2Preview={isV2Preview}
+        />
+        {(isV2Preview || hasUnsavedChanges) && (
+          <MenuItem
+            eventKey="discard"
+            onSelect={HeaderActionsDropdown.discardChanges}
+          >
+            {t('Discard changes')}
+          </MenuItem>
+        )}
+
+        <MenuItem divider />
+
+        <MenuItem onClick={forceRefreshAllCharts}>
+          {t('Force refresh dashboard')}
+        </MenuItem>
+        <RefreshIntervalModal
+          onChange={refreshInterval =>
+            startPeriodicRender(refreshInterval * 1000)
+          }
+          triggerNode={<span>{t('Set auto-refresh interval')}</span>}
+        />
+        {editMode && (
+          <MenuItem
+            target="_blank"
+            href={`/dashboardmodelview/edit/${dashboardId}`}
+          >
+            {t('Edit dashboard metadata')}
+          </MenuItem>
+        )}
+        {editMode && (
+          <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
+        )}
+        {editMode && (
+          <CssEditor
+            triggerNode={<span>{t('Edit CSS')}</span>}
+            initialCss={this.state.css}
+            templates={this.state.cssTemplates}
+            onChange={this.changeCss}
+          />
+        )}
+      </DropdownButton>
+    );
+  }
+}
+
+HeaderActionsDropdown.propTypes = propTypes;
+HeaderActionsDropdown.defaultProps = defaultProps;
+
+export default HeaderActionsDropdown;
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 9d63331..f5ad9d0 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   onSave: PropTypes.func.isRequired,
   isMenuItem: PropTypes.bool,
   canOverwrite: PropTypes.bool.isRequired,
+  isV2Preview: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -82,7 +83,8 @@ class SaveModal extends React.PureComponent {
       positions,
       css,
       expanded_slices: expandedSlices,
-      dashboard_title: dashboardTitle,
+      dashboard_title:
+        saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle,
       default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
@@ -102,12 +104,16 @@ class SaveModal extends React.PureComponent {
   }
 
   render() {
+    const { isV2Preview } = this.props;
     return (
       <ModalTrigger
         ref={this.setModalRef}
         isMenuItem={this.props.isMenuItem}
         triggerNode={this.props.triggerNode}
-        modalTitle={t('Save Dashboard')}
+        modalTitle={t(
+          'Save Dashboard%s',
+          isV2Preview ? ' (⚠️ all saved dashboards will be V2)' : '',
+        )}
         modalBody={
           <FormGroup>
             <Radio
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index d8ed53e..9e68278 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -34,7 +34,7 @@ const defaultProps = {
 const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
 const KEYS_TO_SORT = [
   { key: 'slice_name', label: 'Name' },
-  { key: 'viz_type', label: 'Visualization' },
+  { key: 'viz_type', label: 'Vis type' },
   { key: 'datasource_name', label: 'Datasource' },
   { key: 'changed_on', label: 'Recent' },
 ];
@@ -187,23 +187,24 @@ class SliceAdder extends React.Component {
     return (
       <div className="slice-adder-container">
         <div className="controls">
+          <SearchInput
+            placeholder="Filter your charts"
+            className="search-input"
+            onChange={this.searchUpdated}
+            onKeyPress={this.handleKeyPress}
+          />
+
           <DropdownButton
-            title={KEYS_TO_SORT[this.state.sortBy].label}
+            title={`Sort by ${KEYS_TO_SORT[this.state.sortBy].label}`}
             onSelect={this.handleSelect}
             id="slice-adder-sortby"
           >
             {KEYS_TO_SORT.map((item, index) => (
               <MenuItem key={item.key} eventKey={index}>
-                {item.label}
+                Sort by {item.label}
               </MenuItem>
             ))}
           </DropdownButton>
-
-          <SearchInput
-            className="search-input"
-            onChange={this.searchUpdated}
-            onKeyPress={this.handleKeyPress}
-          />
         </div>
 
         {this.props.isLoading && (
diff --git a/superset/assets/src/dashboard/components/Toast.jsx b/superset/assets/src/dashboard/components/Toast.jsx
index 3c5a3ca..a2b5f0a 100644
--- a/superset/assets/src/dashboard/components/Toast.jsx
+++ b/superset/assets/src/dashboard/components/Toast.jsx
@@ -14,14 +14,9 @@ import {
 const propTypes = {
   toast: toastShape.isRequired,
   onCloseToast: PropTypes.func.isRequired,
-  delay: PropTypes.number,
-  duration: PropTypes.number, // if duration is >0, the toast will close on its own
 };
 
-const defaultProps = {
-  delay: 0,
-  duration: 0,
-};
+const defaultProps = {};
 
 class Toast extends React.Component {
   constructor(props) {
@@ -35,12 +30,12 @@ class Toast extends React.Component {
   }
 
   componentDidMount() {
-    const { delay, duration } = this.props;
+    const { toast } = this.props;
 
-    setTimeout(this.showToast, delay);
+    setTimeout(this.showToast);
 
-    if (duration > 0) {
-      this.hideTimer = setTimeout(this.handleClosePress, delay + duration);
+    if (toast.duration > 0) {
+      this.hideTimer = setTimeout(this.handleClosePress, toast.duration);
     }
   }
 
diff --git a/superset/assets/src/dashboard/components/dnd/handleDrop.js b/superset/assets/src/dashboard/components/dnd/handleDrop.js
index 3739b18..faeeffa 100644
--- a/superset/assets/src/dashboard/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/components/dnd/handleDrop.js
@@ -1,4 +1,5 @@
 import getDropPosition, {
+  clearDropCache,
   DROP_TOP,
   DROP_RIGHT,
   DROP_BOTTOM,
@@ -75,6 +76,7 @@ export default function handleDrop(props, monitor, Component) {
   }
 
   onDrop(dropResult);
+  clearDropCache();
 
   return dropResult;
 }
diff --git a/superset/assets/src/dashboard/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js
index a303e13..cb98a6f 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 = 200;
+const HOVER_THROTTLE_MS = 150;
 
 function handleHover(props, monitor, Component) {
   // this may happen due to throttling
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index ab030f4..9ad9522 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -101,7 +101,7 @@ class ChartHolder extends React.Component {
       <DragDroppable
         component={component}
         parentComponent={parentComponent}
-        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        orientation={parentComponent.type === ROW_TYPE ? 'column' : 'row'}
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
index 459f89a..a49a893 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
@@ -41,8 +41,14 @@ const propTypes = {
 };
 
 const defaultProps = {};
-const markdownPlaceHolder = `### New Markdown
-Insert *bold* or _italic_ text, and (urls)[www.url.com] here.`;
+
+const markdownPlaceHolder = `# ✨Markdown
+## ✨Markdown
+### ✨Markdown
+
+<br />
+
+Click here to edit [markdown](https://bit.ly/1dQOfRK)`;
 
 class Markdown extends React.PureComponent {
   constructor(props) {
@@ -51,7 +57,7 @@ class Markdown extends React.PureComponent {
       isFocused: false,
       markdownSource: props.component.meta.code,
       editor: null,
-      editorMode: props.component.meta.code ? 'preview' : 'edit', // show edit mode when code is empty
+      editorMode: 'preview',
     };
 
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
@@ -61,6 +67,13 @@ class Markdown extends React.PureComponent {
     this.setEditor = this.setEditor.bind(this);
   }
 
+  componentWillReceiveProps(nextProps) {
+    const nextSource = nextProps.component.meta.code;
+    if (this.state.markdownSource !== nextSource) {
+      this.setState({ markdownSource: nextSource });
+    }
+  }
+
   componentDidUpdate(prevProps) {
     if (
       this.state.editor &&
@@ -79,7 +92,10 @@ class Markdown extends React.PureComponent {
   }
 
   handleChangeFocus(nextFocus) {
-    this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+    const nextFocused = !!nextFocus;
+    const nextEditMode = nextFocused ? 'edit' : 'preview';
+    this.setState(() => ({ isFocused: nextFocused }));
+    this.handleChangeEditorMode(nextEditMode);
   }
 
   handleChangeEditorMode(mode) {
@@ -120,10 +136,15 @@ class Markdown extends React.PureComponent {
         mode="markdown"
         theme="textmate"
         onChange={this.handleMarkdownChange}
-        width={'100%'}
-        height={'100%'}
+        width="100%"
+        height="100%"
         editorProps={{ $blockScrolling: true }}
-        value={this.state.markdownSource || markdownPlaceHolder}
+        value={
+          // thisl allows "select all => delete" to give an empty editor
+          typeof this.state.markdownSource === 'string'
+            ? this.state.markdownSource
+            : markdownPlaceHolder
+        }
         readOnly={false}
         onLoad={this.setEditor}
       />
@@ -132,7 +153,10 @@ class Markdown extends React.PureComponent {
 
   renderPreviewMode() {
     return (
-      <ReactMarkdown source={this.state.markdownSource} escapeHtml={false} />
+      <ReactMarkdown
+        source={this.state.markdownSource || markdownPlaceHolder}
+        escapeHtml={false}
+      />
     );
   }
 
@@ -163,7 +187,7 @@ class Markdown extends React.PureComponent {
       <DragDroppable
         component={component}
         parentComponent={parentComponent}
-        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        orientation={parentComponent.type === ROW_TYPE ? 'column' : 'row'}
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
@@ -198,7 +222,9 @@ class Markdown extends React.PureComponent {
                 onResizeStart={onResizeStart}
                 onResize={onResize}
                 onResizeStop={onResizeStop}
-                editMode={editMode}
+                // disable resize when editing because if state is not synced
+                // with props it will reset the editor text to whatever props is
+                editMode={isFocused ? false : editMode}
               >
                 <div
                   ref={dragSourceRef}
@@ -208,10 +234,9 @@ class Markdown extends React.PureComponent {
                     ? this.renderEditMode()
                     : this.renderPreviewMode()}
                 </div>
-
-                {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </ResizableContainer>
             </div>
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
           </WithPopoverMenu>
         )}
       </DragDroppable>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 63619c1..4cba2e6 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -92,6 +92,8 @@ export default class Tab extends React.PureComponent {
   renderTabContent() {
     const {
       component: tabComponent,
+      parentComponent: tabParentComponent,
+      index,
       depth,
       availableColumnCount,
       columnWidth,
@@ -117,6 +119,25 @@ 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 && (
+          <DragDroppable
+            component={tabComponent}
+            parentComponent={tabParentComponent}
+            orientation="column"
+            index={index}
+            depth={depth}
+            onDrop={this.handleDrop}
+            editMode
+            className="empty-tab-droptarget"
+          >
+            {({ dropIndicatorProps }) =>
+              dropIndicatorProps && (
+                <div className="drop-indicator drop-indicator--top" />
+              )
+            }
+          </DragDroppable>
+        )}
       </div>
     );
   }
@@ -136,7 +157,7 @@ export default class Tab extends React.PureComponent {
         // disable drag drop of top-level Tab's to prevent invalid nesting of a child in
         // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
         // reusult in circular children
-        disableDragDrop={depth === DASHBOARD_ROOT_DEPTH + 1}
+        disableDragDrop={depth <= DASHBOARD_ROOT_DEPTH + 1}
         editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 5631a25..c046c02 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -28,7 +28,7 @@ function mapStateToProps(
 
   return {
     chart,
-    datasource: chart && datasources[chart.form_data.datasource],
+    datasource: (chart && datasources[chart.form_data.datasource]) || {},
     slice: sliceEntities.slices[id],
     timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
     filters: filters[id] || EMPTY_FILTERS,
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index 19be06c..32eda1a 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -29,7 +29,7 @@ import { DASHBOARD_HEADER_ID } from '../util/constants';
 
 function mapStateToProps({
   dashboardLayout: undoableLayout,
-  dashboardState: dashboard,
+  dashboardState,
   dashboardInfo,
   charts,
 }) {
@@ -38,19 +38,20 @@ function mapStateToProps({
     undoLength: undoableLayout.past.length,
     redoLength: undoableLayout.future.length,
     layout: undoableLayout.present,
-    filters: dashboard.filters,
+    filters: dashboardState.filters,
     dashboardTitle: (
       (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
     ).text,
-    expandedSlices: dashboard.expandedSlices,
-    css: dashboard.css,
+    expandedSlices: dashboardState.expandedSlices,
+    css: dashboardState.css,
     charts,
     userId: dashboardInfo.userId,
-    isStarred: !!dashboard.isStarred,
-    hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
-    maxUndoHistoryExceeded: !!dashboard.maxUndoHistoryExceeded,
-    editMode: !!dashboard.editMode,
-    showBuilderPane: !!dashboard.showBuilderPane,
+    isStarred: !!dashboardState.isStarred,
+    hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
+    maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
+    editMode: !!dashboardState.editMode,
+    showBuilderPane: !!dashboardState.showBuilderPane,
+    isV2Preview: dashboardState.isV2Preview,
   };
 }
 
diff --git a/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
new file mode 100644
index 0000000..876fa78
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
@@ -0,0 +1,102 @@
+import moment from 'moment';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+import { Logger, LOG_ACTIONS_READ_ABOUT_V2_CHANGES } from '../../logger';
+import { t } from '../../locales';
+
+const propTypes = {
+  v2FeedbackUrl: PropTypes.string,
+  v2AutoConvertDate: PropTypes.string,
+  onClose: PropTypes.func.isRequired,
+  handleConvertToV2: PropTypes.func.isRequired,
+  forceV2Edit: PropTypes.bool.isRequired,
+};
+
+const defaultProps = {
+  v2FeedbackUrl: null,
+  v2AutoConvertDate: null,
+};
+
+function logReadAboutV2Changes() {
+  Logger.append(LOG_ACTIONS_READ_ABOUT_V2_CHANGES, { version: 'v1' }, true);
+}
+
+function PromptV2ConversionModal({
+  v2FeedbackUrl,
+  v2AutoConvertDate,
+  onClose,
+  handleConvertToV2,
+  forceV2Edit,
+}) {
+  const timeUntilAutoConversion = v2AutoConvertDate
+    ? `approximately ${moment(v2AutoConvertDate).toNow(
+        true,
+      )} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY)
+    : 'a limited amount of time';
+
+  return (
+    <Modal onHide={onClose} onExit={onClose} animation show>
+      <Modal.Header closeButton>
+        <div style={{ fontSize: 20, fontWeight: 200, margin: '0px 4px -4px' }}>
+          {t('Convert to Dashboard v2 🎉')}
+        </div>
+      </Modal.Header>
+      <Modal.Body>
+        <h4>{t('Who')}</h4>
+        <p>
+          {t(
+            "As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience.",
+          )}
+        </p>
+        <br />
+        <h4>{t('What and When')}</h4>
+        <p>
+          {t('You have ')}
+          <strong>
+            {timeUntilAutoConversion}
+            {t(' to convert this v1 dashboard to the new v2 format')}
+          </strong>
+          {t(' before it is auto-converted. ')}
+          {forceV2Edit && (
+            <em>
+              {t(
+                'Note that you may only edit dashboards using the v2 experience.',
+              )}
+            </em>
+          )}
+          {t('You may read more about these changes ')}
+          <a
+            target="_blank"
+            rel="noopener noreferrer"
+            href="https://gist.github.com/williaster/bad4ac9c6a71b234cf9fc8ee629844e5#file-superset-dashboard-v2-md"
+            onClick={logReadAboutV2Changes}
+          >
+            here
+          </a>
+          {v2FeedbackUrl ? t(' or ') : ''}
+          {v2FeedbackUrl ? (
+            <a target="_blank" rel="noopener noreferrer" href={v2FeedbackUrl}>
+              {t('provide feedback')}
+            </a>
+          ) : (
+            ''
+          )}.
+        </p>
+      </Modal.Body>
+      <Modal.Footer>
+        <Button onClick={onClose}>
+          {t(`${forceV2Edit ? 'View in' : 'Continue with'}  v1`)}
+        </Button>
+        <Button bsStyle="primary" onClick={handleConvertToV2}>
+          {t('Preview v2')}
+        </Button>
+      </Modal.Footer>
+    </Modal>
+  );
+}
+
+PromptV2ConversionModal.propTypes = propTypes;
+PromptV2ConversionModal.defaultProps = defaultProps;
+
+export default PromptV2ConversionModal;
diff --git a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
new file mode 100644
index 0000000..a0b7eed
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
@@ -0,0 +1,148 @@
+/* eslint-env browser */
+import moment from 'moment';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+import { connect } from 'react-redux';
+import {
+  Logger,
+  LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
+  LOG_ACTIONS_FALLBACK_TO_V1,
+} from '../../logger';
+
+import { t } from '../../locales';
+
+const propTypes = {
+  v2FeedbackUrl: PropTypes.string,
+  v2AutoConvertDate: PropTypes.string,
+  forceV2Edit: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  v2FeedbackUrl: null,
+  v2AutoConvertDate: null,
+  handleFallbackToV1: null,
+};
+
+// This is a gross component but it is temporary!
+class V2PreviewModal extends React.Component {
+  static logReadAboutV2Changes() {
+    Logger.append(
+      LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
+      { version: 'v2-preview' },
+      true,
+    );
+  }
+
+  constructor(props) {
+    super(props);
+    this.handleFallbackToV1 = this.handleFallbackToV1.bind(this);
+  }
+
+  handleFallbackToV1() {
+    Logger.append(
+      LOG_ACTIONS_FALLBACK_TO_V1,
+      {
+        force_v2_edit: this.props.forceV2Edit,
+      },
+      true,
+    );
+    const url = new URL(window.location); // eslint-disable-line
+    url.searchParams.set('version', 'v1');
+    url.searchParams.delete('edit'); // remove JIC they were editing and v1 editing is not allowed
+    window.location = url;
+  }
+
+  render() {
+    const { v2FeedbackUrl, v2AutoConvertDate, onClose } = this.props;
+
+    const timeUntilAutoConversion = v2AutoConvertDate
+      ? `approximately ${moment(v2AutoConvertDate).toNow(
+          true,
+        )} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY)
+      : 'a limited amount of time';
+
+    return (
+      <Modal onHide={onClose} onExit={onClose} animation show>
+        <Modal.Header closeButton>
+          <div
+            style={{ fontSize: 20, fontWeight: 200, margin: '0px 4px -4px' }}
+          >
+            {t('Welcome to the new Dashboard v2 experience! 🎉')}
+          </div>
+        </Modal.Header>
+        <Modal.Body>
+          <h3>{t('Who')}</h3>
+          <p>
+            {t(
+              "As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience. You can learn more about these changes ",
+            )}
+            <a
+              target="_blank"
+              rel="noopener noreferrer"
+              href="https://gist.github.com/williaster/bad4ac9c6a71b234cf9fc8ee629844e5#file-superset-dashboard-v2-md"
+              onClick={V2PreviewModal.logReadAboutV2Changes}
+            >
+              here
+            </a>
+            {v2FeedbackUrl ? t(' or ') : ''}
+            {v2FeedbackUrl ? (
+              <a target="_blank" rel="noopener noreferrer" href={v2FeedbackUrl}>
+                {t('provide feedback')}
+              </a>
+            ) : (
+              ''
+            )}.
+          </p>
+          <br />
+          <h3>{t('What')}</h3>
+          <p>
+            {t('You are ')}
+            <strong>{t('previewing')}</strong>
+            {t(
+              ' an auto-converted v2 version of your v1 dashboard. This conversion may have introduced regressions, such as minor layout variation or incompatible custom CSS. ',
+            )}
+            <strong>
+              {t(
+                'To persist your dashboard as v2, please make any necessary changes and save the dashboard',
+              )}
+            </strong>
+            {t(
+              '. Note that non-owners/-admins will continue to see the original version until you take this action.',
+            )}
+          </p>
+          <br />
+          <h3>{t('When')}</h3>
+          <p>
+            {t('You have ')}
+            <strong>
+              {timeUntilAutoConversion}
+              {t(' to edit and save this version ')}
+            </strong>
+            {t(
+              ' before it is auto-persisted to this preview. Upon save you will no longer be able to use the v1 experience.',
+            )}
+          </p>
+        </Modal.Body>
+        <Modal.Footer>
+          <Button onClick={this.handleFallbackToV1}>
+            {t('Fallback to v1')}
+          </Button>
+          <Button bsStyle="primary" onClick={onClose}>
+            {t('Preview v2')}
+          </Button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
+
+V2PreviewModal.propTypes = propTypes;
+V2PreviewModal.defaultProps = defaultProps;
+
+export default connect(({ dashboardInfo }) => ({
+  v2FeedbackUrl: dashboardInfo.v2FeedbackUrl,
+  v2AutoConvertDate: dashboardInfo.v2AutoConvertDate,
+  forceV2Edit: dashboardInfo.forceV2Edit,
+}))(V2PreviewModal);
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx
similarity index 58%
copy from superset/assets/src/chart/Chart.jsx
copy to superset/assets/src/dashboard/deprecated/chart/Chart.jsx
index 060249f..bade493 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx
@@ -4,20 +4,20 @@ import PropTypes from 'prop-types';
 import Mustache from 'mustache';
 import { Tooltip } from 'react-bootstrap';
 
-import { d3format } from '../modules/utils';
+import { d3format } from '../../../modules/utils';
 import ChartBody from './ChartBody';
-import Loading from '../components/Loading';
-import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger';
-import StackTraceMessage from '../components/StackTraceMessage';
-import RefreshChartOverlay from '../components/RefreshChartOverlay';
-import visMap from '../visualizations';
-import sandboxedEval from '../modules/sandbox';
+import Loading from '../../../components/Loading';
+import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger';
+import StackTraceMessage from '../../../components/StackTraceMessage';
+import RefreshChartOverlay from '../../../components/RefreshChartOverlay';
+import visMap from '../../../visualizations';
+import sandboxedEval from '../../../modules/sandbox';
 import './chart.css';
 
 const propTypes = {
   annotationData: PropTypes.object,
   actions: PropTypes.object,
-  chartId: PropTypes.number.isRequired,
+  chartKey: PropTypes.string.isRequired,
   containerId: PropTypes.string.isRequired,
   datasource: PropTypes.object.isRequired,
   formData: PropTypes.object.isRequired,
@@ -42,6 +42,8 @@ const propTypes = {
   // dashboard callbacks
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
   onQuery: PropTypes.func,
   onDismissRefreshOverlay: PropTypes.func,
 };
@@ -49,6 +51,8 @@ const propTypes = {
 const defaultProps = {
   addFilter: () => ({}),
   getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
 };
 
 class Chart extends React.PureComponent {
@@ -63,6 +67,8 @@ class Chart extends React.PureComponent {
     this.datasource = props.datasource;
     this.addFilter = this.addFilter.bind(this);
     this.getFilters = this.getFilters.bind(this);
+    this.clearFilter = this.clearFilter.bind(this);
+    this.removeFilter = this.removeFilter.bind(this);
     this.headerHeight = this.headerHeight.bind(this);
     this.height = this.height.bind(this);
     this.width = this.width.bind(this);
@@ -70,11 +76,10 @@ class Chart extends React.PureComponent {
 
   componentDidMount() {
     if (this.props.triggerQuery) {
-      const { formData } = this.props;
-      this.props.actions.runQuery(formData, false, this.props.timeout, this.props.chartId);
-    } else {
-      // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data
-      this.renderViz();
+      this.props.actions.runQuery(this.props.formData, false,
+        this.props.timeout,
+        this.props.chartKey,
+      );
     }
   }
 
@@ -88,10 +93,10 @@ class Chart extends React.PureComponent {
 
   componentDidUpdate(prevProps) {
     if (
-      this.props.queryResponse &&
-      ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
-      !this.props.queryResponse.error &&
-      (prevProps.annotationData !== this.props.annotationData ||
+        this.props.queryResponse &&
+        ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
+        !this.props.queryResponse.error && (
+        prevProps.annotationData !== this.props.annotationData ||
         prevProps.queryResponse !== this.props.queryResponse ||
         prevProps.height !== this.props.height ||
         prevProps.width !== this.props.width ||
@@ -113,14 +118,20 @@ class Chart extends React.PureComponent {
     this.props.addFilter(col, vals, merge, refresh);
   }
 
+  clearFilter() {
+    this.props.clearFilter();
+  }
+
+  removeFilter(col, vals, refresh = true) {
+    this.props.removeFilter(col, vals, refresh);
+  }
+
   clearError() {
     this.setState({ errorMsg: null });
   }
 
   width() {
-    return (
-      this.props.width || (this.container && this.container.el && this.container.el.offsetWidth)
-    );
+    return this.props.width || this.container.el.offsetWidth;
   }
 
   headerHeight() {
@@ -128,9 +139,7 @@ class Chart extends React.PureComponent {
   }
 
   height() {
-    return (
-      this.props.height || (this.container && this.container.el && this.container.el.offsetHeight)
-    );
+    return this.props.height || this.container.el.offsetHeight;
   }
 
   d3format(col, number) {
@@ -141,7 +150,7 @@ class Chart extends React.PureComponent {
   }
 
   error(e) {
-    this.props.actions.chartRenderingFailed(e, this.props.chartId);
+    this.props.actions.chartRenderingFailed(e, this.props.chartKey);
   }
 
   verboseMetricName(metric) {
@@ -158,6 +167,7 @@ class Chart extends React.PureComponent {
 
   renderTooltip() {
     if (this.state.tooltip) {
+      /* eslint-disable react/no-danger */
       return (
         <Tooltip
           className="chart-tooltip"
@@ -167,83 +177,77 @@ class Chart extends React.PureComponent {
           positionLeft={this.state.tooltip.x + 30}
           arrowOffsetTop={10}
         >
-          <div // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }}
-          />
+          <div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
         </Tooltip>
       );
+      /* eslint-enable react/no-danger */
     }
     return null;
   }
 
   renderViz() {
-    const { vizType, formData, queryResponse, setControlValue, chartId, chartStatus } = this.props;
-    const visRenderer = visMap[vizType];
+    const viz = visMap[this.props.vizType];
+    const fd = this.props.formData;
+    const qr = this.props.queryResponse;
     const renderStart = Logger.getTimestamp();
     try {
       // Executing user-defined data mutator function
-      if (formData.js_data) {
-        queryResponse.data = sandboxedEval(formData.js_data)(queryResponse.data);
-      }
-      visRenderer(this, queryResponse, setControlValue);
-      if (chartStatus !== 'rendered') {
-        this.props.actions.chartRenderingSucceeded(chartId);
+      if (fd.js_data) {
+        qr.data = sandboxedEval(fd.js_data)(qr.data);
       }
+      // [re]rendering the visualization
+      viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_CHART, {
-        label: 'slice_' + chartId,
-        vis_type: vizType,
+        slice_id: this.props.chartKey,
+        viz_type: this.props.vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(chartId);
+      this.props.actions.chartRenderingSucceeded(this.props.chartKey);
     } catch (e) {
-      console.error(e); // eslint-disable-line no-console
-      this.props.actions.chartRenderingFailed(e, chartId);
+      this.props.actions.chartRenderingFailed(e, this.props.chartKey);
     }
   }
 
   render() {
     const isLoading = this.props.chartStatus === 'loading';
-
-    // this allows <Loading /> to be positioned in the middle of the chart
-    const containerStyles = isLoading ? { height: this.height(), width: this.width() } : null;
     return (
-      <div className={`chart-container ${isLoading ? 'is-loading' : ''}`} style={containerStyles}>
+      <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
         {this.renderTooltip()}
-        {isLoading && <Loading size={75} />}
-        {this.props.chartAlert && (
-          <StackTraceMessage
-            message={this.props.chartAlert}
-            queryResponse={this.props.queryResponse}
-          />
-        )}
+        {isLoading &&
+          <Loading size={25} />
+        }
+        {this.props.chartAlert &&
+        <StackTraceMessage
+          message={this.props.chartAlert}
+          queryResponse={this.props.queryResponse}
+        />
+        }
 
         {!isLoading &&
           !this.props.chartAlert &&
           this.props.refreshOverlayVisible &&
           !this.props.errorMessage &&
-          this.container && (
-            <RefreshChartOverlay
-              height={this.height()}
-              width={this.width()}
-              onQuery={this.props.onQuery}
-              onDismiss={this.props.onDismissRefreshOverlay}
-            />
-          )}
-
-        {!isLoading &&
-          !this.props.chartAlert && (
-            <ChartBody
-              containerId={this.containerId}
-              vizType={this.props.vizType}
-              height={this.height}
-              width={this.width}
-              faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
-              ref={(inner) => {
-                this.container = inner;
-              }}
-            />
-          )}
+          this.container &&
+          <RefreshChartOverlay
+            height={this.height()}
+            width={this.width()}
+            onQuery={this.props.onQuery}
+            onDismiss={this.props.onDismissRefreshOverlay}
+          />
+        }
+        {!isLoading && !this.props.chartAlert &&
+          <ChartBody
+            containerId={this.containerId}
+            vizType={this.props.vizType}
+            height={this.height}
+            width={this.width}
+            faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
+            ref={(inner) => {
+              this.container = inner;
+            }}
+          />
+        }
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx
new file mode 100644
index 0000000..b459f44
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+
+const propTypes = {
+  containerId: PropTypes.string.isRequired,
+  vizType: PropTypes.string.isRequired,
+  height: PropTypes.func.isRequired,
+  width: PropTypes.func.isRequired,
+  faded: PropTypes.bool,
+};
+
+class ChartBody extends React.PureComponent {
+  html(data) {
+    this.el.innerHTML = data;
+  }
+
+  css(property, value) {
+    this.el.style[property] = value;
+  }
+
+  get(n) {
+    return $(this.el).get(n);
+  }
+
+  find(classname) {
+    return $(this.el).find(classname);
+  }
+
+  show() {
+    return $(this.el).show();
+  }
+
+  height() {
+    return this.props.height();
+  }
+
+  width() {
+    return this.props.width();
+  }
+
+  render() {
+    return (
+      <div
+        id={this.props.containerId}
+        className={`slice_container ${this.props.vizType}${this.props.faded ? ' faded' : ''}`}
+        ref={(el) => { this.el = el; }}
+      />
+    );
+  }
+}
+
+ChartBody.propTypes = propTypes;
+
+export default ChartBody;
diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx
new file mode 100644
index 0000000..b731412
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import * as Actions from './chartAction';
+import Chart from './Chart';
+
+function mapStateToProps({ charts }, ownProps) {
+  const chart = charts[ownProps.chartKey];
+  return {
+    annotationData: chart.annotationData,
+    chartAlert: chart.chartAlert,
+    chartStatus: chart.chartStatus,
+    chartUpdateEndTime: chart.chartUpdateEndTime,
+    chartUpdateStartTime: chart.chartUpdateStartTime,
+    latestQueryFormData: chart.latestQueryFormData,
+    lastRendered: chart.lastRendered,
+    queryResponse: chart.queryResponse,
+    queryRequest: chart.queryRequest,
+    triggerQuery: chart.triggerQuery,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    actions: bindActionCreators(Actions, dispatch),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/src/dashboard/deprecated/chart/chart.css b/superset/assets/src/dashboard/deprecated/chart/chart.css
new file mode 100644
index 0000000..eda2054
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/chart.css
@@ -0,0 +1,4 @@
+.chart-tooltip {
+  opacity: 0.75;
+  font-size: 12px;
+}
diff --git a/superset/assets/src/dashboard/deprecated/chart/chartAction.js b/superset/assets/src/dashboard/deprecated/chart/chartAction.js
new file mode 100644
index 0000000..52f9c47
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/chartAction.js
@@ -0,0 +1,195 @@
+import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../../../explore/exploreUtils';
+import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../../../modules/AnnotationTypes';
+import { Logger, LOG_ACTIONS_LOAD_CHART } from '../../../logger';
+import { COMMON_ERR_MESSAGES } from '../../../common';
+import { t } from '../../../locales';
+
+const $ = window.$ = require('jquery');
+
+export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
+export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
+  return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key };
+}
+
+export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
+export function chartUpdateSucceeded(queryResponse, key) {
+  return { type: CHART_UPDATE_SUCCEEDED, queryResponse, key };
+}
+
+export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
+export function chartUpdateStopped(key) {
+  return { type: CHART_UPDATE_STOPPED, key };
+}
+
+export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
+export function chartUpdateTimeout(statusText, timeout, key) {
+  return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key };
+}
+
+export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
+export function chartUpdateFailed(queryResponse, key) {
+  return { type: CHART_UPDATE_FAILED, queryResponse, key };
+}
+
+export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
+export function chartRenderingFailed(error, key) {
+  return { type: CHART_RENDERING_FAILED, error, key };
+}
+
+export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED';
+export function chartRenderingSucceeded(key) {
+  return { type: CHART_RENDERING_SUCCEEDED, key };
+}
+
+export const REMOVE_CHART = 'REMOVE_CHART';
+export function removeChart(key) {
+  return { type: REMOVE_CHART, key };
+}
+
+export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS';
+export function annotationQuerySuccess(annotation, queryResponse, key) {
+  return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key };
+}
+
+export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
+export function annotationQueryStarted(annotation, queryRequest, key) {
+  return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
+}
+
+export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
+export function annotationQueryFailed(annotation, queryResponse, key) {
+  return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
+}
+
+export function runAnnotationQuery(annotation, timeout = 60, formData = null, key) {
+  return function (dispatch, getState) {
+    const sliceKey = key || Object.keys(getState().charts)[0];
+    const fd = formData || getState().charts[sliceKey].latestQueryFormData;
+
+    if (!requiresQuery(annotation.sourceType)) {
+      return Promise.resolve();
+    }
+
+    const granularity = fd.time_grain_sqla || fd.granularity;
+    fd.time_grain_sqla = granularity;
+    fd.granularity = granularity;
+
+    const sliceFormData = Object.keys(annotation.overrides)
+      .reduce((d, k) => ({
+        ...d,
+        [k]: annotation.overrides[k] || fd[k],
+      }), {});
+    const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
+    const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
+    const queryRequest = $.ajax({
+      url,
+      dataType: 'json',
+      timeout: timeout * 1000,
+    });
+    dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
+    return queryRequest
+      .then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
+      .catch((err) => {
+        if (err.statusText === 'timeout') {
+          dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
+        } else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
+          dispatch(annotationQuerySuccess(annotation, err, sliceKey));
+        } else if (err.statusText !== 'abort') {
+          dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
+        }
+      });
+  };
+}
+
+export const TRIGGER_QUERY = 'TRIGGER_QUERY';
+export function triggerQuery(value = true, key) {
+  return { type: TRIGGER_QUERY, value, key };
+}
+
+// this action is used for forced re-render without fetch data
+export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
+export function renderTriggered(value, key) {
+  return { type: RENDER_TRIGGERED, value, key };
+}
+
+export const UPDATE_QUERY_FORM_DATA = 'UPDATE_QUERY_FORM_DATA';
+export function updateQueryFormData(value, key) {
+  return { type: UPDATE_QUERY_FORM_DATA, value, key };
+}
+
+export const RUN_QUERY = 'RUN_QUERY';
+export function runQuery(formData, force = false, timeout = 60, key) {
+  return (dispatch) => {
+    const { url, payload } = getExploreUrlAndPayload({
+      formData,
+      endpointType: 'json',
+      force,
+    });
+    const logStart = Logger.getTimestamp();
+    const queryRequest = $.ajax({
+      type: 'POST',
+      url,
+      dataType: 'json',
+      data: {
+        form_data: JSON.stringify(payload),
+      },
+      timeout: timeout * 1000,
+    });
+    const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
+      .then(() => queryRequest)
+      .then((queryResponse) => {
+        Logger.append(LOG_ACTIONS_LOAD_CHART, {
+          slice_id: 'slice_' + key,
+          is_cached: queryResponse.is_cached,
+          force_refresh: force,
+          row_count: queryResponse.rowcount,
+          datasource: formData.datasource,
+          start_offset: logStart,
+          duration: Logger.getTimestamp() - logStart,
+          has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
+          viz_type: formData.viz_type,
+        });
+        return dispatch(chartUpdateSucceeded(queryResponse, key));
+      })
+      .catch((err) => {
+        Logger.append(LOG_ACTIONS_LOAD_CHART, {
+          slice_id: 'slice_' + key,
+          has_err: true,
+          datasource: formData.datasource,
+          start_offset: logStart,
+          duration: Logger.getTimestamp() - logStart,
+        });
+        if (err.statusText === 'timeout') {
+          dispatch(chartUpdateTimeout(err.statusText, timeout, key));
+        } else if (err.statusText === 'abort') {
+          dispatch(chartUpdateStopped(key));
+        } else {
+          let errObject;
+          if (err.responseJSON) {
+            errObject = err.responseJSON;
+          } else if (err.stack) {
+            errObject = {
+              error: t('Unexpected error: ') + err.description,
+              stacktrace: err.stack,
+            };
+          } else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
+            errObject = {
+              error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
+            };
+          } else {
+            errObject = {
+              error: t('Unexpected error.'),
+            };
+          }
+          dispatch(chartUpdateFailed(errObject, key));
+        }
+      });
+    const annotationLayers = formData.annotation_layers || [];
+    return Promise.all([
+      queryPromise,
+      dispatch(triggerQuery(false, key)),
+      dispatch(updateQueryFormData(payload, key)),
+      ...annotationLayers.map(x => dispatch(runAnnotationQuery(x, timeout, formData, key))),
+    ]);
+  };
+}
diff --git a/superset/assets/src/dashboard/deprecated/chart/chartReducer.js b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js
new file mode 100644
index 0000000..8d11249
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js
@@ -0,0 +1,158 @@
+/* eslint camelcase: 0 */
+import PropTypes from 'prop-types';
+
+import { now } from '../../../modules/dates';
+import * as actions from './chartAction';
+import { t } from '../../../locales';
+
+export const chartPropType = {
+  chartKey: PropTypes.string.isRequired,
+  chartAlert: PropTypes.string,
+  chartStatus: PropTypes.string,
+  chartUpdateEndTime: PropTypes.number,
+  chartUpdateStartTime: PropTypes.number,
+  latestQueryFormData: PropTypes.object,
+  queryRequest: PropTypes.object,
+  queryResponse: PropTypes.object,
+  triggerQuery: PropTypes.bool,
+  lastRendered: PropTypes.number,
+};
+
+export const chart = {
+  chartKey: '',
+  chartAlert: null,
+  chartStatus: 'loading',
+  chartUpdateEndTime: null,
+  chartUpdateStartTime: now(),
+  latestQueryFormData: {},
+  queryRequest: null,
+  queryResponse: null,
+  triggerQuery: true,
+  lastRendered: 0,
+};
+
+export default function chartReducer(charts = {}, action) {
+  const actionHandlers = {
+    [actions.CHART_UPDATE_SUCCEEDED](state) {
+      return { ...state,
+        chartStatus: 'success',
+        queryResponse: action.queryResponse,
+        chartUpdateEndTime: now(),
+      };
+    },
+    [actions.CHART_UPDATE_STARTED](state) {
+      return { ...state,
+        chartStatus: 'loading',
+        chartAlert: null,
+        chartUpdateEndTime: null,
+        chartUpdateStartTime: now(),
+        queryRequest: action.queryRequest,
+      };
+    },
+    [actions.CHART_UPDATE_STOPPED](state) {
+      return { ...state,
+        chartStatus: 'stopped',
+        chartAlert: t('Updating chart was stopped'),
+      };
+    },
+    [actions.CHART_RENDERING_SUCCEEDED](state) {
+      return { ...state,
+        chartStatus: 'rendered',
+      };
+    },
+    [actions.CHART_RENDERING_FAILED](state) {
+      return { ...state,
+        chartStatus: 'failed',
+        chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
+      };
+    },
+    [actions.CHART_UPDATE_TIMEOUT](state) {
+      return { ...state,
+        chartStatus: 'failed',
+        chartAlert: (
+            `${t('Query timeout')} - ` +
+            t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
+            t('Perhaps your data has grown, your database is under unusual load, ' +
+                'or you are simply querying a data source that is too large ' +
+                'to be processed within the timeout range. ' +
+                'If that is the case, we recommend that you summarize your data further.')),
+      };
+    },
+    [actions.CHART_UPDATE_FAILED](state) {
+      return { ...state,
+        chartStatus: 'failed',
+        chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
+        chartUpdateEndTime: now(),
+        queryResponse: action.queryResponse,
+      };
+    },
+    [actions.TRIGGER_QUERY](state) {
+      return { ...state, triggerQuery: action.value };
+    },
+    [actions.RENDER_TRIGGERED](state) {
+      return { ...state, lastRendered: action.value };
+    },
+    [actions.UPDATE_QUERY_FORM_DATA](state) {
+      return { ...state, latestQueryFormData: action.value };
+    },
+    [actions.ANNOTATION_QUERY_STARTED](state) {
+      if (state.annotationQuery &&
+        state.annotationQuery[action.annotation.name]) {
+        state.annotationQuery[action.annotation.name].abort();
+      }
+      const annotationQuery = {
+        ...state.annotationQuery,
+        [action.annotation.name]: action.queryRequest,
+      };
+      return {
+        ...state,
+        annotationQuery,
+      };
+    },
+    [actions.ANNOTATION_QUERY_SUCCESS](state) {
+      const annotationData = {
+        ...state.annotationData,
+        [action.annotation.name]: action.queryResponse.data,
+      };
+      const annotationError = { ...state.annotationError };
+      delete annotationError[action.annotation.name];
+      const annotationQuery = { ...state.annotationQuery };
+      delete annotationQuery[action.annotation.name];
+      return {
+        ...state,
+        annotationData,
+        annotationError,
+        annotationQuery,
+      };
+    },
+    [actions.ANNOTATION_QUERY_FAILED](state) {
+      const annotationData = { ...state.annotationData };
+      delete annotationData[action.annotation.name];
+      const annotationError = {
+        ...state.annotationError,
+        [action.annotation.name]: action.queryResponse ?
+          action.queryResponse.error : t('Network error.'),
+      };
+      const annotationQuery = { ...state.annotationQuery };
+      delete annotationQuery[action.annotation.name];
+      return {
+        ...state,
+        annotationData,
+        annotationError,
+        annotationQuery,
+      };
+    },
+  };
+
+  /* eslint-disable no-param-reassign */
+  if (action.type === actions.REMOVE_CHART) {
+    delete charts[action.key];
+    return charts;
+  }
+
+  if (action.type in actionHandlers) {
+    return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
+  }
+
+  return charts;
+}
diff --git a/superset/assets/src/dashboard/deprecated/v1/actions.js b/superset/assets/src/dashboard/deprecated/v1/actions.js
new file mode 100644
index 0000000..7381486
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/actions.js
@@ -0,0 +1,127 @@
+/* global notify */
+import $ from 'jquery';
+import { getExploreUrlAndPayload } from '../../../explore/exploreUtils';
+
+export const ADD_FILTER = 'ADD_FILTER';
+export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
+  return { type: ADD_FILTER, sliceId, col, vals, merge, refresh };
+}
+
+export const CLEAR_FILTER = 'CLEAR_FILTER';
+export function clearFilter(sliceId) {
+  return { type: CLEAR_FILTER, sliceId };
+}
+
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export function removeFilter(sliceId, col, vals, refresh = true) {
+  return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
+}
+
+export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT';
+export function updateDashboardLayout(layout) {
+  return { type: UPDATE_DASHBOARD_LAYOUT, layout };
+}
+
+export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
+export function updateDashboardTitle(title) {
+  return { type: UPDATE_DASHBOARD_TITLE, title };
+}
+
+export function addSlicesToDashboard(dashboardId, sliceIds) {
+  return () => (
+    $.ajax({
+      type: 'POST',
+      url: `/superset/add_slices/${dashboardId}/`,
+      data: {
+        data: JSON.stringify({ slice_ids: sliceIds }),
+      },
+    })
+      .done(() => {
+        // Refresh page to allow for slices to re-render
+        window.location.reload();
+      })
+  );
+}
+
+export const REMOVE_SLICE = 'REMOVE_SLICE';
+export function removeSlice(slice) {
+  return { type: REMOVE_SLICE, slice };
+}
+
+export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
+export function updateSliceName(slice, sliceName) {
+  return { type: UPDATE_SLICE_NAME, slice, sliceName };
+}
+export function saveSlice(slice, sliceName) {
+  const oldName = slice.slice_name;
+  return (dispatch) => {
+    const sliceParams = {};
+    sliceParams.slice_id = slice.slice_id;
+    sliceParams.action = 'overwrite';
+    sliceParams.slice_name = sliceName;
+
+    const { url, payload } = getExploreUrlAndPayload({
+      formData: slice.form_data,
+      endpointType: 'base',
+      force: false,
+      curUrl: null,
+      requestParams: sliceParams,
+    });
+    return $.ajax({
+      url,
+      type: 'POST',
+      data: {
+        form_data: JSON.stringify(payload),
+      },
+      success: () => {
+        dispatch(updateSliceName(slice, sliceName));
+        notify.success('This slice name was saved successfully.');
+      },
+      error: () => {
+        // if server-side reject the overwrite action,
+        // revert to old state
+        dispatch(updateSliceName(slice, oldName));
+        notify.error("You don't have the rights to alter this slice");
+      },
+    });
+  };
+}
+
+const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
+export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
+export function toggleFaveStar(isStarred) {
+  return { type: TOGGLE_FAVE_STAR, isStarred };
+}
+
+export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
+export function fetchFaveStar(id) {
+  return function (dispatch) {
+    const url = `${FAVESTAR_BASE_URL}/${id}/count`;
+    return $.get(url)
+      .done((data) => {
+        if (data.count > 0) {
+          dispatch(toggleFaveStar(true));
+        }
+      });
+  };
+}
+
+export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
+export function saveFaveStar(id, isStarred) {
+  return function (dispatch) {
+    const urlSuffix = isStarred ? 'unselect' : 'select';
+    const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
+    $.get(url);
+    dispatch(toggleFaveStar(!isStarred));
+  };
+}
+
+export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
+export function toggleExpandSlice(slice, isExpanded) {
+  return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded };
+}
+
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export function setEditMode(editMode) {
+  return { type: SET_EDIT_MODE, editMode };
+}
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx
new file mode 100644
index 0000000..3f802c3
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  triggerNode: PropTypes.node.isRequired,
+  code: PropTypes.string,
+  codeCallback: PropTypes.func,
+};
+
+const defaultProps = {
+  codeCallback: () => {},
+};
+
+export default class CodeModal extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = { code: props.code };
+  }
+  beforeOpen() {
+    let code = this.props.code;
+    if (!code && this.props.codeCallback) {
+      code = this.props.codeCallback();
+    }
+    this.setState({ code });
+  }
+  render() {
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        isButton
+        beforeOpen={this.beforeOpen.bind(this)}
+        modalTitle={t('Active Dashboard Filters')}
+        modalBody={
+          <div className="CodeModal">
+            <pre>
+              {this.state.code}
+            </pre>
+          </div>
+        }
+      />
+    );
+  }
+}
+CodeModal.propTypes = propTypes;
+CodeModal.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx
new file mode 100644
index 0000000..6a6fa47
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx
@@ -0,0 +1,214 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+
+import CssEditor from './CssEditor';
+import RefreshIntervalModal from './RefreshIntervalModal';
+import SaveModal from './SaveModal';
+import SliceAdder from './SliceAdder';
+import { t } from '../../../../locales';
+import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger';
+
+const $ = window.$ = require('jquery');
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  filters: PropTypes.object.isRequired,
+  slices: PropTypes.array,
+  userId: PropTypes.string.isRequired,
+  addSlicesToDashboard: PropTypes.func,
+  onSave: PropTypes.func,
+  onChange: PropTypes.func,
+  renderSlices: PropTypes.func,
+  serialize: PropTypes.func,
+  startPeriodicRender: PropTypes.func,
+  editMode: PropTypes.bool,
+};
+
+function MenuItemContent({ faIcon, text, tooltip, children }) {
+  return (
+    <span>
+      <i className={`fa fa-${faIcon}`} /> {text} {''}
+      <InfoTooltipWithTrigger
+        tooltip={tooltip}
+        label={`dash-${faIcon}`}
+        placement="top"
+      />
+      {children}
+    </span>
+  );
+}
+MenuItemContent.propTypes = {
+  faIcon: PropTypes.string.isRequired,
+  text: PropTypes.string,
+  tooltip: PropTypes.string,
+  children: PropTypes.node,
+};
+
+function ActionMenuItem(props) {
+  return (
+    <MenuItem onClick={props.onClick}>
+      <MenuItemContent {...props} />
+    </MenuItem>
+  );
+}
+ActionMenuItem.propTypes = {
+  onClick: PropTypes.func,
+};
+
+class Controls extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      css: props.dashboard.css || '',
+      cssTemplates: [],
+    };
+    this.refresh = this.refresh.bind(this);
+    this.toggleModal = this.toggleModal.bind(this);
+    this.updateDom = this.updateDom.bind(this);
+  }
+  componentWillMount() {
+    this.updateDom(this.state.css);
+
+    $.get('/csstemplateasyncmodelview/api/read', (data) => {
+      const cssTemplates = data.result.map(row => ({
+        value: row.template_name,
+        css: row.css,
+        label: row.template_name,
+      }));
+      this.setState({ cssTemplates });
+    });
+  }
+  refresh() {
+    // Force refresh all slices
+    this.props.renderSlices(true);
+  }
+  toggleModal(modal) {
+    let currentModal;
+    if (modal !== this.state.currentModal) {
+      currentModal = modal;
+    }
+    this.setState({ currentModal });
+  }
+  changeCss(css) {
+    this.setState({ css }, () => {
+      this.updateDom(css);
+    });
+    this.props.onChange();
+  }
+  updateDom(css) {
+    const className = 'CssEditor-css';
+    const head = document.head || document.getElementsByTagName('head')[0];
+    let style = document.querySelector('.' + className);
+
+    if (!style) {
+      style = document.createElement('style');
+      style.className = className;
+      style.type = 'text/css';
+      head.appendChild(style);
+    }
+    if (style.styleSheet) {
+      style.styleSheet.cssText = css;
+    } else {
+      style.innerHTML = css;
+    }
+  }
+  render() {
+    const { dashboard, userId, filters,
+      addSlicesToDashboard, startPeriodicRender,
+      serialize, onSave, editMode } = this.props;
+    const emailBody = t('Checkout this dashboard: %s', window.location.href);
+    const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
+      + `${dashboard.dashboard_title}&Body=${emailBody}`;
+    let saveText = t('Save as');
+    if (editMode) {
+      saveText = t('Save');
+    }
+    return (
+      <span>
+        <DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
+          <ActionMenuItem
+            text={t('Force Refresh')}
+            tooltip={t('Force refresh the whole dashboard')}
+            faIcon="refresh"
+            onClick={this.refresh}
+          />
+          <RefreshIntervalModal
+            onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
+            triggerNode={
+              <MenuItemContent
+                text={t('Set autorefresh')}
+                tooltip={t('Set the auto-refresh interval for this session')}
+                faIcon="clock-o"
+              />
+            }
+          />
+          {dashboard.dash_save_perm &&
+            <SaveModal
+              dashboard={dashboard}
+              filters={filters}
+              serialize={serialize}
+              onSave={onSave}
+              css={this.state.css}
+              triggerNode={
+                <MenuItemContent
+                  text={saveText}
+                  tooltip={t('Save the dashboard')}
+                  faIcon="save"
+                />
+              }
+            />
+          }
+          {editMode &&
+            <ActionMenuItem
+              text={t('Edit properties')}
+              tooltip={t("Edit the dashboards's properties")}
+              faIcon="edit"
+              onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
+            />
+          }
+          {editMode &&
+            <ActionMenuItem
+              text={t('Email')}
+              tooltip={t('Email a link to this dashboard')}
+              onClick={() => { window.location = emailLink; }}
+              faIcon="envelope"
+            />
+          }
+          {editMode &&
+            <SliceAdder
+              dashboard={dashboard}
+              addSlicesToDashboard={addSlicesToDashboard}
+              userId={userId}
+              triggerNode={
+                <MenuItemContent
+                  text={t('Add Charts')}
+                  tooltip={t('Add some charts to this dashboard')}
+                  faIcon="plus"
+                />
+              }
+            />
+          }
+          {editMode &&
+            <CssEditor
+              dashboard={dashboard}
+              triggerNode={
+                <MenuItemContent
+                  text={t('Edit CSS')}
+                  tooltip={t('Change the style of the dashboard using CSS code')}
+                  faIcon="css3"
+                />
+              }
+              initialCss={this.state.css}
+              templates={this.state.cssTemplates}
+              onChange={this.changeCss.bind(this)}
+            />
+          }
+        </DropdownButton>
+      </span>
+    );
+  }
+}
+Controls.propTypes = propTypes;
+
+export default Controls;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx
new file mode 100644
index 0000000..ee11ff2
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+
+import AceEditor from 'react-ace';
+import 'brace/mode/css';
+import 'brace/theme/github';
+
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  initialCss: PropTypes.string,
+  triggerNode: PropTypes.node.isRequired,
+  onChange: PropTypes.func,
+  templates: PropTypes.array,
+};
+
+const defaultProps = {
+  initialCss: '',
+  onChange: () => {},
+  templates: [],
+};
+
+class CssEditor extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      css: props.initialCss,
+      cssTemplateOptions: [],
+    };
+  }
+  changeCss(css) {
+    this.setState({ css }, () => {
+      this.props.onChange(css);
+    });
+  }
+  changeCssTemplate(opt) {
+    this.changeCss(opt.css);
+  }
+  renderTemplateSelector() {
+    if (this.props.templates) {
+      return (
+        <div style={{ zIndex: 10 }}>
+          <h5>{t('Load a template')}</h5>
+          <Select
+            options={this.props.templates}
+            placeholder={t('Load a CSS template')}
+            onChange={this.changeCssTemplate.bind(this)}
+          />
+        </div>
+      );
+    }
+    return null;
+  }
+  render() {
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        modalTitle={t('CSS')}
+        isMenuItem
+        modalBody={
+          <div>
+            {this.renderTemplateSelector()}
+            <div style={{ zIndex: 1 }}>
+              <h5>{t('Live CSS Editor')}</h5>
+              <div style={{ border: 'solid 1px grey' }}>
+                <AceEditor
+                  mode="css"
+                  theme="github"
+                  minLines={8}
+                  maxLines={30}
+                  onChange={this.changeCss.bind(this)}
+                  height="200px"
+                  width="100%"
+                  editorProps={{ $blockScrolling: true }}
+                  enableLiveAutocompletion
+                  value={this.state.css || ''}
+                />
+              </div>
+            </div>
+          </div>
+        }
+      />
+    );
+  }
+}
+CssEditor.propTypes = propTypes;
+CssEditor.defaultProps = defaultProps;
+
+export default CssEditor;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx
new file mode 100644
index 0000000..6ba4159
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx
@@ -0,0 +1,423 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AlertsWrapper from '../../../../components/AlertsWrapper';
+import GridLayout from './GridLayout';
+import Header from './Header';
+import { exportChart } from '../../../../explore/exploreUtils';
+import { areObjectsEqual } from '../../../../reduxUtils';
+import {
+  Logger,
+  ActionLog,
+  DASHBOARD_EVENT_NAMES,
+  LOG_ACTIONS_MOUNT_DASHBOARD,
+  LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+  LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+  LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
+  LOG_ACTIONS_REFRESH_CHART,
+  LOG_ACTIONS_REFRESH_DASHBOARD,
+} from '../../../../logger';
+
+import { t } from '../../../../locales';
+
+import '../../../../../stylesheets/dashboard_deprecated.css';
+
+const propTypes = {
+  actions: PropTypes.object,
+  initMessages: PropTypes.array,
+  dashboard: PropTypes.object.isRequired,
+  slices: PropTypes.object,
+  datasources: PropTypes.object,
+  filters: PropTypes.object,
+  refresh: PropTypes.bool,
+  timeout: PropTypes.number,
+  userId: PropTypes.string,
+  isStarred: PropTypes.bool,
+  editMode: PropTypes.bool,
+  impressionId: PropTypes.string,
+};
+
+const defaultProps = {
+  initMessages: [],
+  dashboard: {},
+  slices: {},
+  datasources: {},
+  filters: {},
+  refresh: false,
+  timeout: 60,
+  userId: '',
+  isStarred: false,
+  editMode: false,
+};
+
+class Dashboard extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.refreshTimer = null;
+    this.firstLoad = true;
+    this.loadingLog = new ActionLog({
+      impressionId: props.impressionId,
+      source: 'dashboard',
+      sourceId: props.dashboard.id,
+      eventNames: DASHBOARD_EVENT_NAMES,
+    });
+    Logger.start(this.loadingLog);
+
+    // alert for unsaved changes
+    this.state = {
+      unsavedChanges: false,
+    };
+    this.handleSetEditMode = this.handleSetEditMode.bind(this);
+
+    this.rerenderCharts = this.rerenderCharts.bind(this);
+    this.updateDashboardTitle = this.updateDashboardTitle.bind(this);
+    this.onSave = this.onSave.bind(this);
+    this.onChange = this.onChange.bind(this);
+    this.serialize = this.serialize.bind(this);
+    this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices());
+    this.startPeriodicRender = this.startPeriodicRender.bind(this);
+    this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
+    this.fetchSlice = this.fetchSlice.bind(this);
+    this.getFormDataExtra = this.getFormDataExtra.bind(this);
+    this.exploreChart = this.exploreChart.bind(this);
+    this.exportCSV = this.exportCSV.bind(this);
+    this.props.actions.fetchFaveStar = this.props.actions.fetchFaveStar.bind(this);
+    this.props.actions.saveFaveStar = this.props.actions.saveFaveStar.bind(this);
+    this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this);
+    this.props.actions.removeSlice = this.props.actions.removeSlice.bind(this);
+    this.props.actions.removeChart = this.props.actions.removeChart.bind(this);
+    this.props.actions.updateDashboardLayout = this.props.actions.updateDashboardLayout.bind(this);
+    this.props.actions.toggleExpandSlice = this.props.actions.toggleExpandSlice.bind(this);
+    this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
+    this.props.actions.clearFilter = this.props.actions.clearFilter.bind(this);
+    this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
+  }
+
+  componentDidMount() {
+    window.addEventListener('resize', this.rerenderCharts);
+    this.ts_mount = new Date().getTime();
+    Logger.append(LOG_ACTIONS_MOUNT_DASHBOARD, { version: 'v1' });
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (this.firstLoad &&
+      Object.values(nextProps.slices)
+        .every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
+    ) {
+      Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
+        duration: new Date().getTime() - this.ts_mount,
+        version: 'v1',
+      });
+      Logger.send(this.loadingLog);
+      this.firstLoad = false;
+    }
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.refresh) {
+      let changedFilterKey;
+      const prevFiltersKeySet = new Set(Object.keys(prevProps.filters));
+      Object.keys(this.props.filters).some((key) => {
+        prevFiltersKeySet.delete(key);
+        if (prevProps.filters[key] === undefined ||
+          !areObjectsEqual(prevProps.filters[key], this.props.filters[key])) {
+          changedFilterKey = key;
+          return true;
+        }
+        return false;
+      });
+      // has changed filter or removed a filter?
+      if (!!changedFilterKey || prevFiltersKeySet.size) {
+        this.refreshExcept(changedFilterKey);
+      }
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.rerenderCharts);
+  }
+
+  onBeforeUnload(hasChanged) {
+    if (hasChanged) {
+      window.addEventListener('beforeunload', this.unload);
+    } else {
+      window.removeEventListener('beforeunload', this.unload);
+    }
+  }
+
+  onChange() {
+    this.onBeforeUnload(true);
+    this.setState({ unsavedChanges: true });
+  }
+
+  onSave() {
+    this.onBeforeUnload(false);
+    this.setState({ unsavedChanges: false });
+  }
+
+  // return charts in array
+  getAllSlices() {
+    return Object.values(this.props.slices);
+  }
+
+  getFormDataExtra(slice) {
+    const formDataExtra = Object.assign({}, slice.formData);
+    formDataExtra.extra_filters = this.effectiveExtraFilters(slice.slice_id);
+    return formDataExtra;
+  }
+
+  getFilters(sliceId) {
+    return this.props.filters[sliceId];
+  }
+
+  unload() {
+    const message = t('You have unsaved changes.');
+    window.event.returnValue = message; // Gecko + IE
+    return message; // Gecko + Webkit, Safari, Chrome etc.
+  }
+
+  effectiveExtraFilters(sliceId) {
+    const metadata = this.props.dashboard.metadata;
+    const filters = this.props.filters;
+    const f = [];
+    const immuneSlices = metadata.filter_immune_slices || [];
+    if (sliceId && immuneSlices.includes(sliceId)) {
+      // The slice is immune to dashboard filters
+      return f;
+    }
+
+    // Building a list of fields the slice is immune to filters on
+    let immuneToFields = [];
+    if (
+      sliceId &&
+      metadata.filter_immune_slice_fields &&
+      metadata.filter_immune_slice_fields[sliceId]) {
+      immuneToFields = metadata.filter_immune_slice_fields[sliceId];
+    }
+    for (const filteringSliceId in filters) {
+      if (filteringSliceId === sliceId.toString()) {
+        // Filters applied by the slice don't apply to itself
+        continue;
+      }
+      for (const field in filters[filteringSliceId]) {
+        if (!immuneToFields.includes(field)) {
+          f.push({
+            col: field,
+            op: 'in',
+            val: filters[filteringSliceId][field],
+          });
+        }
+      }
+    }
+    return f;
+  }
+
+  refreshExcept(filterKey) {
+    const immune = this.props.dashboard.metadata.filter_immune_slices || [];
+    let slices = this.getAllSlices();
+    if (filterKey) {
+      slices = slices.filter(slice => (
+        String(slice.slice_id) !== filterKey &&
+        immune.indexOf(slice.slice_id) === -1
+      ));
+    }
+    this.fetchSlices(slices);
+  }
+
+  stopPeriodicRender() {
+    if (this.refreshTimer) {
+      clearTimeout(this.refreshTimer);
+      this.refreshTimer = null;
+    }
+  }
+
+  startPeriodicRender(interval) {
+    this.stopPeriodicRender();
+    const immune = this.props.dashboard.metadata.timed_refresh_immune_slices || [];
+    const refreshAll = () => {
+      const affectedSlices = this.getAllSlices()
+        .filter(slice => immune.indexOf(slice.slice_id) === -1);
+      this.fetchSlices(affectedSlices, true, interval * 0.2);
+    };
+    const fetchAndRender = () => {
+      refreshAll();
+      if (interval > 0) {
+        this.refreshTimer = setTimeout(fetchAndRender, interval);
+      }
+    };
+
+    fetchAndRender();
+  }
+
+  updateDashboardTitle(title) {
+    this.props.actions.updateDashboardTitle(title);
+    this.onChange();
+  }
+
+  serialize() {
+    return this.props.dashboard.layout.map(reactPos => ({
+      slice_id: reactPos.i,
+      col: reactPos.x + 1,
+      row: reactPos.y,
+      size_x: reactPos.w,
+      size_y: reactPos.h,
+    }));
+  }
+
+  addSlicesToDashboard(sliceIds) {
+    return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds);
+  }
+
+  fetchSlice(slice, force = false, fetchingAllSlices = false) {
+    if (force && !fetchingAllSlices) {
+      const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
+      Logger.append(
+        LOG_ACTIONS_REFRESH_CHART,
+        {
+          slice_id: slice.slice_id,
+          is_cached: chartQuery.is_cached,
+          version: 'v1',
+        },
+        true,
+      );
+    }
+    return this.props.actions.runQuery(
+      this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey,
+    );
+  }
+
+  // fetch and render an list of slices
+  fetchSlices(slc, force = false, interval = 0) {
+    const slices = slc || this.getAllSlices();
+    Logger.append(
+      LOG_ACTIONS_REFRESH_DASHBOARD,
+      {
+        force,
+        interval,
+        chartCount: slices.length,
+        version: 'v1',
+      },
+      true,
+    );
+    if (!interval) {
+      slices.forEach((slice) => { this.fetchSlice(slice, force, true); });
+      return;
+    }
+
+    const meta = this.props.dashboard.metadata;
+    const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
+    if (typeof meta.stagger_refresh !== 'boolean') {
+      meta.stagger_refresh = meta.stagger_refresh === undefined ?
+        true : meta.stagger_refresh === 'true';
+    }
+    const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
+    slices.forEach((slice, i) => {
+      setTimeout(() => { this.fetchSlice(slice, force, true); }, delay * i);
+    });
+  }
+
+  exploreChart(slice) {
+    const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
+    Logger.append(
+      LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+      {
+        slice_id: slice.slice_id,
+        is_cached: chartQuery && chartQuery.is_cached,
+        version: 'v1',
+      },
+      true,
+    );
+    const formData = this.getFormDataExtra(slice);
+    exportChart(formData);
+  }
+
+  exportCSV(slice) {
+    const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
+    Logger.append(
+      LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+      {
+        slice_id: slice.slice_id,
+        is_cached: chartQuery && chartQuery.is_cached,
+        version: 'v1',
+      },
+      true,
+    );
+    const formData = this.getFormDataExtra(slice);
+    exportChart(formData, 'csv');
+  }
+
+  handleSetEditMode(nextEditMode) {
+    if (this.props.dashboard.forceV2Edit) {
+      this.handleConvertToV2(true);
+    } else {
+      this.props.actions.setEditMode(nextEditMode);
+    }
+  }
+
+  // re-render chart without fetch
+  rerenderCharts() {
+    this.getAllSlices().forEach((slice) => {
+      setTimeout(() => {
+        this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+      }, 50);
+    });
+  }
+
+  render() {
+    const { dashboard, editMode } = this.props;
+    return (
+      <div id="dashboard-container">
+        <div id="dashboard-header">
+          <AlertsWrapper initMessages={this.props.initMessages} />
+          <Header
+            dashboard={this.props.dashboard}
+            unsavedChanges={this.state.unsavedChanges}
+            filters={this.props.filters}
+            userId={this.props.userId}
+            isStarred={this.props.isStarred}
+            updateDashboardTitle={this.updateDashboardTitle}
+            onSave={this.onSave}
+            onChange={this.onChange}
+            serialize={this.serialize}
+            fetchFaveStar={this.props.actions.fetchFaveStar}
+            saveFaveStar={this.props.actions.saveFaveStar}
+            renderSlices={this.fetchAllSlices}
+            startPeriodicRender={this.startPeriodicRender}
+            addSlicesToDashboard={this.addSlicesToDashboard}
+            editMode={this.props.editMode}
+            setEditMode={this.handleSetEditMode}
+          />
+        </div>
+        <div id="grid-container" className="slice-grid gridster">
+          <GridLayout
+            dashboard={this.props.dashboard}
+            datasources={this.props.datasources}
+            filters={this.props.filters}
+            charts={this.props.slices}
+            timeout={this.props.timeout}
+            onChange={this.onChange}
+            getFormDataExtra={this.getFormDataExtra}
+            exploreChart={this.exploreChart}
+            exportCSV={this.exportCSV}
+            fetchSlice={this.fetchSlice}
+            saveSlice={this.props.actions.saveSlice}
+            removeSlice={this.props.actions.removeSlice}
+            removeChart={this.props.actions.removeChart}
+            updateDashboardLayout={this.props.actions.updateDashboardLayout}
+            toggleExpandSlice={this.props.actions.toggleExpandSlice}
+            addFilter={this.props.actions.addFilter}
+            getFilters={this.getFilters}
+            clearFilter={this.props.actions.clearFilter}
+            removeFilter={this.props.actions.removeFilter}
+            editMode={this.props.editMode}
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+Dashboard.propTypes = propTypes;
+Dashboard.defaultProps = defaultProps;
+
+export default Dashboard;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx b/superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx
new file mode 100644
index 0000000..a18a5d2
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx
@@ -0,0 +1,31 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import * as dashboardActions from '../actions';
+import * as chartActions from '../../chart/chartAction';
+import Dashboard from './Dashboard';
+
+function mapStateToProps({ charts, dashboard, impressionId }) {
+  return {
+    initMessages: dashboard.common.flash_messages,
+    timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    dashboard: dashboard.dashboard,
+    slices: charts,
+    datasources: dashboard.datasources,
+    filters: dashboard.filters,
+    refresh: !!dashboard.refresh,
+    userId: dashboard.userId,
+    isStarred: !!dashboard.isStarred,
+    editMode: dashboard.editMode,
+    impressionId,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  const actions = { ...chartActions, ...dashboardActions };
+  return {
+    actions: bindActionCreators(actions, dispatch),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx b/superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx
new file mode 100644
index 0000000..d68b427
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx
@@ -0,0 +1,157 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import SliceHeader from './SliceHeader';
+import ChartContainer from '../../chart/ChartContainer';
+
+import '../../../../../stylesheets/dashboard_deprecated.css';
+
+const propTypes = {
+  timeout: PropTypes.number,
+  datasource: PropTypes.object,
+  isLoading: PropTypes.bool,
+  isCached: PropTypes.bool,
+  cachedDttm: PropTypes.string,
+  isExpanded: PropTypes.bool,
+  widgetHeight: PropTypes.number,
+  widgetWidth: PropTypes.number,
+  slice: PropTypes.object,
+  chartKey: PropTypes.string,
+  formData: PropTypes.object,
+  filters: PropTypes.object,
+  forceRefresh: PropTypes.func,
+  removeSlice: PropTypes.func,
+  updateSliceName: PropTypes.func,
+  toggleExpandSlice: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
+  addFilter: PropTypes.func,
+  getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
+  editMode: PropTypes.bool,
+  annotationQuery: PropTypes.object,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  removeSlice: () => ({}),
+  updateSliceName: () => ({}),
+  toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+  editMode: false,
+};
+
+class GridCell extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    const sliceId = this.props.slice.slice_id;
+    this.addFilter = this.props.addFilter.bind(this, sliceId);
+    this.getFilters = this.props.getFilters.bind(this, sliceId);
+    this.clearFilter = this.props.clearFilter.bind(this, sliceId);
+    this.removeFilter = this.props.removeFilter.bind(this, sliceId);
+  }
+
+  getDescriptionId(slice) {
+    return 'description_' + slice.slice_id;
+  }
+
+  getHeaderId(slice) {
+    return 'header_' + slice.slice_id;
+  }
+
+  width() {
+    return this.props.widgetWidth - 10;
+  }
+
+  height(slice) {
+    const widgetHeight = this.props.widgetHeight;
+    const headerHeight = this.headerHeight(slice);
+    const descriptionId = this.getDescriptionId(slice);
+    let descriptionHeight = 0;
+    if (this.props.isExpanded && this.refs[descriptionId]) {
+      descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
+    }
+
+    return widgetHeight - headerHeight - descriptionHeight;
+  }
+
+  headerHeight(slice) {
+    const headerId = this.getHeaderId(slice);
+    return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
+  }
+
+  render() {
+    const {
+      isExpanded, isLoading, isCached, cachedDttm,
+      removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
+      chartKey, slice, datasource, formData, timeout, annotationQuery,
+      exploreChart, exportCSV,
+    } = this.props;
+    return (
+      <div
+        className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
+        id={`${slice.slice_id}-cell`}
+      >
+        <div ref={this.getHeaderId(slice)}>
+          <SliceHeader
+            slice={slice}
+            isExpanded={isExpanded}
+            isCached={isCached}
+            cachedDttm={cachedDttm}
+            removeSlice={removeSlice}
+            updateSliceName={updateSliceName}
+            toggleExpandSlice={toggleExpandSlice}
+            forceRefresh={forceRefresh}
+            editMode={this.props.editMode}
+            annotationQuery={annotationQuery}
+            exploreChart={exploreChart}
+            exportCSV={exportCSV}
+          />
+        </div>
+        {
+        /* This usage of dangerouslySetInnerHTML is safe since it is being used to render
+           markdown that is sanitized with bleach. See:
+             https://github.com/apache/incubator-superset/pull/4390
+           and
+             https://github.com/apache/incubator-superset/commit/b6fcc22d5a2cb7a5e92599ed5795a0169385a825 */}
+        <div
+          className="slice_description bs-callout bs-callout-default"
+          style={isExpanded ? {} : { display: 'none' }}
+          ref={this.getDescriptionId(slice)}
+          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+        />
+        <div className="row chart-container">
+          <input type="hidden" value="false" />
+          <ChartContainer
+            containerId={`slice-container-${slice.slice_id}`}
+            chartKey={chartKey}
+            datasource={datasource}
+            formData={formData}
+            headerHeight={this.headerHeight(slice)}
+            height={this.height(slice)}
+            width={this.width()}
+            timeout={timeout}
+            vizType={slice.formData.viz_type}
+            addFilter={this.addFilter}
+            getFilters={this.getFilters}
+            clearFilter={this.clearFilter}
+            removeFilter={this.removeFilter}
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+GridCell.propTypes = propTypes;
+GridCell.defaultProps = defaultProps;
+
+export default GridCell;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx
new file mode 100644
index 0000000..ef0ec24
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Responsive, WidthProvider } from 'react-grid-layout';
+
+import GridCell from './GridCell';
+
+require('react-grid-layout/css/styles.css');
+require('react-resizable/css/styles.css');
+
+const ResponsiveReactGridLayout = WidthProvider(Responsive);
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  datasources: PropTypes.object,
+  charts: PropTypes.object.isRequired,
+  filters: PropTypes.object,
+  timeout: PropTypes.number,
+  onChange: PropTypes.func,
+  getFormDataExtra: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
+  fetchSlice: PropTypes.func,
+  saveSlice: PropTypes.func,
+  removeSlice: PropTypes.func,
+  removeChart: PropTypes.func,
+  updateDashboardLayout: PropTypes.func,
+  toggleExpandSlice: PropTypes.func,
+  addFilter: PropTypes.func,
+  getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
+};
+
+const defaultProps = {
+  onChange: () => ({}),
+  getFormDataExtra: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+  fetchSlice: () => ({}),
+  saveSlice: () => ({}),
+  removeSlice: () => ({}),
+  removeChart: () => ({}),
+  updateDashboardLayout: () => ({}),
+  toggleExpandSlice: () => ({}),
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+};
+
+class GridLayout extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.onResizeStop = this.onResizeStop.bind(this);
+    this.onDragStop = this.onDragStop.bind(this);
+    this.forceRefresh = this.forceRefresh.bind(this);
+    this.removeSlice = this.removeSlice.bind(this);
+    this.updateSliceName = this.props.dashboard.dash_edit_perm ?
+      this.updateSliceName.bind(this) : null;
+  }
+
+  onResizeStop(layout) {
+    this.props.updateDashboardLayout(layout);
+    this.props.onChange();
+  }
+
+  onDragStop(layout) {
+    this.props.updateDashboardLayout(layout);
+    this.props.onChange();
+  }
+
+  getWidgetId(slice) {
+    return 'widget_' + slice.slice_id;
+  }
+
+  getWidgetHeight(slice) {
+    const widgetId = this.getWidgetId(slice);
+    if (!widgetId || !this.refs[widgetId]) {
+      return 400;
+    }
+    return this.refs[widgetId].offsetHeight;
+  }
+
+  getWidgetWidth(slice) {
+    const widgetId = this.getWidgetId(slice);
+    if (!widgetId || !this.refs[widgetId]) {
+      return 400;
+    }
+    return this.refs[widgetId].offsetWidth;
+  }
+
+  findSliceIndexById(sliceId) {
+    return this.props.dashboard.slices
+      .map(slice => (slice.slice_id)).indexOf(sliceId);
+  }
+
+  forceRefresh(sliceId) {
+    return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true);
+  }
+
+  removeSlice(slice) {
+    if (!slice) {
+      return;
+    }
+
+    // remove slice dashboard and charts
+    this.props.removeSlice(slice);
+    this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
+    this.props.onChange();
+  }
+
+  updateSliceName(sliceId, sliceName) {
+    const index = this.findSliceIndexById(sliceId);
+    if (index === -1) {
+      return;
+    }
+
+    const currentSlice = this.props.dashboard.slices[index];
+    if (currentSlice.slice_name === sliceName) {
+      return;
+    }
+
+    this.props.saveSlice(currentSlice, sliceName);
+  }
+
+  isExpanded(slice) {
+    return this.props.dashboard.metadata.expanded_slices &&
+      this.props.dashboard.metadata.expanded_slices[slice.slice_id];
+  }
+
+  render() {
+    const cells = this.props.dashboard.slices.map((slice) => {
+      const chartKey = `slice_${slice.slice_id}`;
+      const currentChart = this.props.charts[chartKey];
+      const queryResponse = currentChart.queryResponse || {};
+      return (
+        <div
+          id={'slice_' + slice.slice_id}
+          key={slice.slice_id}
+          data-slice-id={slice.slice_id}
+          className={`widget ${slice.form_data.viz_type}`}
+          ref={this.getWidgetId(slice)}
+        >
+          <GridCell
+            slice={slice}
+            chartKey={chartKey}
+            datasource={this.props.datasources[slice.form_data.datasource]}
+            filters={this.props.filters}
+            formData={this.props.getFormDataExtra(slice)}
+            timeout={this.props.timeout}
+            widgetHeight={this.getWidgetHeight(slice)}
+            widgetWidth={this.getWidgetWidth(slice)}
+            exploreChart={this.props.exploreChart}
+            exportCSV={this.props.exportCSV}
+            isExpanded={!!this.isExpanded(slice)}
+            isLoading={currentChart.chartStatus === 'loading'}
+            isCached={queryResponse.is_cached}
+            cachedDttm={queryResponse.cached_dttm}
+            toggleExpandSlice={this.props.toggleExpandSlice}
+            forceRefresh={this.forceRefresh}
+            removeSlice={this.removeSlice}
+            updateSliceName={this.updateSliceName}
+            addFilter={this.props.addFilter}
+            getFilters={this.props.getFilters}
+            clearFilter={this.props.clearFilter}
+            removeFilter={this.props.removeFilter}
+            editMode={this.props.editMode}
+            annotationQuery={currentChart.annotationQuery}
+            annotationError={currentChart.annotationError}
+          />
+        </div>);
+    });
+
+    return (
+      <ResponsiveReactGridLayout
+        className="layout"
+        layouts={{ lg: this.props.dashboard.layout }}
+        onResizeStop={this.onResizeStop}
+        onDragStop={this.onDragStop}
+        cols={{ lg: 48, md: 48, sm: 40, xs: 32, xxs: 24 }}
+        rowHeight={10}
+        autoSize
+        margin={[20, 20]}
+        useCSSTransforms
+        draggableHandle=".drag"
+      >
+        {cells}
+      </ResponsiveReactGridLayout>
+    );
+  }
+}
+
+GridLayout.propTypes = propTypes;
+GridLayout.defaultProps = defaultProps;
+
+export default GridLayout;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx
new file mode 100644
index 0000000..a84ee89
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx
@@ -0,0 +1,184 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Controls from './Controls';
+import EditableTitle from '../../../../components/EditableTitle';
+import Button from '../../../../components/Button';
+import FaveStar from '../../../../components/FaveStar';
+import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger';
+import PromptV2ConversionModal from '../../PromptV2ConversionModal';
+import {
+  Logger,
+  LOG_ACTIONS_PREVIEW_V2,
+  LOG_ACTIONS_DISMISS_V2_PROMPT,
+  LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
+} from '../../../../logger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  filters: PropTypes.object.isRequired,
+  userId: PropTypes.string.isRequired,
+  isStarred: PropTypes.bool,
+  addSlicesToDashboard: PropTypes.func,
+  onSave: PropTypes.func,
+  onChange: PropTypes.func,
+  fetchFaveStar: PropTypes.func,
+  renderSlices: PropTypes.func,
+  saveFaveStar: PropTypes.func,
+  serialize: PropTypes.func,
+  startPeriodicRender: PropTypes.func,
+  updateDashboardTitle: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
+  setEditMode: PropTypes.func.isRequired,
+  unsavedChanges: PropTypes.bool.isRequired,
+};
+
+class Header extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleSaveTitle = this.handleSaveTitle.bind(this);
+    this.toggleEditMode = this.toggleEditMode.bind(this);
+    this.state = {
+      showV2PromptModal: props.dashboard.promptV2Conversion,
+    };
+    this.toggleShowV2PromptModal = this.toggleShowV2PromptModal.bind(this);
+    this.handleConvertToV2 = this.handleConvertToV2.bind(this);
+  }
+  handleSaveTitle(title) {
+    this.props.updateDashboardTitle(title);
+  }
+  handleConvertToV2(editMode) {
+    Logger.append(
+      LOG_ACTIONS_PREVIEW_V2,
+      {
+        force_v2_edit: this.props.dashboard.forceV2Edit,
+        edit_mode: editMode === true,
+      },
+      true,
+    );
+    const url = new URL(window.location); // eslint-disable-line
+    url.searchParams.set('version', 'v2');
+    if (editMode === true) url.searchParams.set('edit', true);
+    window.location = url; // eslint-disable-line
+  }
+  toggleEditMode() {
+    this.props.setEditMode(!this.props.editMode);
+  }
+  toggleShowV2PromptModal() {
+    const nextShowModal = !this.state.showV2PromptModal;
+    this.setState({ showV2PromptModal: nextShowModal });
+    if (nextShowModal) {
+      Logger.append(
+        LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
+        {
+          force_v2_edit: this.props.dashboard.forceV2Edit,
+        },
+        true,
+      );
+    } else {
+      Logger.append(
+        LOG_ACTIONS_DISMISS_V2_PROMPT,
+        {
+          force_v2_edit: this.props.dashboard.forceV2Edit,
+        },
+        true,
+      );
+    }
+  }
+  renderUnsaved() {
+    if (!this.props.unsavedChanges) {
+      return null;
+    }
+    return (
+      <InfoTooltipWithTrigger
+        label="unsaved"
+        tooltip={t('Unsaved changes')}
+        icon="exclamation-triangle"
+        className="text-danger m-r-5"
+        placement="top"
+      />
+    );
+  }
+  renderEditButton() {
+    if (!this.props.dashboard.dash_save_perm) {
+      return null;
+    }
+    const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
+    return (
+      <Button
+        bsStyle="default"
+        className="m-r-5"
+        style={{ width: '150px' }}
+        onClick={this.toggleEditMode}
+      >
+        {btnText}
+      </Button>);
+  }
+  render() {
+    const dashboard = this.props.dashboard;
+    return (
+      <div className="title">
+        <div className="pull-left">
+          <h1 className="outer-container pull-left">
+            <EditableTitle
+              title={dashboard.dashboard_title}
+              canEdit={dashboard.dash_save_perm && this.props.editMode}
+              onSaveTitle={this.handleSaveTitle}
+              showTooltip={this.props.editMode}
+            />
+            <span className="favstar m-l-5">
+              <FaveStar
+                itemId={dashboard.id}
+                fetchFaveStar={this.props.fetchFaveStar}
+                saveFaveStar={this.props.saveFaveStar}
+                isStarred={this.props.isStarred}
+              />
+            </span>
+            {dashboard.promptV2Conversion && (
+              <span
+                role="none"
+                className="v2-preview-badge"
+                onClick={this.toggleShowV2PromptModal}
+              >
+                {t('Convert to v2')}
+                <span className="fa fa-info-circle m-l-5" />
+              </span>
+            )}
+            {this.renderUnsaved()}
+          </h1>
+        </div>
+        <div className="pull-right" style={{ marginTop: '35px' }}>
+          {this.renderEditButton()}
+          <Controls
+            dashboard={dashboard}
+            filters={this.props.filters}
+            userId={this.props.userId}
+            addSlicesToDashboard={this.props.addSlicesToDashboard}
+            onSave={this.props.onSave}
+            onChange={this.props.onChange}
+            renderSlices={this.props.renderSlices}
+            serialize={this.props.serialize}
+            startPeriodicRender={this.props.startPeriodicRender}
+            editMode={this.props.editMode}
+          />
+        </div>
+        <div className="clearfix" />
+        {this.state.showV2PromptModal &&
+          dashboard.promptV2Conversion &&
+          !this.props.editMode && (
+            <PromptV2ConversionModal
+              onClose={this.toggleShowV2PromptModal}
+              handleConvertToV2={this.handleConvertToV2}
+              forceV2Edit={dashboard.forceV2Edit}
+              v2AutoConvertDate={dashboard.v2AutoConvertDate}
+              v2FeedbackUrl={dashboard.v2FeedbackUrl}
+            />
+          )}
+      </div>
+    );
+  }
+}
+Header.propTypes = propTypes;
+
+export default Header;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx
new file mode 100644
index 0000000..3e43f93
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  triggerNode: PropTypes.node.isRequired,
+  initialRefreshFrequency: PropTypes.number,
+  onChange: PropTypes.func,
+};
+
+const defaultProps = {
+  initialRefreshFrequency: 0,
+  onChange: () => {},
+};
+
+const options = [
+  [0, t('Don\'t refresh')],
+  [10, t('10 seconds')],
+  [30, t('30 seconds')],
+  [60, t('1 minute')],
+  [300, t('5 minutes')],
+  [1800, t('30 minutes')],
+  [3600, t('1 hour')],
+  [21600, t('6 hours')],
+  [43200, t('12 hours')],
+  [86400, t('24 hours')],
+].map(o => ({ value: o[0], label: o[1] }));
+
+class RefreshIntervalModal extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      refreshFrequency: props.initialRefreshFrequency,
+    };
+  }
+  render() {
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        isMenuItem
+        modalTitle={t('Refresh Interval')}
+        modalBody={
+          <div>
+            {t('Choose the refresh frequency for this dashboard')}
+            <Select
+              options={options}
+              value={this.state.refreshFrequency}
+              onChange={(opt) => {
+                this.setState({ refreshFrequency: opt.value });
+                this.props.onChange(opt.value);
+              }}
+            />
+          </div>
+        }
+      />
+    );
+  }
+}
+RefreshIntervalModal.propTypes = propTypes;
+RefreshIntervalModal.defaultProps = defaultProps;
+
+export default RefreshIntervalModal;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx
new file mode 100644
index 0000000..aa622ab
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx
@@ -0,0 +1,161 @@
+/* global notify */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
+import { getAjaxErrorMsg } from '../../../../modules/utils';
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+import Checkbox from '../../../../components/Checkbox';
+
+const $ = window.$ = require('jquery');
+
+const propTypes = {
+  css: PropTypes.string,
+  dashboard: PropTypes.object.isRequired,
+  triggerNode: PropTypes.node.isRequired,
+  filters: PropTypes.object.isRequired,
+  serialize: PropTypes.func,
+  onSave: PropTypes.func,
+};
+
+class SaveModal extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dashboard: props.dashboard,
+      css: props.css,
+      saveType: 'overwrite',
+      newDashName: props.dashboard.dashboard_title + ' [copy]',
+      duplicateSlices: false,
+    };
+    this.modal = null;
+    this.handleSaveTypeChange = this.handleSaveTypeChange.bind(this);
+    this.handleNameChange = this.handleNameChange.bind(this);
+    this.saveDashboard = this.saveDashboard.bind(this);
+  }
+  toggleDuplicateSlices() {
+    this.setState({ duplicateSlices: !this.state.duplicateSlices });
+  }
+  handleSaveTypeChange(event) {
+    this.setState({
+      saveType: event.target.value,
+    });
+  }
+  handleNameChange(event) {
+    this.setState({
+      newDashName: event.target.value,
+      saveType: 'newDashboard',
+    });
+  }
+  saveDashboardRequest(data, url, saveType) {
+    const saveModal = this.modal;
+    const onSaveDashboard = this.props.onSave;
+    Object.assign(data, { css: this.props.css });
+    $.ajax({
+      type: 'POST',
+      url,
+      data: {
+        data: JSON.stringify(data),
+      },
+      success(resp) {
+        saveModal.close();
+        onSaveDashboard();
+        if (saveType === 'newDashboard') {
+          window.location = `/superset/dashboard/${resp.id}/`;
+        } else {
+          notify.success(t('This dashboard was saved successfully.'));
+        }
+      },
+      error(error) {
+        saveModal.close();
+        const errorMsg = getAjaxErrorMsg(error);
+        notify.error(t('Sorry, there was an error saving this dashboard: ') + errorMsg);
+      },
+    });
+  }
+  saveDashboard(saveType, newDashboardTitle) {
+    const dashboard = this.props.dashboard;
+    const positions = this.props.serialize();
+    const data = {
+      positions,
+      css: this.state.css,
+      expanded_slices: dashboard.metadata.expanded_slices || {},
+      dashboard_title: dashboard.dashboard_title,
+      default_filters: JSON.stringify(this.props.filters),
+      duplicate_slices: this.state.duplicateSlices,
+    };
+    let url = null;
+    if (saveType === 'overwrite') {
+      url = `/superset/save_dash/${dashboard.id}/`;
+      this.saveDashboardRequest(data, url, saveType);
+    } else if (saveType === 'newDashboard') {
+      if (!newDashboardTitle) {
+        this.modal.close();
+        showModal({
+          title: t('Error'),
+          body: t('You must pick a name for the new dashboard'),
+        });
+      } else {
+        data.dashboard_title = newDashboardTitle;
+        url = `/superset/copy_dash/${dashboard.id}/`;
+        this.saveDashboardRequest(data, url, saveType);
+      }
+    }
+  }
+  render() {
+    return (
+      <ModalTrigger
+        ref={(modal) => { this.modal = modal; }}
+        isMenuItem
+        triggerNode={this.props.triggerNode}
+        modalTitle={t('Save Dashboard')}
+        modalBody={
+          <FormGroup>
+            <Radio
+              value="overwrite"
+              onChange={this.handleSaveTypeChange}
+              checked={this.state.saveType === 'overwrite'}
+            >
+              {t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
+            </Radio>
+            <hr />
+            <Radio
+              value="newDashboard"
+              onChange={this.handleSaveTypeChange}
+              checked={this.state.saveType === 'newDashboard'}
+            >
+              {t('Save as:')}
+            </Radio>
+            <FormControl
+              type="text"
+              placeholder={t('[dashboard name]')}
+              value={this.state.newDashName}
+              onFocus={this.handleNameChange}
+              onChange={this.handleNameChange}
+            />
+            <div className="m-l-25 m-t-5">
+              <Checkbox
+                checked={this.state.duplicateSlices}
+                onChange={this.toggleDuplicateSlices.bind(this)}
+              />
+              <span className="m-l-5">also copy (duplicate) charts</span>
+            </div>
+          </FormGroup>
+        }
+        modalFooter={
+          <div>
+            <Button
+              bsStyle="primary"
+              onClick={() => { this.saveDashboard(this.state.saveType, this.state.newDashName); }}
+            >
+              {t('Save')}
+            </Button>
+          </div>
+        }
+      />
+    );
+  }
+}
+SaveModal.propTypes = propTypes;
+
+export default SaveModal;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
new file mode 100644
index 0000000..6c2f624
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
@@ -0,0 +1,219 @@
+import React from 'react';
+import $ from 'jquery';
+import PropTypes from 'prop-types';
+import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+require('react-bootstrap-table/css/react-bootstrap-table.css');
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  triggerNode: PropTypes.node.isRequired,
+  userId: PropTypes.string.isRequired,
+  addSlicesToDashboard: PropTypes.func,
+};
+
+class SliceAdder extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      slices: [],
+      slicesLoaded: false,
+      selectionMap: {},
+    };
+
+    this.options = {
+      defaultSortOrder: 'desc',
+      defaultSortName: 'modified',
+      sizePerPage: 10,
+    };
+
+    this.addSlices = this.addSlices.bind(this);
+    this.toggleSlice = this.toggleSlice.bind(this);
+
+    this.selectRowProp = {
+      mode: 'checkbox',
+      clickToSelect: true,
+      onSelect: this.toggleSlice,
+    };
+  }
+
+  componentWillUnmount() {
+    if (this.slicesRequest) {
+      this.slicesRequest.abort();
+    }
+  }
+
+  onEnterModal() {
+    const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
+    this.slicesRequest = $.ajax({
+      url: uri,
+      type: 'GET',
+      success: (response) => {
+        // Prepare slice data for table
+        const slices = response.result.map(slice => ({
+          id: slice.id,
+          sliceName: slice.slice_name,
+          vizType: slice.viz_type,
+          datasourceLink: slice.datasource_link,
+          modified: slice.modified,
+        }));
+
+        this.setState({
+          slices,
+          selectionMap: {},
+          slicesLoaded: true,
+        });
+      },
+      error: (error) => {
+        this.errored = true;
+        this.setState({
+          errorMsg: t('Sorry, there was an error fetching charts to this dashboard: ') +
+          this.getAjaxErrorMsg(error),
+        });
+      },
+    });
+  }
+
+  getAjaxErrorMsg(error) {
+    const respJSON = error.responseJSON;
+    return (respJSON && respJSON.message) ? respJSON.message :
+      error.responseText;
+  }
+
+  addSlices() {
+    const adder = this;
+    this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
+      // if successful, page will be reloaded.
+      .fail((error) => {
+        adder.errored = true;
+        adder.setState({
+          errorMsg: t('Sorry, there was an error adding charts to this dashboard: ') +
+          this.getAjaxErrorMsg(error),
+        });
+      });
+  }
+
+  toggleSlice(slice) {
+    const selectionMap = Object.assign({}, this.state.selectionMap);
+    selectionMap[slice.id] = !selectionMap[slice.id];
+    this.setState({ selectionMap });
+  }
+
+  modifiedDateComparator(a, b, order) {
+    if (order === 'desc') {
+      if (a.changed_on > b.changed_on) {
+        return -1;
+      } else if (a.changed_on < b.changed_on) {
+        return 1;
+      }
+      return 0;
+    }
+
+    if (a.changed_on < b.changed_on) {
+      return -1;
+    } else if (a.changed_on > b.changed_on) {
+      return 1;
+    }
+    return 0;
+  }
+
+  render() {
+    const hideLoad = this.state.slicesLoaded || this.errored;
+    let enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap);
+    if (enableAddSlice) {
+      enableAddSlice = enableAddSlice.some(function (key) {
+        return this.state.selectionMap[key];
+      }, this);
+    }
+    const modalContent = (
+      <div>
+        <img
+          src="/static/assets/images/loading.gif"
+          className={'loading ' + (hideLoad ? 'hidden' : '')}
+          alt={hideLoad ? '' : 'loading'}
+        />
+        <div className={this.errored ? '' : 'hidden'}>
+          {this.state.errorMsg}
+        </div>
+        <div className={this.state.slicesLoaded ? '' : 'hidden'}>
+          <BootstrapTable
+            ref="table"
+            data={this.state.slices}
+            selectRow={this.selectRowProp}
+            options={this.options}
+            hover
+            search
+            pagination
+            condensed
+            height="auto"
+          >
+            <TableHeaderColumn
+              dataField="id"
+              isKey
+              dataSort
+              hidden
+            />
+            <TableHeaderColumn
+              dataField="sliceName"
+              dataSort
+            >
+              {t('Name')}
+            </TableHeaderColumn>
+            <TableHeaderColumn
+              dataField="vizType"
+              dataSort
+            >
+              {t('Viz')}
+            </TableHeaderColumn>
+            <TableHeaderColumn
+              dataField="datasourceLink"
+              dataSort
+              // Will cause react-bootstrap-table to interpret the HTML returned
+              dataFormat={datasourceLink => datasourceLink}
+            >
+              {t('Datasource')}
+            </TableHeaderColumn>
+            <TableHeaderColumn
+              dataField="modified"
+              dataSort
+              sortFunc={this.modifiedDateComparator}
+              // Will cause react-bootstrap-table to interpret the HTML returned
+              dataFormat={modified => modified}
+            >
+              {t('Modified')}
+            </TableHeaderColumn>
+          </BootstrapTable>
+          <button
+            type="button"
+            className="btn btn-default"
+            data-dismiss="modal"
+            onClick={this.addSlices}
+            disabled={!enableAddSlice}
+          >
+            {t('Add Charts')}
+          </button>
+        </div>
+      </div>
+    );
+
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        tooltip={t('Add a new chart to the dashboard')}
+        beforeOpen={this.onEnterModal.bind(this)}
+        isMenuItem
+        modalBody={modalContent}
+        bsSize="large"
+        setModalAsTriggerChildren
+        modalTitle={t('Add Charts to Dashboard')}
+      />
+    );
+  }
+}
+
+SliceAdder.propTypes = propTypes;
+
+export default SliceAdder;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx
new file mode 100644
index 0000000..a8c6aa7
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx
@@ -0,0 +1,194 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { connect } from 'react-redux';
+
+import { t } from '../../../../locales';
+import EditableTitle from '../../../../components/EditableTitle';
+import TooltipWrapper from '../../../../components/TooltipWrapper';
+
+const propTypes = {
+  slice: PropTypes.object.isRequired,
+  supersetCanExplore: PropTypes.bool,
+  sliceCanEdit: PropTypes.bool,
+  isExpanded: PropTypes.bool,
+  isCached: PropTypes.bool,
+  cachedDttm: PropTypes.string,
+  removeSlice: PropTypes.func,
+  updateSliceName: PropTypes.func,
+  toggleExpandSlice: PropTypes.func,
+  forceRefresh: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
+  editMode: PropTypes.bool,
+  annotationQuery: PropTypes.object,
+  annotationError: PropTypes.object,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  removeSlice: () => ({}),
+  updateSliceName: () => ({}),
+  toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+  editMode: false,
+};
+
+class SliceHeader extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.onSaveTitle = this.onSaveTitle.bind(this);
+    this.onToggleExpandSlice = this.onToggleExpandSlice.bind(this);
+    this.exportCSV = this.props.exportCSV.bind(this, this.props.slice);
+    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice);
+    this.forceRefresh = this.props.forceRefresh.bind(this, this.props.slice.slice_id);
+    this.removeSlice = this.props.removeSlice.bind(this, this.props.slice);
+  }
+
+  onSaveTitle(newTitle) {
+    if (this.props.updateSliceName) {
+      this.props.updateSliceName(this.props.slice.slice_id, newTitle);
+    }
+  }
+
+  onToggleExpandSlice() {
+    this.props.toggleExpandSlice(this.props.slice, !this.props.isExpanded);
+  }
+
+  render() {
+    const slice = this.props.slice;
+    const isCached = this.props.isCached;
+    const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
+    const refreshTooltip = isCached ?
+      t('Served from data cached %s . Click to force refresh.', cachedWhen) :
+      t('Force refresh data');
+    const annoationsLoading = t('Annotation layers are still loading.');
+    const annoationsError = t('One ore more annotation layers failed loading.');
+
+    return (
+      <div className="row chart-header">
+        <div className="col-md-12">
+          <div className="header">
+            <EditableTitle
+              title={slice.slice_name}
+              canEdit={!!this.props.updateSliceName && this.props.editMode}
+              onSaveTitle={this.onSaveTitle}
+              showTooltip={this.props.editMode}
+              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
+            />
+            {!!Object.values(this.props.annotationQuery || {}).length &&
+              <TooltipWrapper
+                label="annotations-loading"
+                placement="top"
+                tooltip={annoationsLoading}
+              >
+                <i className="fa fa-refresh warning" />
+              </TooltipWrapper>
+            }
+            {!!Object.values(this.props.annotationError || {}).length &&
+              <TooltipWrapper
+                label="annoation-errors"
+                placement="top"
+                tooltip={annoationsError}
+              >
+                <i className="fa fa-exclamation-circle danger" />
+              </TooltipWrapper>
+            }
+          </div>
+          <div className="chart-controls">
+            <div id={'controls_' + slice.slice_id} className="pull-right">
+              {this.props.editMode &&
+                <a>
+                  <TooltipWrapper
+                    placement="top"
+                    label="move"
+                    tooltip={t('Move chart')}
+                  >
+                    <i className="fa fa-arrows drag" />
+                  </TooltipWrapper>
+                </a>
+              }
+              <a className={`refresh ${isCached ? 'danger' : ''}`} onClick={this.forceRefresh}>
+                <TooltipWrapper
+                  placement="top"
+                  label="refresh"
+                  tooltip={refreshTooltip}
+                >
+                  <i className="fa fa-repeat" />
+                </TooltipWrapper>
+              </a>
+              {slice.description &&
+              <a onClick={this.onToggleExpandSlice}>
+                <TooltipWrapper
+                  placement="top"
+                  label="description"
+                  tooltip={t('Toggle chart description')}
+                >
+                  <i className="fa fa-info-circle slice_info" />
+                </TooltipWrapper>
+              </a>
+              }
+              {this.props.sliceCanEdit &&
+                <a href={slice.edit_url} target="_blank">
+                  <TooltipWrapper
+                    placement="top"
+                    label="edit"
+                    tooltip={t('Edit chart')}
+                  >
+                    <i className="fa fa-pencil" />
+                  </TooltipWrapper>
+                </a>
+              }
+              <a className="exportCSV" onClick={this.exportCSV}>
+                <TooltipWrapper
+                  placement="top"
+                  label="exportCSV"
+                  tooltip={t('Export CSV')}
+                >
+                  <i className="fa fa-table" />
+                </TooltipWrapper>
+              </a>
+              {this.props.supersetCanExplore &&
+                <a className="exploreChart" onClick={this.exploreChart}>
+                  <TooltipWrapper
+                    placement="top"
+                    label="exploreChart"
+                    tooltip={t('Explore chart')}
+                  >
+                    <i className="fa fa-share" />
+                  </TooltipWrapper>
+                </a>
+              }
+              {this.props.editMode &&
+                <a className="remove-chart" onClick={this.removeSlice}>
+                  <TooltipWrapper
+                    placement="top"
+                    label="close"
+                    tooltip={t('Remove chart from dashboard')}
+                  >
+                    <i className="fa fa-close" />
+                  </TooltipWrapper>
+                </a>
+              }
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+SliceHeader.propTypes = propTypes;
+SliceHeader.defaultProps = defaultProps;
+
+function mapStateToProps({ dashboard }) {
+  return {
+    supersetCanExplore: dashboard.dashboard.superset_can_explore,
+    sliceCanEdit: dashboard.dashboard.slice_can_edit,
+  };
+}
+
+export { SliceHeader };
+export default connect(mapStateToProps, () => ({}))(SliceHeader);
diff --git a/superset/assets/src/dashboard/deprecated/v1/index.jsx b/superset/assets/src/dashboard/deprecated/v1/index.jsx
new file mode 100644
index 0000000..d7e898e
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/index.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { createStore, applyMiddleware, compose } from 'redux';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+
+import { initEnhancer } from '../../../reduxUtils';
+import { appSetup } from '../../../common';
+import { initJQueryAjax } from '../../../modules/utils';
+import DashboardContainer from './components/DashboardContainer';
+import rootReducer, { getInitialState } from './reducers';
+
+appSetup();
+initJQueryAjax();
+
+const appContainer = document.getElementById('app');
+const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
+const initState = Object.assign({}, getInitialState(bootstrapData));
+
+const store = createStore(
+  rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
+
+ReactDOM.render(
+  <Provider store={store}>
+    <DashboardContainer />
+  </Provider>,
+  appContainer,
+);
diff --git a/superset/assets/src/dashboard/deprecated/v1/reducers.js b/superset/assets/src/dashboard/deprecated/v1/reducers.js
new file mode 100644
index 0000000..00bf2bf
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/reducers.js
@@ -0,0 +1,272 @@
+/* eslint-disable camelcase */
+import { combineReducers } from 'redux';
+import d3 from 'd3';
+import shortid from 'shortid';
+
+import charts, { chart } from '../chart/chartReducer';
+import * as actions from './actions';
+import { getParam } from '../../../modules/utils';
+import { alterInArr, removeFromArr } from '../../../reduxUtils';
+import { applyDefaultFormData } from '../../../explore/store';
+import { getColorFromScheme } from '../../../modules/colors';
+
+export function getInitialState(bootstrapData) {
+  const {
+    user_id,
+    datasources,
+    common,
+    editMode,
+    prompt_v2_conversion,
+    force_v2_edit,
+    v2_auto_convert_date,
+    v2_feedback_url,
+  } = bootstrapData;
+  delete common.locale;
+  delete common.language_pack;
+
+  const dashboard = {
+    ...bootstrapData.dashboard_data,
+    promptV2Conversion: prompt_v2_conversion,
+    forceV2Edit: force_v2_edit,
+    v2AutoConvertDate: v2_auto_convert_date,
+    v2FeedbackUrl: v2_feedback_url,
+  };
+  let filters = {};
+  try {
+    // allow request parameter overwrite dashboard metadata
+    filters = JSON.parse(
+      getParam('preselect_filters') || dashboard.metadata.default_filters,
+    );
+  } catch (e) {
+    //
+  }
+
+  // Priming the color palette with user's label-color mapping provided in
+  // the dashboard's JSON metadata
+  if (dashboard.metadata && dashboard.metadata.label_colors) {
+    const colorMap = dashboard.metadata.label_colors;
+    for (const label in colorMap) {
+      getColorFromScheme(label, null, colorMap[label]);
+    }
+  }
+
+  dashboard.posDict = {};
+  dashboard.layout = [];
+  if (Array.isArray(dashboard.position_json)) {
+    dashboard.position_json.forEach(position => {
+      dashboard.posDict[position.slice_id] = position;
+    });
+  } else {
+    dashboard.position_json = [];
+  }
+
+  const lastRowId = Math.max(
+    0,
+    Math.max.apply(
+      null,
+      dashboard.position_json.map(pos => pos.row + pos.size_y),
+    ),
+  );
+  let newSliceCounter = 0;
+  dashboard.slices.forEach(slice => {
+    const sliceId = slice.slice_id;
+    let pos = dashboard.posDict[sliceId];
+    if (!pos) {
+      // append new slices to dashboard bottom, 3 slices per row
+      pos = {
+        col: (newSliceCounter % 3) * 16 + 1,
+        row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
+        size_x: 16,
+        size_y: 16,
+      };
+      newSliceCounter++;
+    }
+
+    dashboard.layout.push({
+      i: String(sliceId),
+      x: pos.col - 1,
+      y: pos.row,
+      w: pos.size_x,
+      minW: 2,
+      h: pos.size_y,
+    });
+  });
+
+  // will use charts action/reducers to handle chart render
+  const initCharts = {};
+  dashboard.slices.forEach(slice => {
+    const chartKey = `slice_${slice.slice_id}`;
+    initCharts[chartKey] = {
+      ...chart,
+      chartKey,
+      slice_id: slice.slice_id,
+      form_data: slice.form_data,
+      formData: applyDefaultFormData(slice.form_data),
+    };
+  });
+
+  // also need to add formData for dashboard.slices
+  dashboard.slices = dashboard.slices.map(slice => ({
+    ...slice,
+    formData: applyDefaultFormData(slice.form_data),
+  }));
+
+  return {
+    charts: initCharts,
+    dashboard: {
+      filters,
+      dashboard,
+      userId: user_id,
+      datasources,
+      common,
+      editMode,
+    },
+  };
+}
+
+export const dashboard = function(state = {}, action) {
+  const actionHandlers = {
+    [actions.UPDATE_DASHBOARD_TITLE]() {
+      const newDashboard = {
+        ...state.dashboard,
+        dashboard_title: action.title,
+      };
+      return { ...state, dashboard: newDashboard };
+    },
+    [actions.UPDATE_DASHBOARD_LAYOUT]() {
+      const newDashboard = { ...state.dashboard, layout: action.layout };
+      return { ...state, dashboard: newDashboard };
+    },
+    [actions.REMOVE_SLICE]() {
+      const key = String(action.slice.slice_id);
+      const newLayout = state.dashboard.layout.filter(
+        reactPos => reactPos.i !== key,
+      );
+      const newDashboard = removeFromArr(
+        state.dashboard,
+        'slices',
+        action.slice,
+        'slice_id',
+      );
+      // if this slice is a filter
+      const newFilter = { ...state.filters };
+      let refresh = false;
+      if (state.filters[key]) {
+        delete newFilter[key];
+        refresh = true;
+      }
+      return {
+        ...state,
+        dashboard: { ...newDashboard, layout: newLayout },
+        filters: newFilter,
+        refresh,
+      };
+    },
+    [actions.TOGGLE_FAVE_STAR]() {
+      return { ...state, isStarred: action.isStarred };
+    },
+    [actions.SET_EDIT_MODE]() {
+      return { ...state, editMode: action.editMode };
+    },
+    [actions.TOGGLE_EXPAND_SLICE]() {
+      const updatedExpandedSlices = {
+        ...state.dashboard.metadata.expanded_slices,
+      };
+      const sliceId = action.slice.slice_id;
+      if (action.isExpanded) {
+        updatedExpandedSlices[sliceId] = true;
+      } else {
+        delete updatedExpandedSlices[sliceId];
+      }
+      const metadata = {
+        ...state.dashboard.metadata,
+        expanded_slices: updatedExpandedSlices,
+      };
+      const newDashboard = { ...state.dashboard, metadata };
+      return { ...state, dashboard: newDashboard };
+    },
+
+    // filters
+    [actions.ADD_FILTER]() {
+      const selectedSlice = state.dashboard.slices.find(
+        slice => slice.slice_id === action.sliceId,
+      );
+      if (!selectedSlice) {
+        return state;
+      }
+
+      let filters = state.filters;
+      const { sliceId, col, vals, merge, refresh } = action;
+      const filterKeys = [
+        '__from',
+        '__to',
+        '__time_col',
+        '__time_grain',
+        '__time_origin',
+        '__granularity',
+      ];
+      if (
+        filterKeys.indexOf(col) >= 0 ||
+        selectedSlice.formData.groupby.indexOf(col) !== -1
+      ) {
+        let newFilter = {};
+        if (!(sliceId in filters)) {
+          // Straight up set the filters if none existed for the slice
+          newFilter = { [col]: vals };
+        } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
+          newFilter = { ...filters[sliceId], [col]: vals };
+          // d3.merge pass in array of arrays while some value form filter components
+          // from and to filter box require string to be process and return
+        } else if (filters[sliceId][col] instanceof Array) {
+          newFilter[col] = d3.merge([filters[sliceId][col], vals]);
+        } else {
+          newFilter[col] = d3.merge([[filters[sliceId][col]], vals])[0] || '';
+        }
+        filters = { ...filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+    [actions.CLEAR_FILTER]() {
+      const newFilters = { ...state.filters };
+      delete newFilters[action.sliceId];
+      return { ...state, filter: newFilters, refresh: true };
+    },
+    [actions.REMOVE_FILTER]() {
+      const { sliceId, col, vals, refresh } = action;
+      const excluded = new Set(vals);
+      const valFilter = val => !excluded.has(val);
+
+      let filters = state.filters;
+      // Have to be careful not to modify the dashboard state so that
+      // the render actually triggers
+      if (sliceId in state.filters && col in state.filters[sliceId]) {
+        const newFilter = filters[sliceId][col].filter(valFilter);
+        filters = { ...filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+
+    // slice reducer
+    [actions.UPDATE_SLICE_NAME]() {
+      const newDashboard = alterInArr(
+        state.dashboard,
+        'slices',
+        action.slice,
+        { slice_name: action.sliceName },
+        'slice_id',
+      );
+      return { ...state, dashboard: newDashboard };
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return actionHandlers[action.type]();
+  }
+  return state;
+};
+
+export default combineReducers({
+  charts,
+  dashboard,
+  impressionId: () => shortid.generate(),
+});
diff --git a/superset/assets/src/dashboard/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index 4b3ee49..396a56c 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -1,21 +1,14 @@
 import {
   DASHBOARD_ROOT_ID,
   DASHBOARD_GRID_ID,
-  GRID_MIN_COLUMN_COUNT,
   NEW_COMPONENTS_SOURCE_ID,
 } from '../util/constants';
+import findParentId from '../util/findParentId';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
 import shouldWrapChildInRow from '../util/shouldWrapChildInRow';
-import {
-  CHART_TYPE,
-  COLUMN_TYPE,
-  MARKDOWN_TYPE,
-  ROW_TYPE,
-  TAB_TYPE,
-  TABS_TYPE,
-} from '../util/componentTypes';
+import { ROW_TYPE, TAB_TYPE, TABS_TYPE } from '../util/componentTypes';
 
 import {
   UPDATE_COMPONENTS,
@@ -46,7 +39,6 @@ const actionHandlers = {
 
     const nextComponents = { ...state };
 
-    // recursively find children to remove
     function recursivelyDeleteChildren(componentId, componentParentId) {
       // delete child and it's children
       const component = nextComponents[componentId];
@@ -73,6 +65,14 @@ const actionHandlers = {
     }
 
     recursivelyDeleteChildren(id, parentId);
+    const nextParent = nextComponents[parentId];
+    if (nextParent.type === ROW_TYPE && nextParent.children.length === 0) {
+      const grandparentId = findParentId({
+        childId: parentId,
+        layout: nextComponents,
+      });
+      recursivelyDeleteChildren(parentId, grandparentId);
+    }
 
     return nextComponents;
   },
@@ -81,28 +81,8 @@ const actionHandlers = {
     const {
       payload: { dropResult },
     } = action;
-    const { destination, dragging } = dropResult;
-    const newEntities = newEntitiesFromDrop({ dropResult, layout: state });
 
-    // if column is a parent, set any resizable children to have a minimum width so that
-    // the chances that they are validly movable to future containers is maximized
-    if (
-      destination.type === COLUMN_TYPE &&
-      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
-    ) {
-      const newEntitiesArray = Object.values(newEntities);
-      const component = newEntitiesArray.find(
-        entity => entity.type === dragging.type,
-      );
-
-      newEntities[component.id] = {
-        ...component,
-        meta: {
-          ...component.meta,
-          width: GRID_MIN_COLUMN_COUNT,
-        },
-      };
-    }
+    const newEntities = newEntitiesFromDrop({ dropResult, layout: state });
 
     return {
       ...state,
@@ -139,22 +119,6 @@ const actionHandlers = {
       nextEntities[newRow.id] = newRow;
     }
 
-    // if column is a parent, set any resizable children to have a minimum width so that
-    // the chances that they are validly movable to future containers is maximized
-    if (
-      destination.type === COLUMN_TYPE &&
-      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
-    ) {
-      const component = nextEntities[dragging.id];
-      nextEntities[dragging.id] = {
-        ...component,
-        meta: {
-          ...component.meta,
-          width: GRID_MIN_COLUMN_COUNT,
-        },
-      };
-    }
-
     return {
       ...state,
       ...nextEntities,
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 410ecc0..5312de2 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -82,6 +82,8 @@ export default function dashboardStateReducer(state = {}, action) {
         ...state,
         hasUnsavedChanges: false,
         maxUndoHistoryExceeded: false,
+        editMode: false,
+        isV2Preview: false, // @TODO remove upon v1 deprecation
       };
     },
 
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index d56e480..f4e091e 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -11,7 +11,15 @@ import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
 import { DASHBOARD_HEADER_TYPE, CHART_TYPE } from '../util/componentTypes';
 
 export default function(bootstrapData) {
-  const { user_id, datasources, common } = bootstrapData;
+  const {
+    user_id,
+    datasources,
+    common,
+    editMode,
+    force_v2_edit: forceV2Edit,
+    v2_auto_convert_date: v2AutoConvertDate,
+    v2_feedback_url: v2FeedbackUrl,
+  } = bootstrapData;
   delete common.locale;
   delete common.language_pack;
 
@@ -37,11 +45,10 @@ export default function(bootstrapData) {
 
   // dashboard layout
   const { position_json: positionJson } = dashboard;
+  const shouldConvertToV2 =
+    !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2';
 
-  const layout =
-    !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2'
-      ? layoutConverter(dashboard)
-      : positionJson;
+  const layout = shouldConvertToV2 ? layoutConverter(dashboard) : positionJson;
 
   // store the header as a layout component so we can undo/redo changes
   layout[DASHBOARD_HEADER_ID] = {
@@ -107,8 +114,8 @@ export default function(bootstrapData) {
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
     charts: chartQueries,
+    // read-only data
     dashboardInfo: {
-      // read-only data
       id: dashboard.id,
       slug: dashboard.slug,
       metadata: {
@@ -124,6 +131,9 @@ export default function(bootstrapData) {
       superset_can_explore: dashboard.superset_can_explore,
       slice_can_edit: dashboard.slice_can_edit,
       common,
+      v2AutoConvertDate,
+      v2FeedbackUrl,
+      forceV2Edit,
     },
     dashboardState: {
       sliceIds: Array.from(sliceIds),
@@ -131,10 +141,11 @@ export default function(bootstrapData) {
       filters,
       expandedSlices: dashboard.metadata.expanded_slices || {},
       css: dashboard.css || '',
-      editMode: false,
-      showBuilderPane: false,
+      editMode: dashboard.dash_edit_perm && editMode,
+      showBuilderPane: dashboard.dash_edit_perm && editMode,
       hasUnsavedChanges: false,
       maxUndoHistoryExceeded: false,
+      isV2Preview: shouldConvertToV2,
     },
     dashboardLayout,
     messageToasts: [],
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index 5f87d0c..bbcb7e1 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -73,23 +73,23 @@
   }
 
   .chart-card-container {
-    padding: 16px;
-
     .chart-card {
       border: 1px solid @gray-light;
+      font-weight: 200;
       height: 120px;
       padding: 16px;
+      margin: 16px;
+      position: relative;
       cursor: move;
-    }
 
-    .chart-card.is-selected {
-      opacity: 0.45;
-      cursor: not-allowed;
+      &:hover {
+        background: @gray-bg;
+      }
     }
 
     .card-title {
       margin-bottom: 8px;
-      font-weight: bold;
+      font-weight: 800;
     }
 
     .card-body {
@@ -98,12 +98,31 @@
 
       .item {
         height: 18px;
-      }
 
-      label {
-        margin-right: 5px;
+        span:first-child {
+          font-weight: 400;
+        }
       }
     }
+
+    .chart-card.is-selected {
+      cursor: not-allowed;
+      opacity: 0.4;
+    }
+
+    .is-added-label {
+      background: @almost-black;
+      color: white;
+      font-size: 12px;
+      line-height: 1em;
+      text-transform: uppercase;
+      position: absolute;
+      padding: 4px 8px;
+      position: absolute;
+      top: 32px;
+      right: 32px;
+      pointer-events: none;
+    }
   }
 
   .slice-adder-container {
@@ -114,7 +133,7 @@
       /* the input is wrapped in a div */
       .search-input {
         flex-grow: 1;
-        margin-left: 16px;
+        margin-right: 16px;
       }
 
       .dropdown.btn-group button,
diff --git a/superset/assets/src/dashboard/stylesheets/builder.less b/superset/assets/src/dashboard/stylesheets/builder.less
index ecf192e..e93c6db 100644
--- a/superset/assets/src/dashboard/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/stylesheets/builder.less
@@ -1,7 +1,6 @@
 .dashboard {
   position: relative;
   color: @almost-black;
-  margin-top: -20px;
 }
 
 .dashboard-header {
diff --git a/superset/assets/src/dashboard/stylesheets/components/markdown.less b/superset/assets/src/dashboard/stylesheets/components/markdown.less
index d377c68..2cfd929 100644
--- a/superset/assets/src/dashboard/stylesheets/components/markdown.less
+++ b/superset/assets/src/dashboard/stylesheets/components/markdown.less
@@ -1,6 +1,11 @@
 .dashboard-markdown {
   overflow: hidden;
 
+  .dashboard-component-chart-holder {
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
   .dashboard--editing & {
     cursor: move;
   }
@@ -8,4 +13,4 @@
   #brace-editor {
     border: none;
   }
-}
\ No newline at end of file
+}
diff --git a/superset/assets/src/dashboard/stylesheets/components/new-component.less b/superset/assets/src/dashboard/stylesheets/components/new-component.less
index decb1ad..b330f79 100644
--- a/superset/assets/src/dashboard/stylesheets/components/new-component.less
+++ b/superset/assets/src/dashboard/stylesheets/components/new-component.less
@@ -8,13 +8,17 @@
   cursor: move;
 }
 
+.new-component:not(.static):hover {
+  background: @gray-bg;
+}
+
 .new-component-placeholder {
   position: relative;
   background: @gray-bg;
   width: 40px;
   height: 40px;
   margin-right: 16px;
-  box-shadow: 0 0 1px white;
+  border: 1px solid white;
   display: flex;
   align-items: center;
   justify-content: center;
diff --git a/superset/assets/src/dashboard/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
index 02039b4..b1124da 100644
--- a/superset/assets/src/dashboard/stylesheets/components/tabs.less
+++ b/superset/assets/src/dashboard/stylesheets/components/tabs.less
@@ -8,6 +8,10 @@
   margin-top: 1px;
 }
 
+.dashboard-component-tabs-content .empty-tab-droptarget {
+  min-height: 24px;
+}
+
 .dashboard-component-tabs .nav-tabs {
   border-bottom: none;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 5756786..3db5cdc 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -1,3 +1,40 @@
+/* header has mysterious extra margin */
+header.top {
+  margin-bottom: -20px;
+}
+
+body {
+  h1 {
+    font-weight: 600;
+    line-height: normal;
+    font-size: 24px;
+    letter-spacing: -0.2px;
+    margin-top: 12px;
+    margin-bottom: 12px;
+  }
+  h2 {
+    font-weight: 600;
+    line-height: normal;
+    font-size: 20px;
+    margin-top: 12px;
+    margin-bottom: 8px;
+  }
+  h3,
+  h4,
+  h5,
+  h6 {
+    font-weight: 600;
+    line-height: normal;
+    font-size: 16px;
+    letter-spacing: 0.2px;
+    margin-top: 8px;
+    margin-bottom: 4px;
+  }
+  p {
+    margin: 0 0 8px 0;
+  }
+}
+
 .dashboard .chart-header {
   position: relative;
   font-size: 16px;
@@ -44,6 +81,7 @@
     margin-left: -8px;
     height: 30px;
     width: 30px;
+    z-index: 10;
 
     &.btn.btn-primary {
       border-left-color: white;
@@ -109,14 +147,13 @@
     display: block;
   }
 
-  a[role="menuitem"] & {
+  a[role='menuitem'] & {
     width: 8px;
     height: 8px;
     margin-right: 8px;
   }
 }
 
-
 .modal img.loading {
   width: 50px;
   margin: 0;
@@ -145,11 +182,34 @@
   margin: 0 20px;
 }
 
-.dashboard .title .favstar {
-  font-size: 20px;
-  line-height: 1em;
-  position: relative;
-  top: -5px;
+.dashboard-header .dashboard-component-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  .favstar {
+    font-size: 24px;
+    position: relative;
+    margin-left: 8px;
+  }
+
+  /* @TODO remove upon v1 deprecation */
+  .v2-preview-badge {
+    margin-left: 8px;
+    text-transform: uppercase;
+    font-size: 12px;
+    font-weight: 400;
+    background: linear-gradient(to bottom right, @pink, @purple);
+    color: white;
+    padding: 4px 8px;
+    line-height: 1em;
+    cursor: pointer;
+    opacity: 0.9;
+
+    &:hover {
+      opacity: 1;
+    }
+  }
 }
 
 .ace_gutter {
diff --git a/superset/assets/src/dashboard/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
index d69006c..0c70f58 100644
--- a/superset/assets/src/dashboard/stylesheets/popover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/popover-menu.less
@@ -48,6 +48,7 @@
 
 .dashboard-component-tabs li .popover-menu {
   top: -56px;
+  left: -7px;
 }
 
 .popover-menu .menu-item {
diff --git a/superset/assets/src/dashboard/util/getDropPosition.js b/superset/assets/src/dashboard/util/getDropPosition.js
index 2a02702..74dfcaa 100644
--- a/superset/assets/src/dashboard/util/getDropPosition.js
+++ b/superset/assets/src/dashboard/util/getDropPosition.js
@@ -9,6 +9,16 @@ export const DROP_LEFT = 'DROP_LEFT';
 // this defines how close the mouse must be to the edge of a component to display
 // a sibling type drop indicator
 const SIBLING_DROP_THRESHOLD = 20;
+const NON_SHALLOW_DROP_THRESHOLD = 20;
+
+// We cache the last recorded clientOffset per component in order to
+// have access to it beyond the handleHover phase and into the handleDrop phase
+// of drag-and-drop. we do not have access to it during drop because react-dnd's
+// monitor.getClientOffset() returns null at this point
+let CACHED_CLIENT_OFFSET = {};
+export function clearDropCache() {
+  CACHED_CLIENT_OFFSET = {};
+}
 
 export default function getDropPosition(monitor, Component) {
   const {
@@ -22,10 +32,15 @@ export default function getDropPosition(monitor, Component) {
   const draggingItem = monitor.getItem();
 
   // if dropped self on self, do nothing
+  if (!draggingItem || draggingItem.id === component.id) {
+    return null;
+  }
+
+  // TODO need a better solution to prevent nested tabs
   if (
-    !draggingItem ||
-    draggingItem.id === component.id ||
-    !isDraggingOverShallow
+    draggingItem.type === TABS_TYPE &&
+    component.type === TAB_TYPE &&
+    componentDepth === 2
   ) {
     return null;
   }
@@ -66,7 +81,37 @@ export default function getDropPosition(monitor, Component) {
   }
 
   const refBoundingRect = Component.ref.getBoundingClientRect();
-  const clientOffset = monitor.getClientOffset();
+  const clientOffset =
+    monitor.getClientOffset() || CACHED_CLIENT_OFFSET[component.id];
+
+  if (!clientOffset || !refBoundingRect) {
+    return null;
+  }
+
+  CACHED_CLIENT_OFFSET[component.id] = clientOffset;
+  const deltaTop = Math.abs(clientOffset.y - refBoundingRect.top);
+  const deltaBottom = Math.abs(clientOffset.y - refBoundingRect.bottom);
+  const deltaLeft = Math.abs(clientOffset.x - refBoundingRect.left);
+  const deltaRight = Math.abs(clientOffset.x - refBoundingRect.right);
+
+  // Most of the time we only want a drop indicator for shallow (top-level, non-nested) drop targets
+  // However there are some cases where considering only shallow targets would result in NO drop
+  // indicators which is a bad UX.
+  // e.g.,
+  //    when dragging row-a over a chart that's in another row-b, the chart is the shallow droptarget
+  //    but row-a is not a valid child or sibling. in this case we want to show a sibling drop
+  //    indicator for row-b, which is NOT a shallow drop target.
+  // BUT if we ALWAYS consider non-shallow drop targets we may get multiple indicators shown at the
+  // same time, which is also a bad UX. to prevent this we can enforce a threshold proximity of the
+  // mouse to the edge of a non-shallow target
+  if (
+    !isDraggingOverShallow &&
+    [deltaTop, deltaBottom, deltaLeft, deltaRight].every(
+      delta => delta > NON_SHALLOW_DROP_THRESHOLD,
+    )
+  ) {
+    return null;
+  }
 
   // Drop based on mouse position relative to component center
   if (validSibling && !validChild) {
@@ -83,11 +128,6 @@ export default function getDropPosition(monitor, Component) {
 
   // either is valid, so choose location based on boundary deltas
   if (validSibling && validChild) {
-    const deltaTop = Math.abs(clientOffset.y - refBoundingRect.top);
-    const deltaBottom = Math.abs(clientOffset.y - refBoundingRect.bottom);
-    const deltaLeft = Math.abs(clientOffset.x - refBoundingRect.left);
-    const deltaRight = Math.abs(clientOffset.x - refBoundingRect.right);
-
     // if near enough to a sibling boundary, drop there
     if (siblingDropOrientation === 'vertical') {
       if (deltaLeft < SIBLING_DROP_THRESHOLD) return DROP_LEFT;
diff --git a/superset/assets/src/dashboard/util/injectCustomCss.js b/superset/assets/src/dashboard/util/injectCustomCss.js
new file mode 100644
index 0000000..985d4aa
--- /dev/null
+++ b/superset/assets/src/dashboard/util/injectCustomCss.js
@@ -0,0 +1,17 @@
+export default function injectCustomCss(css) {
+  const className = 'CssEditor-css';
+  const head = document.head || document.getElementsByTagName('head')[0];
+  let style = document.querySelector(`.${className}`);
+
+  if (!style) {
+    style = document.createElement('style');
+    style.className = className;
+    style.type = 'text/css';
+    head.appendChild(style);
+  }
+  if (style.styleSheet) {
+    style.styleSheet.cssText = css;
+  } else {
+    style.innerHTML = css;
+  }
+}
diff --git a/superset/assets/src/dashboard/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
index 80bf69e..c975496 100644
--- a/superset/assets/src/dashboard/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -69,7 +69,7 @@ const parentMaxDepthLookup = {
     [DIVIDER_TYPE]: depthTwo,
     [HEADER_TYPE]: depthTwo,
     [ROW_TYPE]: depthTwo,
-    [TABS_TYPE]: rootDepth, // you cannot drop a Tabs within a Tab
+    [TABS_TYPE]: depthTwo,
   },
 
   [COLUMN_TYPE]: {
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index 1242d2b..3427520 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -35,6 +35,7 @@ export const toastShape = PropTypes.shape({
     DANGER_TOAST,
   ]).isRequired,
   text: PropTypes.string.isRequired,
+  duration: PropTypes.number,
 });
 
 export const chartPropShape = PropTypes.shape({
diff --git a/superset/assets/src/logger.js b/superset/assets/src/logger.js
index 06059b2..ea8e0fb 100644
--- a/superset/assets/src/logger.js
+++ b/superset/assets/src/logger.js
@@ -141,6 +141,13 @@ export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart';
 export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART = 'export_csv_dashboard_chart';
 export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter';
 
+// @TODO remove upon v1 deprecation
+export const LOG_ACTIONS_PREVIEW_V2 = 'preview_dashboard_v2';
+export const LOG_ACTIONS_FALLBACK_TO_V1 = 'fallback_to_dashboard_v1';
+export const LOG_ACTIONS_READ_ABOUT_V2_CHANGES = 'read_about_v2_changes';
+export const LOG_ACTIONS_DISMISS_V2_PROMPT = 'dismiss_v2_conversion_prompt';
+export const LOG_ACTIONS_SHOW_V2_INFO_PROMPT = 'show_v2_conversion_prompt';
+
 export const DASHBOARD_EVENT_NAMES = [
   LOG_ACTIONS_MOUNT_DASHBOARD,
   LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
@@ -152,6 +159,12 @@ export const DASHBOARD_EVENT_NAMES = [
   LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
   LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
   LOG_ACTIONS_REFRESH_DASHBOARD,
+
+  LOG_ACTIONS_PREVIEW_V2,
+  LOG_ACTIONS_FALLBACK_TO_V1,
+  LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
+  LOG_ACTIONS_DISMISS_V2_PROMPT,
+  LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
 ];
 
 export const EXPLORE_EVENT_NAMES = [
diff --git a/superset/assets/stylesheets/dashboard_deprecated.css b/superset/assets/stylesheets/dashboard_deprecated.css
new file mode 100644
index 0000000..57bc44c
--- /dev/null
+++ b/superset/assets/stylesheets/dashboard_deprecated.css
@@ -0,0 +1,181 @@
+/* header has mysterious extra margin */
+header.top {
+  margin-bottom: -20px;
+}
+h1.outer-container {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.v2-preview-badge {
+  margin-left: 8px;
+  text-transform: uppercase;
+  font-size: 12px;
+  font-weight: 400;
+  background: linear-gradient(to bottom right, #E32364, #2C2261);
+  color: white;
+  padding: 4px 8px;
+  line-height: 1em;
+  cursor: pointer;
+  opacity: 0.9;
+}
+
+.v2-preview-badge:hover {
+  opacity: 1;
+}
+
+.dashboard a i {
+  cursor: pointer;
+}
+.dashboard i.drag {
+  cursor: move !important;
+}
+.dashboard .slice-grid .preview-holder {
+  z-index: 1;
+  position: absolute;
+  background-color: #AAA;
+  border-color: #AAA;
+  opacity: 0.3;
+}
+div.widget .chart-controls {
+  background-clip: content-box;
+  position: absolute;
+  z-index: 100;
+  right: 0;
+  top: 5px;
+  padding: 5px 5px;
+  opacity: 0;
+  transition: opacity 0.5s ease-in-out;
+}
+div.widget:hover .chart-controls {
+  opacity: 0.75;
+  transition: opacity 0.5s ease-in-out;
+}
+.slice-grid div.widget {
+  border-radius: 0;
+  border: 0;
+  box-shadow: none;
+  background-color: #fff;
+  overflow: visible;
+}
+
+.slice-grid .slice_container {
+  background-color: #fff;
+}
+
+.dashboard .slice-grid .dragging,
+.dashboard .slice-grid .resizing {
+  opacity: 0.5;
+}
+.dashboard img.loading {
+  width: 20px;
+  margin: 5px;
+  position: absolute;
+}
+
+.dashboard .slice_title {
+  text-align: center;
+  font-weight: bold;
+  font-size: 14px;
+  padding: 5px;
+}
+.dashboard div.slice_content {
+  width: 100%;
+  height: 100%;
+}
+
+.modal img.loading {
+  width: 50px;
+  margin: 0;
+  position: relative;
+}
+
+.react-bs-container-body {
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.hidden, #pageDropDown {
+  display: none;
+}
+
+.slice-grid div.separator.widget {
+ border: 1px solid transparent;
+  box-shadow: none;
+  z-index: 1;
+}
+.slice-grid div.separator.widget:hover {
+  border: 1px solid #EEE;
+}
+.slice-grid div.separator.widget .chart-header {
+  background-color: transparent;
+  color: transparent;
+}
+.slice-grid div.separator.widget h1,h2,h3,h4 {
+  margin-top: 0px;
+}
+
+.slice-cell {
+  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
+  transition: box-shadow 1s ease-in;
+  height: 100%;
+}
+
+.slice-cell-highlight {
+  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0.2);
+  height: 100%;
+}
+
+.slice-cell .editable-title input[type="button"] {
+  font-weight: bold;
+}
+
+.dashboard .separator.widget .slice_container {
+  padding: 0;
+  overflow: visible;
+}
+.dashboard .separator.widget .slice_container hr {
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+.separator .chart-container {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+
+.dashboard .title {
+  margin: 0 20px;
+}
+
+.dashboard .title .favstar {
+  font-size: 20px;
+  position: relative;
+}
+
+.chart-header .header {
+  font-size: 16px;
+  margin: 0 -10px;
+}
+.ace_gutter {
+    z-index: 0;
+}
+.ace_content {
+    z-index: 0;
+}
+.ace_scrollbar {
+    z-index: 0;
+}
+.slice_container .alert {
+    margin: 10px;
+}
+
+i.danger {
+  color: red;
+}
+
+i.warning {
+  color: orange;
+}
diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js
index 27bafa8..96554d6 100644
--- a/superset/assets/webpack.config.js
+++ b/superset/assets/webpack.config.js
@@ -20,6 +20,7 @@ const config = {
     addSlice: ['babel-polyfill', APP_DIR + '/src/addSlice/index.jsx'],
     explore: ['babel-polyfill', APP_DIR + '/src/explore/index.jsx'],
     dashboard: ['babel-polyfill', APP_DIR + '/src/dashboard/index.jsx'],
+    dashboard_deprecated: ['babel-polyfill', APP_DIR + '/src/dashboard/deprecated/v1/index.jsx'],
     sqllab: ['babel-polyfill', APP_DIR + '/src/SqlLab/index.jsx'],
     welcome: ['babel-polyfill', APP_DIR + '/src/welcome/index.jsx'],
     profile: ['babel-polyfill', APP_DIR + '/src/profile/index.jsx'],
diff --git a/superset/config.py b/superset/config.py
index 530b126..d03520f 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -413,6 +413,14 @@ SQL_QUERY_MUTATOR = None
 # using flask-compress
 ENABLE_FLASK_COMPRESS = True
 
+# Dashboard v1 deprecation configuration
+DASH_V2_IS_DEFAULT_VIEW_FOR_EDITORS = True
+CAN_FALLBACK_TO_DASH_V1_EDIT_MODE = True
+
+# these are incorporated into messages displayed to users
+PLANNED_V2_AUTO_CONVERT_DATE = None  # e.g. '2018-06-16'
+V2_FEEDBACK_URL = None  # e.g., 'https://goo.gl/forms/...'
+
 try:
     if CONFIG_PATH_ENV_VAR in os.environ:
         # Explicitly import config module that is not in pythonpath; useful
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index e08053c..2cb0e95 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -306,8 +306,8 @@ class SqlaTable(Model, BaseDatasource):
     @property
     def link(self):
         name = escape(self.name)
-        return Markup(
-            '<a href="{self.explore_url}">{name}</a>'.format(**locals()))
+        anchor = '<a target="_blank" href="{self.explore_url}">{name}</a>'
+        return Markup(anchor.format(**locals()))
 
     @property
     def schema_perm(self):
diff --git a/superset/templates/superset/dashboard_v1_deprecated.html b/superset/templates/superset/dashboard_v1_deprecated.html
new file mode 100644
index 0000000..1a158d9
--- /dev/null
+++ b/superset/templates/superset/dashboard_v1_deprecated.html
@@ -0,0 +1,10 @@
+{% extends "superset/basic.html" %}
+
+{% block body %}
+<div
+  id="app"
+  class="dashboard container-fluid"
+  data-bootstrap="{{ bootstrap_data }}"
+>
+</div>
+{% endblock %}
diff --git a/superset/views/core.py b/superset/views/core.py
index e9c1e00..6bbd7ae 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1601,6 +1601,33 @@ class Superset(BaseSupersetView):
     @staticmethod
     def _set_dash_metadata(dashboard, data):
         positions = data['positions']
+        is_v2_dash = (
+            isinstance(positions, dict) and
+            positions.get('DASHBOARD_VERSION_KEY') == 'v2'
+        )
+
+        # @TODO remove upon v1 deprecation
+        if not is_v2_dash:
+            positions = data['positions']
+            slice_ids = [int(d['slice_id']) for d in positions]
+            dashboard.slices = [o for o in dashboard.slices if o.id in slice_ids]
+            positions = sorted(data['positions'], key=lambda x: int(x['slice_id']))
+            dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
+            md = dashboard.params_dict
+            dashboard.css = data['css']
+            dashboard.dashboard_title = data['dashboard_title']
+
+            if 'filter_immune_slices' not in md:
+                md['filter_immune_slices'] = []
+            if 'timed_refresh_immune_slices' not in md:
+                md['timed_refresh_immune_slices'] = []
+            if 'filter_immune_slice_fields' not in md:
+                md['filter_immune_slice_fields'] = {}
+            md['expanded_slices'] = data['expanded_slices']
+            md['default_filters'] = data.get('default_filters', '')
+            dashboard.json_metadata = json.dumps(md, indent=4)
+            return
+
         # find slices in the position data
         slice_ids = []
         slice_id_to_name = {}
@@ -2077,12 +2104,6 @@ class Superset(BaseSupersetView):
                         'superset/request_access/?'
                         'dashboard_id={dash.id}&'.format(**locals()))
 
-        # Hack to log the dashboard_id properly, even when getting a slug
-        @log_this
-        def dashboard(**kwargs):  # noqa
-            pass
-        dashboard(dashboard_id=dash.id)
-
         dash_edit_perm = check_ownership(dash, raise_if_false=False) and \
             security_manager.can_access('can_save_dash', 'Superset')
         dash_save_perm = security_manager.can_access('can_save_dash', 'Superset')
@@ -2090,6 +2111,58 @@ class Superset(BaseSupersetView):
         slice_can_edit = security_manager.can_access('can_edit', 'SliceModelView')
 
         standalone_mode = request.args.get('standalone') == 'true'
+        edit_mode = request.args.get('edit') == 'true'
+
+        # TODO remove switch upon v1 deprecation 🎉
+        # during v2 rollout, multiple factors determine whether we show v1 or v2
+        # if layout == v1
+        #   view = v1 for non-editors
+        #   view = v1 or v2 for editors depending on config + request (force)
+        #   edit = v1 or v2 for editors depending on config + request (force)
+        #
+        # if layout == v2 (not backwards compatible)
+        #   view = v2
+        #   edit = v2
+        dashboard_layout = dash.data.get('position_json', {})
+        is_v2_dash = (
+            isinstance(dashboard_layout, dict) and
+            dashboard_layout.get('DASHBOARD_VERSION_KEY') == 'v2'
+        )
+
+        force_v1 = request.args.get('version') == 'v1' and not is_v2_dash
+        force_v2 = request.args.get('version') == 'v2'
+        force_v2_edit = (
+            is_v2_dash or
+            not app.config.get('CAN_FALLBACK_TO_DASH_V1_EDIT_MODE')
+        )
+        v2_is_default_view = app.config.get('DASH_V2_IS_DEFAULT_VIEW_FOR_EDITORS')
+        prompt_v2_conversion = False
+        if is_v2_dash:
+            dashboard_view = 'v2'
+        elif not dash_edit_perm:
+            dashboard_view = 'v1'
+        else:
+            if force_v2 or (v2_is_default_view and not force_v1):
+                dashboard_view = 'v2'
+            else:
+                dashboard_view = 'v1'
+                prompt_v2_conversion = not force_v1
+
+        # Hack to log the dashboard_id properly, even when getting a slug
+        @log_this
+        def dashboard(**kwargs):  # noqa
+            pass
+
+        # TODO remove extra logging upon v1 deprecation 🎉
+        dashboard(
+            dashboard_id=dash.id,
+            dashboard_version='v2' if is_v2_dash else 'v1',
+            dashboard_view=dashboard_view,
+            dash_edit_perm=dash_edit_perm,
+            force_v1=force_v1,
+            force_v2=force_v2,
+            force_v2_edit=force_v2_edit,
+            edit_mode=edit_mode)
 
         dashboard_data = dash.data
         dashboard_data.update({
@@ -2105,15 +2178,27 @@ class Superset(BaseSupersetView):
             'dashboard_data': dashboard_data,
             'datasources': {ds.uid: ds.data for ds in datasources},
             'common': self.common_bootsrap_payload(),
-            'editMode': request.args.get('edit') == 'true',
+            'editMode': edit_mode,
+            # TODO remove the following upon v1 deprecation 🎉
+            'force_v2_edit': force_v2_edit,
+            'prompt_v2_conversion': prompt_v2_conversion,
+            'v2_auto_convert_date': app.config.get('PLANNED_V2_AUTO_CONVERT_DATE'),
+            'v2_feedback_url': app.config.get('V2_FEEDBACK_URL'),
         }
 
         if request.args.get('json') == 'true':
             return json_success(json.dumps(bootstrap_data))
 
+        if dashboard_view == 'v2':
+            entry = 'dashboard'
+            template = 'superset/dashboard.html'
+        else:
+            entry = 'dashboard_deprecated'
+            template = 'superset/dashboard_v1_deprecated.html'
+
         return self.render_template(
-            'superset/dashboard.html',
-            entry='dashboard',
+            template,
+            entry=entry,
             standalone_mode=standalone_mode,
             title=dash.dashboard_title,
             bootstrap_data=json.dumps(bootstrap_data),
diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py
index e9cd85b..f1b035a 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -61,7 +61,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -89,7 +91,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='world_health').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -134,7 +138,9 @@ class DashboardTests(SupersetTestCase):
             .first()
         )
         origin_title = dash.dashboard_title
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -170,7 +176,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -242,7 +250,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         origin_slices_length = len(dash.slices)
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)


[incubator-superset] 20/26: [dashboard v2] better grid drop ux, fix tab bugs 🐛 (#5151)

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

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

commit b1386abde8f1a99d2df40912f055324b84faec20
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Wed Jun 6 17:47:21 2018 -0700

    [dashboard v2]  better grid drop ux, fix tab bugs 🐛 (#5151)
    
    * [dashboard v2] add empty droptarget to dashboard grid for better ux and update test
    
    * [dashboard] reset tab index upon top-level tab deletion, fix findparentid bug
    
    * [dashboard] update v1<>v2 modal link for tracking
---
 .../dashboard/components/DashboardGrid_spec.jsx    |  9 ++---
 .../src/dashboard/components/DashboardBuilder.jsx  | 18 +++++-----
 .../src/dashboard/components/DashboardGrid.jsx     | 39 +++++++++++-----------
 .../deprecated/PromptV2ConversionModal.jsx         |  2 +-
 .../src/dashboard/deprecated/V2PreviewModal.jsx    |  2 +-
 superset/assets/src/dashboard/util/findParentId.js |  2 +-
 6 files changed, 34 insertions(+), 38 deletions(-)

diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
index 3121e7e..1160d65 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
@@ -42,12 +42,9 @@ describe('DashboardGrid', () => {
     expect(wrapper.find(DashboardComponent)).to.have.length(2);
   });
 
-  it('should render an empty DragDroppables target when the gridComponent has no children', () => {
-    const withChildren = setup({ editMode: true });
-    const withoutChildren = setup({
-      editMode: true,
-      gridComponent: { ...props.gridComponent, children: [] },
-    });
+  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);
   });
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 2156ed3..59a9152 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -57,6 +57,7 @@ class DashboardBuilder extends React.Component {
       tabIndex: 0, // top-level tabs
     };
     this.handleChangeTab = this.handleChangeTab.bind(this);
+    this.handleDeleteTopLevelTabs = this.handleDeleteTopLevelTabs.bind(this);
   }
 
   getChildContext() {
@@ -65,6 +66,11 @@ class DashboardBuilder extends React.Component {
     };
   }
 
+  handleDeleteTopLevelTabs() {
+    this.props.deleteTopLevelTabs();
+    this.setState({ tabIndex: 0 });
+  }
+
   handleChangeTab({ tabIndex }) {
     this.setState(() => ({ tabIndex }));
     setTimeout(() => {
@@ -77,13 +83,7 @@ class DashboardBuilder extends React.Component {
   }
 
   render() {
-    const {
-      handleComponentDrop,
-      dashboardLayout,
-      deleteTopLevelTabs,
-      editMode,
-    } = this.props;
-
+    const { handleComponentDrop, dashboardLayout, editMode } = this.props;
     const { tabIndex } = this.state;
     const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
@@ -124,7 +124,7 @@ class DashboardBuilder extends React.Component {
                   <IconButton
                     className="fa fa-level-down"
                     label="Collapse tab content"
-                    onClick={deleteTopLevelTabs}
+                    onClick={this.handleDeleteTopLevelTabs}
                   />,
                 ]}
                 editMode={editMode}
@@ -155,7 +155,7 @@ class DashboardBuilder extends React.Component {
                 */
                 <TabContainer
                   id={DASHBOARD_GRID_ID}
-                  activeKey={tabIndex}
+                  activeKey={Math.min(tabIndex, childIds.length - 1)}
                   onSelect={this.handleChangeTab}
                   animation
                   mountOnEnter
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index 4689051..f5ca6e5 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -108,26 +108,25 @@ class DashboardGrid extends React.PureComponent {
             />
           ))}
 
-          {/* make the grid droppable in the case that there are no children */}
-          {editMode &&
-            gridComponent.children.length === 0 && (
-              <DragDroppable
-                component={gridComponent}
-                depth={depth}
-                parentComponent={null}
-                index={gridComponent.children.length}
-                orientation="column"
-                onDrop={handleComponentDrop}
-                className="empty-grid-droptarget--bottom"
-                editMode
-              >
-                {({ dropIndicatorProps }) =>
-                  dropIndicatorProps && (
-                    <div className="drop-indicator drop-indicator--top" />
-                  )
-                }
-              </DragDroppable>
-            )}
+          {/* make the area below components droppable */}
+          {editMode && (
+            <DragDroppable
+              component={gridComponent}
+              depth={depth}
+              parentComponent={null}
+              index={gridComponent.children.length}
+              orientation="column"
+              onDrop={handleComponentDrop}
+              className="empty-grid-droptarget--bottom"
+              editMode
+            >
+              {({ dropIndicatorProps }) =>
+                dropIndicatorProps && (
+                  <div className="drop-indicator drop-indicator--top" />
+                )
+              }
+            </DragDroppable>
+          )}
 
           {isResizing &&
             Array(GRID_COLUMN_COUNT)
diff --git a/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
index 876fa78..a621635 100644
--- a/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
+++ b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
@@ -69,7 +69,7 @@ function PromptV2ConversionModal({
           <a
             target="_blank"
             rel="noopener noreferrer"
-            href="https://gist.github.com/williaster/bad4ac9c6a71b234cf9fc8ee629844e5#file-superset-dashboard-v2-md"
+            href="http://bit.ly/superset-dash-v2"
             onClick={logReadAboutV2Changes}
           >
             here
diff --git a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
index a0b7eed..828651f 100644
--- a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
+++ b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
@@ -81,7 +81,7 @@ class V2PreviewModal extends React.Component {
             <a
               target="_blank"
               rel="noopener noreferrer"
-              href="https://gist.github.com/williaster/bad4ac9c6a71b234cf9fc8ee629844e5#file-superset-dashboard-v2-md"
+              href="http://bit.ly/superset-dash-v2"
               onClick={V2PreviewModal.logReadAboutV2Changes}
             >
               here
diff --git a/superset/assets/src/dashboard/util/findParentId.js b/superset/assets/src/dashboard/util/findParentId.js
index c2e285d..9e47bf2 100644
--- a/superset/assets/src/dashboard/util/findParentId.js
+++ b/superset/assets/src/dashboard/util/findParentId.js
@@ -2,7 +2,7 @@ export default function findParentId({ childId, layout = {} }) {
   let parentId = null;
 
   const ids = Object.keys(layout);
-  for (let i = 0; i < ids.length - 1; i += 1) {
+  for (let i = 0; i <= ids.length - 1; i += 1) {
     const id = ids[i];
     const component = layout[id] || {};
     if (


[incubator-superset] 21/26: Fix: Should pass slice_can_edit flag down (#5159)

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

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

commit f9733979d02b11632e78495274b1a855572949de
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Thu Jun 7 11:03:11 2018 -0700

    Fix: Should pass slice_can_edit flag down (#5159)
---
 superset/assets/src/dashboard/components/SliceHeader.jsx | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 50a2a5d..d812181 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   annotationError: PropTypes.object,
   sliceName: PropTypes.string,
   supersetCanExplore: PropTypes.bool,
+  sliceCanEdit: PropTypes.bool,
 };
 
 const defaultProps = {
@@ -40,6 +41,7 @@ const defaultProps = {
   isExpanded: false,
   sliceName: '',
   supersetCanExplore: false,
+  sliceCanEdit: false,
 };
 
 class SliceHeader extends React.PureComponent {
@@ -56,6 +58,7 @@ class SliceHeader extends React.PureComponent {
       innerRef,
       sliceName,
       supersetCanExplore,
+      sliceCanEdit,
     } = this.props;
 
     const annoationsLoading = t('Annotation layers are still loading.');
@@ -104,6 +107,7 @@ class SliceHeader extends React.PureComponent {
               exploreChart={exploreChart}
               exportCSV={exportCSV}
               supersetCanExplore={supersetCanExplore}
+              sliceCanEdit={sliceCanEdit}
             />
           )}
         </div>


[incubator-superset] 12/26: [dashboard v2] check for default_filters before json_loads-ing them (#5064)

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

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

commit a02d8584df96454ea7dff5879120e4a2cab8b73b
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Wed May 23 15:54:12 2018 -0700

    [dashboard v2] check for default_filters before json_loads-ing them (#5064)
    
    [dashboard v2] check for default_filters before json-loads-ing them
---
 superset/models/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/superset/models/core.py b/superset/models/core.py
index df0aafc..0ef3331 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -348,7 +348,7 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
             json_metadata = json.loads(self.json_metadata)
             default_filters = json_metadata.get('default_filters')
             # make sure default_filters is not empty
-            if json.loads(default_filters):
+            if default_filters and json.loads(default_filters):
                 filters = parse.quote(default_filters.encode('utf8'))
                 return '/superset/dashboard/{}/?preselect_filters={}'.format(
                     self.slug or self.id, filters)


[incubator-superset] 19/26: Fix dashboard position row data (#5131)

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

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

commit 017e797e352d9d00b035f5b330fcac8fe84187e5
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Wed Jun 6 17:06:12 2018 -0700

    Fix dashboard position row data (#5131)
    
    * add slice_name to markdown
    
    (cherry picked from commit 14b01f1)
    
    * set min grid width be 1 column
    
    * remove empty column
    
    * check total columns count <= 12
    
    * scan position data and fix rows
    
    * fix dashboard url with default_filters
---
 superset/assets/src/dashboard/util/constants.js    |   2 +-
 .../src/dashboard/util/dashboardLayoutConverter.js | 214 +++++++++++++++++++--
 superset/models/core.py                            |  14 +-
 3 files changed, 208 insertions(+), 22 deletions(-)

diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index ef2c8bb..bfe24cc 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -19,7 +19,7 @@ export const DASHBOARD_ROOT_DEPTH = 0;
 export const GRID_BASE_UNIT = 8;
 export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
 export const GRID_COLUMN_COUNT = 12;
-export const GRID_MIN_COLUMN_COUNT = 2;
+export const GRID_MIN_COLUMN_COUNT = 1;
 export const GRID_MIN_ROW_UNITS = 5;
 export const GRID_MAX_ROW_UNITS = 100;
 export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index cf7a493..c6c124b 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -13,7 +13,12 @@ import {
   DASHBOARD_GRID_TYPE,
 } from './componentTypes';
 
-import { DASHBOARD_GRID_ID } from './constants';
+import {
+  DASHBOARD_GRID_ID,
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+  GRID_COLUMN_COUNT,
+} from './constants';
 
 const MAX_RECURSIVE_LEVEL = 6;
 const GRID_RATIO = 4;
@@ -72,19 +77,32 @@ function getColContainer() {
 }
 
 function getChartHolder(item) {
-  const { size_x, size_y, slice_id, code } = item;
+  const { size_x, size_y, slice_id, code, slice_name } = item;
 
-  const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
-  const height = Math.max(1, Math.round(size_y / GRID_RATIO));
+  const width = Math.max(
+    GRID_MIN_COLUMN_COUNT,
+    Math.round(size_x / GRID_RATIO),
+  );
+  const height = Math.max(
+    GRID_MIN_ROW_UNITS,
+    Math.round(size_y / GRID_RATIO * 100 / ROW_HEIGHT),
+  );
   if (code !== undefined) {
+    let markdownContent = '';
+    if (slice_name) {
+      markdownContent = `##### **${slice_name}**\n`;
+    }
+    if (code) {
+      markdownContent += code;
+    }
     return {
       type: MARKDOWN_TYPE,
       id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
       children: [],
       meta: {
         width,
-        height: Math.round(height * 100 / ROW_HEIGHT),
-        code,
+        height,
+        code: markdownContent,
       },
     };
   }
@@ -94,7 +112,7 @@ function getChartHolder(item) {
     children: [],
     meta: {
       width,
-      height: Math.round(height * 100 / ROW_HEIGHT),
+      height,
       chartId: parseInt(slice_id, 10),
     },
   };
@@ -135,6 +153,50 @@ function hasOverlap(positions, xAxis = true) {
     });
 }
 
+function isWideLeafComponent(component) {
+  return (
+    [CHART_TYPE, MARKDOWN_TYPE].indexOf(component.type) > -1 &&
+    component.meta.width > GRID_MIN_COLUMN_COUNT
+  );
+}
+
+function canReduceColumnWidth(columnComponent, root) {
+  return (
+    columnComponent.type === COLUMN_TYPE &&
+    columnComponent.meta.width > GRID_MIN_COLUMN_COUNT &&
+    columnComponent.children.every(
+      childId =>
+        isWideLeafComponent(root[childId]) ||
+        (root[childId].type === ROW_TYPE &&
+          root[childId].children.every(id => isWideLeafComponent(root[id]))),
+    )
+  );
+}
+
+function reduceRowWidth(rowComponent, root) {
+  // find widest free chart and reduce width
+  const widestChartId = rowComponent.children
+    .filter(childId => isWideLeafComponent(root[childId]))
+    .reduce((prev, current) => {
+      if (root[prev].meta.width >= root[current].meta.width) {
+        return prev;
+      }
+      return current;
+    });
+
+  if (widestChartId) {
+    root[widestChartId].meta.width -= 1;
+  }
+  return getChildrenSum(rowComponent.children, 'width', root);
+}
+
+function reduceComponentWidth(component) {
+  if (isWideLeafComponent(component)) {
+    component.meta.width -= 1;
+  }
+  return component.meta.width;
+}
+
 function doConvert(positions, level, parent, root) {
   if (positions.length === 0) {
     return;
@@ -235,7 +297,6 @@ function doConvert(positions, level, parent, root) {
             // create a new column
             const colContainer = getColContainer();
             root[colContainer.id] = colContainer;
-            rowContainer.children.push(colContainer.id);
 
             if (!hasOverlap(lower, false)) {
               lower.sort(sortByRowId).forEach(item => {
@@ -248,11 +309,15 @@ function doConvert(positions, level, parent, root) {
             }
 
             // add col meta
-            colContainer.meta.width = getChildrenMax(
-              colContainer.children,
-              'width',
-              root,
-            );
+            if (colContainer.children.length) {
+              rowContainer.children.push(colContainer.id);
+              // add col meta
+              colContainer.meta.width = getChildrenMax(
+                colContainer.children,
+                'width',
+                root,
+              );
+            }
           }
 
           currentItems = upper.slice();
@@ -278,6 +343,50 @@ export function convertToLayout(positions) {
   Object.values(root).forEach(item => {
     if (ROW_TYPE === item.type) {
       const meta = item.meta;
+      if (meta.width > GRID_COLUMN_COUNT) {
+        let currentWidth = meta.width;
+        while (
+          currentWidth > GRID_COLUMN_COUNT &&
+          item.children.filter(id => isWideLeafComponent(root[id])).length
+        ) {
+          currentWidth = reduceRowWidth(item, root);
+        }
+
+        // reduce column width
+        if (currentWidth > GRID_COLUMN_COUNT) {
+          // find column that: width > 2 and each row has at least 1 chart can reduce
+          // 2 loops: same column may reduce multiple times
+          let colIds;
+          do {
+            colIds = item.children.filter(colId =>
+              canReduceColumnWidth(root[colId], root),
+            );
+            let idx = 0;
+            while (idx < colIds.length && currentWidth > GRID_COLUMN_COUNT) {
+              const currentColumn = colIds[idx];
+              root[currentColumn].children.forEach(childId => {
+                if (root[childId].type === ROW_TYPE) {
+                  root[childId].meta.width = reduceRowWidth(
+                    root[childId],
+                    root,
+                  );
+                } else {
+                  root[childId].meta.width = reduceComponentWidth(
+                    root[childId],
+                  );
+                }
+              });
+              root[currentColumn].meta.width = getChildrenMax(
+                root[currentColumn].children,
+                'width',
+                root,
+              );
+              currentWidth = getChildrenSum(item.children, 'width', root);
+              idx += 1;
+            }
+          } while (colIds.length && currentWidth > GRID_COLUMN_COUNT);
+        }
+      }
       delete meta.width;
     }
   });
@@ -286,13 +395,84 @@ export function convertToLayout(positions) {
   return root;
 }
 
+function mergePosition(position, bottomLine, maxColumn) {
+  const { col, size_x, size_y } = position;
+  const endColumn = col + size_x > maxColumn ? bottomLine.length : col + size_x;
+  const nextSectionStart =
+    bottomLine.slice(col).findIndex(value => value > bottomLine[col]) + 1;
+
+  const currentBottom =
+    nextSectionStart > 0 && nextSectionStart < size_x
+      ? Math.max.apply(null, bottomLine.slice(col, col + size_x + 1))
+      : bottomLine[col];
+  bottomLine.fill(currentBottom + size_y, col, endColumn);
+}
+
+// In original position data, a lot of position's row attribute are not correct, and same positions are
+// assigned to more than 1 chart. The convert function depends on row id, col id to split
+// the whole dashboard into nested rows and columns. Bad row id will lead to many empty spaces, or a few
+// charts are overlapped in the same row.
+// This function read positions by row first. Then based on previous col id, width and height attribute,
+// re-calculate next position's row id.
+function scanDashboardPositionsData(positions) {
+  const bottomLine = new Array(49).fill(0);
+  bottomLine[0] = Number.MAX_VALUE;
+  const maxColumn = Math.max.apply(
+    null,
+    positions.slice().map(position => position.col),
+  );
+
+  const positionsByRowId = {};
+  positions
+    .slice()
+    .sort(sortByRowId)
+    .forEach(position => {
+      const { row } = position;
+      if (positionsByRowId[row] === undefined) {
+        positionsByRowId[row] = [];
+      }
+      positionsByRowId[row].push(position);
+    });
+  const rawPositions = Object.values(positionsByRowId);
+  const updatedPositions = [];
+
+  while (rawPositions.length) {
+    const nextRow = rawPositions.shift();
+    let nextCol = 1;
+    while (nextRow.length) {
+      // special treatment for duplicated positions: display wider one first
+      const availableIndexByColumn = nextRow
+        .filter(position => position.col === nextCol)
+        .map((position, index) => index);
+      if (availableIndexByColumn.length) {
+        const idx =
+          availableIndexByColumn.length > 1
+            ? availableIndexByColumn.sort(
+                (idx1, idx2) => nextRow[idx2].size_x - nextRow[idx1].size_x,
+              )[0]
+            : availableIndexByColumn[0];
+
+        const nextPosition = nextRow.splice(idx, 1)[0];
+        mergePosition(nextPosition, bottomLine, maxColumn + 1);
+        nextPosition.row = bottomLine[nextPosition.col] - nextPosition.size_y;
+        updatedPositions.push(nextPosition);
+        nextCol += nextPosition.size_x;
+      } else {
+        nextCol = nextRow[0].col;
+      }
+    }
+  }
+
+  return updatedPositions;
+}
+
 export default function(dashboard) {
   const positions = [];
-
-  // position data clean up. some dashboard didn't have position_json
   let { position_json } = dashboard;
   const positionDict = {};
   if (Array.isArray(position_json)) {
+    // scan and fix positions data: extra spaces, dup rows, .etc
+    position_json = scanDashboardPositionsData(position_json);
     position_json.forEach(position => {
       positionDict[position.slice_id] = position;
     });
@@ -300,12 +480,13 @@ export default function(dashboard) {
     position_json = [];
   }
 
+  // position data clean up. some dashboard didn't have position_json
   const lastRowId = Math.max(
     0,
     Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
   );
   let newSliceCounter = 0;
-  dashboard.slices.forEach(({ slice_id, form_data }) => {
+  dashboard.slices.forEach(({ slice_id, form_data, slice_name }) => {
     let position = positionDict[slice_id];
     if (!position) {
       // append new slices to dashboard bottom, 3 slices per row
@@ -322,6 +503,7 @@ export default function(dashboard) {
       position = {
         ...position,
         code: form_data.code,
+        slice_name,
       };
     }
     positions.push(position);
diff --git a/superset/models/core.py b/superset/models/core.py
index 3b663af..faf0a6f 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -347,11 +347,15 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
             # add default_filters to the preselect_filters of dashboard
             json_metadata = json.loads(self.json_metadata)
             default_filters = json_metadata.get('default_filters')
-            # make sure default_filters is not empty
-            if default_filters and json.loads(default_filters):
-                filters = parse.quote(default_filters.encode('utf8'))
-                return '/superset/dashboard/{}/?preselect_filters={}'.format(
-                    self.slug or self.id, filters)
+            # make sure default_filters is not empty and is valid
+            if default_filters and default_filters != '{}':
+                try:
+                    if json.loads(default_filters):
+                        filters = parse.quote(default_filters.encode('utf8'))
+                        return '/superset/dashboard/{}/?preselect_filters={}'.format(
+                            self.slug or self.id, filters)
+                except Exception:
+                    pass
         return '/superset/dashboard/{}/'.format(self.slug or self.id)
 
     @property


[incubator-superset] 09/26: Markdown for dashboard (#4962)

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

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

commit d6dcec21d6c676282a3231df20a35347c9c68251
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Sat May 12 01:34:13 2018 -0700

    Markdown for dashboard (#4962)
---
 superset/assets/package.json                       |   3 +-
 .../assets/src/dashboard/actions/sliceEntities.js  |  46 +++--
 .../dashboard/components/BuilderComponentPane.jsx  |  18 +-
 .../src/dashboard/components/DashboardBuilder.jsx  |   2 +
 .../components/gridComponents/Markdown.jsx         | 225 +++++++++++++++++++++
 .../dashboard/components/gridComponents/index.js   |   4 +
 .../components/gridComponents/new/NewMarkdown.jsx  |  16 ++
 .../components/menu/MarkdownModeDropdown.jsx       |  39 ++++
 .../src/dashboard/containers/DashboardBuilder.jsx  |   2 +
 .../src/dashboard/reducers/getInitialState.js      |  38 ++--
 .../dashboard/stylesheets/builder-sidepane.less    |  12 +-
 .../dashboard/stylesheets/components/index.less    |   1 +
 .../dashboard/stylesheets/components/markdown.less |  11 +
 .../src/dashboard/util/dashboardLayoutConverter.js |  84 +++++---
 superset/assets/src/modules/utils.js               |   4 +
 superset/views/core.py                             |   2 +-
 16 files changed, 430 insertions(+), 77 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 6f3b20a..0ddceb2 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -85,7 +85,7 @@
     "prop-types": "^15.6.0",
     "re-resizable": "^4.3.1",
     "react": "^15.6.2",
-    "react-ace": "^5.0.1",
+    "react-ace": "^5.10.0",
     "react-addons-css-transition-group": "^15.6.0",
     "react-addons-shallow-compare": "^15.4.2",
     "react-alert": "^2.3.0",
@@ -100,6 +100,7 @@
     "react-gravatar": "^2.6.1",
     "react-grid-layout": "0.16.6",
     "react-map-gl": "^3.0.4",
+    "react-markdown": "^3.3.0",
     "react-redux": "^5.0.2",
     "react-resizable": "^1.3.3",
     "react-search-input": "^0.11.3",
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index 37781f9..b635ea0 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -1,6 +1,8 @@
 /* eslint camelcase: 0 */
 import $ from 'jquery';
 
+import { getDatasourceParameter } from '../../modules/utils';
+
 export const SET_ALL_SLICES = 'SET_ALL_SLICES';
 export function setAllSlices(slices) {
   return { type: SET_ALL_SLICES, slices };
@@ -29,22 +31,34 @@ export function fetchAllSlices(userId) {
         success: response => {
           const slices = {};
           response.result.forEach(slice => {
-            const form_data = JSON.parse(slice.params);
-            slices[slice.id] = {
-              slice_id: slice.id,
-              slice_url: slice.slice_url,
-              slice_name: slice.slice_name,
-              edit_url: slice.edit_url,
-              form_data,
-              datasource: form_data.datasource,
-              datasource_name: slice.datasource_name_text,
-              datasource_link: slice.datasource_link,
-              changed_on: new Date(slice.changed_on).getTime(),
-              description: slice.description,
-              description_markdown: slice.description_markeddown,
-              viz_type: slice.viz_type,
-              modified: slice.modified,
-            };
+            let form_data = JSON.parse(slice.params);
+            let datasource = form_data.datasource;
+            if (!datasource) {
+              datasource = getDatasourceParameter(
+                slice.datasource_id,
+                slice.datasource_type,
+              );
+              form_data = {
+                ...form_data,
+                datasource,
+              };
+            }
+            if (['markup', 'separator'].indexOf(slice.viz_type) === -1) {
+              slices[slice.id] = {
+                slice_id: slice.id,
+                slice_url: slice.slice_url,
+                slice_name: slice.slice_name,
+                edit_url: slice.edit_url,
+                form_data,
+                datasource_name: slice.datasource_name_text,
+                datasource_link: slice.datasource_link,
+                changed_on: new Date(slice.changed_on).getTime(),
+                description: slice.description,
+                description_markdown: slice.description_markeddown,
+                viz_type: slice.viz_type,
+                modified: slice.modified,
+              };
+            }
           });
           return dispatch(setAllSlices(slices));
         },
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index b42650e..c35a637 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -9,11 +9,13 @@ import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
 import NewTabs from './gridComponents/new/NewTabs';
+import NewMarkdown from './gridComponents/new/NewMarkdown';
 import SliceAdder from '../containers/SliceAdder';
 import { t } from '../../locales';
 
 const propTypes = {
   topOffset: PropTypes.number,
+  toggleBuilderPane: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -52,7 +54,12 @@ class BuilderComponentPane extends React.PureComponent {
               >
                 <div className="component-layer slide-content">
                   <div className="dashboard-builder-sidepane-header">
-                    {t('Saved components')}
+                    <span>{t('Insert')}</span>
+                    <i
+                      className="fa fa-times trigger"
+                      onClick={this.props.toggleBuilderPane}
+                      role="none"
+                    />
                   </div>
                   <div
                     className="new-component static"
@@ -67,17 +74,12 @@ class BuilderComponentPane extends React.PureComponent {
                     <i className="fa fa-arrow-right trigger" />
                   </div>
 
-                  <div className="dashboard-builder-sidepane-header">
-                    {t('Containers')}
-                  </div>
                   <NewTabs />
                   <NewRow />
                   <NewColumn />
 
-                  <div className="dashboard-builder-sidepane-header">
-                    {t('More components')}
-                  </div>
                   <NewHeader />
+                  <NewMarkdown />
                   <NewDivider />
                 </div>
                 <div className="slices-layer slide-content">
@@ -87,7 +89,7 @@ class BuilderComponentPane extends React.PureComponent {
                     role="none"
                   >
                     <i className="fa fa-arrow-left trigger" />
-                    {t('All components')}
+                    <span>{t('All components')}</span>
                   </div>
                   <SliceAdder height={calculatedHeight} />
                 </div>
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 7f92948..0951ebf 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -34,6 +34,7 @@ const propTypes = {
   editMode: PropTypes.bool.isRequired,
   showBuilderPane: PropTypes.bool,
   handleComponentDrop: PropTypes.func.isRequired,
+  toggleBuilderPane: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -184,6 +185,7 @@ class DashboardBuilder extends React.Component {
             this.props.showBuilderPane && (
               <BuilderComponentPane
                 topOffset={topLevelTabs ? TABS_HEIGHT : 0}
+                toggleBuilderPane={this.props.toggleBuilderPane}
               />
             )}
         </div>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
new file mode 100644
index 0000000..459f89a
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
@@ -0,0 +1,225 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactMarkdown from 'react-markdown';
+import AceEditor from 'react-ace';
+import 'brace/mode/markdown';
+import 'brace/theme/textmate';
+
+import DeleteComponentButton from '../DeleteComponentButton';
+import DragDroppable from '../dnd/DragDroppable';
+import ResizableContainer from '../resizable/ResizableContainer';
+import MarkdownModeDropdown from '../menu/MarkdownModeDropdown';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+import { componentShape } from '../../util/propShapes';
+import { ROW_TYPE, COLUMN_TYPE } from '../../util/componentTypes';
+import {
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+  GRID_BASE_UNIT,
+} from '../../util/constants';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  parentId: PropTypes.string.isRequired,
+  component: componentShape.isRequired,
+  parentComponent: componentShape.isRequired,
+  index: PropTypes.number.isRequired,
+  depth: PropTypes.number.isRequired,
+  editMode: PropTypes.bool.isRequired,
+
+  // grid related
+  availableColumnCount: PropTypes.number.isRequired,
+  columnWidth: PropTypes.number.isRequired,
+  onResizeStart: PropTypes.func.isRequired,
+  onResize: PropTypes.func.isRequired,
+  onResizeStop: PropTypes.func.isRequired,
+
+  // dnd
+  deleteComponent: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {};
+const markdownPlaceHolder = `### New Markdown
+Insert *bold* or _italic_ text, and (urls)[www.url.com] here.`;
+
+class Markdown extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+      markdownSource: props.component.meta.code,
+      editor: null,
+      editorMode: props.component.meta.code ? 'preview' : 'edit', // show edit mode when code is empty
+    };
+
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
+    this.handleChangeEditorMode = this.handleChangeEditorMode.bind(this);
+    this.handleMarkdownChange = this.handleMarkdownChange.bind(this);
+    this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.setEditor = this.setEditor.bind(this);
+  }
+
+  componentDidUpdate(prevProps) {
+    if (
+      this.state.editor &&
+      (prevProps.component.meta.width !== this.props.component.meta.width ||
+        prevProps.columnWidth !== this.props.columnWidth)
+    ) {
+      this.state.editor.resize(true);
+    }
+  }
+
+  setEditor(editor) {
+    editor.getSession().setUseWrapMode(true);
+    this.setState({
+      editor,
+    });
+  }
+
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+  }
+
+  handleChangeEditorMode(mode) {
+    if (this.state.editorMode === 'edit') {
+      const { updateComponents, component } = this.props;
+      if (component.meta.code !== this.state.markdownSource) {
+        updateComponents({
+          [component.id]: {
+            ...component,
+            meta: {
+              ...component.meta,
+              code: this.state.markdownSource,
+            },
+          },
+        });
+      }
+    }
+
+    this.setState(() => ({
+      editorMode: mode,
+    }));
+  }
+
+  handleMarkdownChange(nextValue) {
+    this.setState({
+      markdownSource: nextValue,
+    });
+  }
+
+  handleDeleteComponent() {
+    const { deleteComponent, id, parentId } = this.props;
+    deleteComponent(id, parentId);
+  }
+
+  renderEditMode() {
+    return (
+      <AceEditor
+        mode="markdown"
+        theme="textmate"
+        onChange={this.handleMarkdownChange}
+        width={'100%'}
+        height={'100%'}
+        editorProps={{ $blockScrolling: true }}
+        value={this.state.markdownSource || markdownPlaceHolder}
+        readOnly={false}
+        onLoad={this.setEditor}
+      />
+    );
+  }
+
+  renderPreviewMode() {
+    return (
+      <ReactMarkdown source={this.state.markdownSource} escapeHtml={false} />
+    );
+  }
+
+  render() {
+    const { isFocused } = this.state;
+
+    const {
+      component,
+      parentComponent,
+      index,
+      depth,
+      availableColumnCount,
+      columnWidth,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      handleComponentDrop,
+      editMode,
+    } = this.props;
+
+    // inherit the size of parent columns
+    const widthMultiple =
+      parentComponent.type === COLUMN_TYPE
+        ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
+        : component.meta.width || GRID_MIN_COLUMN_COUNT;
+
+    return (
+      <DragDroppable
+        component={component}
+        parentComponent={parentComponent}
+        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        index={index}
+        depth={depth}
+        onDrop={handleComponentDrop}
+        disableDragDrop={isFocused}
+        editMode={editMode}
+      >
+        {({ dropIndicatorProps, dragSourceRef }) => (
+          <WithPopoverMenu
+            onChangeFocus={this.handleChangeFocus}
+            menuItems={[
+              <MarkdownModeDropdown
+                id={`${component.id}-mode`}
+                value={this.state.editorMode}
+                onChange={this.handleChangeEditorMode}
+              />,
+              <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
+            ]}
+            editMode={editMode}
+          >
+            <div className="dashboard-markdown">
+              <ResizableContainer
+                id={component.id}
+                adjustableWidth={parentComponent.type === ROW_TYPE}
+                adjustableHeight
+                widthStep={columnWidth}
+                widthMultiple={widthMultiple}
+                heightStep={GRID_BASE_UNIT}
+                heightMultiple={component.meta.height}
+                minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+                minHeightMultiple={GRID_MIN_ROW_UNITS}
+                maxWidthMultiple={availableColumnCount + widthMultiple}
+                onResizeStart={onResizeStart}
+                onResize={onResize}
+                onResizeStop={onResizeStop}
+                editMode={editMode}
+              >
+                <div
+                  ref={dragSourceRef}
+                  className="dashboard-component dashboard-component-chart-holder"
+                >
+                  {editMode && this.state.editorMode === 'edit'
+                    ? this.renderEditMode()
+                    : this.renderPreviewMode()}
+                </div>
+
+                {dropIndicatorProps && <div {...dropIndicatorProps} />}
+              </ResizableContainer>
+            </div>
+          </WithPopoverMenu>
+        )}
+      </DragDroppable>
+    );
+  }
+}
+
+Markdown.propTypes = propTypes;
+Markdown.defaultProps = defaultProps;
+
+export default Markdown;
diff --git a/superset/assets/src/dashboard/components/gridComponents/index.js b/superset/assets/src/dashboard/components/gridComponents/index.js
index 016ab03..c56bed0 100644
--- a/superset/assets/src/dashboard/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/components/gridComponents/index.js
@@ -1,5 +1,6 @@
 import {
   CHART_TYPE,
+  MARKDOWN_TYPE,
   COLUMN_TYPE,
   DIVIDER_TYPE,
   HEADER_TYPE,
@@ -9,6 +10,7 @@ import {
 } from '../../util/componentTypes';
 
 import ChartHolder from './ChartHolder';
+import Markdown from './Markdown';
 import Column from './Column';
 import Divider from './Divider';
 import Header from './Header';
@@ -17,6 +19,7 @@ import Tab from './Tab';
 import Tabs from './Tabs';
 
 export { default as ChartHolder } from './ChartHolder';
+export { default as Markdown } from './Markdown';
 export { default as Column } from './Column';
 export { default as Divider } from './Divider';
 export { default as Header } from './Header';
@@ -26,6 +29,7 @@ export { default as Tabs } from './Tabs';
 
 export default {
   [CHART_TYPE]: ChartHolder,
+  [MARKDOWN_TYPE]: Markdown,
   [COLUMN_TYPE]: Column,
   [DIVIDER_TYPE]: Divider,
   [HEADER_TYPE]: Header,
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewMarkdown.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewMarkdown.jsx
new file mode 100644
index 0000000..e4c8892
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewMarkdown.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { MARKDOWN_TYPE } from '../../../util/componentTypes';
+import { NEW_MARKDOWN_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewDivider() {
+  return (
+    <DraggableNewComponent
+      id={NEW_MARKDOWN_ID}
+      type={MARKDOWN_TYPE}
+      label="Markdown"
+      className="fa fa-code"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/menu/MarkdownModeDropdown.jsx b/superset/assets/src/dashboard/components/menu/MarkdownModeDropdown.jsx
new file mode 100644
index 0000000..10aa932
--- /dev/null
+++ b/superset/assets/src/dashboard/components/menu/MarkdownModeDropdown.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { t } from '../../../locales';
+
+import PopoverDropdown from './PopoverDropdown';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  value: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+const dropdownOptions = [
+  {
+    value: 'edit',
+    label: t('Edit'),
+  },
+  {
+    value: 'preview',
+    label: t('Preview'),
+  },
+];
+
+export default class MarkdownModeDropdown extends React.PureComponent {
+  render() {
+    const { id, value, onChange } = this.props;
+
+    return (
+      <PopoverDropdown
+        id={id}
+        options={dropdownOptions}
+        value={value}
+        onChange={onChange}
+      />
+    );
+  }
+}
+
+MarkdownModeDropdown.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
index 6bece3d..fde1e76 100644
--- a/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
@@ -2,6 +2,7 @@ import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 import DashboardBuilder from '../components/DashboardBuilder';
 
+import { toggleBuilderPane } from '../actions/dashboardState';
 import {
   deleteTopLevelTabs,
   handleComponentDrop,
@@ -20,6 +21,7 @@ function mapDispatchToProps(dispatch) {
     {
       deleteTopLevelTabs,
       handleComponentDrop,
+      toggleBuilderPane,
     },
     dispatch,
   );
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index ba24b36..b209043 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -74,26 +74,28 @@ export default function(bootstrapData) {
   const sliceIds = new Set();
   dashboard.slices.forEach(slice => {
     const key = slice.slice_id;
-    chartQueries[key] = {
-      ...chart,
-      id: key,
-      form_data: slice.form_data,
-      formData: applyDefaultFormData(slice.form_data),
-    };
+    if (['separator', 'markup'].indexOf(slice.form_data.viz_type) === -1) {
+      chartQueries[key] = {
+        ...chart,
+        id: key,
+        form_data: slice.form_data,
+        formData: applyDefaultFormData(slice.form_data),
+      };
 
-    slices[key] = {
-      slice_id: key,
-      slice_url: slice.slice_url,
-      slice_name: slice.slice_name,
-      form_data: slice.form_data,
-      edit_url: slice.edit_url,
-      viz_type: slice.form_data.viz_type,
-      datasource: slice.form_data.datasource,
-      description: slice.description,
-      description_markeddown: slice.description_markeddown,
-    };
+      slices[key] = {
+        slice_id: key,
+        slice_url: slice.slice_url,
+        slice_name: slice.slice_name,
+        form_data: slice.form_data,
+        edit_url: slice.edit_url,
+        viz_type: slice.form_data.viz_type,
+        datasource: slice.form_data.datasource,
+        description: slice.description,
+        description_markeddown: slice.description_markeddown,
+      };
 
-    sliceIds.add(key);
+      sliceIds.add(key);
+    }
 
     // sync layout names with current slice names in case a slice was edited
     // in explore since the layout was updated. name updates go through layout for undo/redo
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index d45da4f..5f87d0c 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -10,13 +10,21 @@
     border-top: 1px solid @gray-light;
     border-bottom: 1px solid @gray-light;
     padding: 16px;
+    display: flex;
+    align-items: center;
   }
 
   .trigger {
-    height: 18px;
-    width: 25px;
+    font-size: 16px;
     color: @almost-black;
     opacity: 1;
+    margin-left: auto;
+    cursor: pointer;
+  }
+
+  .slices-layer .trigger {
+    margin-left: 0;
+    margin-right: 20px;
   }
 
   .viewport {
diff --git a/superset/assets/src/dashboard/stylesheets/components/index.less b/superset/assets/src/dashboard/stylesheets/components/index.less
index 5a1803e..5f8d610 100644
--- a/superset/assets/src/dashboard/stylesheets/components/index.less
+++ b/superset/assets/src/dashboard/stylesheets/components/index.less
@@ -5,3 +5,4 @@
 @import './new-component.less';
 @import './row.less';
 @import './tabs.less';
+@import './markdown.less';
\ No newline at end of file
diff --git a/superset/assets/src/dashboard/stylesheets/components/markdown.less b/superset/assets/src/dashboard/stylesheets/components/markdown.less
new file mode 100644
index 0000000..d377c68
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/components/markdown.less
@@ -0,0 +1,11 @@
+.dashboard-markdown {
+  overflow: hidden;
+
+  .dashboard--editing & {
+    cursor: move;
+  }
+
+  #brace-editor {
+    border: none;
+  }
+}
\ No newline at end of file
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index f3f6061..e28e3be 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -5,6 +5,7 @@ import {
   ROW_TYPE,
   COLUMN_TYPE,
   CHART_TYPE,
+  MARKDOWN_TYPE,
   DASHBOARD_ROOT_TYPE,
   DASHBOARD_GRID_TYPE,
 } from './componentTypes';
@@ -76,11 +77,22 @@ function getColContainer() {
 }
 
 function getChartHolder(item) {
-  const { size_x, size_y, slice_id } = item;
+  const { size_x, size_y, slice_id, code } = item;
 
   const width = Math.max(1, Math.floor(size_x / GRID_RATIO));
   const height = Math.max(1, Math.round(size_y / GRID_RATIO));
-
+  if (code !== undefined) {
+    return {
+      type: MARKDOWN_TYPE,
+      id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
+      children: [],
+      meta: {
+        width,
+        height: Math.round(height * 100 / ROW_HEIGHT),
+        code,
+      },
+    };
+  }
   return {
     type: CHART_TYPE,
     id: `DASHBOARD_CHART_TYPE-${generateId()}`,
@@ -135,7 +147,7 @@ function doConvert(positions, level, parent, root) {
 
   if (positions.length === 1 || level >= MAX_RECURSIVE_LEVEL) {
     // special treatment for single chart dash, always wrap chart inside a row
-    if (parent.type === 'DASHBOARD_GRID_TYPE') {
+    if (parent.type === DASHBOARD_GRID_TYPE) {
       const rowContainer = getRowContainer();
       root[rowContainer.id] = rowContainer;
       parent.children.push(rowContainer.id);
@@ -181,7 +193,7 @@ function doConvert(positions, level, parent, root) {
       return;
     }
 
-    if (layer.length === 1) {
+    if (layer.length === 1 && parent.type === COLUMN_TYPE) {
       const chartHolder = getChartHolder(layer[0]);
       root[chartHolder.id] = chartHolder;
       parent.children.push(chartHolder.id);
@@ -262,6 +274,35 @@ function doConvert(positions, level, parent, root) {
   });
 }
 
+export function convertToLayout(positions) {
+  const root = {
+    [DASHBOARD_VERSION_KEY]: 'v2',
+    [DASHBOARD_ROOT_ID]: {
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: [DASHBOARD_GRID_ID],
+    },
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: [],
+    },
+  };
+
+  doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
+
+  // remove row's width/height and col's height
+  Object.values(root).forEach(item => {
+    if (ROW_TYPE === item.type) {
+      const meta = item.meta;
+      delete meta.width;
+    }
+  });
+
+  // console.log(JSON.stringify(root));
+  return root;
+}
+
 export default function(dashboard) {
   const positions = [];
 
@@ -281,7 +322,7 @@ export default function(dashboard) {
     Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
   );
   let newSliceCounter = 0;
-  dashboard.slices.forEach(({ slice_id }) => {
+  dashboard.slices.forEach(({ slice_id, form_data }) => {
     let position = positionDict[slice_id];
     if (!position) {
       // append new slices to dashboard bottom, 3 slices per row
@@ -294,33 +335,14 @@ export default function(dashboard) {
       };
       newSliceCounter += 1;
     }
-
-    positions.push(position);
-  });
-
-  const root = {
-    [DASHBOARD_VERSION_KEY]: 'v2',
-    [DASHBOARD_ROOT_ID]: {
-      type: DASHBOARD_ROOT_TYPE,
-      id: DASHBOARD_ROOT_ID,
-      children: [DASHBOARD_GRID_ID],
-    },
-    [DASHBOARD_GRID_ID]: {
-      type: DASHBOARD_GRID_TYPE,
-      id: DASHBOARD_GRID_ID,
-      children: [],
-    },
-  };
-
-  doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
-
-  // remove row's width/height and col's height
-  Object.values(root).forEach(item => {
-    if (ROW_TYPE === item.type) {
-      const meta = item.meta;
-      delete meta.width;
+    if (form_data && ['markup', 'separator'].indexOf(form_data.viz_type) > -1) {
+      position = {
+        ...position,
+        code: form_data.code,
+      };
     }
+    positions.push(position);
   });
 
-  return root;
+  return convertToLayout(positions);
 }
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index c4ea9ce..eb937bb 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -202,6 +202,10 @@ export function getAjaxErrorMsg(error) {
           error.responseText;
 }
 
+export function getDatasourceParameter(datasourceId, datasourceType) {
+  return `${datasourceId}__${datasourceType}`;
+}
+
 export function customizeToolTip(chart, xAxisFormatter, yAxisFormatters) {
   chart.useInteractiveGuideline(true);
   chart.interactiveLayer.tooltip.contentGenerator(function (d) {
diff --git a/superset/views/core.py b/superset/views/core.py
index 26356c5..1e0d78f 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -519,7 +519,7 @@ appbuilder.add_view_no_menu(SliceAsync)
 class SliceAddView(SliceModelView):  # noqa
     list_columns = [
         'id', 'slice_name', 'slice_url', 'edit_url', 'viz_type', 'params',
-        'description', 'description_markeddown',
+        'description', 'description_markeddown', 'datasource_id', 'datasource_type',
         'datasource_name_text', 'datasource_link',
         'owners', 'modified', 'changed_on']
 


[incubator-superset] 02/26: fix rebase error, clean up css organization and use @less vars

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

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

commit c62af8c902694248d9789b7dd7e2c3fd748e0f42
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Mon Mar 5 17:40:45 2018 -0800

    fix rebase error, clean up css organization and use @less vars
---
 superset/assets/images/favicon.png                 | Bin 18166 -> 6927 bytes
 superset/assets/images/superset-logo@2x.png        | Bin 0 -> 4132 bytes
 .../dashboard/v2/components/Dashboard.jsx          |   1 +
 .../dashboard/v2/components/DashboardBuilder.jsx   |   2 -
 .../dashboard/v2/components/DashboardGrid.jsx      |   2 -
 .../dashboard/v2/components/gridComponents/Row.jsx |   1 -
 .../v2/components/gridComponents/Spacer.jsx        |   6 -
 .../v2/components/gridComponents/components.css    | 455 ---------------------
 .../v2/components/gridComponents/index.js          |   2 -
 .../v2/components/resizable/ResizableContainer.jsx |   2 -
 .../dashboard/v2/stylesheets/buttons.less          |  17 +
 .../dashboard/v2/stylesheets/components/chart.less |  21 +
 .../v2/stylesheets/components/column.less          |  21 +
 .../v2/stylesheets/components/divider.less         |  24 ++
 .../v2/stylesheets/components/header.less          |  26 ++
 .../dashboard/v2/stylesheets/components/index.less |   8 +
 .../v2/stylesheets/components/new-component.less   |  38 ++
 .../dashboard/v2/stylesheets/components/row.less   |  39 ++
 .../v2/stylesheets/components/spacer.less          |  13 +
 .../dashboard/v2/stylesheets/components/tabs.less  |  65 +++
 .../dnd/dnd.css => stylesheets/dnd.less}           |   0
 .../grid.css => stylesheets/grid.less}             |  10 +-
 .../dashboard/v2/stylesheets/hover-menu.less       |  44 ++
 .../dashboard/v2/stylesheets/index.less            |   9 +
 .../dashboard/v2/stylesheets/popover-menu.less     | 116 ++++++
 .../resizable.css => stylesheets/resizable.less}   |  16 +-
 .../dashboard/v2/stylesheets/variables.less        |   7 +
 .../dashboard/components/DashboardContainer.jsx    |  24 +-
 superset/assets/stylesheets/superset.less          |  11 -
 superset/config.py                                 |   2 +-
 superset/templates/appbuilder/navbar.html          |   3 +-
 31 files changed, 480 insertions(+), 505 deletions(-)

diff --git a/superset/assets/images/favicon.png b/superset/assets/images/favicon.png
index f03cd5c..55316fa 100644
Binary files a/superset/assets/images/favicon.png and b/superset/assets/images/favicon.png differ
diff --git a/superset/assets/images/superset-logo@2x.png b/superset/assets/images/superset-logo@2x.png
new file mode 100644
index 0000000..839f617
Binary files /dev/null and b/superset/assets/images/superset-logo@2x.png differ
diff --git a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
index 5936006..a2ed1a0 100644
--- a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
@@ -6,6 +6,7 @@ import StaticDashboard from './StaticDashboard';
 import DashboardHeader from './DashboardHeader';
 
 import '../../../../stylesheets/dashboard-v2.css';
+import '../stylesheets/index.less';
 
 const propTypes = {
   actions: PropTypes.shape({
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index 94069b7..1878db6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -7,8 +7,6 @@ import cx from 'classnames';
 import BuilderComponentPane from './BuilderComponentPane';
 import DashboardGrid from '../containers/DashboardGrid';
 
-import './dnd/dnd.css';
-
 const propTypes = {
   editMode: PropTypes.bool,
 };
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
index 6cbcee5..c92161a 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -12,8 +12,6 @@ import {
   GRID_COLUMN_COUNT,
 } from '../util/constants';
 
-import './gridComponents/grid.css';
-
 const propTypes = {
   dashboard: PropTypes.object.isRequired,
   updateComponents: PropTypes.func.isRequired,
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
index 632a3f3..3386f8c 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -97,7 +97,6 @@ class Row extends React.PureComponent {
     } = this.props;
 
     const rowItems = [];
-    console.log('render row', rowComponent);
 
     // this adds a gutter between each child in the row.
     (rowComponent.children || []).forEach((id, childIndex) => {
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
index 4b54edd..faac589 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
@@ -3,16 +3,10 @@ import PropTypes from 'prop-types';
 
 import DeleteComponentButton from '../DeleteComponentButton';
 import DragDroppable from '../dnd/DragDroppable';
-// import DragHandle from '../dnd/DragHandle';
 import HoverMenu from '../menu/HoverMenu';
 import ResizableContainer from '../resizable/ResizableContainer';
 import { componentShape } from '../../util/propShapes';
 
-import {
-//   GRID_MIN_COLUMN_COUNT,
-  // GRID_MIN_ROW_UNITS,
-} from '../../util/constants';
-
 const propTypes = {
   id: PropTypes.string.isRequired,
   parentId: PropTypes.string.isRequired,
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css b/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css
deleted file mode 100644
index a88ea09..0000000
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/components.css
+++ /dev/null
@@ -1,455 +0,0 @@
-/* Header */
-.dashboard-component-header {
-  width: 100%;
-  line-height: 1em;
-  font-weight: 700;
-  background-color: inherit;
-  padding: 16px 0;
-  color: #263238;
-}
-
-.header-small {
-  font-size: 16px;
-}
-
-.header-medium {
-  font-size: 22px;
-}
-
-.header-large {
-  font-size: 32px;
-}
-
-  .dragdroppable-row .dragdroppable-row .dashboard-component-header,
-  .dragdroppable-row .dragdroppable-row .dashboard-component-divider {
-    padding-left: 16px;
-    padding-right: 16px;
-  }
-
-/* Chart */
-.dashboard-component-chart {
-  width: 100%;
-  height: 100%;
-  color: #879399;
-  background-color: #fff;
-  padding: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.dashboard-component-chart .fa {
-  font-size: 100px;
-  opacity: 0.3;
-}
-
-.grid-container--resizing .dashboard-component-chart,
-.dashboard-builder--dragging .dashboard-component-chart,
-.dashboard-component-chart:hover {
-  box-shadow: inset 0 0 0 1px #CFD8DC;
-}
-
-/* Divider */
-.dashboard-component-divider {
-  width: 100%;
-  padding: 24px 0; /* this is padding not margin to enable a larger mouse target */
-  background-color: transparent;
-}
-
-.dashboard-component-divider:after {
-  content: "";
-  height: 1px;
-  width: 100%;
-  background-color: #CFD8DC;
-  display: block;
-}
-
-.new-component-placeholder.divider-placeholder:after {
-  content: "";
-  height: 2px;
-  width: 100%;
-  background-color: #CFD8DC;
-}
-
-.dragdroppable .dashboard-component-divider {
-  cursor: move;
-}
-
-/* Tabs -- this overwrites Superset bootstrap theme tab styling */
-.dashboard-component-tabs {
-  width: 100%;
-  background-color: white;
-}
-.dashboard-component-tabs .dashboard-component-tabs-content {
-  min-height: 48px;
-  margin-top: 1px;
-}
-
-.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 {
-  padding: 0 16px;
-}
-
-.dashboard-component-tabs .nav-tabs > li > a {
-  color: #263238;
-  border: none;
-  padding: 12px 0 14px 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: #000000;
-}
-
-
-.dashboard-component-tabs .nav-tabs > li > a:focus {
-  outline: none;
-  background: #fff;
-}
-
-.dashboard-component-tabs .nav-tabs > li .dragdroppable-tab {
-  cursor: move;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator {
-  height: 40px !important;
-  top: -10px !important;
-  opacity: 0.5;
-}
-
-.dashboard-component-tabs .fa-plus-square {
-  background: linear-gradient(135deg, #E32464, #2C2261);
-  -webkit-background-clip: text;
-  -webkit-text-fill-color: transparent;
-  display: initial;
-  font-size: 16px;
-}
-
-/* New components */
-.new-component {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: nowrap;
-  align-items: center;
-  padding: 16px;
-  background: white;
-}
-
-.new-component-placeholder {
-  position: relative;
-  background: #f5f5f5;
-  width: 40px;
-  height: 40px;
-  margin-right: 16px;
-  box-shadow: 0 0 1px #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #aaa;
-  font-size: 1.5em;
-}
-
-/* Spacer */
-.grid-container {
-   flex-grow: 1;
-   min-width: 66%;
-   margin: 24px 32px;
-   height: 100%;
-   position: relative;
-}
-
-.new-component-placeholder.spacer-placeholder {
-  font-size: 1em;
-}
-
-.new-component-placeholder.fa-window-restore {
-  font-size: 1em;
-}
-
-.new-component-placeholder.spacer-placeholder:after {
-  content: "";
-  position: absolute;
-  height: 60%;
-  width: 60%;
-  border: 1px dashed #aaa;
-}
-
-/* columns and rows */
-.grid-column {
-  width: 100%;
-  min-height: 56px;
-}
-
-.grid-column > .hover-menu--top {
-  top: -20px;
-}
-
-.grid-row {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  align-items: flex-start;
-  width: 100%;
-  height: fit-content;
-  background-color: transparent;
-}
-
-.grid-row--transparent {
-  background-color: transparent;
-}
-
-.grid-row--white {
-  background-color: #fff;
-}
-
-.dashboard-component-header.grid-row--white {
-  padding-left: 16px;
-}
-
-.grid-row.grid-row--empty {
-  align-items: center; /* this centers the empty note content */
-  height: 80px;
-}
-
-.grid-row--empty:after {
-  position: absolute;
-  top: 0;
-  left: 0;
-  content: "Empty row";
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-  color: #aaa;
-}
-
-.grid-column--empty:after {
-  content: "Empty column";
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #CFD8DC;
-}
-
-/* spacer */
-.grid-spacer {
-  width: 100%;
-  height: 100%;
-  background-color: transparent;
-}
-
-.dragdroppable .grid-spacer {
-  cursor: move;
-}
-
-.dragdroppable:hover .grid-spacer {
-  box-shadow: inset 0 0 0 1px #CFD8DC;
-}
-
-/* popover menu */
-.with-popover-menu {
-  position: relative;
-  outline: none;
-}
-
-.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */
-  width: 100%;
-  height: 100%;
-}
-
-.with-popover-menu--focused:after {
-  content: "";
-  position: absolute;
-  top: 1;
-  left: -1;
-  width: 100%;
-  height: 100%;
-  box-shadow: inset 0 0 0 2px #44C0FF;
-  pointer-events: none;
-  z-index: 9;
-}
-
-.popover-menu {
-  position: absolute;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  flex-wrap: nowrap;
-  left: 1px;
-  top: -42px;
-  height: 40px;
-  padding: 0 16px;
-  background: #fff;
-  box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2);
-  font-size: 14px;
-  cursor: default;
-  z-index: 10;
-}
-
-.popover-menu .menu-item {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-/* vertical spacer after each menu item */
-.popover-menu .menu-item:not(:only-child):not(:last-child):after {
-  content: "";
-  width: 1;
-  height: 100%;
-  background: #CFD8DC;
-  margin: 0 16px;
-}
-
-.popover-menu .popover-dropdown.btn {
-  border: none;
-  padding: 0;
-  font-size: inherit;
-  color: #000;
-}
-
-.popover-menu .popover-dropdown.btn:hover,
-.popover-menu .popover-dropdown.btn:active,
-.popover-menu .popover-dropdown.btn:focus,
-.hover-dropdown .btn:hover,
-.hover-dropdown .btn:active,
-.hover-dropdown .btn:focus {
-  background: initial;
-  box-shadow: none;
-}
-
-.hover-dropdown li.dropdown-item:hover a,
-.popover-menu li.dropdown-item:hover a {
-  background: #CFD8DC;
-}
-
-.popover-dropdown .caret { /* without this the caret doesn't take up full width / is clipped */
-  width: auto;
-  border-top-color: transparent;
-}
-
-
-.hover-dropdown li.dropdown-item.active a,
-.popover-menu li.dropdown-item.active a {
-  background: #fff;
-  font-weight: bold;
-  color: #000;
-}
-
-/* row style menu */
-.row-style-option {
-  display: inline-block;
-}
-
-.row-style-option:before {
-  content: "";
-  width: 1em;
-  height: 1em;
-  margin-right: 8px;
-  display: inline-block;
-  vertical-align: middle;
-}
-
-.row-style-option.grid-row--white {
-  padding-left: 0;
-  background: transparent;
-}
-
-.row-style-option.grid-row--white:before {
-  background: #fff;
-  border: 1px solid #CFD8DC;
-}
-
-.row-style-option.grid-row--transparent:before {
-  background: #CFD8DC;
-}
-
-/* hover menu */
-.hover-menu {
-  opacity: 0;
-  position: absolute;
-  z-index: 2;
-}
-
-.hover-menu--left {
-  width: 20px;
-  height: 100%;
-  top: 0;
-  left: -20px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-}
-
-.hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) {
-  margin-bottom: 8px;
-}
-
-.dragdroppable-row .dragdroppable-row .hover-menu--left {
-  left: 1px;
-}
-
-.hover-menu--top {
-  width: 100%;
-  height: 20px;
-  top: 0;
-  left: 0;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-}
-
-.hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
-  margin-right: 8px;
-}
-
-.dragdroppable:hover .hover-menu,
-.dragdroppable .hover-menu:hover {
-  opacity: 1;
-}
-
-
-/* Menu fa buttons */
-.icon-button {
-  color: #879399;
-  font-size: 1em;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  outline: none;
-}
-
-.icon-button:hover,
-.icon-button:active,
-.icon-button:focus {
-  color: #484848;
-  outline: none;
-  text-decoration: none;
-}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
index c84864e..3a3fad5 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
@@ -1,5 +1,3 @@
-import './components.css';
-
 import {
   CHART_TYPE,
   COLUMN_TYPE,
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
index bd590ae..5e43678 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -11,8 +11,6 @@ import {
   GRID_GUTTER_SIZE,
 } from '../../util/constants';
 
-import './resizable.css';
-
 const propTypes = {
   id: PropTypes.string.isRequired,
   children: PropTypes.node,
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less b/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
new file mode 100644
index 0000000..a8dd661
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
@@ -0,0 +1,17 @@
+.icon-button {
+  color: @gray;
+  font-size: 1em;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  outline: none;
+}
+
+.icon-button:hover,
+.icon-button:active,
+.icon-button:focus {
+  color: @almost-black;
+  outline: none;
+  text-decoration: none;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
new file mode 100644
index 0000000..2bdf3cc
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
@@ -0,0 +1,21 @@
+.dashboard-component-chart {
+  width: 100%;
+  height: 100%;
+  color: @gray-dark;
+  background-color: white;
+  padding: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.dashboard-component-chart .fa {
+  font-size: 100px;
+  opacity: 0.3;
+}
+
+.grid-container--resizing .dashboard-component-chart,
+.dashboard-builder--dragging .dashboard-component-chart,
+.dashboard-component-chart:hover {
+  box-shadow: inset 0 0 0 1px @gray-light;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
new file mode 100644
index 0000000..b96b14b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
@@ -0,0 +1,21 @@
+.grid-column {
+  width: 100%;
+  min-height: 56px;
+}
+
+.grid-column > .hover-menu--top {
+  top: -20px;
+}
+
+.grid-column--empty:after {
+  content: "Empty column";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: @gray-light;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
new file mode 100644
index 0000000..f1d3d86
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
@@ -0,0 +1,24 @@
+.dashboard-component-divider {
+  width: 100%;
+  padding: 24px 0; /* this is padding not margin to enable a larger mouse target */
+  background-color: transparent;
+}
+
+.dashboard-component-divider:after {
+  content: "";
+  height: 1px;
+  width: 100%;
+  background-color: @gray-light;
+  display: block;
+}
+
+.new-component-placeholder.divider-placeholder:after {
+  content: "";
+  height: 2px;
+  width: 100%;
+  background-color: @gray-light;
+}
+
+.dragdroppable .dashboard-component-divider {
+  cursor: move;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
new file mode 100644
index 0000000..77066da
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
@@ -0,0 +1,26 @@
+.dashboard-component-header {
+  width: 100%;
+  line-height: 1em;
+  font-weight: 700;
+  background-color: inherit;
+  padding: 16px 0;
+  color: @almost-black;
+}
+
+.header-small {
+  font-size: 16px;
+}
+
+.header-medium {
+  font-size: 22px;
+}
+
+.header-large {
+  font-size: 32px;
+}
+
+.dragdroppable-row .dragdroppable-row .dashboard-component-header,
+.dragdroppable-row .dragdroppable-row .dashboard-component-divider {
+  padding-left: 16px;
+  padding-right: 16px;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less
new file mode 100644
index 0000000..5da54e5
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less
@@ -0,0 +1,8 @@
+@import './chart.less';
+@import './column.less';
+@import './divider.less';
+@import './header.less';
+@import './new-component.less';
+@import './row.less';
+@import './spacer.less';
+@import './tabs.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
new file mode 100644
index 0000000..31e84cb
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
@@ -0,0 +1,38 @@
+.new-component {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  align-items: center;
+  padding: 16px;
+  background: white;
+}
+
+.new-component-placeholder {
+  position: relative;
+  background: @gray-bg;
+  width: 40px;
+  height: 40px;
+  margin-right: 16px;
+  box-shadow: 0 0 1px white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: @gray;
+  font-size: 1.5em;
+}
+
+.new-component-placeholder.spacer-placeholder {
+  font-size: 1em;
+}
+
+.new-component-placeholder.fa-window-restore {
+  font-size: 1em;
+}
+
+.new-component-placeholder.spacer-placeholder:after {
+  content: "";
+  position: absolute;
+  height: 60%;
+  width: 60%;
+  border: 1px dashed @gray;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
new file mode 100644
index 0000000..8859926
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
@@ -0,0 +1,39 @@
+.grid-row {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  width: 100%;
+  height: fit-content;
+  background-color: transparent;
+}
+
+.grid-row--transparent {
+  background-color: transparent;
+}
+
+.grid-row--white {
+  background-color: white;
+}
+
+.dashboard-component-header.grid-row--white {
+  padding-left: 16px;
+}
+
+.grid-row.grid-row--empty {
+  align-items: center; /* this centers the empty note content */
+  height: 80px;
+}
+
+.grid-row--empty:after {
+  position: absolute;
+  top: 0;
+  left: 0;
+  content: "Empty row";
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  color: @gray;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/spacer.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/spacer.less
new file mode 100644
index 0000000..8716c21
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/spacer.less
@@ -0,0 +1,13 @@
+.grid-spacer {
+  width: 100%;
+  height: 100%;
+  background-color: transparent;
+}
+
+.dragdroppable .grid-spacer {
+  cursor: move;
+}
+
+.dragdroppable:hover .grid-spacer {
+  box-shadow: inset 0 0 0 1px @gray-light;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
new file mode 100644
index 0000000..23e0469
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
@@ -0,0 +1,65 @@
+.dashboard-component-tabs {
+  width: 100%;
+  background-color: white;
+}
+.dashboard-component-tabs .dashboard-component-tabs-content {
+  min-height: 48px;
+  margin-top: 1px;
+}
+
+.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 {
+  padding: 0 16px;
+}
+
+.dashboard-component-tabs .nav-tabs > li > a {
+  color: #263238;
+  border: none;
+  padding: 12px 0 14px 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: #000000;
+}
+
+.dashboard-component-tabs .nav-tabs > li > a:focus {
+  outline: none;
+  background: #fff;
+}
+
+.dashboard-component-tabs .nav-tabs > li .dragdroppable-tab {
+  cursor: move;
+}
+
+.dashboard-component-tabs .nav-tabs > li .drop-indicator {
+  height: 40px !important;
+  top: -10px !important;
+  opacity: 0.5;
+}
+
+.dashboard-component-tabs .fa-plus-square {
+  background: linear-gradient(135deg, #E32464, #2C2261);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  display: initial;
+  font-size: 16px;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/dnd/dnd.css
rename to superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
similarity index 65%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css
rename to superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
index 6119eab..c26ee0a 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/grid.css
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
@@ -1,3 +1,11 @@
+.grid-container {
+   flex-grow: 1;
+   min-width: 66%;
+   margin: 24px 32px;
+   height: 100%;
+   position: relative;
+}
+
 /* Editing guides */
 .grid-column-guide {
   position: absolute;
@@ -12,6 +20,6 @@
   position: absolute;
   left: 0;
   height: 1;
-  background-color: #44C0FF;
+  background-color: var(--indicator-color);
   pointer-events: none;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
new file mode 100644
index 0000000..bc2935c
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
@@ -0,0 +1,44 @@
+.hover-menu {
+  opacity: 0;
+  position: absolute;
+  z-index: 2;
+}
+
+.hover-menu--left {
+  width: 20px;
+  height: 100%;
+  top: 0;
+  left: -20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+.hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) {
+  margin-bottom: 8px;
+}
+
+.dragdroppable-row .dragdroppable-row .hover-menu--left {
+  left: 1px;
+}
+
+.hover-menu--top {
+  width: 100%;
+  height: 20px;
+  top: 0;
+  left: 0;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
+  margin-right: 8px;
+}
+
+.dragdroppable:hover .hover-menu,
+.dragdroppable .hover-menu:hover {
+  opacity: 1;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
new file mode 100644
index 0000000..125c894
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
@@ -0,0 +1,9 @@
+@import './variables.less';
+
+@import './buttons.less';
+@import './dnd.less';
+@import './grid.less';
+@import './hover-menu.less';
+@import './popover-menu.less';
+@import './resizable.less';
+@import './components/index.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
new file mode 100644
index 0000000..f68cf13
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
@@ -0,0 +1,116 @@
+.with-popover-menu {
+  position: relative;
+  outline: none;
+}
+
+.grid-row.grid-row--empty .with-popover-menu { /* drop indicator doesn't show up without this */
+  width: 100%;
+  height: 100%;
+}
+
+.with-popover-menu--focused:after {
+  content: "";
+  position: absolute;
+  top: 1;
+  left: -1;
+  width: 100%;
+  height: 100%;
+  box-shadow: inset 0 0 0 2px @indicator-color;
+  pointer-events: none;
+  z-index: 9;
+}
+
+.popover-menu {
+  position: absolute;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+  left: 1px;
+  top: -42px;
+  height: 40px;
+  padding: 0 16px;
+  background: white;
+  box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2);
+  font-size: 14px;
+  cursor: default;
+  z-index: 10;
+}
+
+.popover-menu .menu-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+/* vertical spacer after each menu item */
+.popover-menu .menu-item:not(:only-child):not(:last-child):after {
+  content: "";
+  width: 1;
+  height: 100%;
+  background: @gray-light;
+  margin: 0 16px;
+}
+
+.popover-menu .popover-dropdown.btn {
+  border: none;
+  padding: 0;
+  font-size: inherit;
+  color: @almost-black;
+}
+
+.popover-menu .popover-dropdown.btn:hover,
+.popover-menu .popover-dropdown.btn:active,
+.popover-menu .popover-dropdown.btn:focus,
+.hover-dropdown .btn:hover,
+.hover-dropdown .btn:active,
+.hover-dropdown .btn:focus {
+  background: initial;
+  box-shadow: none;
+}
+
+.hover-dropdown li.dropdown-item:hover a,
+.popover-menu li.dropdown-item:hover a {
+  background: @gray-light;
+}
+
+.popover-dropdown .caret { /* without this the caret doesn't take up full width / is clipped */
+  width: auto;
+  border-top-color: transparent;
+}
+
+
+.hover-dropdown li.dropdown-item.active a,
+.popover-menu li.dropdown-item.active a {
+  background: white;
+  font-weight: bold;
+  color: @almost-black;
+}
+
+/* row style menu */
+.row-style-option {
+  display: inline-block;
+}
+
+.row-style-option:before {
+  content: "";
+  width: 1em;
+  height: 1em;
+  margin-right: 8px;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.row-style-option.grid-row--white {
+  padding-left: 0;
+  background: transparent;
+}
+
+.row-style-option.grid-row--white:before {
+  background: white;
+  border: 1px solid @gray-light;
+}
+
+.row-style-option.grid-row--transparent:before {
+  background: @gray-light;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
similarity index 81%
rename from superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css
rename to superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
index 1d5de72..0ccd2f8 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/resizable.css
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
@@ -11,7 +11,7 @@
   left: 0;
   width: 100%;
   height: 100%;
-  box-shadow: inset 0 0 0 2px #44C0FF;
+  box-shadow: inset 0 0 0 2px @indicator-color;
 }
 
 .resize-handle {
@@ -27,8 +27,8 @@
   position: absolute;
   border: solid;
   border-width: 0 1.5px 1.5px 0;
-  border-right-color: #879399;
-  border-bottom-color: #879399;
+  border-right-color: @gray;
+  border-bottom-color: @gray;
   right: 16;
   bottom: 16;
   width: 8px;
@@ -41,8 +41,8 @@
   right: -2px;
   top: 47%;
   position: absolute;
-  border-left: 1px solid #879399;
-  border-right: 1px solid #879399;
+  border-left: 1px solid @gray;
+  border-right: 1px solid @gray;
 }
 
   .grid-spacer + span .resize-handle--right {
@@ -55,12 +55,12 @@
   bottom: 10px;
   left: 47%;
   position: absolute;
-  border-top: 1px solid #879399;
-  border-bottom: 1px solid #879399;
+  border-top: 1px solid @gray;
+  border-bottom: 1px solid @gray;
 }
 
 .grid-resizable-container--resizing > span .resize-handle {
-  border-color: #44C0FF;
+  border-color: @indicator-color;
 }
 
 /* re-resizable sets an empty div to 100% width and height, which doesn't
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
new file mode 100644
index 0000000..f3a61df
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
@@ -0,0 +1,7 @@
+@indicator-color: #44C0FF;
+
+@almost-black: #263238;
+@gray-dark: #484848;
+@gray: #879399;
+@gray-light: #CFD8DC;
+@gray-bg: #f5f5f5;
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
index 6df72ff..d429461 100644
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ b/superset/assets/src/dashboard/components/DashboardContainer.jsx
@@ -5,19 +5,19 @@ import * as dashboardActions from '../actions';
 import * as chartActions from '../../chart/chartAction';
 import Dashboard from '../v2/components/Dashboard';
 
-function mapStateToProps({ charts, dashboard, impressionId }) {
+function mapStateToProps(/* { charts, dashboard, impressionId } */) {
   return {
-    initMessages: dashboard.common.flash_messages,
-    timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    dashboard: dashboard.dashboard,
-    slices: charts,
-    datasources: dashboard.datasources,
-    filters: dashboard.filters,
-    refresh: !!dashboard.refresh,
-    userId: dashboard.userId,
-    isStarred: !!dashboard.isStarred,
-    editMode: dashboard.editMode,
-    impressionId,
+    // initMessages: dashboard.common.flash_messages,
+    // timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    // dashboard: dashboard.dashboard,
+    // slices: charts,
+    // datasources: dashboard.datasources,
+    // filters: dashboard.filters,
+    // refresh: !!dashboard.refresh,
+    // userId: dashboard.userId,
+    // isStarred: !!dashboard.isStarred,
+    // editMode: dashboard.editMode,
+    // impressionId,
   };
 }
 
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 6513b6f..2c405cd 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -208,17 +208,6 @@ div.widget {
     }
   }
 }
-/* brand icon */
-.navbar-brand > img.logo {
-  margin-left: 15px;
-  width: 36px;
-  display: inline;
-}
-.navbar-brand > span {
-  margin-left: 2px;
-  font-size: 15px;
-  font-weight: bold;
-}
 
 .navbar .alert {
     padding: 5px 10px;
diff --git a/superset/config.py b/superset/config.py
index ea5a520..530b126 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -97,7 +97,7 @@ ENABLE_PROXY_FIX = False
 APP_NAME = 'Superset'
 
 # Uncomment to setup an App icon
-APP_ICON = '/static/assets/images/favicon.png'
+APP_ICON = '/static/assets/images/superset-logo@2x.png'
 
 # Druid query timezone
 # tz.tzutc() : Using utc timezone
diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html
index c2f0668..acb292c 100644
--- a/superset/templates/appbuilder/navbar.html
+++ b/superset/templates/appbuilder/navbar.html
@@ -12,11 +12,10 @@
       </button>
       <a class="navbar-brand" href="/superset/profile/{{ current_user.username }}/">
         <img
-          class="logo"
+          width="126"
           src="{{ appbuilder.app_icon }}"
           alt="{{ appbuilder.app_name }}"
         />
-        <span>Superset</span>
       </a>
     </div>
     <div class="navbar-collapse collapse">


[incubator-superset] 14/26: [dashboard v2] tests! (#5066)

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

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

commit 4bee61374c013be7ec3ee067a30b6fe5ea25b6ec
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Thu May 24 11:18:25 2018 -0700

    [dashboard v2] tests! (#5066)
    
    * [dashboard v2][tests] add tests for newComponentFactory, isValidChild, dropOverflowsParent, and dnd-reorder
    
    * [dashboard v2][tests] add tests for componentIsResizable, findParentId, getChartIdsFromLayout, newEntitiesFromDrop, and getDropPosition
    
    * [dashboard v2][tests] add mockStore, mockState, and tests for DragDroppable, DashboardBuilder, DashboardGrid, ToastPresenter, and Toast
    
    * [dashboard builder][tests] separate files for state tree fixtures, add ChartHolder, Chart, Divider, Header, Row tests and WithDragDropContext helper
    
    * [dashboard v2][tests] fix dragdrop context with util/getDragDropManager, add test for menu/* and resizable/*, and new components
    
    * [dashboard v2][tests] fix and re-write Dashboard tests, add getFormDataWithExtraFilters_spec
    
    * [dashboard v2][tests] add reducer tests, fix lint error
    
    * [dashboard-v2][tests] add actions/dashboardLayout_spec
    
    * [dashboard v2] fix some prop bugs, open side pane on edit, fix slice name bug
    
    * [dashboard v2] fix slice name save bug
    
    * [dashboard v2] fix lint errors
    
    * [dashboard v2] fix filters bug and add test
    
    * [dashboard v2] fix getFormDataWithExtraFilters_spec
---
 superset/assets/package.json                       |   1 +
 superset/assets/spec/helpers/browser.js            |   4 +-
 .../assets/spec/javascripts/dashboard/.eslintrc    |  33 ++
 .../assets/spec/javascripts/dashboard/.prettierrc  |   4 +
 .../spec/javascripts/dashboard/Dashboard_spec.jsx  | 212 ----------
 .../dashboard/actions/dashboardLayout_spec.js      | 442 ++++++++++++++++++++
 .../dashboard/{ => components}/CodeModal_spec.jsx  |   6 +-
 .../dashboard/{ => components}/CssEditor_spec.jsx  |   6 +-
 .../dashboard/components/DashboardBuilder_spec.jsx | 146 +++++++
 .../dashboard/components/DashboardGrid_spec.jsx    |  82 ++++
 .../dashboard/components/Dashboard_spec.jsx        | 249 ++++++++++++
 .../{ => components}/RefreshIntervalModal_spec.jsx |   2 +-
 .../dashboard/components/ToastPresenter_spec.jsx   |  41 ++
 .../dashboard/components/Toast_spec.jsx            |  43 ++
 .../components/dnd/DragDroppable_spec.jsx          |  90 +++++
 .../components/gridComponents/ChartHolder_spec.jsx | 112 ++++++
 .../components/gridComponents/Chart_spec.jsx       |  92 +++++
 .../components/gridComponents/Column_spec.jsx      | 144 +++++++
 .../components/gridComponents/Divider_spec.jsx     |  70 ++++
 .../components/gridComponents/Header_spec.jsx      | 100 +++++
 .../components/gridComponents/Row_spec.jsx         | 120 ++++++
 .../components/gridComponents/Tab_spec.jsx         | 126 ++++++
 .../components/gridComponents/Tabs_spec.jsx        | 140 +++++++
 .../new/DraggableNewComponent_spec.jsx             |  68 ++++
 .../gridComponents/new/NewColumn_spec.jsx          |  29 ++
 .../gridComponents/new/NewDivider_spec.jsx         |  29 ++
 .../gridComponents/new/NewHeader_spec.jsx          |  29 ++
 .../components/gridComponents/new/NewRow_spec.jsx  |  29 ++
 .../components/gridComponents/new/NewTabs_spec.jsx |  29 ++
 .../dashboard/components/menu/HoverMenu_spec.jsx   |  13 +
 .../components/menu/WithPopoverMenu_spec.jsx       |  71 ++++
 .../resizable/ResizableContainer_spec.jsx          |  20 +
 .../components/resizable/ResizableHandle_spec.jsx  |  29 ++
 .../assets/spec/javascripts/dashboard/fixtures.jsx |  32 +-
 .../dashboard/fixtures/mockChartQueries.js         |  61 +++
 .../dashboard/fixtures/mockDashboardInfo.js        |  12 +
 .../dashboard/fixtures/mockDashboardLayout.js      | 140 +++++++
 .../dashboard/fixtures/mockDashboardState.js       |  13 +
 .../dashboard/fixtures/mockDatasource.js           | 206 ++++++++++
 .../dashboard/fixtures/mockMessageToasts.js        |   9 +
 .../dashboard/fixtures/mockSliceEntities.js        |  39 ++
 .../javascripts/dashboard/fixtures/mockState.js    |  18 +
 .../javascripts/dashboard/fixtures/mockStore.js    |  22 +
 .../dashboard/helpers/WithDragDropContext.jsx      |  27 ++
 .../dashboard/reducers/dashboardLayout_spec.js     | 443 +++++++++++++++++++++
 .../dashboard/reducers/dashboardState_spec.js      | 239 +++++++++++
 .../dashboard/reducers/messageToasts_spec.js       |  32 ++
 .../dashboard/reducers/sliceEntities_spec.js       |  51 +++
 .../spec/javascripts/dashboard/reducers_spec.js    |  38 --
 .../dashboard/util/componentIsResizable_spec.js    |  42 ++
 .../javascripts/dashboard/util/dnd-reorder_spec.js |  62 +++
 .../dashboard/util/dropOverflowsParent_spec.js     | 125 ++++++
 .../dashboard/util/findParentId_spec.js            |  29 ++
 .../dashboard/util/getChartIdsFromLayout_spec.js   |  41 ++
 .../dashboard/util/getDropPosition_spec.js         | 422 ++++++++++++++++++++
 .../util/getFormDataWithExtraFilters_spec.js       |  70 ++++
 .../dashboard/util/isValidChild_spec.js            | 147 +++++++
 .../dashboard/util/newComponentFactory_spec.js     |  51 +++
 .../dashboard/util/newEntitiesFromDrop_spec.js     |  82 ++++
 superset/assets/src/chart/chartAction.js           |   1 -
 .../src/dashboard/actions/dashboardLayout.js       | 159 +++-----
 .../assets/src/dashboard/actions/messageToasts.js  |   8 +-
 .../assets/src/dashboard/components/Dashboard.jsx  |  65 +--
 .../src/dashboard/components/DashboardBuilder.jsx  |  18 +-
 .../assets/src/dashboard/components/SliceAdder.jsx |  31 +-
 .../src/dashboard/components/SliceHeader.jsx       |   9 +-
 .../src/dashboard/components/dnd/DragDroppable.jsx |  46 ++-
 .../dashboard/components/gridComponents/Chart.jsx  |   2 +-
 .../components/gridComponents/ChartHolder.jsx      |   4 +-
 .../dashboard/components/gridComponents/Header.jsx |  11 +-
 .../dashboard/components/gridComponents/Row.jsx    |   2 +-
 superset/assets/src/dashboard/containers/Chart.jsx |   6 +-
 .../src/dashboard/fixtures/emptyDashboardLayout.js |   4 +-
 .../src/dashboard/reducers/dashboardLayout.js      |  10 +-
 .../src/dashboard/reducers/dashboardState.js       |  25 +-
 .../src/dashboard/reducers/getInitialState.js      |   4 +-
 .../assets/src/dashboard/reducers/sliceEntities.js |  14 +-
 .../util/charts/getEffectiveExtraFilters.js        |  10 +-
 .../util/charts/getFormDataWithExtraFilters.js     |   8 +-
 .../assets/src/dashboard/util/componentTypes.js    |   4 +-
 .../src/dashboard/util/dropOverflowsParent.js      |  45 ++-
 superset/assets/src/dashboard/util/findParentId.js |   6 +-
 .../src/dashboard/util/getChartIdsFromLayout.js    |  13 +-
 .../src/dashboard/util/getDragDropManager.js       |  17 +
 superset/assets/src/dashboard/util/isValidChild.js |   5 +-
 .../src/dashboard/util/newEntitiesFromDrop.js      |   7 +-
 superset/assets/src/dashboard/util/propShapes.jsx  |   4 +-
 superset/assets/src/logger.js                      |  11 +-
 superset/assets/yarn.lock                          | 216 +++++++++-
 superset/views/core.py                             |   2 +-
 90 files changed, 5273 insertions(+), 558 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 0ddceb2..6d116a4 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -61,6 +61,7 @@
     "deck.gl": "^5.1.4",
     "deep-equal": "^1.0.1",
     "distributions": "^1.0.0",
+    "dnd-core": "^2.6.0",
     "dompurify": "^1.0.3",
     "fastdom": "^1.0.6",
     "geojson-extent": "^0.3.2",
diff --git a/superset/assets/spec/helpers/browser.js b/superset/assets/spec/helpers/browser.js
index d465d86..b30d3c7 100644
--- a/superset/assets/spec/helpers/browser.js
+++ b/superset/assets/spec/helpers/browser.js
@@ -10,6 +10,8 @@ const exposedProperties = ['window', 'navigator', 'document'];
 global.jsdom = jsdom.jsdom;
 global.document = global.jsdom('<!doctype html><html><body></body></html>');
 global.window = document.defaultView;
+global.HTMLElement = window.HTMLElement;
+
 Object.keys(document.defaultView).forEach((property) => {
   if (typeof global[property] === 'undefined') {
     exposedProperties.push(property);
@@ -38,5 +40,5 @@ global.sinon.useFakeXMLHttpRequest();
 
 global.window.XMLHttpRequest = global.XMLHttpRequest;
 global.window.location = { href: 'about:blank' };
-global.window.performance = { now: () => (new Date().getTime()) };
+global.window.performance = { now: () => new Date().getTime() };
 global.$ = require('jquery')(global.window);
diff --git a/superset/assets/spec/javascripts/dashboard/.eslintrc b/superset/assets/spec/javascripts/dashboard/.eslintrc
new file mode 100644
index 0000000..a3f86e3
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/.eslintrc
@@ -0,0 +1,33 @@
+{
+  "extends": "prettier",
+  "plugins": ["prettier"],
+  "rules": {
+    "prefer-template": 2,
+    "new-cap": 2,
+    "no-restricted-syntax": 2,
+    "guard-for-in": 2,
+    "prefer-arrow-callback": 2,
+    "func-names": 2,
+    "react/jsx-no-bind": 2,
+    "no-confusing-arrow": 2,
+    "jsx-a11y/no-static-element-interactions": 2,
+    "jsx-a11y/anchor-has-content": 2,
+    "react/require-default-props": 2,
+    "no-plusplus": 2,
+    "no-mixed-operators": 0,
+    "no-continue": 2,
+    "no-bitwise": 2,
+    "no-undef": 2,
+    "no-multi-assign": 2,
+    "no-restricted-properties": 2,
+    "no-prototype-builtins": 2,
+    "jsx-a11y/href-no-hash": 2,
+    "class-methods-use-this": 2,
+    "import/no-named-as-default": 2,
+    "import/prefer-default-export": 2,
+    "react/no-unescaped-entities": 2,
+    "react/no-string-refs": 2,
+    "react/jsx-indent": 0,
+    "prettier/prettier": "error"
+  }
+}
diff --git a/superset/assets/spec/javascripts/dashboard/.prettierrc b/superset/assets/spec/javascripts/dashboard/.prettierrc
new file mode 100644
index 0000000..a20502b
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/.prettierrc
@@ -0,0 +1,4 @@
+{
+  "singleQuote": true,
+  "trailingComma": "all"
+}
diff --git a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
deleted file mode 100644
index f4def13..0000000
--- a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
+++ /dev/null
@@ -1,212 +0,0 @@
-/* eslint camelcase: 0 */
-import React from 'react';
-import { shallow } from 'enzyme';
-import { describe, it } from 'mocha';
-import { expect } from 'chai';
-import sinon from 'sinon';
-
-import * as sliceActions from '../../../src/dashboard/actions/sliceEntities';
-import * as dashboardActions from '../../../src/dashboard/actions/dashboardState';
-import * as chartActions from '../../../src/chart/chartAction';
-import Dashboard from '../../../src/dashboard/components/Dashboard';
-import { defaultFilters, dashboardState, dashboardInfo, dashboardLayout,
-  charts, datasources, sliceEntities } from './fixtures';
-
-describe('Dashboard', () => {
-  const mockedProps = {
-    actions: { ...chartActions, ...dashboardActions, ...sliceActions },
-    initMessages: [],
-    dashboardState,
-    dashboardInfo,
-    charts,
-    slices: sliceEntities.slices,
-    datasources,
-    layout: dashboardLayout.present,
-    timeout: 60,
-    userId: dashboardInfo.userId,
-  };
-
-  it('should render', () => {
-    const wrapper = shallow(<Dashboard {...mockedProps} />);
-    expect(wrapper.find('#dashboard-container')).to.have.length(1);
-    expect(wrapper.instance().getAllCharts()).to.have.length(3);
-  });
-
-  it('should handle metadata default_filters', () => {
-    const wrapper = shallow(<Dashboard {...mockedProps} />);
-    expect(wrapper.instance().props.dashboardState.filters).deep.equal(defaultFilters);
-  });
-
-  describe('getFormDataExtra', () => {
-    let wrapper;
-    let selectedChart;
-    beforeEach(() => {
-      wrapper = shallow(<Dashboard {...mockedProps} />);
-      selectedChart = charts[248];
-    });
-
-    it('should carry default_filters', () => {
-      const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
-      expect(extraFilters[0]).to.deep.equal({ col: 'region', op: 'in', val: [] });
-      expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['United States'] });
-    });
-
-    it('should carry updated filter', () => {
-      const newState = {
-        ...wrapper.props('dashboardState'),
-        filters: {
-          256: { region: [] },
-          257: { country_name: ['France'] },
-        },
-      };
-      wrapper.setProps({
-        dashboardState: newState,
-      });
-      const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
-      expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['France'] });
-    });
-  });
-
-  describe('refreshExcept', () => {
-    let wrapper;
-    let spy;
-    beforeEach(() => {
-      wrapper = shallow(<Dashboard {...mockedProps} />);
-      spy = sinon.spy(mockedProps.actions, 'runQuery');
-    });
-    afterEach(() => {
-      spy.restore();
-    });
-
-    it('should not refresh filter slice', () => {
-      const filterKey = Object.keys(defaultFilters)[1];
-      wrapper.instance().refreshExcept(filterKey);
-      expect(spy.callCount).to.equal(1);
-      const slice_id = spy.getCall(0).args[0].slice_id;
-      expect(slice_id).to.equal(248);
-    });
-
-    it('should refresh all slices', () => {
-      wrapper.instance().refreshExcept();
-      expect(spy.callCount).to.equal(3);
-    });
-  });
-
-  describe('componentDidUpdate', () => {
-    let wrapper;
-    let refreshExceptSpy;
-    let fetchSlicesStub;
-    let prevProp;
-    beforeEach(() => {
-      wrapper = shallow(<Dashboard {...mockedProps} />);
-      prevProp = wrapper.instance().props;
-      refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
-      fetchSlicesStub = sinon.stub(mockedProps.actions, 'fetchCharts');
-    });
-    afterEach(() => {
-      fetchSlicesStub.restore();
-      refreshExceptSpy.restore();
-    });
-
-    describe('should check if filter has change', () => {
-      beforeEach(() => {
-        refreshExceptSpy.reset();
-      });
-      it('no change', () => {
-        const newState = {
-          ...wrapper.props('dashboardState'),
-          filters: {
-            256: { region: [] },
-            257: { country_name: ['United States'] },
-          },
-        };
-        wrapper.setProps({
-          dashboardState: newState,
-        });
-        wrapper.instance().componentDidUpdate(prevProp);
-        expect(refreshExceptSpy.callCount).to.equal(0);
-      });
-
-      it('remove filter', () => {
-        const newState = {
-          ...wrapper.props('dashboardState'),
-          refresh: true,
-          filters: {
-            256: { region: [] },
-          },
-        };
-        wrapper.setProps({
-          dashboardState: newState,
-        });
-        wrapper.instance().componentDidUpdate(prevProp);
-        expect(refreshExceptSpy.callCount).to.equal(1);
-      });
-
-      it('change filter', () => {
-        const newState = {
-          ...wrapper.props('dashboardState'),
-          refresh: true,
-          filters: {
-            256: { region: [] },
-            257: { country_name: ['Canada'] },
-          },
-        };
-        wrapper.setProps({
-          dashboardState: newState,
-        });
-        wrapper.instance().componentDidUpdate(prevProp);
-        expect(refreshExceptSpy.callCount).to.equal(1);
-      });
-
-      it('add filter', () => {
-        const newState = {
-          ...wrapper.props('dashboardState'),
-          refresh: true,
-          filters: {
-            256: { region: [] },
-            257: { country_name: ['Canada'] },
-            258: { another_filter: ['new'] },
-          },
-        };
-        wrapper.setProps({
-          dashboardState: newState,
-        });
-        wrapper.instance().componentDidUpdate(prevProp);
-        expect(refreshExceptSpy.callCount).to.equal(1);
-      });
-    });
-
-    it('should refresh if refresh flag is true', () => {
-      const newState = {
-        ...wrapper.props('dashboardState'),
-        refresh: true,
-        filters: {
-          256: { region: ['Asian'] },
-        },
-      };
-      wrapper.setProps({
-        dashboardState: newState,
-      });
-      wrapper.instance().componentDidUpdate(prevProp);
-      expect(refreshExceptSpy.callCount).to.equal(1);
-      expect(refreshExceptSpy.lastCall.args[0]).to.equal('256');
-    });
-
-    it('should not refresh filter_immune_slices', () => {
-      const newState = {
-        ...wrapper.props('dashboardState'),
-        refresh: true,
-        filters: {
-          256: { region: [] },
-          257: { country_name: ['Canada'] },
-        },
-      };
-      wrapper.setProps({
-        dashboardState: newState,
-      });
-      wrapper.instance().componentDidUpdate(prevProp);
-      expect(refreshExceptSpy.callCount).to.equal(1);
-      expect(refreshExceptSpy.lastCall.args[0]).to.equal('257');
-    });
-  });
-});
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
new file mode 100644
index 0000000..0c4fe12
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -0,0 +1,442 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import { ActionCreators as UndoActionCreators } from 'redux-undo';
+
+import {
+  UPDATE_COMPONENTS,
+  updateComponents,
+  DELETE_COMPONENT,
+  deleteComponent,
+  CREATE_COMPONENT,
+  CREATE_TOP_LEVEL_TABS,
+  createTopLevelTabs,
+  DELETE_TOP_LEVEL_TABS,
+  deleteTopLevelTabs,
+  resizeComponent,
+  MOVE_COMPONENT,
+  handleComponentDrop,
+  updateDashboardTitle,
+  undoLayoutAction,
+  redoLayoutAction,
+} from '../../../../src/dashboard/actions/dashboardLayout';
+
+import { setUnsavedChanges } from '../../../../src/dashboard/actions/dashboardState';
+import { addInfoToast } from '../../../../src/dashboard/actions/messageToasts';
+
+import {
+  DASHBOARD_GRID_TYPE,
+  ROW_TYPE,
+  CHART_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+import {
+  DASHBOARD_HEADER_ID,
+  DASHBOARD_GRID_ID,
+  DASHBOARD_ROOT_ID,
+  NEW_COMPONENTS_SOURCE_ID,
+  NEW_ROW_ID,
+} from '../../../../src/dashboard/util/constants';
+
+describe('dashboardLayout actions', () => {
+  const mockState = {
+    dashboardState: {},
+    dashboardInfo: {},
+    dashboardLayout: {
+      past: [],
+      present: {},
+      future: {},
+    },
+  };
+
+  function setup(stateOverrides) {
+    const state = { ...mockState, ...stateOverrides };
+    const getState = sinon.spy(() => state);
+    const dispatch = sinon.spy();
+
+    return { getState, dispatch, state };
+  }
+
+  describe('updateComponents', () => {
+    it('should dispatch an updateLayout action', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: true },
+      });
+      const nextComponents = { 1: {} };
+      const thunk = updateComponents(nextComponents);
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(1);
+      expect(dispatch.getCall(0).args[0]).to.deep.equal({
+        type: UPDATE_COMPONENTS,
+        payload: { nextComponents },
+      });
+    });
+
+    it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+      const nextComponents = { 1: {} };
+      const thunk = updateComponents(nextComponents);
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal(
+        setUnsavedChanges(true),
+      );
+    });
+  });
+
+  describe('deleteComponents', () => {
+    it('should dispatch an deleteComponent action', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: true },
+      });
+      const thunk = deleteComponent('id', 'parentId');
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(1);
+      expect(dispatch.getCall(0).args[0]).to.deep.equal({
+        type: DELETE_COMPONENT,
+        payload: { id: 'id', parentId: 'parentId' },
+      });
+    });
+
+    it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+      const thunk = deleteComponent('id', 'parentId');
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal(
+        setUnsavedChanges(true),
+      );
+    });
+  });
+
+  describe('updateDashboardTitle', () => {
+    it('should dispatch an updateComponent action for the header component', () => {
+      const { getState, dispatch } = setup();
+      const thunk1 = updateDashboardTitle('new text');
+      thunk1(dispatch, getState);
+
+      const thunk2 = dispatch.getCall(0).args[0];
+      thunk2(dispatch, getState);
+
+      expect(dispatch.getCall(1).args[0]).to.deep.equal({
+        type: UPDATE_COMPONENTS,
+        payload: {
+          nextComponents: {
+            [DASHBOARD_HEADER_ID]: {
+              meta: { text: 'new text' },
+            },
+          },
+        },
+      });
+    });
+  });
+
+  // describe('createComponent', () => {});
+  describe('createTopLevelTabs', () => {
+    it('should dispatch a createTopLevelTabs action', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: true },
+      });
+      const dropResult = {};
+      const thunk = createTopLevelTabs(dropResult);
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(1);
+      expect(dispatch.getCall(0).args[0]).to.deep.equal({
+        type: CREATE_TOP_LEVEL_TABS,
+        payload: { dropResult },
+      });
+    });
+
+    it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+      const dropResult = {};
+      const thunk = createTopLevelTabs(dropResult);
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal(
+        setUnsavedChanges(true),
+      );
+    });
+  });
+
+  describe('deleteTopLevelTabs', () => {
+    it('should dispatch a deleteTopLevelTabs action', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: true },
+      });
+      const dropResult = {};
+      const thunk = deleteTopLevelTabs(dropResult);
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(1);
+      expect(dispatch.getCall(0).args[0]).to.deep.equal({
+        type: DELETE_TOP_LEVEL_TABS,
+        payload: {},
+      });
+    });
+
+    it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+      const dropResult = {};
+      const thunk = deleteTopLevelTabs(dropResult);
+      thunk(dispatch, getState);
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal(
+        setUnsavedChanges(true),
+      );
+    });
+  });
+
+  describe('resizeComponent', () => {
+    const dashboardLayout = {
+      ...mockState.dashboardLayout,
+      present: {
+        1: {
+          id: 1,
+          children: [],
+          meta: {
+            width: 1,
+            height: 1,
+          },
+        },
+      },
+    };
+
+    it('should update the size of the component', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: true },
+        dashboardLayout,
+      });
+
+      const thunk1 = resizeComponent({ id: 1, width: 10, height: 3 });
+      thunk1(dispatch, getState);
+
+      const thunk2 = dispatch.getCall(0).args[0];
+      thunk2(dispatch, getState);
+
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal({
+        type: UPDATE_COMPONENTS,
+        payload: {
+          nextComponents: {
+            1: {
+              id: 1,
+              children: [],
+              meta: {
+                width: 10,
+                height: 3,
+              },
+            },
+          },
+        },
+      });
+    });
+
+    it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+        dashboardLayout,
+      });
+      const thunk1 = resizeComponent({ id: 1, width: 10, height: 3 });
+      thunk1(dispatch, getState);
+
+      const thunk2 = dispatch.getCall(0).args[0];
+      thunk2(dispatch, getState);
+
+      expect(dispatch.callCount).to.equal(3);
+    });
+  });
+
+  describe('handleComponentDrop', () => {
+    it('should create a component if it is new', () => {
+      const { getState, dispatch } = setup();
+      const dropResult = {
+        source: { id: NEW_COMPONENTS_SOURCE_ID },
+        destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
+        dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
+      };
+
+      const thunk1 = handleComponentDrop(dropResult);
+      thunk1(dispatch, getState);
+
+      const thunk2 = dispatch.getCall(0).args[0];
+      thunk2(dispatch, getState);
+
+      expect(dispatch.getCall(1).args[0]).to.deep.equal({
+        type: CREATE_COMPONENT,
+        payload: {
+          dropResult,
+        },
+      });
+    });
+
+    it('should move a component if the component is not new', () => {
+      const { getState, dispatch } = setup({
+        dashboardLayout: { present: { id: { type: ROW_TYPE } } },
+      });
+      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 },
+      };
+
+      const thunk = handleComponentDrop(dropResult);
+      thunk(dispatch, getState);
+
+      expect(dispatch.getCall(0).args[0]).to.deep.equal({
+        type: MOVE_COMPONENT,
+        payload: {
+          dropResult,
+        },
+      });
+    });
+
+    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 } },
+          },
+        },
+      });
+      const dropResult = {
+        source: { id: 'source', type: ROW_TYPE },
+        destination: { id: 'destination', type: ROW_TYPE },
+        dragging: { id: 'dragging', type: CHART_TYPE },
+      };
+
+      const thunk = handleComponentDrop(dropResult);
+      thunk(dispatch, getState);
+      expect(dispatch.getCall(0).args[0].type).to.deep.equal(
+        addInfoToast('').type,
+      );
+    });
+
+    it('should delete the parent Tabs if the moved Tab was the only child', () => {
+      const { getState, dispatch } = setup({
+        dashboardLayout: {
+          present: {
+            parentId: { id: 'parentId', children: ['tabsId'] },
+            tabsId: { id: 'tabsId', type: TABS_TYPE, children: [] },
+            [DASHBOARD_GRID_ID]: {
+              id: DASHBOARD_GRID_ID,
+              type: DASHBOARD_GRID_TYPE,
+            },
+            tabId: { id: 'tabId', type: TAB_TYPE },
+          },
+        },
+      });
+
+      const dropResult = {
+        source: { id: 'tabsId', type: TABS_TYPE },
+        destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
+        dragging: { id: 'tabId', type: TAB_TYPE },
+      };
+
+      const moveThunk = handleComponentDrop(dropResult);
+      moveThunk(dispatch, getState);
+
+      // first call is move action which is not a thunk
+      const deleteThunk = dispatch.getCall(1).args[0];
+      deleteThunk(dispatch, getState);
+
+      expect(dispatch.getCall(2).args[0]).to.deep.equal({
+        type: DELETE_COMPONENT,
+        payload: {
+          id: 'tabsId',
+          parentId: 'parentId',
+        },
+      });
+    });
+
+    it('should create top-level tabs if dropped on root', () => {
+      const { getState, dispatch } = setup();
+      const dropResult = {
+        source: { id: NEW_COMPONENTS_SOURCE_ID },
+        destination: { id: DASHBOARD_ROOT_ID },
+        dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
+      };
+
+      const thunk1 = handleComponentDrop(dropResult);
+      thunk1(dispatch, getState);
+
+      const thunk2 = dispatch.getCall(0).args[0];
+      thunk2(dispatch, getState);
+
+      expect(dispatch.getCall(1).args[0]).to.deep.equal({
+        type: CREATE_TOP_LEVEL_TABS,
+        payload: {
+          dropResult,
+        },
+      });
+    });
+  });
+
+  describe('undoLayoutAction', () => {
+    it('should dispatch a redux-undo .undo() action ', () => {
+      const { getState, dispatch } = setup({
+        dashboardLayout: { past: ['non-empty'] },
+      });
+      const thunk = undoLayoutAction();
+      thunk(dispatch, getState);
+
+      expect(dispatch.callCount).to.equal(1);
+      expect(dispatch.getCall(0).args[0]).to.deep.equal(
+        UndoActionCreators.undo(),
+      );
+    });
+
+    it('should dispatch a setUnsavedChanges(false) action history length is zero', () => {
+      const { getState, dispatch } = setup({
+        dashboardLayout: { past: [] },
+      });
+      const thunk = undoLayoutAction();
+      thunk(dispatch, getState);
+
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal(
+        setUnsavedChanges(false),
+      );
+    });
+  });
+
+  describe('redoLayoutAction', () => {
+    it('should dispatch a redux-undo .redo() action ', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: true },
+      });
+      const thunk = redoLayoutAction();
+      thunk(dispatch, getState);
+
+      expect(dispatch.callCount).to.equal(1);
+      expect(dispatch.getCall(0).args[0]).to.deep.equal(
+        UndoActionCreators.redo(),
+      );
+    });
+
+    it('should dispatch a setUnsavedChanges(true) action if hasUnsavedChanges=false', () => {
+      const { getState, dispatch } = setup({
+        dashboardState: { hasUnsavedChanges: false },
+      });
+      const thunk = redoLayoutAction();
+      thunk(dispatch, getState);
+
+      expect(dispatch.callCount).to.equal(2);
+      expect(dispatch.getCall(1).args[0]).to.deep.equal(
+        setUnsavedChanges(true),
+      );
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/CodeModal_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/CodeModal_spec.jsx
similarity index 72%
rename from superset/assets/spec/javascripts/dashboard/CodeModal_spec.jsx
rename to superset/assets/spec/javascripts/dashboard/components/CodeModal_spec.jsx
index a93c557..d316dc3 100644
--- a/superset/assets/spec/javascripts/dashboard/CodeModal_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/CodeModal_spec.jsx
@@ -3,16 +3,14 @@ import { mount } from 'enzyme';
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 
-import CodeModal from '../../../src/dashboard/components/CodeModal';
+import CodeModal from '../../../../src/dashboard/components/CodeModal';
 
 describe('CodeModal', () => {
   const mockedProps = {
     triggerNode: <i className="fa fa-edit" />,
   };
   it('is valid', () => {
-    expect(
-      React.isValidElement(<CodeModal {...mockedProps} />),
-    ).to.equal(true);
+    expect(React.isValidElement(<CodeModal {...mockedProps} />)).to.equal(true);
   });
   it('renders the trigger node', () => {
     const wrapper = mount(<CodeModal {...mockedProps} />);
diff --git a/superset/assets/spec/javascripts/dashboard/CssEditor_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/CssEditor_spec.jsx
similarity index 72%
rename from superset/assets/spec/javascripts/dashboard/CssEditor_spec.jsx
rename to superset/assets/spec/javascripts/dashboard/components/CssEditor_spec.jsx
index c325dc1..8c991fa 100644
--- a/superset/assets/spec/javascripts/dashboard/CssEditor_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/CssEditor_spec.jsx
@@ -3,16 +3,14 @@ import { mount } from 'enzyme';
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 
-import CssEditor from '../../../src/dashboard/components/CssEditor';
+import CssEditor from '../../../../src/dashboard/components/CssEditor';
 
 describe('CssEditor', () => {
   const mockedProps = {
     triggerNode: <i className="fa fa-edit" />,
   };
   it('is valid', () => {
-    expect(
-      React.isValidElement(<CssEditor {...mockedProps} />),
-    ).to.equal(true);
+    expect(React.isValidElement(<CssEditor {...mockedProps} />)).to.equal(true);
   });
   it('renders the trigger node', () => {
     const wrapper = mount(<CssEditor {...mockedProps} />);
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
new file mode 100644
index 0000000..6b5d051
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
@@ -0,0 +1,146 @@
+import { Provider } from 'react-redux';
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import ParentSize from '@vx/responsive/build/components/ParentSize';
+import { Sticky, StickyContainer } from 'react-sticky';
+import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
+
+import BuilderComponentPane from '../../../../src/dashboard/components/BuilderComponentPane';
+import DashboardBuilder from '../../../../src/dashboard/components/DashboardBuilder';
+import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent';
+import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader';
+import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid';
+import DragDroppable from '../../../../src/dashboard/components/dnd/DragDroppable';
+
+import {
+  dashboardLayout as undoableDashboardLayout,
+  dashboardLayoutWithTabs as undoableDashboardLayoutWithTabs,
+} from '../fixtures/mockDashboardLayout';
+
+import { mockStore, mockStoreWithTabs } from '../fixtures/mockStore';
+
+const dashboardLayout = undoableDashboardLayout.present;
+const layoutWithTabs = undoableDashboardLayoutWithTabs.present;
+
+describe('DashboardBuilder', () => {
+  const props = {
+    dashboardLayout,
+    deleteTopLevelTabs() {},
+    editMode: false,
+    showBuilderPane: false,
+    handleComponentDrop() {},
+  };
+
+  function setup(overrideProps, useProvider = false, store = mockStore) {
+    const builder = <DashboardBuilder {...props} {...overrideProps} />;
+    return useProvider
+      ? mount(<Provider store={store}>{builder}</Provider>)
+      : shallow(builder);
+  }
+
+  it('should render a StickyContainer with class "dashboard"', () => {
+    const wrapper = setup();
+    const stickyContainer = wrapper.find(StickyContainer);
+    expect(stickyContainer).to.have.length(1);
+    expect(stickyContainer.prop('className')).to.equal('dashboard');
+  });
+
+  it('should add the "dashboard--editing" class if editMode=true', () => {
+    const wrapper = setup({ editMode: true });
+    const stickyContainer = wrapper.find(StickyContainer);
+    expect(stickyContainer.prop('className')).to.equal(
+      'dashboard dashboard--editing',
+    );
+  });
+
+  it('should render a DashboardHeader', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DashboardHeader)).to.have.length(1);
+  });
+
+  it('should render a DragDroppable DashboardHeader if editMode=true and no top-level Tabs exist', () => {
+    const withoutTabs = setup();
+    const withoutTabsEditMode = setup({ editMode: true });
+    const withTabs = setup({
+      dashboardLayout: layoutWithTabs,
+    });
+
+    expect(withoutTabs.find(DragDroppable)).to.have.length(0);
+    expect(withoutTabsEditMode.find(DragDroppable)).to.have.length(1);
+    expect(withTabs.find(DragDroppable)).to.have.length(0);
+  });
+
+  it('should render a Sticky top-level Tabs if the dashboard has tabs', () => {
+    const wrapper = setup(
+      { dashboardLayout: layoutWithTabs },
+      true,
+      mockStoreWithTabs,
+    );
+    const sticky = wrapper.find(Sticky);
+    const dashboardComponent = sticky.find(DashboardComponent);
+
+    const tabChildren = layoutWithTabs.TABS_ID.children;
+    expect(sticky).to.have.length(1);
+    expect(dashboardComponent).to.have.length(1 + tabChildren.length); // tab + tabs
+    expect(dashboardComponent.at(0).prop('id')).to.equal('TABS_ID');
+    tabChildren.forEach((tabId, i) => {
+      expect(dashboardComponent.at(i + 1).prop('id')).to.equal(tabId);
+    });
+  });
+
+  it('should render a TabContainer and TabContent', () => {
+    const wrapper = setup({ dashboardLayout: layoutWithTabs });
+    const parentSize = wrapper.find(ParentSize).dive();
+    expect(parentSize.find(TabContainer)).to.have.length(1);
+    expect(parentSize.find(TabContent)).to.have.length(1);
+  });
+
+  it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on TabContainer for perf', () => {
+    const wrapper = setup({ dashboardLayout: layoutWithTabs });
+    const tabProps = wrapper
+      .find(ParentSize)
+      .dive()
+      .find(TabContainer)
+      .props();
+    expect(tabProps.animation).to.equal(true);
+    expect(tabProps.mountOnEnter).to.equal(true);
+    expect(tabProps.unmountOnExit).to.equal(false);
+  });
+
+  it('should render a TabPane and DashboardGrid for each Tab', () => {
+    const wrapper = setup({ dashboardLayout: layoutWithTabs });
+    const parentSize = wrapper.find(ParentSize).dive();
+
+    const expectedCount = layoutWithTabs.TABS_ID.children.length;
+    expect(parentSize.find(TabPane)).to.have.length(expectedCount);
+    expect(parentSize.find(DashboardGrid)).to.have.length(expectedCount);
+  });
+
+  it('should render a BuilderComponentPane if editMode=showBuilderPane=true', () => {
+    const wrapper = setup();
+    expect(wrapper.find(BuilderComponentPane)).to.have.length(0);
+
+    wrapper.setProps({ ...props, editMode: true, showBuilderPane: true });
+    expect(wrapper.find(BuilderComponentPane)).to.have.length(1);
+  });
+
+  it('should change tabs if a top-level Tab is clicked', () => {
+    const wrapper = setup(
+      { dashboardLayout: layoutWithTabs },
+      true,
+      mockStoreWithTabs,
+    );
+
+    expect(wrapper.find(TabContainer).prop('activeKey')).to.equal(0);
+
+    wrapper
+      .find('.dashboard-component-tabs .nav-tabs a')
+      .at(1)
+      .simulate('click');
+
+    expect(wrapper.find(TabContainer).prop('activeKey')).to.equal(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
new file mode 100644
index 0000000..7e9de51
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent';
+import DashboardGrid from '../../../../src/dashboard/components/DashboardGrid';
+import DragDroppable from '../../../../src/dashboard/components/dnd/DragDroppable';
+import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
+
+import { DASHBOARD_GRID_TYPE } from '../../../../src/dashboard/util/componentTypes';
+import { GRID_COLUMN_COUNT } from '../../../../src/dashboard/util/constants';
+
+describe('DashboardGrid', () => {
+  const props = {
+    depth: 1,
+    editMode: false,
+    gridComponent: {
+      ...newComponentFactory(DASHBOARD_GRID_TYPE),
+      children: ['a'],
+    },
+    handleComponentDrop() {},
+    resizeComponent() {},
+    width: 500,
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<DashboardGrid {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render a div with class "dashboard-grid"', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.dashboard-grid')).to.have.length(1);
+  });
+
+  it('should render one DashboardComponent for each gridComponent child', () => {
+    const wrapper = setup({
+      gridComponent: { ...props.gridComponent, children: ['a', 'b'] },
+    });
+    expect(wrapper.find(DashboardComponent)).to.have.length(2);
+  });
+
+  it('should render two empty DragDroppables targets when editMode=true', () => {
+    const wrapper = setup({ editMode: true });
+    expect(wrapper.find(DragDroppable)).to.have.length(2);
+  });
+
+  it('should render grid column guides when resizing', () => {
+    const wrapper = setup({ editMode: true });
+    expect(wrapper.find('.grid-column-guide')).to.have.length(0);
+
+    wrapper.setState({ isResizing: true });
+
+    expect(wrapper.find('.grid-column-guide')).to.have.length(
+      GRID_COLUMN_COUNT,
+    );
+  });
+
+  it('should render a grid row guide when resizing', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.grid-row-guide')).to.have.length(0);
+    wrapper.setState({ isResizing: true, rowGuideTop: 10 });
+    expect(wrapper.find('.grid-row-guide')).to.have.length(1);
+  });
+
+  it('should call resizeComponent when a child DashboardComponent calls resizeStop', () => {
+    const resizeComponent = sinon.spy();
+    const args = { id: 'id', widthMultiple: 1, heightMultiple: 3 };
+    const wrapper = setup({ resizeComponent });
+    const dashboardComponent = wrapper.find(DashboardComponent).first();
+    dashboardComponent.prop('onResizeStop')(args);
+
+    expect(resizeComponent.callCount).to.equal(1);
+    expect(resizeComponent.getCall(0).args[0]).to.deep.equal({
+      id: 'id',
+      width: 1,
+      height: 3,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
new file mode 100644
index 0000000..545b890
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -0,0 +1,249 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import Dashboard from '../../../../src/dashboard/components/Dashboard';
+import DashboardBuilder from '../../../../src/dashboard/containers/DashboardBuilder';
+
+// mock data
+import chartQueries, { sliceId as chartId } from '../fixtures/mockChartQueries';
+import datasources from '../fixtures/mockDatasource';
+import dashboardInfo from '../fixtures/mockDashboardInfo';
+import { dashboardLayout } from '../fixtures/mockDashboardLayout';
+import dashboardState from '../fixtures/mockDashboardState';
+import sliceEntities from '../fixtures/mockSliceEntities';
+
+import { CHART_TYPE } from '../../../../src/dashboard/util/componentTypes';
+import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
+
+describe('Dashboard', () => {
+  const props = {
+    actions: {
+      addSliceToDashboard() {},
+      removeSliceFromDashboard() {},
+      runQuery() {},
+    },
+    initMessages: [],
+    dashboardState,
+    dashboardInfo,
+    charts: chartQueries,
+    slices: sliceEntities.slices,
+    datasources,
+    layout: dashboardLayout.present,
+    timeout: 60,
+    userId: dashboardInfo.userId,
+    impressionId: 'id',
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<Dashboard {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render a DashboardBuilder', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DashboardBuilder)).to.have.length(1);
+  });
+
+  describe('refreshExcept', () => {
+    const overrideCharts = {
+      ...chartQueries,
+      1001: {
+        ...chartQueries[chartId],
+        id: 1001,
+      },
+    };
+
+    const overrideSlices = {
+      ...props.slices,
+      1001: {
+        ...props.slices[chartId],
+        slice_id: 1001,
+      },
+    };
+
+    it('should call runQuery for all non-exempt slices', () => {
+      const wrapper = setup({ charts: overrideCharts, slices: overrideSlices });
+      const spy = sinon.spy(props.actions, 'runQuery');
+      wrapper.instance().refreshExcept('1001');
+      spy.restore();
+      expect(spy.callCount).to.equal(Object.keys(overrideCharts).length - 1);
+    });
+
+    it('should not call runQuery for filter_immune_slices', () => {
+      const wrapper = setup({
+        charts: overrideCharts,
+        dashboardInfo: {
+          ...dashboardInfo,
+          metadata: {
+            ...dashboardInfo.metadata,
+            filter_immune_slices: Object.keys(overrideCharts).map(id =>
+              Number(id),
+            ),
+          },
+        },
+      });
+      const spy = sinon.spy(props.actions, 'runQuery');
+      wrapper.instance().refreshExcept();
+      spy.restore();
+      expect(spy.callCount).to.equal(0);
+    });
+  });
+
+  describe('componentWillReceiveProps', () => {
+    const layoutWithExtraChart = {
+      ...props.layout,
+      1001: newComponentFactory(CHART_TYPE, { chartId: 1001 }),
+    };
+
+    it('should call addSliceToDashboard if a new slice is added to the layout', () => {
+      const wrapper = setup();
+      const spy = sinon.spy(props.actions, 'addSliceToDashboard');
+      wrapper.instance().componentWillReceiveProps({
+        ...props,
+        layout: layoutWithExtraChart,
+      });
+      spy.restore();
+      expect(spy.callCount).to.equal(1);
+    });
+
+    it('should call removeSliceFromDashboard if a slice is removed from the layout', () => {
+      const wrapper = setup({ layout: layoutWithExtraChart });
+      const spy = sinon.spy(props.actions, 'removeSliceFromDashboard');
+      const nextLayout = { ...layoutWithExtraChart };
+      delete nextLayout[1001];
+
+      wrapper.instance().componentWillReceiveProps({
+        ...props,
+        layout: nextLayout,
+      });
+      spy.restore();
+      expect(spy.callCount).to.equal(1);
+    });
+  });
+
+  describe('componentDidUpdate', () => {
+    const overrideDashboardState = {
+      ...dashboardState,
+      filters: {
+        1: { region: [] },
+        2: { country_name: ['USA'] },
+      },
+      refresh: true,
+    };
+
+    it('should not call refresh when there is no change', () => {
+      const wrapper = setup({ dashboardState: overrideDashboardState });
+      const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+      const prevProps = wrapper.instance().props;
+      wrapper.setProps({
+        dashboardState: {
+          ...overrideDashboardState,
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      refreshExceptSpy.restore();
+      expect(refreshExceptSpy.callCount).to.equal(0);
+    });
+
+    it('should call refresh if a filter is added', () => {
+      const wrapper = setup({ dashboardState: overrideDashboardState });
+      const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+      const prevProps = wrapper.instance().props;
+      wrapper.setProps({
+        dashboardState: {
+          ...overrideDashboardState,
+          filters: {
+            ...overrideDashboardState.filters,
+            3: { another_filter: ['please'] },
+          },
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      refreshExceptSpy.restore();
+      expect(refreshExceptSpy.callCount).to.equal(1);
+    });
+
+    it('should call refresh if a filter is removed', () => {
+      const wrapper = setup({ dashboardState: overrideDashboardState });
+      const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+      const prevProps = wrapper.instance().props;
+      wrapper.setProps({
+        dashboardState: {
+          ...overrideDashboardState,
+          filters: {},
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      refreshExceptSpy.restore();
+      expect(refreshExceptSpy.callCount).to.equal(1);
+    });
+
+    it('should call refresh if a filter is changed', () => {
+      const wrapper = setup({ dashboardState: overrideDashboardState });
+      const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+      const prevProps = wrapper.instance().props;
+      wrapper.setProps({
+        dashboardState: {
+          ...overrideDashboardState,
+          filters: {
+            ...overrideDashboardState.filters,
+            2: { country_name: ['Canada'] },
+          },
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      refreshExceptSpy.restore();
+      expect(refreshExceptSpy.callCount).to.equal(1);
+    });
+
+    it('should not call refresh if filters change and refresh is false', () => {
+      const wrapper = setup({ dashboardState: overrideDashboardState });
+      const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+      const prevProps = wrapper.instance().props;
+      wrapper.setProps({
+        dashboardState: {
+          ...overrideDashboardState,
+          filters: {
+            ...overrideDashboardState.filters,
+            2: { country_name: ['Canada'] },
+          },
+          refresh: false,
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      refreshExceptSpy.restore();
+      expect(refreshExceptSpy.callCount).to.equal(0);
+    });
+
+    it('should not refresh filter_immune_slices', () => {
+      const wrapper = setup({
+        dashboardState: overrideDashboardState,
+        dashboardInfo: {
+          ...dashboardInfo,
+          metadata: {
+            ...dashboardInfo.metadata,
+            filter_immune_slices: [chartId],
+          },
+        },
+      });
+      const refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
+      const prevProps = wrapper.instance().props;
+      wrapper.setProps({
+        dashboardState: {
+          ...overrideDashboardState,
+          filters: {
+            ...overrideDashboardState.filters,
+            2: { country_name: ['Canada'] },
+          },
+          refresh: false,
+        },
+      });
+      wrapper.instance().componentDidUpdate(prevProps);
+      refreshExceptSpy.restore();
+      expect(refreshExceptSpy.callCount).to.equal(0);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/RefreshIntervalModal_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx
similarity index 85%
rename from superset/assets/spec/javascripts/dashboard/RefreshIntervalModal_spec.jsx
rename to superset/assets/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx
index 3a2f700..564857c 100644
--- a/superset/assets/spec/javascripts/dashboard/RefreshIntervalModal_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx
@@ -3,7 +3,7 @@ import { mount } from 'enzyme';
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 
-import RefreshIntervalModal from '../../../src/dashboard/components/RefreshIntervalModal';
+import RefreshIntervalModal from '../../../../src/dashboard/components/RefreshIntervalModal';
 
 describe('RefreshIntervalModal', () => {
   const mockedProps = {
diff --git a/superset/assets/spec/javascripts/dashboard/components/ToastPresenter_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/ToastPresenter_spec.jsx
new file mode 100644
index 0000000..7545ad6
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/ToastPresenter_spec.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import mockMessageToasts from '../fixtures/mockMessageToasts';
+import Toast from '../../../../src/dashboard/components/Toast';
+import ToastPresenter from '../../../../src/dashboard/components/ToastPresenter';
+
+describe('ToastPresenter', () => {
+  const props = {
+    toasts: mockMessageToasts,
+    removeToast() {},
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<ToastPresenter {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render a div with class toast-presenter', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.toast-presenter')).to.have.length(1);
+  });
+
+  it('should render a Toast for each toast object', () => {
+    const wrapper = setup();
+    expect(wrapper.find(Toast)).to.have.length(props.toasts.length);
+  });
+
+  it('should pass removeToast to the Toast component', () => {
+    const removeToast = () => {};
+    const wrapper = setup({ removeToast });
+    expect(
+      wrapper
+        .find(Toast)
+        .first()
+        .prop('onCloseToast'),
+    ).to.equal(removeToast);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/Toast_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Toast_spec.jsx
new file mode 100644
index 0000000..6ed0bc5
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/Toast_spec.jsx
@@ -0,0 +1,43 @@
+import { Alert } from 'react-bootstrap';
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import mockMessageToasts from '../fixtures/mockMessageToasts';
+import Toast from '../../../../src/dashboard/components/Toast';
+
+describe('Toast', () => {
+  const props = {
+    toast: mockMessageToasts[0],
+    onCloseToast() {},
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<Toast {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render an Alert', () => {
+    const wrapper = setup();
+    expect(wrapper.find(Alert)).to.have.length(1);
+  });
+
+  it('should render toastText within the alert', () => {
+    const wrapper = setup();
+    const alert = wrapper.find(Alert).dive();
+
+    expect(alert.childAt(1).text()).to.equal(props.toast.text);
+  });
+
+  it('should call onCloseToast upon alert dismissal', done => {
+    const onCloseToast = id => {
+      expect(id).to.equal(props.toast.id);
+      done();
+    };
+    const wrapper = setup({ onCloseToast });
+    const handleClosePress = wrapper.instance().handleClosePress;
+    expect(wrapper.find(Alert).prop('onDismiss')).to.equal(handleClosePress);
+    handleClosePress(); // there is a timeout for onCloseToast to be called
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/dnd/DragDroppable_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/dnd/DragDroppable_spec.jsx
new file mode 100644
index 0000000..c7e2c2a
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/dnd/DragDroppable_spec.jsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import newComponentFactory from '../../../../../src/dashboard/util/newComponentFactory';
+import {
+  CHART_TYPE,
+  ROW_TYPE,
+} from '../../../../../src/dashboard/util/componentTypes';
+import { UnwrappedDragDroppable as DragDroppable } from '../../../../../src/dashboard/components/dnd/DragDroppable';
+
+describe('DragDroppable', () => {
+  const props = {
+    component: newComponentFactory(CHART_TYPE),
+    parentComponent: newComponentFactory(ROW_TYPE),
+    editMode: false,
+    depth: 1,
+    index: 0,
+    isDragging: false,
+    isDraggingOver: false,
+    isDraggingOverShallow: false,
+    droppableRef() {},
+    dragSourceRef() {},
+    dragPreviewRef() {},
+  };
+
+  function setup(overrideProps, shouldMount = false) {
+    const method = shouldMount ? mount : shallow;
+    const wrapper = method(<DragDroppable {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render a div with class dragdroppable', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.dragdroppable')).to.have.length(1);
+  });
+
+  it('should add class dragdroppable--dragging when dragging', () => {
+    const wrapper = setup({ isDragging: true });
+    expect(wrapper.find('.dragdroppable')).to.have.length(1);
+  });
+
+  it('should call its child function', () => {
+    const childrenSpy = sinon.spy();
+    setup({ children: childrenSpy });
+    expect(childrenSpy.callCount).to.equal(1);
+  });
+
+  it('should call its child function with "dragSourceRef" if editMode=true', () => {
+    const children = sinon.spy();
+    const dragSourceRef = () => {};
+    setup({ children, editMode: false, dragSourceRef });
+    setup({ children, editMode: true, dragSourceRef });
+
+    expect(children.getCall(0).args[0].dragSourceRef).to.equal(undefined);
+    expect(children.getCall(1).args[0].dragSourceRef).to.equal(dragSourceRef);
+  });
+
+  it('should call its child function with "dropIndicatorProps" dependent on editMode, isDraggingOver, state.dropIndicator is set', () => {
+    const children = sinon.spy();
+    const wrapper = setup({ children, editMode: false, isDraggingOver: false });
+    wrapper.setState({ dropIndicator: 'nonsense' });
+    wrapper.setProps({ ...props, editMode: true, isDraggingOver: true });
+
+    expect(children.callCount).to.equal(3); // initial + setState + setProps
+    expect(children.getCall(0).args[0].dropIndicatorProps).to.equal(undefined);
+    expect(children.getCall(2).args[0].dropIndicatorProps).to.deep.equal({
+      className: 'drop-indicator',
+    });
+  });
+
+  it('should call props.dragPreviewRef and props.droppableRef on mount', () => {
+    const dragPreviewRef = sinon.spy();
+    const droppableRef = sinon.spy();
+
+    setup({ dragPreviewRef, droppableRef }, true);
+    expect(dragPreviewRef.callCount).to.equal(1);
+    expect(droppableRef.callCount).to.equal(1);
+  });
+
+  it('should set this.mounted dependent on life cycle', () => {
+    const wrapper = setup({}, true);
+    const instance = wrapper.instance();
+    expect(instance.mounted).to.equal(true);
+    wrapper.unmount();
+    expect(instance.mounted).to.equal(false);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx
new file mode 100644
index 0000000..821b637
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx
@@ -0,0 +1,112 @@
+import { Provider } from 'react-redux';
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import Chart from '../../../../../src/dashboard/containers/Chart';
+import ChartHolder from '../../../../../src/dashboard/components/gridComponents/ChartHolder';
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
+
+import { mockStore } from '../../fixtures/mockStore';
+import { sliceId } from '../../fixtures/mockSliceEntities';
+import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+
+describe('ChartHolder', () => {
+  const props = {
+    id: String(sliceId),
+    parentId: 'ROW_ID',
+    component: mockLayout.present.CHART_ID,
+    depth: 2,
+    parentComponent: mockLayout.present.ROW_ID,
+    index: 0,
+    editMode: false,
+    availableColumnCount: 12,
+    columnWidth: 50,
+    onResizeStart() {},
+    onResize() {},
+    onResizeStop() {},
+    handleComponentDrop() {},
+    updateComponents() {},
+    deleteComponent() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <Provider store={mockStore}>
+        <WithDragDropContext>
+          <ChartHolder {...props} {...overrideProps} />
+        </WithDragDropContext>
+      </Provider>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should render a ResizableContainer', () => {
+    const wrapper = setup();
+    expect(wrapper.find(ResizableContainer)).to.have.length(1);
+  });
+
+  it('should only have an adjustableWidth if its parent is a Row', () => {
+    let wrapper = setup();
+    expect(wrapper.find(ResizableContainer).prop('adjustableWidth')).to.equal(
+      true,
+    );
+
+    wrapper = setup({ ...props, parentComponent: mockLayout.present.CHART_ID });
+    expect(wrapper.find(ResizableContainer).prop('adjustableWidth')).to.equal(
+      false,
+    );
+  });
+
+  it('should pass correct props to ResizableContainer', () => {
+    const wrapper = setup();
+    const resizableProps = wrapper.find(ResizableContainer).props();
+    expect(resizableProps.widthStep).to.equal(props.columnWidth);
+    expect(resizableProps.widthMultiple).to.equal(props.component.meta.width);
+    expect(resizableProps.heightMultiple).to.equal(props.component.meta.height);
+    expect(resizableProps.maxWidthMultiple).to.equal(
+      props.component.meta.width + props.availableColumnCount,
+    );
+  });
+
+  it('should render a div with class "dashboard-component-chart-holder"', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.dashboard-component-chart-holder')).to.have.length(1);
+  });
+
+  it('should render a Chart', () => {
+    const wrapper = setup();
+    expect(wrapper.find(Chart)).to.have.length(1);
+  });
+
+  it('should render a HoverMenu with DeleteComponentButton in editMode', () => {
+    let wrapper = setup();
+    expect(wrapper.find(HoverMenu)).to.have.length(0);
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+
+    // we cannot set props on the Divider because of the WithDragDropContext wrapper
+    wrapper = setup({ editMode: true });
+    expect(wrapper.find(HoverMenu)).to.have.length(1);
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+  });
+
+  it('should call deleteComponent when deleted', () => {
+    const deleteComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, deleteComponent });
+    wrapper.find(DeleteComponentButton).simulate('click');
+    expect(deleteComponent.callCount).to.equal(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
new file mode 100644
index 0000000..05756f4
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import Chart from '../../../../../src/dashboard/components/gridComponents/Chart';
+import SliceHeader from '../../../../../src/dashboard/components/SliceHeader';
+import ChartContainer from '../../../../../src/chart/ChartContainer';
+
+import mockDatasource from '../../fixtures/mockDatasource';
+import sliceEntities, { sliceId } from '../../fixtures/mockSliceEntities';
+import chartQueries, {
+  sliceId as queryId,
+} from '../../fixtures/mockChartQueries';
+
+describe('Chart', () => {
+  const props = {
+    id: sliceId,
+    width: 100,
+    height: 100,
+    updateSliceName() {},
+
+    // from redux
+    chart: chartQueries[queryId],
+    formData: chartQueries[queryId].formData,
+    datasource: mockDatasource[sliceEntities.slices[sliceId].datasource],
+    slice: {
+      ...sliceEntities.slices[sliceId],
+      description_markeddown: 'markdown',
+    },
+    sliceName: sliceEntities.slices[sliceId].slice_name,
+    timeout: 60,
+    filters: {},
+    refreshChart() {},
+    toggleExpandSlice() {},
+    addFilter() {},
+    removeFilter() {},
+    editMode: false,
+    isExpanded: false,
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<Chart {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render a SliceHeader', () => {
+    const wrapper = setup();
+    expect(wrapper.find(SliceHeader)).to.have.length(1);
+  });
+
+  it('should render a ChartContainer', () => {
+    const wrapper = setup();
+    expect(wrapper.find(ChartContainer)).to.have.length(1);
+  });
+
+  it('should render a description if it has one and isExpanded=true', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.slice_description')).to.have.length(0);
+
+    wrapper.setProps({ ...props, isExpanded: true });
+    expect(wrapper.find('.slice_description')).to.have.length(1);
+  });
+
+  it('should call refreshChart when SliceHeader calls forceRefresh', () => {
+    const refreshChart = sinon.spy();
+    const wrapper = setup({ refreshChart });
+    wrapper.instance().forceRefresh();
+    expect(refreshChart.callCount).to.equal(1);
+  });
+
+  it('should call addFilter when ChartContainer calls addFilter', () => {
+    const addFilter = sinon.spy();
+    const wrapper = setup({ addFilter });
+    wrapper.instance().addFilter();
+    expect(addFilter.callCount).to.equal(1);
+  });
+
+  it('should call removeFilter when ChartContainer calls removeFilter', () => {
+    const removeFilter = sinon.spy();
+    const wrapper = setup({ removeFilter });
+    wrapper.instance().removeFilter();
+    expect(removeFilter.callCount).to.equal(1);
+  });
+
+  it('should return props.filters when its getFilters method is called', () => {
+    const filters = { column: ['value'] };
+    const wrapper = setup({ filters });
+    expect(wrapper.instance().getFilters()).to.equal(filters);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Column_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Column_spec.jsx
new file mode 100644
index 0000000..e97414b
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Column_spec.jsx
@@ -0,0 +1,144 @@
+import { Provider } from 'react-redux';
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import BackgroundStyleDropdown from '../../../../../src/dashboard/components/menu/BackgroundStyleDropdown';
+import Column from '../../../../../src/dashboard/components/gridComponents/Column';
+import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+import IconButton from '../../../../../src/dashboard/components/IconButton';
+import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
+import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
+
+import { mockStore } from '../../fixtures/mockStore';
+import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+
+describe('Column', () => {
+  const columnWithoutChildren = {
+    ...mockLayout.present.COLUMN_ID,
+    children: [],
+  };
+  const props = {
+    id: 'COLUMN_ID',
+    parentId: 'ROW_ID',
+    component: mockLayout.present.COLUMN_ID,
+    parentComponent: mockLayout.present.ROW_ID,
+    index: 0,
+    depth: 2,
+    editMode: false,
+    availableColumnCount: 12,
+    minColumnWidth: 2,
+    columnWidth: 50,
+    occupiedColumnCount: 6,
+    onResizeStart() {},
+    onResize() {},
+    onResizeStop() {},
+    handleComponentDrop() {},
+    deleteComponent() {},
+    updateComponents() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <Provider store={mockStore}>
+        <WithDragDropContext>
+          <Column {...props} {...overrideProps} />
+        </WithDragDropContext>
+      </Provider>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    // don't count child DragDroppables
+    const wrapper = setup({ component: columnWithoutChildren });
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should render a WithPopoverMenu', () => {
+    // don't count child DragDroppables
+    const wrapper = setup({ component: columnWithoutChildren });
+    expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
+  });
+
+  it('should render a ResizableContainer', () => {
+    // don't count child DragDroppables
+    const wrapper = setup({ component: columnWithoutChildren });
+    expect(wrapper.find(ResizableContainer)).to.have.length(1);
+  });
+
+  it('should render a HoverMenu in editMode', () => {
+    let wrapper = setup({ component: columnWithoutChildren });
+    expect(wrapper.find(HoverMenu)).to.have.length(0);
+
+    // we cannot set props on the Row because of the WithDragDropContext wrapper
+    wrapper = setup({ component: columnWithoutChildren, editMode: true });
+    expect(wrapper.find(HoverMenu)).to.have.length(1);
+  });
+
+  it('should render a DeleteComponentButton in editMode', () => {
+    let wrapper = setup({ component: columnWithoutChildren });
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+
+    // we cannot set props on the Row because of the WithDragDropContext wrapper
+    wrapper = setup({ component: columnWithoutChildren, editMode: true });
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+  });
+
+  it('should render a BackgroundStyleDropdown when focused', () => {
+    let wrapper = setup({ component: columnWithoutChildren });
+    expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(0);
+
+    // we cannot set props on the Row because of the WithDragDropContext wrapper
+    wrapper = setup({ component: columnWithoutChildren, editMode: true });
+    wrapper
+      .find(IconButton)
+      .at(1) // first one is delete button
+      .simulate('click');
+
+    expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(1);
+  });
+
+  it('should call deleteComponent when deleted', () => {
+    const deleteComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, deleteComponent });
+    wrapper.find(DeleteComponentButton).simulate('click');
+    expect(deleteComponent.callCount).to.equal(1);
+  });
+
+  it('should pass its own width as availableColumnCount to children', () => {
+    const wrapper = setup();
+    const dashboardComponent = wrapper.find(DashboardComponent).first();
+    expect(dashboardComponent.props().availableColumnCount).to.equal(
+      props.component.meta.width,
+    );
+  });
+
+  it('should pass appropriate dimensions to ResizableContainer', () => {
+    const wrapper = setup({ component: columnWithoutChildren });
+    const columnWidth = columnWithoutChildren.meta.width;
+    const resizableProps = wrapper.find(ResizableContainer).props();
+    expect(resizableProps.adjustableWidth).to.equal(true);
+    expect(resizableProps.adjustableHeight).to.equal(false);
+    expect(resizableProps.widthStep).to.equal(props.columnWidth);
+    expect(resizableProps.widthMultiple).to.equal(columnWidth);
+    expect(resizableProps.minWidthMultiple).to.equal(props.minColumnWidth);
+    expect(resizableProps.maxWidthMultiple).to.equal(
+      props.availableColumnCount + columnWidth,
+    );
+  });
+
+  it('should increment the depth of its children', () => {
+    const wrapper = setup();
+    const dashboardComponent = wrapper.find(DashboardComponent);
+    expect(dashboardComponent.props().depth).to.equal(props.depth + 1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Divider_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Divider_spec.jsx
new file mode 100644
index 0000000..c8317f8
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Divider_spec.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import Divider from '../../../../../src/dashboard/components/gridComponents/Divider';
+import newComponentFactory from '../../../../../src/dashboard/util/newComponentFactory';
+import {
+  DIVIDER_TYPE,
+  DASHBOARD_GRID_TYPE,
+} from '../../../../../src/dashboard/util/componentTypes';
+
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+
+describe('Divider', () => {
+  const props = {
+    id: 'id',
+    parentId: 'parentId',
+    component: newComponentFactory(DIVIDER_TYPE),
+    depth: 1,
+    parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
+    index: 0,
+    editMode: false,
+    handleComponentDrop() {},
+    deleteComponent() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <WithDragDropContext>
+        <Divider {...props} {...overrideProps} />
+      </WithDragDropContext>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should render a div with class "dashboard-component-divider"', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.dashboard-component-divider')).to.have.length(1);
+  });
+
+  it('should render a HoverMenu with DeleteComponentButton in editMode', () => {
+    let wrapper = setup();
+    expect(wrapper.find(HoverMenu)).to.have.length(0);
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+
+    // we cannot set props on the Divider because of the WithDragDropContext wrapper
+    wrapper = setup({ editMode: true });
+    expect(wrapper.find(HoverMenu)).to.have.length(1);
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+  });
+
+  it('should call deleteComponent when deleted', () => {
+    const deleteComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, deleteComponent });
+    wrapper.find(DeleteComponentButton).simulate('click');
+    expect(deleteComponent.callCount).to.equal(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx
new file mode 100644
index 0000000..1d54775
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Header_spec.jsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import EditableTitle from '../../../../../src/components/EditableTitle';
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import Header from '../../../../../src/dashboard/components/gridComponents/Header';
+import newComponentFactory from '../../../../../src/dashboard/util/newComponentFactory';
+import {
+  HEADER_TYPE,
+  DASHBOARD_GRID_TYPE,
+} from '../../../../../src/dashboard/util/componentTypes';
+
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+
+describe('Header', () => {
+  const props = {
+    id: 'id',
+    parentId: 'parentId',
+    component: newComponentFactory(HEADER_TYPE),
+    depth: 1,
+    parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
+    index: 0,
+    editMode: false,
+    handleComponentDrop() {},
+    deleteComponent() {},
+    updateComponents() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <WithDragDropContext>
+        <Header {...props} {...overrideProps} />
+      </WithDragDropContext>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should render a WithPopoverMenu', () => {
+    const wrapper = setup();
+    expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
+  });
+
+  it('should render a HoverMenu in editMode', () => {
+    let wrapper = setup();
+    expect(wrapper.find(HoverMenu)).to.have.length(0);
+
+    // we cannot set props on the Header because of the WithDragDropContext wrapper
+    wrapper = setup({ editMode: true });
+    expect(wrapper.find(HoverMenu)).to.have.length(1);
+  });
+
+  it('should render an EditableTitle with meta.text', () => {
+    const wrapper = setup();
+    expect(wrapper.find(EditableTitle)).to.have.length(1);
+    expect(wrapper.find('input').prop('value')).to.equal(
+      props.component.meta.text,
+    );
+  });
+
+  it('should call updateComponents when EditableTitle changes', () => {
+    const updateComponents = sinon.spy();
+    const wrapper = setup({ editMode: true, updateComponents });
+    wrapper.find(EditableTitle).prop('onSaveTitle')('New title');
+
+    const headerId = props.component.id;
+    expect(updateComponents.callCount).to.equal(1);
+    expect(updateComponents.getCall(0).args[0][headerId].meta.text).to.equal(
+      'New title',
+    );
+  });
+
+  it('should render a DeleteComponentButton when focused in editMode', () => {
+    const wrapper = setup({ editMode: true });
+    wrapper.find(WithPopoverMenu).simulate('click'); // focus
+
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+  });
+
+  it('should call deleteComponent when deleted', () => {
+    const deleteComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, deleteComponent });
+    wrapper.find(WithPopoverMenu).simulate('click'); // focus
+    wrapper.find(DeleteComponentButton).simulate('click');
+
+    expect(deleteComponent.callCount).to.equal(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Row_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Row_spec.jsx
new file mode 100644
index 0000000..a718ff4
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Row_spec.jsx
@@ -0,0 +1,120 @@
+import { Provider } from 'react-redux';
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import BackgroundStyleDropdown from '../../../../../src/dashboard/components/menu/BackgroundStyleDropdown';
+import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+import IconButton from '../../../../../src/dashboard/components/IconButton';
+import Row from '../../../../../src/dashboard/components/gridComponents/Row';
+import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
+
+import { mockStore } from '../../fixtures/mockStore';
+import { DASHBOARD_GRID_ID } from '../../../../../src/dashboard/util/constants';
+import { dashboardLayout as mockLayout } from '../../fixtures/mockDashboardLayout';
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+
+describe('Row', () => {
+  const rowWithoutChildren = { ...mockLayout.present.ROW_ID, children: [] };
+  const props = {
+    id: 'ROW_ID',
+    parentId: DASHBOARD_GRID_ID,
+    component: mockLayout.present.ROW_ID,
+    parentComponent: mockLayout.present[DASHBOARD_GRID_ID],
+    index: 0,
+    depth: 2,
+    editMode: false,
+    availableColumnCount: 12,
+    columnWidth: 50,
+    occupiedColumnCount: 6,
+    onResizeStart() {},
+    onResize() {},
+    onResizeStop() {},
+    handleComponentDrop() {},
+    deleteComponent() {},
+    updateComponents() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <Provider store={mockStore}>
+        <WithDragDropContext>
+          <Row {...props} {...overrideProps} />
+        </WithDragDropContext>
+      </Provider>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    // don't count child DragDroppables
+    const wrapper = setup({ component: rowWithoutChildren });
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should render a WithPopoverMenu', () => {
+    // don't count child DragDroppables
+    const wrapper = setup({ component: rowWithoutChildren });
+    expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
+  });
+
+  it('should render a HoverMenu in editMode', () => {
+    let wrapper = setup({ component: rowWithoutChildren });
+    expect(wrapper.find(HoverMenu)).to.have.length(0);
+
+    // we cannot set props on the Row because of the WithDragDropContext wrapper
+    wrapper = setup({ component: rowWithoutChildren, editMode: true });
+    expect(wrapper.find(HoverMenu)).to.have.length(1);
+  });
+
+  it('should render a DeleteComponentButton in editMode', () => {
+    let wrapper = setup({ component: rowWithoutChildren });
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+
+    // we cannot set props on the Row because of the WithDragDropContext wrapper
+    wrapper = setup({ component: rowWithoutChildren, editMode: true });
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+  });
+
+  it('should render a BackgroundStyleDropdown when focused', () => {
+    let wrapper = setup({ component: rowWithoutChildren });
+    expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(0);
+
+    // we cannot set props on the Row because of the WithDragDropContext wrapper
+    wrapper = setup({ component: rowWithoutChildren, editMode: true });
+    wrapper
+      .find(IconButton)
+      .at(1) // first one is delete button
+      .simulate('click');
+
+    expect(wrapper.find(BackgroundStyleDropdown)).to.have.length(1);
+  });
+
+  it('should call deleteComponent when deleted', () => {
+    const deleteComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, deleteComponent });
+    wrapper.find(DeleteComponentButton).simulate('click');
+    expect(deleteComponent.callCount).to.equal(1);
+  });
+
+  it('should pass appropriate availableColumnCount to children', () => {
+    const wrapper = setup();
+    const dashboardComponent = wrapper.find(DashboardComponent).first();
+    expect(dashboardComponent.props().availableColumnCount).to.equal(
+      props.availableColumnCount - props.occupiedColumnCount,
+    );
+  });
+
+  it('should increment the depth of its children', () => {
+    const wrapper = setup();
+    const dashboardComponent = wrapper.find(DashboardComponent).first();
+    expect(dashboardComponent.props().depth).to.equal(props.depth + 1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx
new file mode 100644
index 0000000..a984565
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx
@@ -0,0 +1,126 @@
+import { Provider } from 'react-redux';
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import EditableTitle from '../../../../../src/components/EditableTitle';
+import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
+import Tab, {
+  RENDER_TAB,
+  RENDER_TAB_CONTENT,
+} from '../../../../../src/dashboard/components/gridComponents/Tab';
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+import { dashboardLayoutWithTabs } from '../../fixtures/mockDashboardLayout';
+import { mockStoreWithTabs } from '../../fixtures/mockStore';
+
+describe('Tabs', () => {
+  const props = {
+    id: 'TAB_ID',
+    parentId: 'TABS_ID',
+    component: dashboardLayoutWithTabs.present.TAB_ID,
+    parentComponent: dashboardLayoutWithTabs.present.TABS_ID,
+    index: 0,
+    depth: 1,
+    editMode: false,
+    renderType: RENDER_TAB,
+    onDropOnTab() {},
+    onDeleteTab() {},
+    availableColumnCount: 12,
+    columnWidth: 50,
+    onResizeStart() {},
+    onResize() {},
+    onResizeStop() {},
+    createComponent() {},
+    handleComponentDrop() {},
+    onChangeTab() {},
+    deleteComponent() {},
+    updateComponents() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <Provider store={mockStoreWithTabs}>
+        <WithDragDropContext>
+          <Tab {...props} {...overrideProps} />
+        </WithDragDropContext>
+      </Provider>,
+    );
+    return wrapper;
+  }
+
+  describe('renderType=RENDER_TAB', () => {
+    it('should render a DragDroppable', () => {
+      const wrapper = setup();
+      expect(wrapper.find(DragDroppable)).to.have.length(1);
+    });
+
+    it('should render an EditableTitle with meta.text', () => {
+      const wrapper = setup();
+      const title = wrapper.find(EditableTitle);
+      expect(title).to.have.length(1);
+      expect(title.find('input').prop('value')).to.equal(
+        props.component.meta.text,
+      );
+    });
+
+    it('should call updateComponents when EditableTitle changes', () => {
+      const updateComponents = sinon.spy();
+      const wrapper = setup({ editMode: true, updateComponents });
+      wrapper.find(EditableTitle).prop('onSaveTitle')('New title');
+
+      expect(updateComponents.callCount).to.equal(1);
+      expect(updateComponents.getCall(0).args[0].TAB_ID.meta.text).to.equal(
+        'New title',
+      );
+    });
+
+    it('should render a WithPopoverMenu', () => {
+      const wrapper = setup();
+      expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
+    });
+
+    it('should render a DeleteComponentButton when focused if its not the only tab', () => {
+      let wrapper = setup();
+      wrapper.find(WithPopoverMenu).simulate('click'); // focus
+      expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+
+      wrapper = setup({ editMode: true });
+      wrapper.find(WithPopoverMenu).simulate('click');
+      expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+
+      wrapper = setup({
+        editMode: true,
+        parentComponent: {
+          ...props.parentComponent,
+          children: props.parentComponent.children.slice(0, 1),
+        },
+      });
+      wrapper.find(WithPopoverMenu).simulate('click');
+      expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+    });
+
+    it('should call deleteComponent when deleted', () => {
+      const deleteComponent = sinon.spy();
+      const wrapper = setup({ editMode: true, deleteComponent });
+      wrapper.find(WithPopoverMenu).simulate('click'); // focus
+      wrapper.find(DeleteComponentButton).simulate('click');
+
+      expect(deleteComponent.callCount).to.equal(1);
+    });
+  });
+
+  describe('renderType=RENDER_TAB_CONTENT', () => {
+    it('should render a DashboardComponent', () => {
+      const wrapper = setup({ renderType: RENDER_TAB_CONTENT });
+      // We expect 2 because this Tab has a Row child and the row has a Chart
+      expect(wrapper.find(DashboardComponent)).to.have.length(2);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
new file mode 100644
index 0000000..d521fe5
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
@@ -0,0 +1,140 @@
+import { Provider } from 'react-redux';
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
+
+import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
+import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
+import Tabs from '../../../../../src/dashboard/components/gridComponents/Tabs';
+import WithDragDropContext from '../../helpers/WithDragDropContext';
+import { dashboardLayoutWithTabs } from '../../fixtures/mockDashboardLayout';
+import { mockStoreWithTabs } from '../../fixtures/mockStore';
+import { DASHBOARD_ROOT_ID } from '../../../../../src/dashboard/util/constants';
+
+describe('Tabs', () => {
+  const props = {
+    id: 'TABS_ID',
+    parentId: DASHBOARD_ROOT_ID,
+    component: dashboardLayoutWithTabs.present.TABS_ID,
+    parentComponent: dashboardLayoutWithTabs.present[DASHBOARD_ROOT_ID],
+    index: 0,
+    depth: 1,
+    renderTabContent: true,
+    editMode: false,
+    availableColumnCount: 12,
+    columnWidth: 50,
+    onResizeStart() {},
+    onResize() {},
+    onResizeStop() {},
+    createComponent() {},
+    handleComponentDrop() {},
+    onChangeTab() {},
+    deleteComponent() {},
+    updateComponents() {},
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <Provider store={mockStoreWithTabs}>
+        <WithDragDropContext>
+          <Tabs {...props} {...overrideProps} />
+        </WithDragDropContext>
+      </Provider>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    // test just Tabs with no children DragDroppables
+    const wrapper = setup({ component: { ...props.component, children: [] } });
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should render BootstrapTabs', () => {
+    const wrapper = setup();
+    expect(wrapper.find(BootstrapTabs)).to.have.length(1);
+  });
+
+  it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on BootstrapTabs for perf', () => {
+    const wrapper = setup();
+    const tabProps = wrapper.find(BootstrapTabs).props();
+    expect(tabProps.animation).to.equal(true);
+    expect(tabProps.mountOnEnter).to.equal(true);
+    expect(tabProps.unmountOnExit).to.equal(false);
+  });
+
+  it('should render a BootstrapTab for each child', () => {
+    const wrapper = setup();
+    expect(wrapper.find(BootstrapTab)).to.have.length(
+      props.component.children.length,
+    );
+  });
+
+  it('should render an extra (+) BootstrapTab in editMode', () => {
+    const wrapper = setup({ editMode: true });
+    expect(wrapper.find(BootstrapTab)).to.have.length(
+      props.component.children.length + 1,
+    );
+  });
+
+  it('should render a DashboardComponent for each child', () => {
+    // note: this does not test Tab content
+    const wrapper = setup({ renderTabContent: false });
+    expect(wrapper.find(DashboardComponent)).to.have.length(
+      props.component.children.length,
+    );
+  });
+
+  it('should call createComponent if the (+) tab is clicked', () => {
+    const createComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, createComponent });
+    wrapper
+      .find('.dashboard-component-tabs .nav-tabs a')
+      .last()
+      .simulate('click');
+
+    expect(createComponent.callCount).to.equal(1);
+  });
+
+  it('should call onChangeTab when a tab is clicked', () => {
+    const onChangeTab = sinon.spy();
+    const wrapper = setup({ editMode: true, onChangeTab });
+    wrapper
+      .find('.dashboard-component-tabs .nav-tabs a')
+      .at(1) // will not call if it is already selected
+      .simulate('click');
+
+    expect(onChangeTab.callCount).to.equal(1);
+  });
+
+  it('should render a HoverMenu in editMode', () => {
+    let wrapper = setup();
+    expect(wrapper.find(HoverMenu)).to.have.length(0);
+
+    wrapper = setup({ editMode: true });
+    expect(wrapper.find(HoverMenu)).to.have.length(1);
+  });
+
+  it('should render a DeleteComponentButton in editMode', () => {
+    let wrapper = setup();
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+
+    wrapper = setup({ editMode: true });
+    expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+  });
+
+  it('should call deleteComponent when deleted', () => {
+    const deleteComponent = sinon.spy();
+    const wrapper = setup({ editMode: true, deleteComponent });
+    wrapper.find(DeleteComponentButton).simulate('click');
+
+    expect(deleteComponent.callCount).to.equal(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/DraggableNewComponent_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/DraggableNewComponent_spec.jsx
new file mode 100644
index 0000000..4334b37
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/DraggableNewComponent_spec.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DragDroppable from '../../../../../../src/dashboard/components/dnd/DragDroppable';
+import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
+import WithDragDropContext from '../../../helpers/WithDragDropContext';
+
+import { NEW_COMPONENTS_SOURCE_ID } from '../../../../../../src/dashboard/util/constants';
+import {
+  NEW_COMPONENT_SOURCE_TYPE,
+  CHART_TYPE,
+} from '../../../../../../src/dashboard/util/componentTypes';
+
+describe('DraggableNewComponent', () => {
+  const props = {
+    id: 'id',
+    type: CHART_TYPE,
+    label: 'label!',
+    className: 'a_class',
+  };
+
+  function setup(overrideProps) {
+    // We have to wrap provide DragDropContext for the underlying DragDroppable
+    // otherwise we cannot assert on DragDroppable children
+    const wrapper = mount(
+      <WithDragDropContext>
+        <DraggableNewComponent {...props} {...overrideProps} />
+      </WithDragDropContext>,
+    );
+    return wrapper;
+  }
+
+  it('should render a DragDroppable', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DragDroppable)).to.have.length(1);
+  });
+
+  it('should pass component={ type, id } to DragDroppable', () => {
+    const wrapper = setup();
+    const dragdroppable = wrapper.find(DragDroppable);
+    expect(dragdroppable.prop('component')).to.deep.equal({
+      id: props.id,
+      type: props.type,
+    });
+  });
+
+  it('should pass appropriate parent source and id to DragDroppable', () => {
+    const wrapper = setup();
+    const dragdroppable = wrapper.find(DragDroppable);
+    expect(dragdroppable.prop('parentComponent')).to.deep.equal({
+      id: NEW_COMPONENTS_SOURCE_ID,
+      type: NEW_COMPONENT_SOURCE_TYPE,
+    });
+  });
+
+  it('should render the passed label', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.new-component').text()).to.equal(props.label);
+  });
+
+  it('should add the passed className', () => {
+    const wrapper = setup();
+    const className = `.new-component-placeholder.${props.className}`;
+    expect(wrapper.find(className)).to.have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewColumn_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewColumn_spec.jsx
new file mode 100644
index 0000000..cb07cb9
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewColumn_spec.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
+import NewColumn from '../../../../../../src/dashboard/components/gridComponents/new/NewColumn';
+
+import { NEW_COLUMN_ID } from '../../../../../../src/dashboard/util/constants';
+import { COLUMN_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
+
+describe('NewColumn', () => {
+  function setup() {
+    return shallow(<NewColumn />);
+  }
+
+  it('should render a DraggableNewComponent', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
+  });
+
+  it('should set appropriate type and id', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent).props()).to.include({
+      type: COLUMN_TYPE,
+      id: NEW_COLUMN_ID,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewDivider_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewDivider_spec.jsx
new file mode 100644
index 0000000..71703b3
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewDivider_spec.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
+import NewDivider from '../../../../../../src/dashboard/components/gridComponents/new/NewDivider';
+
+import { NEW_DIVIDER_ID } from '../../../../../../src/dashboard/util/constants';
+import { DIVIDER_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
+
+describe('NewDivider', () => {
+  function setup() {
+    return shallow(<NewDivider />);
+  }
+
+  it('should render a DraggableNewComponent', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
+  });
+
+  it('should set appropriate type and id', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent).props()).to.include({
+      type: DIVIDER_TYPE,
+      id: NEW_DIVIDER_ID,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewHeader_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewHeader_spec.jsx
new file mode 100644
index 0000000..a499fe8
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewHeader_spec.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
+import NewHeader from '../../../../../../src/dashboard/components/gridComponents/new/NewHeader';
+
+import { NEW_HEADER_ID } from '../../../../../../src/dashboard/util/constants';
+import { HEADER_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
+
+describe('NewHeader', () => {
+  function setup() {
+    return shallow(<NewHeader />);
+  }
+
+  it('should render a DraggableNewComponent', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
+  });
+
+  it('should set appropriate type and id', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent).props()).to.include({
+      type: HEADER_TYPE,
+      id: NEW_HEADER_ID,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewRow_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewRow_spec.jsx
new file mode 100644
index 0000000..e91893d
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewRow_spec.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
+import NewRow from '../../../../../../src/dashboard/components/gridComponents/new/NewRow';
+
+import { NEW_ROW_ID } from '../../../../../../src/dashboard/util/constants';
+import { ROW_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
+
+describe('NewRow', () => {
+  function setup() {
+    return shallow(<NewRow />);
+  }
+
+  it('should render a DraggableNewComponent', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
+  });
+
+  it('should set appropriate type and id', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent).props()).to.include({
+      type: ROW_TYPE,
+      id: NEW_ROW_ID,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewTabs_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewTabs_spec.jsx
new file mode 100644
index 0000000..4e71c8c
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/new/NewTabs_spec.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DraggableNewComponent from '../../../../../../src/dashboard/components/gridComponents/new/DraggableNewComponent';
+import NewTabs from '../../../../../../src/dashboard/components/gridComponents/new/NewTabs';
+
+import { NEW_TABS_ID } from '../../../../../../src/dashboard/util/constants';
+import { TABS_TYPE } from '../../../../../../src/dashboard/util/componentTypes';
+
+describe('NewTabs', () => {
+  function setup() {
+    return shallow(<NewTabs />);
+  }
+
+  it('should render a DraggableNewComponent', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent)).to.have.length(1);
+  });
+
+  it('should set appropriate type and id', () => {
+    const wrapper = setup();
+    expect(wrapper.find(DraggableNewComponent).props()).to.include({
+      type: TABS_TYPE,
+      id: NEW_TABS_ID,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/menu/HoverMenu_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/menu/HoverMenu_spec.jsx
new file mode 100644
index 0000000..1f85085
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/menu/HoverMenu_spec.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import HoverMenu from '../../../../../src/dashboard/components/menu/HoverMenu';
+
+describe('HoverMenu', () => {
+  it('should render a div.hover-menu', () => {
+    const wrapper = shallow(<HoverMenu />);
+    expect(wrapper.find('.hover-menu')).to.have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/menu/WithPopoverMenu_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/menu/WithPopoverMenu_spec.jsx
new file mode 100644
index 0000000..5add770
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/menu/WithPopoverMenu_spec.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
+
+describe('WithPopoverMenu', () => {
+  const props = {
+    children: <div id="child" />,
+    disableClick: false,
+    menuItems: [<div id="menu1" />, <div id="menu2" />],
+    onChangeFocus() {},
+    shouldFocus: () => true, // needed for mock
+    isFocused: false,
+    editMode: false,
+  };
+
+  function setup(overrideProps) {
+    const wrapper = shallow(<WithPopoverMenu {...props} {...overrideProps} />);
+    return wrapper;
+  }
+
+  it('should render a div with class "with-popover-menu"', () => {
+    const wrapper = setup();
+    expect(wrapper.find('.with-popover-menu')).to.have.length(1);
+  });
+
+  it('should render the passed children', () => {
+    const wrapper = setup();
+    expect(wrapper.find('#child')).to.have.length(1);
+  });
+
+  it('should focus on click in editMode', () => {
+    const wrapper = setup();
+    expect(wrapper.state('isFocused')).to.equal(false);
+
+    wrapper.simulate('click');
+    expect(wrapper.state('isFocused')).to.equal(false);
+
+    wrapper.setProps({ ...props, editMode: true });
+    wrapper.simulate('click');
+    expect(wrapper.state('isFocused')).to.equal(true);
+  });
+
+  it('should render menuItems when focused', () => {
+    const wrapper = setup({ editMode: true });
+    expect(wrapper.find('#menu1')).to.have.length(0);
+    expect(wrapper.find('#menu2')).to.have.length(0);
+
+    wrapper.simulate('click');
+    expect(wrapper.find('#menu1')).to.have.length(1);
+    expect(wrapper.find('#menu2')).to.have.length(1);
+  });
+
+  it('should not focus when disableClick=true', () => {
+    const wrapper = setup({ disableClick: true, editMode: true });
+    expect(wrapper.state('isFocused')).to.equal(false);
+
+    wrapper.simulate('click');
+    expect(wrapper.state('isFocused')).to.equal(false);
+  });
+
+  it('should use the passed shouldFocus func to determine if it should focus', () => {
+    const wrapper = setup({ editMode: true, shouldFocus: () => false });
+    expect(wrapper.state('isFocused')).to.equal(false);
+
+    wrapper.simulate('click');
+    expect(wrapper.state('isFocused')).to.equal(false);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/resizable/ResizableContainer_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/resizable/ResizableContainer_spec.jsx
new file mode 100644
index 0000000..69fca76
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/resizable/ResizableContainer_spec.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import Resizable from 're-resizable';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import ResizableContainer from '../../../../../src/dashboard/components/resizable/ResizableContainer';
+
+describe('ResizableContainer', () => {
+  const props = { editMode: false, id: 'id' };
+
+  function setup(propOverrides) {
+    return shallow(<ResizableContainer {...props} {...propOverrides} />);
+  }
+
+  it('should render a Resizable', () => {
+    const wrapper = setup();
+    expect(wrapper.find(Resizable)).to.have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/components/resizable/ResizableHandle_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/resizable/ResizableHandle_spec.jsx
new file mode 100644
index 0000000..0c37855
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/components/resizable/ResizableHandle_spec.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import ResizableHandle from '../../../../../src/dashboard/components/resizable/ResizableHandle';
+
+describe('ResizableHandle', () => {
+  it('should render a right resize handle', () => {
+    const wrapper = shallow(<ResizableHandle.right />);
+    expect(wrapper.find('.resize-handle.resize-handle--right')).to.have.length(
+      1,
+    );
+  });
+
+  it('should render a bottom resize handle', () => {
+    const wrapper = shallow(<ResizableHandle.bottom />);
+    expect(wrapper.find('.resize-handle.resize-handle--bottom')).to.have.length(
+      1,
+    );
+  });
+
+  it('should render a bottomRight resize handle', () => {
+    const wrapper = shallow(<ResizableHandle.bottomRight />);
+    expect(
+      wrapper.find('.resize-handle.resize-handle--bottom-right'),
+    ).to.have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
index 1565ccd..7a12454 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
@@ -12,11 +12,13 @@ export const regionFilter = {
   form_data: {
     datasource: '2__table',
     date_filter: false,
-    filters: [{
-      col: 'country_name',
-      op: 'in',
-      val: ['United States', 'France', 'Japan'],
-    }],
+    filters: [
+      {
+        col: 'country_name',
+        op: 'in',
+        val: ['United States', 'France', 'Japan'],
+      },
+    ],
     granularity_sqla: null,
     groupby: ['region', 'country_name'],
     having: '',
@@ -35,7 +37,8 @@ export const regionFilter = {
   },
   slice_id: 256,
   slice_name: 'Region Filters',
-  slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20256%7D',
+  slice_url:
+    '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20256%7D',
 };
 export const countryFilter = {
   datasource: null,
@@ -64,7 +67,8 @@ export const countryFilter = {
   },
   slice_id: 257,
   slice_name: 'Country Filters',
-  slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20257%7D',
+  slice_url:
+    '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20257%7D',
 };
 export const slice = {
   datasource: null,
@@ -115,7 +119,8 @@ export const slice = {
   },
   slice_id: 248,
   slice_name: 'Filtered Population',
-  slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
+  slice_url:
+    '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
 };
 
 const mockDashboardData = {
@@ -152,12 +157,15 @@ const mockDashboardData = {
   standalone_mode: false,
 };
 export const {
-  dashboardState, dashboardInfo,
-  charts, datasources, sliceEntities,
-  dashboardLayout } = getInitialState({
+  dashboardState,
+  dashboardInfo,
+  charts,
+  datasources,
+  sliceEntities,
+  dashboardLayout,
+} = getInitialState({
   common: {},
   dashboard_data: mockDashboardData,
   datasources: {},
   user_id: '1',
 });
-
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockChartQueries.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockChartQueries.js
new file mode 100644
index 0000000..b5004a1
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockChartQueries.js
@@ -0,0 +1,61 @@
+import { datasourceId } from './mockDatasource';
+
+export const sliceId = 18;
+
+export default {
+  [sliceId]: {
+    id: sliceId,
+    chartAlert: null,
+    chartStatus: 'rendered',
+    chartUpdateEndTime: 1525852456388,
+    chartUpdateStartTime: 1525852454838,
+    latestQueryFormData: {},
+    queryRequest: {},
+    queryResponse: {},
+    triggerQuery: false,
+    lastRendered: 0,
+    form_data: {
+      slice_id: sliceId,
+      viz_type: 'pie',
+      row_limit: 50000,
+      metric: 'sum__num',
+      since: '100 years ago',
+      groupby: ['gender'],
+      metrics: ['sum__num'],
+      compare_lag: '10',
+      limit: '25',
+      until: 'now',
+      granularity: 'ds',
+      markup_type: 'markdown',
+      where: '',
+      compare_suffix: 'o10Y',
+      datasource: datasourceId,
+    },
+    formData: {
+      datasource: datasourceId,
+      viz_type: 'pie',
+      slice_id: sliceId,
+      granularity_sqla: null,
+      time_grain_sqla: null,
+      since: '100 years ago',
+      until: 'now',
+      metrics: ['sum__num'],
+      groupby: ['gender'],
+      limit: '25',
+      pie_label_type: 'key',
+      donut: false,
+      show_legend: true,
+      labels_outside: true,
+      color_scheme: 'bnbColors',
+      where: '',
+      having: '',
+      filters: [],
+      row_limit: 50000,
+      metric: 'sum__num',
+      compare_lag: '10',
+      granularity: 'ds',
+      markup_type: 'markdown',
+      compare_suffix: 'o10Y',
+    },
+  },
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js
new file mode 100644
index 0000000..4dd2670
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardInfo.js
@@ -0,0 +1,12 @@
+export default {
+  id: 1234,
+  slug: 'dashboardSlug',
+  metadata: {},
+  userId: 'mock_user_id',
+  dash_edit_perm: true,
+  dash_save_perm: true,
+  common: {
+    flash_messages: [],
+    conf: { ENABLE_JAVASCRIPT_CONTROLS: false, SUPERSET_WEBSERVER_TIMEOUT: 60 },
+  },
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
new file mode 100644
index 0000000..865af0a
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardLayout.js
@@ -0,0 +1,140 @@
+import {
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_HEADER_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+  CHART_TYPE,
+  ROW_TYPE,
+  COLUMN_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+import {
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_HEADER_ID,
+  DASHBOARD_GRID_ID,
+} from '../../../../src/dashboard/util/constants';
+
+import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
+
+import { sliceId as chartId } from './mockChartQueries';
+
+export const sliceId = chartId;
+
+export const dashboardLayout = {
+  past: [],
+  present: {
+    [DASHBOARD_ROOT_ID]: {
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: [DASHBOARD_GRID_ID],
+    },
+
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: ['ROW_ID'],
+      meta: {},
+    },
+
+    [DASHBOARD_HEADER_ID]: {
+      type: DASHBOARD_HEADER_TYPE,
+      id: DASHBOARD_HEADER_ID,
+      meta: {
+        text: 'New dashboard',
+      },
+    },
+
+    ROW_ID: {
+      ...newComponentFactory(ROW_TYPE),
+      id: 'ROW_ID',
+      children: ['COLUMN_ID'],
+    },
+
+    COLUMN_ID: {
+      ...newComponentFactory(COLUMN_TYPE),
+      id: 'COLUMN_ID',
+      children: ['CHART_ID'],
+    },
+
+    CHART_ID: {
+      ...newComponentFactory(CHART_TYPE),
+      id: 'CHART_ID',
+      meta: {
+        chartId,
+        width: 3,
+        height: 10,
+        chartName: 'Mock chart name',
+      },
+    },
+  },
+  future: [],
+};
+
+export const dashboardLayoutWithTabs = {
+  past: [],
+  present: {
+    [DASHBOARD_ROOT_ID]: {
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: ['TABS_ID'],
+    },
+
+    TABS_ID: {
+      id: 'TABS_ID',
+      type: TABS_TYPE,
+      children: ['TAB_ID', 'TAB_ID2'],
+    },
+
+    TAB_ID: {
+      id: 'TAB_ID',
+      type: TAB_TYPE,
+      children: ['ROW_ID'],
+      meta: {
+        text: 'tab1',
+      },
+    },
+
+    TAB_ID2: {
+      id: 'TAB_ID2',
+      type: TAB_TYPE,
+      children: [],
+      meta: {
+        text: 'tab2',
+      },
+    },
+
+    CHART_ID: {
+      ...newComponentFactory(CHART_TYPE),
+      id: 'CHART_ID',
+      meta: {
+        chartId,
+        width: 3,
+        height: 10,
+        chartName: 'Mock chart name',
+      },
+    },
+
+    ROW_ID: {
+      ...newComponentFactory(ROW_TYPE),
+      id: 'ROW_ID',
+      children: ['CHART_ID'],
+    },
+
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: [],
+      meta: {},
+    },
+
+    [DASHBOARD_HEADER_ID]: {
+      type: DASHBOARD_HEADER_TYPE,
+      id: DASHBOARD_HEADER_ID,
+      meta: {
+        text: 'New dashboard',
+      },
+    },
+  },
+  future: [],
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
new file mode 100644
index 0000000..9d05344
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
@@ -0,0 +1,13 @@
+import { id as sliceId } from './mockChartQueries';
+
+export default {
+  sliceIds: [sliceId],
+  refresh: false,
+  filters: {},
+  expandedSlices: {},
+  editMode: false,
+  showBuilderPane: false,
+  hasUnsavedChanges: false,
+  maxUndoHistoryExceeded: false,
+  isStarred: true,
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDatasource.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDatasource.js
new file mode 100644
index 0000000..1de7915
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDatasource.js
@@ -0,0 +1,206 @@
+export const id = 7;
+export const datasourceId = `${id}__table`;
+
+export default {
+  [datasourceId]: {
+    verbose_map: {
+      count: 'COUNT(*)',
+      __timestamp: 'Time',
+      sum__sum_girls: 'sum__sum_girls',
+      name: 'name',
+      avg__sum_girls: 'avg__sum_girls',
+      gender: 'gender',
+      sum_girls: 'sum_girls',
+      ds: 'ds',
+      sum__sum_boys: 'sum__sum_boys',
+      state: 'state',
+      num: 'num',
+      sum__num: 'sum__num',
+      sum_boys: 'sum_boys',
+      avg__num: 'avg__num',
+      avg__sum_boys: 'avg__sum_boys',
+    },
+    gb_cols: [['gender', 'gender'], ['name', 'name'], ['state', 'state']],
+    metrics: [
+      {
+        expression: 'SUM(birth_names.num)',
+        warning_text: null,
+        verbose_name: 'sum__num',
+        metric_name: 'sum__num',
+        description: null,
+      },
+      {
+        expression: 'AVG(birth_names.num)',
+        warning_text: null,
+        verbose_name: 'avg__num',
+        metric_name: 'avg__num',
+        description: null,
+      },
+      {
+        expression: 'SUM(birth_names.sum_boys)',
+        warning_text: null,
+        verbose_name: 'sum__sum_boys',
+        metric_name: 'sum__sum_boys',
+        description: null,
+      },
+      {
+        expression: 'AVG(birth_names.sum_boys)',
+        warning_text: null,
+        verbose_name: 'avg__sum_boys',
+        metric_name: 'avg__sum_boys',
+        description: null,
+      },
+      {
+        expression: 'SUM(birth_names.sum_girls)',
+        warning_text: null,
+        verbose_name: 'sum__sum_girls',
+        metric_name: 'sum__sum_girls',
+        description: null,
+      },
+      {
+        expression: 'AVG(birth_names.sum_girls)',
+        warning_text: null,
+        verbose_name: 'avg__sum_girls',
+        metric_name: 'avg__sum_girls',
+        description: null,
+      },
+      {
+        expression: 'COUNT(*)',
+        warning_text: null,
+        verbose_name: 'COUNT(*)',
+        metric_name: 'count',
+        description: null,
+      },
+    ],
+    column_formats: {},
+    columns: [
+      {
+        type: 'DATETIME',
+        description: null,
+        filterable: false,
+        verbose_name: null,
+        is_dttm: true,
+        expression: '',
+        groupby: false,
+        column_name: 'ds',
+      },
+      {
+        type: 'VARCHAR(16)',
+        description: null,
+        filterable: true,
+        verbose_name: null,
+        is_dttm: false,
+        expression: '',
+        groupby: true,
+        column_name: 'gender',
+      },
+      {
+        type: 'VARCHAR(255)',
+        description: null,
+        filterable: true,
+        verbose_name: null,
+        is_dttm: false,
+        expression: '',
+        groupby: true,
+        column_name: 'name',
+      },
+      {
+        type: 'BIGINT',
+        description: null,
+        filterable: false,
+        verbose_name: null,
+        is_dttm: false,
+        expression: '',
+        groupby: false,
+        column_name: 'num',
+      },
+      {
+        type: 'VARCHAR(10)',
+        description: null,
+        filterable: true,
+        verbose_name: null,
+        is_dttm: false,
+        expression: '',
+        groupby: true,
+        column_name: 'state',
+      },
+      {
+        type: 'BIGINT',
+        description: null,
+        filterable: false,
+        verbose_name: null,
+        is_dttm: false,
+        expression: '',
+        groupby: false,
+        column_name: 'sum_boys',
+      },
+      {
+        type: 'BIGINT',
+        description: null,
+        filterable: false,
+        verbose_name: null,
+        is_dttm: false,
+        expression: '',
+        groupby: false,
+        column_name: 'sum_girls',
+      },
+    ],
+    id,
+    granularity_sqla: [['ds', 'ds']],
+    name: 'birth_names',
+    database: {
+      allow_multi_schema_metadata_fetch: null,
+      name: 'main',
+      backend: 'sqlite',
+    },
+    time_grain_sqla: [
+      [null, 'Time Column'],
+      ['PT1H', 'hour'],
+      ['P1D', 'day'],
+      ['P1W', 'week'],
+      ['P1M', 'month'],
+    ],
+    filterable_cols: [
+      ['gender', 'gender'],
+      ['name', 'name'],
+      ['state', 'state'],
+    ],
+    all_cols: [
+      ['ds', 'ds'],
+      ['gender', 'gender'],
+      ['name', 'name'],
+      ['num', 'num'],
+      ['state', 'state'],
+      ['sum_boys', 'sum_boys'],
+      ['sum_girls', 'sum_girls'],
+    ],
+    filter_select: true,
+    order_by_choices: [
+      ['["ds", true]', 'ds [asc]'],
+      ['["ds", false]', 'ds [desc]'],
+      ['["gender", true]', 'gender [asc]'],
+      ['["gender", false]', 'gender [desc]'],
+      ['["name", true]', 'name [asc]'],
+      ['["name", false]', 'name [desc]'],
+      ['["num", true]', 'num [asc]'],
+      ['["num", false]', 'num [desc]'],
+      ['["state", true]', 'state [asc]'],
+      ['["state", false]', 'state [desc]'],
+      ['["sum_boys", true]', 'sum_boys [asc]'],
+      ['["sum_boys", false]', 'sum_boys [desc]'],
+      ['["sum_girls", true]', 'sum_girls [asc]'],
+      ['["sum_girls", false]', 'sum_girls [desc]'],
+    ],
+    metrics_combo: [
+      ['count', 'COUNT(*)'],
+      ['avg__num', 'avg__num'],
+      ['avg__sum_boys', 'avg__sum_boys'],
+      ['avg__sum_girls', 'avg__sum_girls'],
+      ['sum__num', 'sum__num'],
+      ['sum__sum_boys', 'sum__sum_boys'],
+      ['sum__sum_girls', 'sum__sum_girls'],
+    ],
+    type: 'table',
+    edit_url: '/tablemodelview/edit/7',
+  },
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockMessageToasts.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockMessageToasts.js
new file mode 100644
index 0000000..07726a8
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockMessageToasts.js
@@ -0,0 +1,9 @@
+import {
+  INFO_TOAST,
+  DANGER_TOAST,
+} from '../../../../src/dashboard/util/constants';
+
+export default [
+  { id: 'info_id', toastType: INFO_TOAST, text: 'info toast' },
+  { id: 'danger_id', toastType: DANGER_TOAST, text: 'danger toast' },
+];
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
new file mode 100644
index 0000000..7c43bea
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockSliceEntities.js
@@ -0,0 +1,39 @@
+import { sliceId as id } from './mockChartQueries';
+import { datasourceId } from './mockDatasource';
+
+export const sliceId = id;
+
+export default {
+  slices: {
+    [sliceId]: {
+      slice_id: sliceId,
+      slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%2018%7D',
+      slice_name: 'Genders',
+      form_data: {
+        slice_id: sliceId,
+        viz_type: 'pie',
+        row_limit: 50000,
+        metric: 'sum__num',
+        since: '100 years ago',
+        groupby: ['gender'],
+        metrics: ['sum__num'],
+        compare_lag: '10',
+        limit: '25',
+        until: 'now',
+        granularity: 'ds',
+        markup_type: 'markdown',
+        where: '',
+        compare_suffix: 'o10Y',
+        datasource: datasourceId,
+      },
+      edit_url: `/slicemodelview/edit/${sliceId}`,
+      viz_type: 'pie',
+      datasource: datasourceId,
+      description: null,
+      description_markeddown: '',
+    },
+  },
+  isLoading: false,
+  errorMessage: null,
+  lastUpdated: 0,
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js
new file mode 100644
index 0000000..655f0bf
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockState.js
@@ -0,0 +1,18 @@
+import chartQueries from './mockChartQueries';
+import { dashboardLayout } from './mockDashboardLayout';
+import dashboardInfo from './mockDashboardInfo';
+import dashboardState from './mockDashboardState';
+import messageToasts from './mockMessageToasts';
+import datasources from './mockDatasource';
+import sliceEntities from './mockSliceEntities';
+
+export default {
+  datasources,
+  sliceEntities,
+  charts: chartQueries,
+  dashboardInfo,
+  dashboardState,
+  dashboardLayout,
+  messageToasts,
+  impressionId: 'mock_impression_id',
+};
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js
new file mode 100644
index 0000000..97132ac
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockStore.js
@@ -0,0 +1,22 @@
+import { createStore, applyMiddleware, compose } from 'redux';
+import thunk from 'redux-thunk';
+
+import rootReducer from '../../../../src/dashboard/reducers/index';
+
+import mockState from './mockState';
+import { dashboardLayoutWithTabs } from './mockDashboardLayout';
+
+export const mockStore = createStore(
+  rootReducer,
+  mockState,
+  compose(applyMiddleware(thunk)),
+);
+
+export const mockStoreWithTabs = createStore(
+  rootReducer,
+  {
+    ...mockState,
+    dashboardLayout: dashboardLayoutWithTabs,
+  },
+  compose(applyMiddleware(thunk)),
+);
diff --git a/superset/assets/spec/javascripts/dashboard/helpers/WithDragDropContext.jsx b/superset/assets/spec/javascripts/dashboard/helpers/WithDragDropContext.jsx
new file mode 100644
index 0000000..3e892a6
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/helpers/WithDragDropContext.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import getDragDropManager from '../../../../src/dashboard/util/getDragDropManager';
+
+// A helper component that provides a DragDropContext for components that require it
+class WithDragDropContext extends React.Component {
+  getChildContext() {
+    return {
+      dragDropManager: this.context.dragDropManager || getDragDropManager(),
+    };
+  }
+
+  render() {
+    return this.props.children;
+  }
+}
+
+WithDragDropContext.propTypes = {
+  children: PropTypes.node.isRequired,
+};
+
+WithDragDropContext.childContextTypes = {
+  dragDropManager: PropTypes.object.isRequired,
+};
+
+export default WithDragDropContext;
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
new file mode 100644
index 0000000..cbe1729
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
@@ -0,0 +1,443 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import layoutReducer from '../../../../src/dashboard/reducers/dashboardLayout';
+
+import {
+  UPDATE_COMPONENTS,
+  DELETE_COMPONENT,
+  CREATE_COMPONENT,
+  MOVE_COMPONENT,
+  CREATE_TOP_LEVEL_TABS,
+  DELETE_TOP_LEVEL_TABS,
+} from '../../../../src/dashboard/actions/dashboardLayout';
+
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  ROW_TYPE,
+  TAB_TYPE,
+  TABS_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+import {
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_GRID_ID,
+  GRID_MIN_COLUMN_COUNT,
+  NEW_COMPONENTS_SOURCE_ID,
+  NEW_TABS_ID,
+  NEW_ROW_ID,
+} from '../../../../src/dashboard/util/constants';
+
+describe('dashboardLayout reducer', () => {
+  it('should return initial state for unrecognized actions', () => {
+    expect(layoutReducer(undefined, {})).to.deep.equal({});
+  });
+
+  it('should delete a component, remove its reference in its parent, and recursively all of its children', () => {
+    expect(
+      layoutReducer(
+        {
+          toDelete: {
+            id: 'toDelete',
+            children: ['child1'],
+          },
+          child1: {
+            id: 'child1',
+            children: ['child2'],
+          },
+          child2: {
+            id: 'child2',
+            children: [],
+          },
+          parentId: {
+            id: 'parentId',
+            children: ['toDelete', 'anotherId'],
+          },
+        },
+        {
+          type: DELETE_COMPONENT,
+          payload: { id: 'toDelete', parentId: 'parentId' },
+        },
+      ),
+    ).to.deep.equal({
+      parentId: {
+        id: 'parentId',
+        children: ['anotherId'],
+      },
+    });
+  });
+
+  it('should update components', () => {
+    expect(
+      layoutReducer(
+        {
+          update: {
+            id: 'update',
+            children: [],
+          },
+          update2: {
+            id: 'update2',
+            children: [],
+          },
+          dontUpdate: {
+            id: 'dontUpdate',
+            something: 'something',
+            children: ['abcd'],
+          },
+        },
+        {
+          type: UPDATE_COMPONENTS,
+          payload: {
+            nextComponents: {
+              update: {
+                id: 'update',
+                newField: 'newField',
+              },
+              update2: {
+                id: 'update2',
+                newField: 'newField',
+              },
+            },
+          },
+        },
+      ),
+    ).to.deep.equal({
+      update: {
+        id: 'update',
+        newField: 'newField',
+      },
+      update2: {
+        id: 'update2',
+        newField: 'newField',
+      },
+      dontUpdate: {
+        id: 'dontUpdate',
+        something: 'something',
+        children: ['abcd'],
+      },
+    });
+  });
+
+  it('should move a component', () => {
+    const layout = {
+      source: {
+        id: 'source',
+        type: ROW_TYPE,
+        children: ['dontMove', 'toMove'],
+      },
+      destination: {
+        id: 'destination',
+        type: ROW_TYPE,
+        children: ['anotherChild'],
+      },
+      toMove: {
+        id: 'toMove',
+        type: CHART_TYPE,
+        children: [],
+      },
+    };
+
+    const dropResult = {
+      source: { id: 'source', type: ROW_TYPE, index: 1 },
+      destination: { id: 'destination', type: ROW_TYPE, index: 0 },
+      dragging: { id: 'toMove', type: CHART_TYPE },
+    };
+
+    expect(
+      layoutReducer(layout, {
+        type: MOVE_COMPONENT,
+        payload: { dropResult },
+      }),
+    ).to.deep.equal({
+      source: {
+        id: 'source',
+        type: ROW_TYPE,
+        children: ['dontMove'],
+      },
+      destination: {
+        id: 'destination',
+        type: ROW_TYPE,
+        children: ['toMove', 'anotherChild'],
+      },
+      toMove: {
+        id: 'toMove',
+        type: CHART_TYPE,
+        children: [],
+      },
+    });
+  });
+
+  it('should set the width of a moved component with column type parent to the minimum width', () => {
+    const layout = {
+      source: {
+        id: 'source',
+        type: ROW_TYPE,
+        children: ['dontMove', 'toMove'],
+      },
+      destination: {
+        id: 'destination',
+        type: COLUMN_TYPE,
+        children: [],
+        meta: { width: 100 },
+      },
+      toMove: {
+        id: 'toMove',
+        type: CHART_TYPE,
+        children: [],
+        meta: { width: 1001 },
+      },
+    };
+
+    const dropResult = {
+      source: { id: 'source', type: ROW_TYPE, index: 1 },
+      destination: { id: 'destination', type: COLUMN_TYPE, index: 0 },
+      dragging: { id: 'toMove', type: CHART_TYPE },
+    };
+
+    const result = layoutReducer(layout, {
+      type: MOVE_COMPONENT,
+      payload: { dropResult },
+    });
+
+    expect(result.toMove.meta.width).to.equal(GRID_MIN_COLUMN_COUNT);
+  });
+
+  it('should wrap a moved component in a row if need be', () => {
+    const layout = {
+      source: {
+        id: 'source',
+        type: ROW_TYPE,
+        children: ['dontMove', 'toMove'],
+      },
+      destination: {
+        id: 'destination',
+        type: DASHBOARD_GRID_TYPE,
+        children: [],
+      },
+      toMove: {
+        id: 'toMove',
+        type: CHART_TYPE,
+        children: [],
+      },
+    };
+
+    const dropResult = {
+      source: { id: 'source', type: ROW_TYPE, index: 1 },
+      destination: { id: 'destination', type: DASHBOARD_GRID_TYPE, index: 0 },
+      dragging: { id: 'toMove', type: CHART_TYPE },
+    };
+
+    const result = layoutReducer(layout, {
+      type: MOVE_COMPONENT,
+      payload: { dropResult },
+    });
+
+    const newRow = Object.values(result).find(
+      component =>
+        ['source', 'destination', 'toMove'].indexOf(component.id) === -1,
+    );
+
+    expect(newRow.children[0]).to.equal('toMove');
+    expect(result.destination.children[0]).to.equal(newRow.id);
+    expect(Object.keys(result)).to.have.length(4);
+  });
+
+  it('should add top-level tabs from a new tabs component, moving grid children to new tab', () => {
+    const layout = {
+      [DASHBOARD_ROOT_ID]: {
+        id: DASHBOARD_ROOT_ID,
+        children: [DASHBOARD_GRID_ID],
+      },
+      [DASHBOARD_GRID_ID]: {
+        id: DASHBOARD_GRID_ID,
+        children: ['child'],
+      },
+      child: {
+        id: 'child',
+        children: [],
+      },
+    };
+
+    const dropResult = {
+      source: { id: NEW_COMPONENTS_SOURCE_ID, type: '' },
+      destination: {
+        id: DASHBOARD_ROOT_ID,
+        type: DASHBOARD_ROOT_TYPE,
+        index: 0,
+      },
+      dragging: { id: NEW_TABS_ID, type: TABS_TYPE },
+    };
+
+    const result = layoutReducer(layout, {
+      type: CREATE_TOP_LEVEL_TABS,
+      payload: { dropResult },
+    });
+
+    const tabComponent = Object.values(result).find(
+      component => component.type === TAB_TYPE,
+    );
+
+    const tabsComponent = Object.values(result).find(
+      component => component.type === TABS_TYPE,
+    );
+
+    expect(Object.keys(result)).to.have.length(5); // initial + Tabs + Tab
+    expect(result[DASHBOARD_ROOT_ID].children[0]).to.equal(tabsComponent.id);
+    expect(result[tabsComponent.id].children[0]).to.equal(tabComponent.id);
+    expect(result[tabComponent.id].children[0]).to.equal('child');
+    expect(result[DASHBOARD_GRID_ID].children).to.have.length(0);
+  });
+
+  it('should add top-level tabs from an existing tabs component, moving grid children to new tab', () => {
+    const layout = {
+      [DASHBOARD_ROOT_ID]: {
+        id: DASHBOARD_ROOT_ID,
+        children: [DASHBOARD_GRID_ID],
+      },
+      [DASHBOARD_GRID_ID]: {
+        id: DASHBOARD_GRID_ID,
+        children: ['child', 'tabs', 'child2'],
+      },
+      child: {
+        id: 'child',
+        children: [],
+      },
+      child2: {
+        id: 'child2',
+        children: [],
+      },
+      tabs: {
+        id: 'tabs',
+        type: TABS_TYPE,
+        children: ['tab'],
+      },
+      tab: {
+        id: 'tab',
+        type: TAB_TYPE,
+        children: [],
+      },
+    };
+
+    const dropResult = {
+      source: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE, index: 1 },
+      destination: {
+        id: DASHBOARD_ROOT_ID,
+        type: DASHBOARD_ROOT_TYPE,
+        index: 0,
+      },
+      dragging: { id: 'tabs', type: TABS_TYPE },
+    };
+
+    const result = layoutReducer(layout, {
+      type: CREATE_TOP_LEVEL_TABS,
+      payload: { dropResult },
+    });
+
+    expect(Object.keys(result)).to.have.length(Object.keys(layout).length);
+    expect(result[DASHBOARD_ROOT_ID].children[0]).to.equal('tabs');
+    expect(result.tabs.children[0]).to.equal('tab');
+    expect(result.tab.children).to.deep.equal(['child', 'child2']);
+    expect(result[DASHBOARD_GRID_ID].children).to.have.length(0);
+  });
+
+  it('should remove top-level tabs, moving children to the grid', () => {
+    const layout = {
+      [DASHBOARD_ROOT_ID]: {
+        id: DASHBOARD_ROOT_ID,
+        children: ['tabs'],
+      },
+      [DASHBOARD_GRID_ID]: {
+        id: DASHBOARD_GRID_ID,
+        children: [],
+      },
+      child: {
+        id: 'child',
+        children: [],
+      },
+      child2: {
+        id: 'child2',
+        children: [],
+      },
+      tabs: {
+        id: 'tabs',
+        type: TABS_TYPE,
+        children: ['tab'],
+      },
+      tab: {
+        id: 'tab',
+        type: TAB_TYPE,
+        children: ['child', 'child2'],
+      },
+    };
+
+    const dropResult = {
+      source: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE, index: 1 },
+      destination: {
+        id: DASHBOARD_ROOT_ID,
+        type: DASHBOARD_ROOT_TYPE,
+        index: 0,
+      },
+      dragging: { id: 'tabs', type: TABS_TYPE },
+    };
+
+    const result = layoutReducer(layout, {
+      type: DELETE_TOP_LEVEL_TABS,
+      payload: { dropResult },
+    });
+
+    expect(result).to.deep.equal({
+      [DASHBOARD_ROOT_ID]: {
+        id: DASHBOARD_ROOT_ID,
+        children: [DASHBOARD_GRID_ID],
+      },
+      [DASHBOARD_GRID_ID]: {
+        id: DASHBOARD_GRID_ID,
+        children: ['child', 'child2'],
+      },
+      child: {
+        id: 'child',
+        children: [],
+      },
+      child2: {
+        id: 'child2',
+        children: [],
+      },
+    });
+  });
+
+  it('should create a component', () => {
+    const layout = {
+      [DASHBOARD_ROOT_ID]: {
+        id: DASHBOARD_ROOT_ID,
+        children: [DASHBOARD_GRID_ID],
+      },
+      [DASHBOARD_GRID_ID]: {
+        id: DASHBOARD_GRID_ID,
+        children: ['child'],
+      },
+      child: { id: 'child' },
+    };
+
+    const dropResult = {
+      source: { id: NEW_COMPONENTS_SOURCE_ID, type: '' },
+      destination: {
+        id: DASHBOARD_GRID_ID,
+        type: DASHBOARD_GRID_TYPE,
+        index: 1,
+      },
+      dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
+    };
+
+    const result = layoutReducer(layout, {
+      type: CREATE_COMPONENT,
+      payload: { dropResult },
+    });
+
+    const newId = result[DASHBOARD_GRID_ID].children[1];
+    expect(result[DASHBOARD_GRID_ID].children).to.have.length(2);
+    expect(result[newId].type).to.equal(ROW_TYPE);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
new file mode 100644
index 0000000..078019d
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
@@ -0,0 +1,239 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import {
+  ADD_SLICE,
+  ADD_FILTER,
+  ON_CHANGE,
+  ON_SAVE,
+  REMOVE_SLICE,
+  REMOVE_FILTER,
+  SET_EDIT_MODE,
+  SET_MAX_UNDO_HISTORY_EXCEEDED,
+  SET_UNSAVED_CHANGES,
+  TOGGLE_BUILDER_PANE,
+  TOGGLE_EXPAND_SLICE,
+  TOGGLE_FAVE_STAR,
+} from '../../../../src/dashboard/actions/dashboardState';
+
+import dashboardStateReducer from '../../../../src/dashboard/reducers/dashboardState';
+
+describe('dashboardState reducer', () => {
+  it('should return initial state', () => {
+    expect(dashboardStateReducer(undefined, {})).to.deep.equal({});
+  });
+
+  it('should add a slice', () => {
+    expect(
+      dashboardStateReducer(
+        { sliceIds: [1] },
+        { type: ADD_SLICE, slice: { slice_id: 2 } },
+      ),
+    ).to.deep.equal({ sliceIds: [1, 2] });
+  });
+
+  it('should remove a slice', () => {
+    expect(
+      dashboardStateReducer(
+        { sliceIds: [1, 2], filters: {} },
+        { type: REMOVE_SLICE, sliceId: 2 },
+      ),
+    ).to.deep.equal({ sliceIds: [1], refresh: false, filters: {} });
+  });
+
+  it('should reset filters if a removed slice is a filter', () => {
+    expect(
+      dashboardStateReducer(
+        { sliceIds: [1, 2], filters: { 2: {}, 1: {} } },
+        { type: REMOVE_SLICE, sliceId: 2 },
+      ),
+    ).to.deep.equal({ sliceIds: [1], filters: { 1: {} }, refresh: true });
+  });
+
+  it('should toggle fav star', () => {
+    expect(
+      dashboardStateReducer(
+        { isStarred: false },
+        { type: TOGGLE_FAVE_STAR, isStarred: true },
+      ),
+    ).to.deep.equal({ isStarred: true });
+  });
+
+  it('should toggle edit mode', () => {
+    expect(
+      dashboardStateReducer(
+        { editMode: false },
+        { type: SET_EDIT_MODE, editMode: true },
+      ),
+    ).to.deep.equal({ editMode: true, showBuilderPane: true });
+  });
+
+  it('should toggle builder pane', () => {
+    expect(
+      dashboardStateReducer(
+        { showBuilderPane: false },
+        { type: TOGGLE_BUILDER_PANE },
+      ),
+    ).to.deep.equal({ showBuilderPane: true });
+
+    expect(
+      dashboardStateReducer(
+        { showBuilderPane: true },
+        { type: TOGGLE_BUILDER_PANE },
+      ),
+    ).to.deep.equal({ showBuilderPane: false });
+  });
+
+  it('should toggle expanded slices', () => {
+    expect(
+      dashboardStateReducer(
+        { expandedSlices: { 1: true, 2: false } },
+        { type: TOGGLE_EXPAND_SLICE, sliceId: 1 },
+      ),
+    ).to.deep.equal({ expandedSlices: { 2: false } });
+
+    expect(
+      dashboardStateReducer(
+        { expandedSlices: { 1: true, 2: false } },
+        { type: TOGGLE_EXPAND_SLICE, sliceId: 2 },
+      ),
+    ).to.deep.equal({ expandedSlices: { 1: true, 2: true } });
+  });
+
+  it('should set hasUnsavedChanges', () => {
+    expect(dashboardStateReducer({}, { type: ON_CHANGE })).to.deep.equal({
+      hasUnsavedChanges: true,
+    });
+
+    expect(
+      dashboardStateReducer(
+        {},
+        { type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges: false } },
+      ),
+    ).to.deep.equal({
+      hasUnsavedChanges: false,
+    });
+  });
+
+  it('should set maxUndoHistoryExceeded', () => {
+    expect(
+      dashboardStateReducer(
+        {},
+        {
+          type: SET_MAX_UNDO_HISTORY_EXCEEDED,
+          payload: { maxUndoHistoryExceeded: true },
+        },
+      ),
+    ).to.deep.equal({
+      maxUndoHistoryExceeded: true,
+    });
+  });
+
+  it('should set unsaved changes and max undo history to false on save', () => {
+    expect(
+      dashboardStateReducer({ hasUnsavedChanges: true }, { type: ON_SAVE }),
+    ).to.deep.equal({
+      hasUnsavedChanges: false,
+      maxUndoHistoryExceeded: false,
+    });
+  });
+
+  describe('add filter', () => {
+    it('should add a new filter if it does not exist', () => {
+      expect(
+        dashboardStateReducer(
+          {
+            filters: {},
+            sliceIds: [1],
+          },
+          {
+            type: ADD_FILTER,
+            chart: { id: 1, formData: { groupby: 'column' } },
+            col: 'column',
+            vals: ['b', 'a'],
+            refresh: true,
+            merge: true,
+          },
+        ),
+      ).to.deep.equal({
+        filters: { 1: { column: ['b', 'a'] } },
+        refresh: true,
+        sliceIds: [1],
+      });
+    });
+
+    it('should overwrite a filter if merge is false', () => {
+      expect(
+        dashboardStateReducer(
+          {
+            filters: {
+              1: { column: ['z'] },
+            },
+            sliceIds: [1],
+          },
+          {
+            type: ADD_FILTER,
+            chart: { id: 1, formData: { groupby: 'column' } },
+            col: 'column',
+            vals: ['b', 'a'],
+            refresh: true,
+            merge: false,
+          },
+        ),
+      ).to.deep.equal({
+        filters: { 1: { column: ['b', 'a'] } },
+        refresh: true,
+        sliceIds: [1],
+      });
+    });
+
+    it('should merge a filter if merge is true', () => {
+      expect(
+        dashboardStateReducer(
+          {
+            filters: {
+              1: { column: ['z'] },
+            },
+            sliceIds: [1],
+          },
+          {
+            type: ADD_FILTER,
+            chart: { id: 1, formData: { groupby: 'column' } },
+            col: 'column',
+            vals: ['b', 'a'],
+            refresh: true,
+            merge: true,
+          },
+        ),
+      ).to.deep.equal({
+        filters: { 1: { column: ['z', 'b', 'a'] } },
+        refresh: true,
+        sliceIds: [1],
+      });
+    });
+  });
+
+  it('should remove a filter', () => {
+    expect(
+      dashboardStateReducer(
+        {
+          filters: {
+            1: {
+              column: ['a', 'b', 'c'],
+            },
+          },
+        },
+        {
+          type: REMOVE_FILTER,
+          sliceId: 1,
+          col: 'column',
+          vals: ['b', 'a'], // these are removed
+          refresh: true,
+        },
+      ),
+    ).to.deep.equal({
+      filters: { 1: { column: ['c'] } },
+      refresh: true,
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/messageToasts_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/messageToasts_spec.js
new file mode 100644
index 0000000..5280312
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/reducers/messageToasts_spec.js
@@ -0,0 +1,32 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import {
+  ADD_TOAST,
+  REMOVE_TOAST,
+} from '../../../../src/dashboard/actions/messageToasts';
+import messageToastsReducer from '../../../../src/dashboard/reducers/messageToasts';
+
+describe('messageToasts reducer', () => {
+  it('should return initial state', () => {
+    expect(messageToastsReducer(undefined, {})).to.deep.equal([]);
+  });
+
+  it('should add a toast', () => {
+    expect(
+      messageToastsReducer([], {
+        type: ADD_TOAST,
+        payload: { text: 'test', id: 'id', type: 'test_type' },
+      }),
+    ).to.deep.equal([{ text: 'test', id: 'id', type: 'test_type' }]);
+  });
+
+  it('should add a toast', () => {
+    expect(
+      messageToastsReducer([{ id: 'id' }, { id: 'id2' }], {
+        type: REMOVE_TOAST,
+        payload: { id: 'id' },
+      }),
+    ).to.deep.equal([{ id: 'id2' }]);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/sliceEntities_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/sliceEntities_spec.js
new file mode 100644
index 0000000..7e3bb76
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/reducers/sliceEntities_spec.js
@@ -0,0 +1,51 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import {
+  FETCH_ALL_SLICES_FAILED,
+  FETCH_ALL_SLICES_STARTED,
+  SET_ALL_SLICES,
+} from '../../../../src/dashboard/actions/sliceEntities';
+
+import sliceEntitiesReducer from '../../../../src/dashboard/reducers/sliceEntities';
+
+describe('sliceEntities reducer', () => {
+  it('should return initial state', () => {
+    expect(sliceEntitiesReducer({}, {})).to.deep.equal({});
+  });
+
+  it('should set loading when fetching slices', () => {
+    expect(
+      sliceEntitiesReducer(
+        { isLoading: false },
+        { type: FETCH_ALL_SLICES_STARTED },
+      ).isLoading,
+    ).to.equal(true);
+  });
+
+  it('should set slices', () => {
+    const result = sliceEntitiesReducer(
+      { slices: { a: {} } },
+      { type: SET_ALL_SLICES, slices: { 1: {}, 2: {} } },
+    );
+
+    expect(result.slices).to.deep.equal({
+      1: {},
+      2: {},
+      a: {},
+    });
+    expect(result.isLoading).to.equal(false);
+  });
+
+  it('should set an error on error', () => {
+    const result = sliceEntitiesReducer(
+      {},
+      {
+        type: FETCH_ALL_SLICES_FAILED,
+        error: { responseJSON: { message: 'errorrr' } },
+      },
+    );
+    expect(result.isLoading).to.equal(false);
+    expect(result.errorMessage.indexOf('errorrr')).to.be.above(-1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/reducers_spec.js b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
deleted file mode 100644
index 580a574..0000000
--- a/superset/assets/spec/javascripts/dashboard/reducers_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { describe, it } from 'mocha';
-import { expect } from 'chai';
-
-import reducers from '../../../src/dashboard/reducers/dashboardState';
-import * as actions from '../../../src/dashboard/actions/dashboardState';
-import { defaultFilters, dashboardState as initState } from './fixtures';
-
-describe('Dashboard reducers', () => {
-  it('should initialized', () => {
-    expect(initState.sliceIds.size).to.equal(3);
-  });
-
-  it('should remove slice', () => {
-    const action = {
-      type: actions.REMOVE_SLICE,
-      sliceId: 248,
-    };
-
-    const { sliceIds, filters, refresh } = reducers(initState, action);
-    expect(sliceIds.size).to.be.equal(2);
-    expect(filters).to.deep.equal(defaultFilters);
-    expect(refresh).to.equal(false);
-  });
-
-  it('should remove filter slice', () => {
-    const action = {
-      type: actions.REMOVE_SLICE,
-      sliceId: 256,
-    };
-    const initFilters = Object.keys(initState.filters);
-    expect(initFilters).to.have.length(2);
-
-    const { sliceIds, filters, refresh } = reducers(initState, action);
-    expect(sliceIds.size).to.equal(2);
-    expect(Object.keys(filters)).to.have.length(1);
-    expect(refresh).to.equal(true);
-  });
-});
diff --git a/superset/assets/spec/javascripts/dashboard/util/componentIsResizable_spec.js b/superset/assets/spec/javascripts/dashboard/util/componentIsResizable_spec.js
new file mode 100644
index 0000000..b49a91f
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/componentIsResizable_spec.js
@@ -0,0 +1,42 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import componentIsResizable from '../../../../src/dashboard/util/componentIsResizable';
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  MARKDOWN_TYPE,
+  ROW_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+const notResizable = [
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  ROW_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+];
+
+const resizable = [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE];
+
+describe('componentIsResizable', () => {
+  resizable.forEach(type => {
+    it(`should return true for ${type}`, () => {
+      expect(componentIsResizable({ type })).to.equal(true);
+    });
+  });
+
+  notResizable.forEach(type => {
+    it(`should return false for ${type}`, () => {
+      expect(componentIsResizable({ type })).to.equal(false);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/dnd-reorder_spec.js b/superset/assets/spec/javascripts/dashboard/util/dnd-reorder_spec.js
new file mode 100644
index 0000000..4ff6a52
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/dnd-reorder_spec.js
@@ -0,0 +1,62 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import reorderItem from '../../../../src/dashboard/util/dnd-reorder';
+
+describe('dnd-reorderItem', () => {
+  it('should remove the item from its source entity and add it to its destination entity', () => {
+    const result = reorderItem({
+      entitiesMap: {
+        a: {
+          id: 'a',
+          children: ['x', 'y', 'z'],
+        },
+        b: {
+          id: 'b',
+          children: ['banana'],
+        },
+      },
+      source: { id: 'a', index: 2 },
+      destination: { id: 'b', index: 1 },
+    });
+
+    expect(result.a.children).to.deep.equal(['x', 'y']);
+    expect(result.b.children).to.deep.equal(['banana', 'z']);
+  });
+
+  it('should correctly move elements within the same list', () => {
+    const result = reorderItem({
+      entitiesMap: {
+        a: {
+          id: 'a',
+          children: ['x', 'y', 'z'],
+        },
+      },
+      source: { id: 'a', index: 2 },
+      destination: { id: 'a', index: 0 },
+    });
+
+    expect(result.a.children).to.deep.equal(['z', 'x', 'y']);
+  });
+
+  it('should copy items that do not move into the result', () => {
+    const extraEntity = {};
+    const result = reorderItem({
+      entitiesMap: {
+        a: {
+          id: 'a',
+          children: ['x', 'y', 'z'],
+        },
+        b: {
+          id: 'b',
+          children: ['banana'],
+        },
+        iAmExtra: extraEntity,
+      },
+      source: { id: 'a', index: 2 },
+      destination: { id: 'b', index: 1 },
+    });
+
+    expect(result.iAmExtra === extraEntity).to.equal(true);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js b/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
new file mode 100644
index 0000000..b153e1e
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
@@ -0,0 +1,125 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import dropOverflowsParent from '../../../../src/dashboard/util/dropOverflowsParent';
+import { NEW_COMPONENTS_SOURCE_ID } from '../../../../src/dashboard/util/constants';
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  ROW_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+describe('dropOverflowsParent', () => {
+  it('returns true if a parent does NOT have adequate width for child', () => {
+    const dropResult = {
+      source: { id: '_' },
+      destination: { id: 'a' },
+      dragging: { id: 'z' },
+    };
+
+    const layout = {
+      a: {
+        id: 'a',
+        type: ROW_TYPE,
+        children: ['b', 'b', 'b', 'b'], // width = 4x bs = 12
+      },
+      b: {
+        id: 'b',
+        type: CHART_TYPE,
+        meta: {
+          width: 3,
+        },
+      },
+      z: {
+        id: 'z',
+        type: CHART_TYPE,
+        meta: {
+          width: 2,
+        },
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
+  });
+
+  it('returns false if a parent DOES not have adequate width for child', () => {
+    const dropResult = {
+      source: { id: '_' },
+      destination: { id: 'a' },
+      dragging: { id: 'z' },
+    };
+
+    const layout = {
+      a: {
+        id: 'a',
+        type: ROW_TYPE,
+        children: ['b', 'b'],
+      },
+      b: {
+        id: 'b',
+        type: CHART_TYPE,
+        meta: {
+          width: 3,
+        },
+      },
+      z: {
+        id: 'z',
+        type: CHART_TYPE,
+        meta: {
+          width: 2,
+        },
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+  });
+
+  it('it should base result off of column width (instead of its children) if dropped on column', () => {
+    const dropResult = {
+      source: { id: 'z' },
+      destination: { id: 'a' },
+      dragging: { id: 'b' },
+    };
+
+    const layout = {
+      a: {
+        id: 'a',
+        type: COLUMN_TYPE,
+        meta: { width: 10 },
+      },
+      b: {
+        id: 'b',
+        type: CHART_TYPE,
+        meta: {
+          width: 2,
+        },
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+    expect(
+      dropOverflowsParent(dropResult, {
+        ...layout,
+        a: { ...layout.a, meta: { width: 1 } },
+      }),
+    ).to.equal(true);
+  });
+
+  it('should work with new components that are not in the layout', () => {
+    const dropResult = {
+      source: { id: NEW_COMPONENTS_SOURCE_ID },
+      destination: { id: 'a' },
+      dragging: { type: CHART_TYPE },
+    };
+
+    const layout = {
+      a: {
+        id: 'a',
+        type: ROW_TYPE,
+        children: [],
+      },
+    };
+
+    expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/findParentId_spec.js b/superset/assets/spec/javascripts/dashboard/util/findParentId_spec.js
new file mode 100644
index 0000000..71c8aec
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/findParentId_spec.js
@@ -0,0 +1,29 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import findParentId from '../../../../src/dashboard/util/findParentId';
+
+describe('findParentId', () => {
+  const layout = {
+    a: {
+      id: 'a',
+      children: ['b', 'r', 'k'],
+    },
+    b: {
+      id: 'b',
+      children: ['x', 'y', 'z'],
+    },
+    z: {
+      id: 'z',
+      children: [],
+    },
+  };
+  it('should return the correct parentId', () => {
+    expect(findParentId({ childId: 'b', layout })).to.equal('a');
+    expect(findParentId({ childId: 'z', layout })).to.equal('b');
+  });
+
+  it('should return null if the parent cannot be found', () => {
+    expect(findParentId({ childId: 'a', layout })).to.equal(null);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/getChartIdsFromLayout_spec.js b/superset/assets/spec/javascripts/dashboard/util/getChartIdsFromLayout_spec.js
new file mode 100644
index 0000000..71bbccd
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getChartIdsFromLayout_spec.js
@@ -0,0 +1,41 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import getChartIdsFromLayout from '../../../../src/dashboard/util/getChartIdsFromLayout';
+import {
+  ROW_TYPE,
+  CHART_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+describe('getChartIdsFromLayout', () => {
+  const mockLayout = {
+    a: {
+      id: 'a',
+      type: CHART_TYPE,
+      meta: { chartId: 'A' },
+    },
+    b: {
+      id: 'b',
+      type: CHART_TYPE,
+      meta: { chartId: 'B' },
+    },
+    c: {
+      id: 'c',
+      type: ROW_TYPE,
+      meta: { chartId: 'C' },
+    },
+  };
+
+  it('should return an array of chartIds', () => {
+    const result = getChartIdsFromLayout(mockLayout);
+    expect(Array.isArray(result)).to.equal(true);
+    expect(result.includes('A')).to.equal(true);
+    expect(result.includes('B')).to.equal(true);
+  });
+
+  it('should return ids only from CHART_TYPE components', () => {
+    const result = getChartIdsFromLayout(mockLayout);
+    expect(result.length).to.equal(2);
+    expect(result.includes('C')).to.equal(false);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/getDropPosition_spec.js b/superset/assets/spec/javascripts/dashboard/util/getDropPosition_spec.js
new file mode 100644
index 0000000..287b7a6
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getDropPosition_spec.js
@@ -0,0 +1,422 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import getDropPosition, {
+  DROP_TOP,
+  DROP_RIGHT,
+  DROP_BOTTOM,
+  DROP_LEFT,
+} from '../../../../src/dashboard/util/getDropPosition';
+
+import {
+  CHART_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  HEADER_TYPE,
+  ROW_TYPE,
+  TAB_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+describe('getDropPosition', () => {
+  // helper to easily configure test
+  function getMocks({
+    parentType,
+    componentType,
+    draggingType,
+    depth = 1,
+    hasChildren = false,
+    orientation = 'row',
+    clientOffset = { x: 0, y: 0 },
+    boundingClientRect = {
+      top: 0,
+      right: 0,
+      bottom: 0,
+      left: 0,
+    },
+    isDraggingOverShallow = true,
+  }) {
+    const monitorMock = {
+      getItem: () => ({
+        id: 'id',
+        type: draggingType,
+      }),
+      getClientOffset: () => clientOffset,
+    };
+
+    const ComponentMock = {
+      props: {
+        depth,
+        parentComponent: {
+          type: parentType,
+        },
+        component: {
+          type: componentType,
+          children: hasChildren ? [''] : [],
+        },
+        orientation,
+        isDraggingOverShallow,
+      },
+      ref: {
+        getBoundingClientRect: () => boundingClientRect,
+      },
+    };
+
+    return [monitorMock, ComponentMock];
+  }
+
+  describe('invalid child + invalid sibling', () => {
+    it('should return null', () => {
+      const result = getDropPosition(
+        // TAB is an invalid child + sibling of GRID > ROW
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: TAB_TYPE,
+        }),
+      );
+      expect(result).to.equal(null);
+    });
+  });
+
+  describe('valid child + invalid sibling', () => {
+    it('should return DROP_LEFT if component has NO children, and orientation is "row"', () => {
+      // HEADER is a valid child + invalid sibling of ROOT > GRID
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_ROOT_TYPE,
+          componentType: DASHBOARD_GRID_TYPE,
+          draggingType: HEADER_TYPE,
+        }),
+      );
+      expect(result).to.equal(DROP_LEFT);
+    });
+
+    it('should return DROP_RIGHT if component HAS children, and orientation is "row"', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_ROOT_TYPE,
+          componentType: DASHBOARD_GRID_TYPE,
+          draggingType: HEADER_TYPE,
+          hasChildren: true,
+        }),
+      );
+      expect(result).to.equal(DROP_RIGHT);
+    });
+
+    it('should return DROP_TOP if component has NO children, and orientation is "column"', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_ROOT_TYPE,
+          componentType: DASHBOARD_GRID_TYPE,
+          draggingType: HEADER_TYPE,
+          orientation: 'column',
+        }),
+      );
+      expect(result).to.equal(DROP_TOP);
+    });
+
+    it('should return DROP_BOTTOM if component HAS children, and orientation is "column"', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_ROOT_TYPE,
+          componentType: DASHBOARD_GRID_TYPE,
+          draggingType: HEADER_TYPE,
+          orientation: 'column',
+          hasChildren: true,
+        }),
+      );
+      expect(result).to.equal(DROP_BOTTOM);
+    });
+  });
+
+  describe('invalid child + valid sibling', () => {
+    it('should return DROP_TOP if orientation="row" and clientOffset is closer to component top than bottom', () => {
+      const result = getDropPosition(
+        // HEADER is an invalid child but valid sibling of GRID > ROW
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: HEADER_TYPE,
+          clientOffset: { y: 10 },
+          boundingClientRect: {
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_TOP);
+    });
+
+    it('should return DROP_BOTTOM if orientation="row" and clientOffset is closer to component bottom than top', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: HEADER_TYPE,
+          clientOffset: { y: 55 },
+          boundingClientRect: {
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_BOTTOM);
+    });
+
+    it('should return DROP_LEFT if orientation="column" and clientOffset is closer to component left than right', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: HEADER_TYPE,
+          orientation: 'column',
+          clientOffset: { x: 45 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_LEFT);
+    });
+
+    it('should return DROP_RIGHT if orientation="column" and clientOffset is closer to component right than left', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: HEADER_TYPE,
+          orientation: 'column',
+          clientOffset: { x: 55 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_RIGHT);
+    });
+  });
+
+  describe('child + valid sibling (row orientation)', () => {
+    it('should return DROP_LEFT if component has NO children, and clientOffset is NOT near top/bottom sibling boundary', () => {
+      const result = getDropPosition(
+        // CHART is a valid child + sibling of GRID > ROW
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          clientOffset: { x: 10, y: 50 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_LEFT);
+    });
+
+    it('should return DROP_RIGHT if component HAS children, and clientOffset is NOT near top/bottom sibling boundary', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          hasChildren: true,
+          clientOffset: { x: 10, y: 50 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_RIGHT);
+    });
+
+    it('should return DROP_TOP regardless of component children if clientOffset IS near top sibling boundary', () => {
+      const noChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          clientOffset: { x: 10, y: 2 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      const withChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          hasChildren: true,
+          clientOffset: { x: 10, y: 2 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(noChildren).to.equal(DROP_TOP);
+      expect(withChildren).to.equal(DROP_TOP);
+    });
+
+    it('should return DROP_BOTTOM regardless of component children if clientOffset IS near bottom sibling boundary', () => {
+      const noChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          clientOffset: { x: 10, y: 95 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      const withChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          hasChildren: true,
+          clientOffset: { x: 10, y: 95 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(noChildren).to.equal(DROP_BOTTOM);
+      expect(withChildren).to.equal(DROP_BOTTOM);
+    });
+  });
+
+  describe('child + valid sibling (column orientation)', () => {
+    it('should return DROP_TOP if component has NO children, and clientOffset is NOT near left/right sibling boundary', () => {
+      const result = getDropPosition(
+        // CHART is a valid child + sibling of GRID > ROW
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          orientation: 'column',
+          clientOffset: { x: 50, y: 0 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_TOP);
+    });
+
+    it('should return DROP_BOTTOM if component HAS children, and clientOffset is NOT near left/right sibling boundary', () => {
+      const result = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          orientation: 'column',
+          hasChildren: true,
+          clientOffset: { x: 50, y: 0 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(result).to.equal(DROP_BOTTOM);
+    });
+
+    it('should return DROP_LEFT regardless of component children if clientOffset IS near left sibling boundary', () => {
+      const noChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          orientation: 'column',
+          clientOffset: { x: 10, y: 2 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      const withChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          orientation: 'column',
+          hasChildren: true,
+          clientOffset: { x: 10, y: 2 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(noChildren).to.equal(DROP_LEFT);
+      expect(withChildren).to.equal(DROP_LEFT);
+    });
+
+    it('should return DROP_RIGHT regardless of component children if clientOffset IS near right sibling boundary', () => {
+      const noChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          orientation: 'column',
+          clientOffset: { x: 90, y: 95 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      const withChildren = getDropPosition(
+        ...getMocks({
+          parentType: DASHBOARD_GRID_TYPE,
+          componentType: ROW_TYPE,
+          draggingType: CHART_TYPE,
+          orientation: 'column',
+          hasChildren: true,
+          clientOffset: { x: 90, y: 95 },
+          boundingClientRect: {
+            left: 0,
+            right: 100,
+            top: 0,
+            bottom: 100,
+          },
+        }),
+      );
+      expect(noChildren).to.equal(DROP_RIGHT);
+      expect(withChildren).to.equal(DROP_RIGHT);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js b/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
new file mode 100644
index 0000000..388630b
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getFormDataWithExtraFilters_spec.js
@@ -0,0 +1,70 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import getFormDataWithExtraFilters from '../../../../src/dashboard/util/charts/getFormDataWithExtraFilters';
+
+describe('getFormDataWithExtraFilters', () => {
+  const chartId = 'chartId';
+  const mockArgs = {
+    chart: {
+      id: chartId,
+      formData: {
+        filters: [
+          {
+            col: 'country_name',
+            op: 'in',
+            val: ['United States'],
+          },
+        ],
+      },
+    },
+    dashboardMetadata: {
+      filter_immune_slices: [],
+      filter_immune_slice_fields: {},
+    },
+    filters: {
+      filterId: {
+        region: ['Spain'],
+        color: ['pink', 'purple'],
+      },
+    },
+    sliceId: chartId,
+  };
+
+  it('should include filters from the passed filters', () => {
+    const result = getFormDataWithExtraFilters(mockArgs);
+    expect(result.extra_filters).to.have.length(2);
+    expect(result.extra_filters[0]).to.deep.equal({
+      col: 'region',
+      op: 'in',
+      val: ['Spain'],
+    });
+    expect(result.extra_filters[1]).to.deep.equal({
+      col: 'color',
+      op: 'in',
+      val: ['pink', 'purple'],
+    });
+  });
+
+  it('should not add additional filters if the slice is immune to them', () => {
+    const result = getFormDataWithExtraFilters({
+      ...mockArgs,
+      dashboardMetadata: {
+        filter_immune_slices: [chartId],
+      },
+    });
+    expect(result.extra_filters).to.have.length(0);
+  });
+
+  it('should not add additional filters for fields to which the slice is immune', () => {
+    const result = getFormDataWithExtraFilters({
+      ...mockArgs,
+      dashboardMetadata: {
+        filter_immune_slice_fields: {
+          [chartId]: ['region'],
+        },
+      },
+    });
+    expect(result.extra_filters).to.have.length(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
new file mode 100644
index 0000000..ec57494
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
@@ -0,0 +1,147 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import isValidChild from '../../../../src/dashboard/util/isValidChild';
+
+import {
+  CHART_TYPE as CHART,
+  COLUMN_TYPE as COLUMN,
+  DASHBOARD_GRID_TYPE as GRID,
+  DASHBOARD_ROOT_TYPE as ROOT,
+  DIVIDER_TYPE as DIVIDER,
+  HEADER_TYPE as HEADER,
+  MARKDOWN_TYPE as MARKDOWN,
+  ROW_TYPE as ROW,
+  TABS_TYPE as TABS,
+  TAB_TYPE as TAB,
+} from '../../../../src/dashboard/util/componentTypes';
+
+const getIndentation = depth =>
+  Array(depth * 3)
+    .fill('')
+    .join('-');
+
+describe('isValidChild', () => {
+  describe('valid calls', () => {
+    // these are representations of nested structures for easy testing
+    //  [ROOT (depth 0) > GRID (depth 1) > HEADER (depth 2)]
+    // every unique parent > child relationship is tested, but because this
+    // test representation WILL result in duplicates, we hash each test
+    // to keep track of which we've run
+    const didTest = {};
+    const validExamples = [
+      [ROOT, GRID, CHART], // chart is valid because it is wrapped in a row
+      [ROOT, GRID, MARKDOWN], // markdown is valid because it is wrapped in a row
+      [ROOT, GRID, COLUMN], // column is valid because it is wrapped in a row
+      [ROOT, GRID, HEADER],
+      [ROOT, GRID, ROW, MARKDOWN],
+      [ROOT, GRID, ROW, CHART],
+
+      [ROOT, GRID, ROW, COLUMN, HEADER],
+      [ROOT, GRID, ROW, COLUMN, DIVIDER],
+      [ROOT, GRID, ROW, COLUMN, CHART],
+      [ROOT, GRID, ROW, COLUMN, MARKDOWN],
+
+      [ROOT, GRID, ROW, COLUMN, ROW, CHART],
+      [ROOT, GRID, ROW, COLUMN, ROW, MARKDOWN],
+
+      [ROOT, GRID, ROW, COLUMN, ROW, COLUMN, CHART],
+      [ROOT, GRID, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
+      [ROOT, GRID, TABS, TAB, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
+
+      // tab equivalents
+      [ROOT, TABS, TAB, CHART],
+      [ROOT, TABS, TAB, MARKDOWN],
+      [ROOT, TABS, TAB, COLUMN],
+      [ROOT, TABS, TAB, HEADER],
+      [ROOT, TABS, TAB, ROW, MARKDOWN],
+      [ROOT, TABS, TAB, ROW, CHART],
+
+      [ROOT, TABS, TAB, ROW, COLUMN, HEADER],
+      [ROOT, TABS, TAB, ROW, COLUMN, DIVIDER],
+      [ROOT, TABS, TAB, ROW, COLUMN, CHART],
+      [ROOT, TABS, TAB, ROW, COLUMN, MARKDOWN],
+
+      [ROOT, TABS, TAB, ROW, COLUMN, ROW, CHART],
+      [ROOT, TABS, TAB, ROW, COLUMN, ROW, MARKDOWN],
+
+      [ROOT, TABS, TAB, ROW, COLUMN, ROW, COLUMN, CHART],
+      [ROOT, TABS, TAB, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
+      [ROOT, TABS, TAB, TABS, TAB, ROW, COLUMN, ROW, COLUMN, MARKDOWN],
+    ];
+
+    validExamples.forEach((example, exampleIdx) => {
+      let childDepth = 0;
+      example.forEach((childType, i) => {
+        const parentDepth = childDepth - 1;
+        const parentType = example[i - 1];
+        const testKey = `${parentType}-${childType}-${parentDepth}`;
+
+        if (i > 0 && !didTest[testKey]) {
+          didTest[testKey] = true;
+
+          it(`(${exampleIdx})${getIndentation(
+            childDepth,
+          )}${parentType} (depth ${parentDepth}) > ${childType} ✅`, () => {
+            expect(
+              isValidChild({
+                parentDepth,
+                parentType,
+                childType,
+              }),
+            ).to.equal(true);
+          });
+        }
+        // see isValidChild.js for why tabs do not increment the depth of their children
+        childDepth += childType !== TABS && childType !== TAB ? 1 : 0;
+      });
+    });
+  });
+
+  describe('invalid calls', () => {
+    // In order to assert that a parent > child hierarchy at a given depth is invalid
+    // we also define some valid hierarchies in doing so. we indicate which
+    // parent > [child] relationships should be asserted as invalid using a nested array
+    const invalidExamples = [
+      [ROOT, [DIVIDER]],
+      [ROOT, [CHART]],
+      [ROOT, [MARKDOWN]],
+      [ROOT, GRID, [TAB]],
+      [ROOT, GRID, TABS, [ROW]],
+      [ROOT, GRID, TABS, TAB, [TABS]],
+      [ROOT, GRID, ROW, [TABS]],
+      [ROOT, GRID, ROW, [TAB]],
+      [ROOT, GRID, ROW, [DIVIDER]],
+      [ROOT, GRID, ROW, COLUMN, [TABS]],
+      [ROOT, GRID, ROW, COLUMN, [TAB]],
+      [ROOT, GRID, ROW, COLUMN, ROW, [DIVIDER]],
+      [ROOT, GRID, ROW, COLUMN, ROW, COLUMN, [ROW]], // too nested
+    ];
+
+    invalidExamples.forEach((example, exampleIdx) => {
+      let childDepth = 0;
+      example.forEach((childType, i) => {
+        const shouldTestChild = Array.isArray(childType);
+
+        if (i > 0 && shouldTestChild) {
+          const parentDepth = childDepth - 1;
+          const parentType = example[i - 1];
+
+          it(`(${exampleIdx})${getIndentation(
+            childDepth,
+          )}${parentType} (depth ${parentDepth}) > ${childType} ❌`, () => {
+            expect(
+              isValidChild({
+                parentDepth,
+                parentType,
+                childType,
+              }),
+            ).to.equal(false);
+          });
+        }
+        // see isValidChild.js for why tabs do not increment the depth of their children
+        childDepth += childType !== TABS && childType !== TAB ? 1 : 0;
+      });
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/newComponentFactory_spec.js b/superset/assets/spec/javascripts/dashboard/util/newComponentFactory_spec.js
new file mode 100644
index 0000000..f52eba9
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/newComponentFactory_spec.js
@@ -0,0 +1,51 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import newComponentFactory from '../../../../src/dashboard/util/newComponentFactory';
+
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  MARKDOWN_TYPE,
+  NEW_COMPONENT_SOURCE_TYPE,
+  ROW_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+const types = [
+  CHART_TYPE,
+  COLUMN_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  DIVIDER_TYPE,
+  HEADER_TYPE,
+  MARKDOWN_TYPE,
+  NEW_COMPONENT_SOURCE_TYPE,
+  ROW_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+];
+
+describe('newEntityFactory', () => {
+  types.forEach(type => {
+    it(`returns a new ${type}`, () => {
+      const result = newComponentFactory(type);
+
+      expect(result.type).to.equal(type);
+      expect(typeof result.id).to.equal('string');
+      expect(typeof result.meta).to.equal('object');
+      expect(Array.isArray(result.children)).to.equal(true);
+    });
+  });
+
+  it('adds passed meta data to the entity', () => {
+    const banana = 'banana';
+    const result = newComponentFactory(CHART_TYPE, { banana });
+    expect(result.meta.banana).to.equal(banana);
+  });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js b/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
new file mode 100644
index 0000000..677c329
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
@@ -0,0 +1,82 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import newEntitiesFromDrop from '../../../../src/dashboard/util/newEntitiesFromDrop';
+import {
+  CHART_TYPE,
+  DASHBOARD_GRID_TYPE,
+  ROW_TYPE,
+  TABS_TYPE,
+  TAB_TYPE,
+} from '../../../../src/dashboard/util/componentTypes';
+
+describe('newEntitiesFromDrop', () => {
+  it('should return a new Entity of appropriate type, and add it to the drop target children', () => {
+    const result = newEntitiesFromDrop({
+      dropResult: {
+        destination: { id: 'a', index: 0 },
+        dragging: { type: CHART_TYPE },
+      },
+      layout: {
+        a: {
+          id: 'a',
+          type: ROW_TYPE,
+          children: [],
+        },
+      },
+    });
+
+    const newId = result.a.children[0];
+    expect(result.a.children.length).to.equal(1);
+    expect(Object.keys(result).length).to.equal(2);
+    expect(result[newId].type).to.equal(CHART_TYPE);
+  });
+
+  it('should create Tab AND Tabs components if the drag entity is Tabs', () => {
+    const result = newEntitiesFromDrop({
+      dropResult: {
+        destination: { id: 'a', index: 0 },
+        dragging: { type: TABS_TYPE },
+      },
+      layout: {
+        a: {
+          id: 'a',
+          type: DASHBOARD_GRID_TYPE,
+          children: [],
+        },
+      },
+    });
+
+    const newTabsId = result.a.children[0];
+    const newTabId = result[newTabsId].children[0];
+
+    expect(result.a.children.length).to.equal(1);
+    expect(Object.keys(result).length).to.equal(3);
+    expect(result[newTabsId].type).to.equal(TABS_TYPE);
+    expect(result[newTabId].type).to.equal(TAB_TYPE);
+  });
+
+  it('should create a Row if the drag entity should be wrapped in a row', () => {
+    const result = newEntitiesFromDrop({
+      dropResult: {
+        destination: { id: 'a', index: 0 },
+        dragging: { type: CHART_TYPE },
+      },
+      layout: {
+        a: {
+          id: 'a',
+          type: DASHBOARD_GRID_TYPE,
+          children: [],
+        },
+      },
+    });
+
+    const newRowId = result.a.children[0];
+    const newChartId = result[newRowId].children[0];
+
+    expect(result.a.children.length).to.equal(1);
+    expect(Object.keys(result).length).to.equal(3);
+    expect(result[newRowId].type).to.equal(ROW_TYPE);
+    expect(result[newChartId].type).to.equal(CHART_TYPE);
+  });
+});
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index 33f49d1..529d295 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -201,4 +201,3 @@ export function refreshChart(chart, force, timeout) {
     dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id))
   );
 }
-
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index c64ea0d..d210ee6 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -12,20 +12,17 @@ import {
 import dropOverflowsParent from '../util/dropOverflowsParent';
 import findParentId from '../util/findParentId';
 
-// Component CRUD -------------------------------------------------------------
-export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
-function updateLayoutComponents(nextComponents) {
-  return {
-    type: UPDATE_COMPONENTS,
-    payload: {
-      nextComponents,
-    },
-  };
-}
-
-export function updateComponents(nextComponents) {
-  return (dispatch, getState) => {
-    dispatch(updateLayoutComponents(nextComponents));
+// this is a helper that takes an action as input and dispatches
+// an additional setUnsavedChanges(true) action after the dispatch in the case
+// that dashboardState.hasUnsavedChanges is false.
+function setUnsavedChangesAfterAction(action) {
+  return (...args) => (dispatch, getState) => {
+    const result = action(...args);
+    if (typeof result === 'function') {
+      dispatch(result(dispatch, getState));
+    } else {
+      dispatch(result);
+    }
 
     if (!getState().dashboardState.hasUnsavedChanges) {
       dispatch(setUnsavedChanges(true));
@@ -33,6 +30,18 @@ export function updateComponents(nextComponents) {
   };
 }
 
+// Component CRUD -------------------------------------------------------------
+export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
+
+export const updateComponents = setUnsavedChangesAfterAction(
+  nextComponents => ({
+    type: UPDATE_COMPONENTS,
+    payload: {
+      nextComponents,
+    },
+  }),
+);
+
 export function updateDashboardTitle(text) {
   return (dispatch, getState) => {
     const { dashboardLayout } = getState();
@@ -50,90 +59,42 @@ export function updateDashboardTitle(text) {
 }
 
 export const DELETE_COMPONENT = 'DELETE_COMPONENT';
-function deleteLayoutComponent(id, parentId) {
-  return {
-    type: DELETE_COMPONENT,
-    payload: {
-      id,
-      parentId,
-    },
-  };
-}
-
-export function deleteComponent(id, parentId) {
-  return (dispatch, getState) => {
-    dispatch(deleteLayoutComponent(id, parentId));
-
-    if (!getState().dashboardState.hasUnsavedChanges) {
-      dispatch(setUnsavedChanges(true));
-    }
-  };
-}
+export const deleteComponent = setUnsavedChangesAfterAction((id, parentId) => ({
+  type: DELETE_COMPONENT,
+  payload: {
+    id,
+    parentId,
+  },
+}));
 
 export const CREATE_COMPONENT = 'CREATE_COMPONENT';
-function createLayoutComponent(dropResult) {
-  return {
-    type: CREATE_COMPONENT,
-    payload: {
-      dropResult,
-    },
-  };
-}
-
-export function createComponent(dropResult) {
-  return (dispatch, getState) => {
-    dispatch(createLayoutComponent(dropResult));
-
-    if (!getState().dashboardState.hasUnsavedChanges) {
-      dispatch(setUnsavedChanges(true));
-    }
-  };
-}
+export const createComponent = setUnsavedChangesAfterAction(dropResult => ({
+  type: CREATE_COMPONENT,
+  payload: {
+    dropResult,
+  },
+}));
 
 // Tabs -----------------------------------------------------------------------
 export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
-function createTopLevelTabsAction(dropResult) {
-  return {
-    type: CREATE_TOP_LEVEL_TABS,
-    payload: {
-      dropResult,
-    },
-  };
-}
-
-export function createTopLevelTabs(dropResult) {
-  return (dispatch, getState) => {
-    dispatch(createTopLevelTabsAction(dropResult));
-
-    if (!getState().dashboardState.hasUnsavedChanges) {
-      dispatch(setUnsavedChanges(true));
-    }
-  };
-}
+export const createTopLevelTabs = setUnsavedChangesAfterAction(dropResult => ({
+  type: CREATE_TOP_LEVEL_TABS,
+  payload: {
+    dropResult,
+  },
+}));
 
 export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
-function deleteTopLevelTabsAction() {
-  return {
-    type: DELETE_TOP_LEVEL_TABS,
-    payload: {},
-  };
-}
-
-export function deleteTopLevelTabs(dropResult) {
-  return (dispatch, getState) => {
-    dispatch(deleteTopLevelTabsAction(dropResult));
-
-    if (!getState().dashboardState.hasUnsavedChanges) {
-      dispatch(setUnsavedChanges(true));
-    }
-  };
-}
+export const deleteTopLevelTabs = setUnsavedChangesAfterAction(() => ({
+  type: DELETE_TOP_LEVEL_TABS,
+  payload: {},
+}));
 
 // Resize ---------------------------------------------------------------------
 export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
 export function resizeComponent({ id, width, height }) {
   return (dispatch, getState) => {
-    const { dashboardLayout: undoableLayout, dashboardState } = getState();
+    const { dashboardLayout: undoableLayout } = getState();
     const { present: dashboard } = undoableLayout;
     const component = dashboard[id];
     const widthChanged = width && component.meta.width !== width;
@@ -168,16 +129,13 @@ export function resizeComponent({ id, width, height }) {
       });
 
       dispatch(updateComponents(updatedComponents));
-      if (!dashboardState.hasUnsavedChanges) {
-        dispatch(setUnsavedChanges(true));
-      }
     }
   };
 }
 
 // Drag and drop --------------------------------------------------------------
 export const MOVE_COMPONENT = 'MOVE_COMPONENT';
-export function moveComponent(dropResult) {
+function moveComponent(dropResult) {
   return {
     type: MOVE_COMPONENT,
     payload: {
@@ -220,29 +178,24 @@ export function handleComponentDrop(dropResult) {
       dispatch(moveComponent(dropResult));
     }
 
-    const { dashboardLayout: undoableLayout, dashboardState } = getState();
+    const { dashboardLayout: undoableLayout } = getState();
 
     // if we moved a Tab and the parent Tabs no longer has children, delete it.
     if (!isNewComponent) {
       const { present: layout } = undoableLayout;
       const sourceComponent = layout[source.id];
-
       if (
         sourceComponent.type === TABS_TYPE &&
         sourceComponent.children.length === 0
       ) {
         const parentId = findParentId({
           childId: source.id,
-          components: layout,
+          layout,
         });
         dispatch(deleteComponent(source.id, parentId));
       }
     }
 
-    if (!dashboardState.hasUnsavedChanges) {
-      dispatch(setUnsavedChanges(true));
-    }
-
     return null;
   };
 }
@@ -263,12 +216,6 @@ export function undoLayoutAction() {
   };
 }
 
-export function redoLayoutAction() {
-  return (dispatch, getState) => {
-    dispatch(UndoActionCreators.redo());
-
-    if (!getState().dashboardState.hasUnsavedChanges) {
-      dispatch(setUnsavedChanges(true));
-    }
-  };
-}
+export const redoLayoutAction = setUnsavedChangesAfterAction(
+  UndoActionCreators.redo,
+);
diff --git a/superset/assets/src/dashboard/actions/messageToasts.js b/superset/assets/src/dashboard/actions/messageToasts.js
index 367b36f..fde02c4 100644
--- a/superset/assets/src/dashboard/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/actions/messageToasts.js
@@ -1,3 +1,5 @@
+import shortid from 'shortid';
+
 import {
   INFO_TOAST,
   SUCCESS_TOAST,
@@ -6,11 +8,7 @@ import {
 } from '../util/constants';
 
 function getToastUuid(type) {
-  return `${Math.random()
-    .toString(16)
-    .slice(2)}-${type}-${Math.random()
-    .toString(16)
-    .slice(2)}`;
+  return `${type}-${shortid.generate()}`;
 }
 
 export const ADD_TOAST = 'ADD_TOAST';
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 369ed46..644ddf0 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -91,6 +91,7 @@ class Dashboard extends React.PureComponent {
 
     const currentChartIds = getChartIdsFromLayout(this.props.layout);
     const nextChartIds = getChartIdsFromLayout(nextProps.layout);
+
     if (currentChartIds.length < nextChartIds.length) {
       // adding new chart
       const newChartId = nextChartIds.find(
@@ -109,23 +110,26 @@ class Dashboard extends React.PureComponent {
   componentDidUpdate(prevProps) {
     const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
     if (refresh) {
-      let changedFilterKey;
-      const prevFiltersKeySet = new Set(
-        Object.keys(prevProps.dashboardState.filters),
-      );
-      Object.keys(filters).some(key => {
-        prevFiltersKeySet.delete(key);
+      // refresh charts if a filter was removed, added, or changed
+      let changedFilterKey = null;
+      const currFilterKeys = Object.keys(filters);
+      const prevFilterKeys = Object.keys(prevProps.dashboardState.filters);
+
+      currFilterKeys.forEach(key => {
+        const prevFilter = prevProps.dashboardState.filters[key];
         if (
-          prevProps.dashboardState.filters[key] === undefined ||
-          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])
+          // filter was added or changed
+          typeof prevFilter === 'undefined' ||
+          !areObjectsEqual(prevFilter, filters[key])
         ) {
           changedFilterKey = key;
-          return true;
         }
-        return false;
       });
-      // has changed filter or removed a filter?
-      if (!!changedFilterKey || prevFiltersKeySet.size) {
+
+      if (
+        !!changedFilterKey ||
+        currFilterKeys.length !== prevFilterKeys.length
+      ) {
         this.refreshExcept(changedFilterKey);
       }
     }
@@ -144,27 +148,24 @@ class Dashboard extends React.PureComponent {
 
   refreshExcept(filterKey) {
     const immune = this.props.dashboardInfo.metadata.filter_immune_slices || [];
-    let charts = this.getAllCharts();
-    if (filterKey) {
-      charts = charts.filter(
-        chart =>
-          String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1,
-      );
-    }
-    charts.forEach(chart => {
-      const updatedFormData = getFormDataWithExtraFilters({
-        chart,
-        dashboardMetadata: this.props.dashboardInfo.metadata,
-        filters: this.props.dashboardState.filters,
-        sliceId: chart.id,
-      });
 
-      this.props.actions.runQuery(
-        updatedFormData,
-        false,
-        this.props.timeout,
-        chart.id,
-      );
+    this.getAllCharts().forEach(chart => {
+      // filterKey is a string, immune array contains numbers
+      if (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1) {
+        const updatedFormData = getFormDataWithExtraFilters({
+          chart,
+          dashboardMetadata: this.props.dashboardInfo.metadata,
+          filters: this.props.dashboardState.filters,
+          sliceId: chart.id,
+        });
+
+        this.props.actions.runQuery(
+          updatedFormData,
+          false,
+          this.props.timeout,
+          chart.id,
+        );
+      }
     });
   }
 
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 0951ebf..0f42f1b 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -1,7 +1,5 @@
 /* eslint-env browser */
 import cx from 'classnames';
-import { DragDropContext } from 'react-dnd';
-import HTML5Backend from 'react-dnd-html5-backend';
 // ParentSize uses resize observer so the dashboard will update size
 // when its container size changes, due to e.g., builder side panel opening
 import ParentSize from '@vx/responsive/build/components/ParentSize';
@@ -19,6 +17,8 @@ import DashboardComponent from '../containers/DashboardComponent';
 import ToastPresenter from '../containers/ToastPresenter';
 import WithPopoverMenu from './menu/WithPopoverMenu';
 
+import getDragDropManager from '../util/getDragDropManager';
+
 import {
   DASHBOARD_GRID_ID,
   DASHBOARD_ROOT_ID,
@@ -59,6 +59,12 @@ class DashboardBuilder extends React.Component {
     this.handleChangeTab = this.handleChangeTab.bind(this);
   }
 
+  getChildContext() {
+    return {
+      dragDropManager: this.context.dragDropManager || getDragDropManager(),
+    };
+  }
+
   handleChangeTab({ tabIndex }) {
     this.setState(() => ({ tabIndex }));
     setTimeout(() => {
@@ -170,7 +176,8 @@ class DashboardBuilder extends React.Component {
                       >
                         <DashboardGrid
                           gridComponent={dashboardLayout[id]}
-                          depth={DASHBOARD_ROOT_DEPTH + 1}
+                          // see isValidChild for why tabs do not increment the depth of their children
+                          depth={DASHBOARD_ROOT_DEPTH + (topLevelTabs ? 0 : 1)}
                           width={width}
                         />
                       </TabPane>
@@ -197,5 +204,8 @@ class DashboardBuilder extends React.Component {
 
 DashboardBuilder.propTypes = propTypes;
 DashboardBuilder.defaultProps = defaultProps;
+DashboardBuilder.childContextTypes = {
+  dragDropManager: PropTypes.object.isRequired,
+};
 
-export default DragDropContext(HTML5Backend)(DashboardBuilder);
+export default DashboardBuilder;
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 47451c4..d8ed53e 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -19,13 +19,13 @@ const propTypes = {
   lastUpdated: PropTypes.number.isRequired,
   errorMessage: PropTypes.string,
   userId: PropTypes.string.isRequired,
-  selectedSliceIds: PropTypes.object,
+  selectedSliceIds: PropTypes.arrayOf(PropTypes.number).isRequired,
   editMode: PropTypes.bool,
   height: PropTypes.number,
 };
 
 const defaultProps = {
-  selectedSliceIds: new Set(),
+  selectedSliceIds: [],
   editMode: false,
   errorMessage: '',
   height: window.innerHeight,
@@ -63,6 +63,7 @@ class SliceAdder extends React.Component {
       filteredSlices: [],
       searchTerm: '',
       sortBy: KEYS_TO_SORT.findIndex(item => item.key === 'changed_on'),
+      selectedSliceIdsSet: new Set(props.selectedSliceIds),
     };
 
     this.rowRenderer = this.rowRenderer.bind(this);
@@ -76,14 +77,19 @@ class SliceAdder extends React.Component {
   }
 
   componentWillReceiveProps(nextProps) {
+    const nextState = {};
     if (nextProps.lastUpdated !== this.props.lastUpdated) {
-      this.setState({
-        filteredSlices: Object.values(nextProps.slices)
-          .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
-          .sort(
-            SliceAdder.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key),
-          ),
-      });
+      nextState.filteredSlices = Object.values(nextProps.slices)
+        .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
+        .sort(SliceAdder.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key));
+    }
+
+    if (nextProps.selectedSliceIds !== this.props.selectedSliceIds) {
+      nextState.selectedSliceIdsSet = new Set(nextProps.selectedSliceIds);
+    }
+
+    if (Object.keys(nextState).length) {
+      this.setState(nextState);
     }
   }
 
@@ -128,12 +134,15 @@ class SliceAdder extends React.Component {
   }
 
   rowRenderer({ key, index, style }) {
-    const cellData = this.state.filteredSlices[index];
-    const isSelected = this.props.selectedSliceIds.has(cellData.slice_id);
+    const { filteredSlices, selectedSliceIdsSet } = this.state;
+    const cellData = filteredSlices[index];
+    const isSelected = selectedSliceIdsSet.has(cellData.slice_id);
     const type = CHART_TYPE;
     const id = NEW_CHART_ID;
+
     const meta = {
       chartId: cellData.slice_id,
+      sliceName: cellData.slice_name,
     };
 
     return (
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 3151841..50a2a5d 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -65,10 +65,15 @@ class SliceHeader extends React.PureComponent {
       <div className="chart-header" ref={innerRef}>
         <div className="header">
           <EditableTitle
-            title={sliceName}
+            title={
+              sliceName ||
+              (this.props.editMode
+                ? '---' // this makes an empty title clickable
+                : '')
+            }
             canEdit={this.props.editMode}
             onSaveTitle={this.props.updateSliceName}
-            showTooltip={this.props.editMode}
+            showTooltip={false}
           />
           {!!Object.values(this.props.annotationQuery).length && (
             <TooltipWrapper
diff --git a/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx b/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
index bfe4973..ef116ea 100644
--- a/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
+++ b/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
@@ -47,7 +47,8 @@ const defaultProps = {
   useEmptyDragPreview: false,
 };
 
-class DragDroppable extends React.Component {
+// export unwrapped component for testing
+export class UnwrappedDragDroppable extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
@@ -92,6 +93,25 @@ class DragDroppable extends React.Component {
     } = this.props;
 
     const { dropIndicator } = this.state;
+    const dropIndicatorProps =
+      isDraggingOver && dropIndicator
+        ? {
+            className: cx(
+              'drop-indicator',
+              dropIndicator === DROP_TOP && 'drop-indicator--top',
+              dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
+              dropIndicator === DROP_LEFT && 'drop-indicator--left',
+              dropIndicator === DROP_RIGHT && 'drop-indicator--right',
+            ),
+          }
+        : null;
+
+    const childProps = editMode
+      ? {
+          dragSourceRef,
+          dropIndicatorProps,
+        }
+      : {};
 
     return (
       <div
@@ -105,33 +125,17 @@ class DragDroppable extends React.Component {
           className,
         )}
       >
-        {children(
-          !editMode
-            ? {}
-            : {
-                dragSourceRef,
-                dropIndicatorProps: isDraggingOver &&
-                  dropIndicator && {
-                    className: cx(
-                      'drop-indicator',
-                      dropIndicator === DROP_TOP && 'drop-indicator--top',
-                      dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
-                      dropIndicator === DROP_LEFT && 'drop-indicator--left',
-                      dropIndicator === DROP_RIGHT && 'drop-indicator--right',
-                    ),
-                  },
-              },
-        )}
+        {children(childProps)}
       </div>
     );
   }
 }
 
-DragDroppable.propTypes = propTypes;
-DragDroppable.defaultProps = defaultProps;
+UnwrappedDragDroppable.propTypes = propTypes;
+UnwrappedDragDroppable.defaultProps = defaultProps;
 
 // note that the composition order here determines using
 // component.method() vs decoratedComponentInstance.method() in the drag/drop config
 export default DropTarget(...dropConfig)(
-  DragSource(...dragConfig)(DragDroppable),
+  DragSource(...dragConfig)(UnwrappedDragDroppable),
 );
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 2aedca7..1ace51d 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -140,7 +140,7 @@ class Chart extends React.Component {
     return this.props.refreshChart(this.props.chart, true, this.props.timeout);
   }
 
-  removeFilter(args) {
+  removeFilter(...args) {
     this.props.removeFilter(this.props.id, ...args);
   }
 
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index bc9f430..ab030f4 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -68,7 +68,7 @@ class ChartHolder extends React.Component {
         ...component,
         meta: {
           ...component.meta,
-          chartName: nextName,
+          sliceName: nextName,
         },
       },
     });
@@ -133,7 +133,7 @@ class ChartHolder extends React.Component {
                 id={component.meta.chartId}
                 width={widthMultiple * columnWidth}
                 height={component.meta.height * GRID_BASE_UNIT - CHART_MARGIN}
-                sliceName={component.meta.chartName}
+                sliceName={component.meta.sliceName || ''}
                 updateSliceName={this.handleUpdateSliceName}
               />
               {editMode && (
diff --git a/superset/assets/src/dashboard/components/gridComponents/Header.jsx b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
index 5114a77..683af9e 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Header.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
@@ -107,11 +107,12 @@ class Header extends React.PureComponent {
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            {editMode && (
-              <HoverMenu position="left">
-                <DragHandle position="left" />
-              </HoverMenu>
-            )}
+            {editMode &&
+            depth <= 2 && ( // drag handle looks bad when nested
+                <HoverMenu position="left">
+                  <DragHandle position="left" />
+                </HoverMenu>
+              )}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
index 3119a08..28e7042 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
@@ -122,7 +122,7 @@ class Row extends React.PureComponent {
             menuItems={[
               <BackgroundStyleDropdown
                 id={`${rowComponent.id}-background`}
-                value={rowComponent.meta.background}
+                value={backgroundStyle.value}
                 onChange={this.handleChangeBackground}
               />,
             ]}
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 2b38d8a..4ad1270 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -11,6 +11,8 @@ import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilt
 import { updateComponents } from '../actions/dashboardLayout';
 import Chart from '../components/gridComponents/Chart';
 
+const EMPTY_FILTERS = {};
+
 function mapStateToProps(
   {
     charts: chartQueries,
@@ -22,7 +24,7 @@ function mapStateToProps(
   ownProps,
 ) {
   const { id } = ownProps;
-  const chart = chartQueries[id];
+  const chart = chartQueries[id] || {};
   const { filters } = dashboardState;
 
   return {
@@ -30,7 +32,7 @@ function mapStateToProps(
     datasource: chart && datasources[chart.form_data.datasource],
     slice: sliceEntities.slices[id],
     timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    filters,
+    filters: filters[id] || EMPTY_FILTERS,
     // note: this method caches filters if possible to prevent render cascades
     formData: getFormDataWithExtraFilters({
       chart,
diff --git a/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
index cee948a..e306288 100644
--- a/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
+++ b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
@@ -1,6 +1,6 @@
 import {
   DASHBOARD_GRID_TYPE,
-  DASHBOARD_HEADER_TYPE,
+  HEADER_TYPE,
   DASHBOARD_ROOT_TYPE,
 } from '../util/componentTypes';
 
@@ -25,7 +25,7 @@ export default {
   },
 
   [DASHBOARD_HEADER_ID]: {
-    type: DASHBOARD_HEADER_TYPE,
+    type: HEADER_TYPE,
     id: DASHBOARD_HEADER_ID,
     meta: {
       text: 'New dashboard',
diff --git a/superset/assets/src/dashboard/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index 573a143..4b3ee49 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -82,7 +82,7 @@ const actionHandlers = {
       payload: { dropResult },
     } = action;
     const { destination, dragging } = dropResult;
-    const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+    const newEntities = newEntitiesFromDrop({ dropResult, layout: state });
 
     // if column is a parent, set any resizable children to have a minimum width so that
     // the chances that they are validly movable to future containers is maximized
@@ -139,18 +139,18 @@ const actionHandlers = {
       nextEntities[newRow.id] = newRow;
     }
 
-    // inherit the width of a column parent
+    // if column is a parent, set any resizable children to have a minimum width so that
+    // the chances that they are validly movable to future containers is maximized
     if (
       destination.type === COLUMN_TYPE &&
       [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
     ) {
       const component = nextEntities[dragging.id];
-      const parentColumn = nextEntities[destination.id];
       nextEntities[dragging.id] = {
         ...component,
         meta: {
           ...component.meta,
-          width: parentColumn.meta.width,
+          width: GRID_MIN_COLUMN_COUNT,
         },
       };
     }
@@ -201,7 +201,7 @@ const actionHandlers = {
     }
 
     // create new component
-    const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+    const newEntities = newEntitiesFromDrop({ dropResult, layout: state });
     const newEntitiesArray = Object.values(newEntities);
     const tabComponent = newEntitiesArray.find(
       component => component.type === TAB_TYPE,
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 2523494..c7f2277 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -1,6 +1,4 @@
 /* eslint-disable camelcase */
-import { merge as mergeArray } from 'd3';
-
 import {
   ADD_SLICE,
   ADD_FILTER,
@@ -27,7 +25,7 @@ export default function dashboardStateReducer(state = {}, action) {
       updatedSliceIds.add(action.slice.slice_id);
       return {
         ...state,
-        sliceIds: updatedSliceIds,
+        sliceIds: Array.from(updatedSliceIds),
       };
     },
     [REMOVE_SLICE]() {
@@ -45,7 +43,7 @@ export default function dashboardStateReducer(state = {}, action) {
       }
       return {
         ...state,
-        sliceIds: updatedSliceIds,
+        sliceIds: Array.from(updatedSliceIds),
         filters: newFilter,
         refresh,
       };
@@ -54,7 +52,11 @@ export default function dashboardStateReducer(state = {}, action) {
       return { ...state, isStarred: action.isStarred };
     },
     [SET_EDIT_MODE]() {
-      return { ...state, editMode: action.editMode };
+      return {
+        ...state,
+        editMode: action.editMode,
+        showBuilderPane: !!action.editMode,
+      };
     },
     [SET_MAX_UNDO_HISTORY_EXCEEDED]() {
       const { maxUndoHistoryExceeded = true } = action.payload;
@@ -86,7 +88,7 @@ export default function dashboardStateReducer(state = {}, action) {
 
     // filters
     [ADD_FILTER]() {
-      const hasSelectedFilter = state.sliceIds.has(action.chart.id);
+      const hasSelectedFilter = state.sliceIds.includes(action.chart.id);
       if (!hasSelectedFilter) {
         return state;
       }
@@ -115,9 +117,9 @@ export default function dashboardStateReducer(state = {}, action) {
           // d3.merge pass in array of arrays while some value form filter components
           // from and to filter box require string to be process and return
         } else if (filters[sliceId][col] instanceof Array) {
-          newFilter[col] = mergeArray([filters[sliceId][col], vals]);
+          newFilter[col] = [...filters[sliceId][col], ...vals];
         } else {
-          newFilter[col] = mergeArray([[filters[sliceId][col]], vals])[0] || '';
+          newFilter[col] = [filters[sliceId][col], ...vals];
         }
         filters = { ...filters, [sliceId]: newFilter };
       }
@@ -126,14 +128,15 @@ export default function dashboardStateReducer(state = {}, action) {
     [REMOVE_FILTER]() {
       const { sliceId, col, vals, refresh } = action;
       const excluded = new Set(vals);
-      const valFilter = val => !excluded.has(val);
 
       let filters = state.filters;
       // Have to be careful not to modify the dashboard state so that
       // the render actually triggers
       if (sliceId in state.filters && col in state.filters[sliceId]) {
-        const newFilter = filters[sliceId][col].filter(valFilter);
-        filters = { ...filters, [sliceId]: newFilter };
+        const newFilter = filters[sliceId][col].filter(
+          val => !excluded.has(val),
+        );
+        filters = { ...filters, [sliceId]: { [col]: newFilter } };
       }
       return { ...state, filters, refresh };
     },
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index 534b15d..d56e480 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -99,7 +99,7 @@ export default function(bootstrapData) {
     // functionality and python updates slice names based on layout upon dashboard save
     const layoutId = chartIdToLayoutId[key];
     if (layoutId && layout[layoutId]) {
-      layout[layoutId].meta.chartName = slice.slice_name;
+      layout[layoutId].meta.sliceName = slice.slice_name;
     }
   });
 
@@ -126,7 +126,7 @@ export default function(bootstrapData) {
       common,
     },
     dashboardState: {
-      sliceIds,
+      sliceIds: Array.from(sliceIds),
       refresh: false,
       filters,
       expandedSlices: dashboard.metadata.expanded_slices || {},
diff --git a/superset/assets/src/dashboard/reducers/sliceEntities.js b/superset/assets/src/dashboard/reducers/sliceEntities.js
index c1453f5..c5e46c2 100644
--- a/superset/assets/src/dashboard/reducers/sliceEntities.js
+++ b/superset/assets/src/dashboard/reducers/sliceEntities.js
@@ -2,7 +2,6 @@ import {
   FETCH_ALL_SLICES_FAILED,
   FETCH_ALL_SLICES_STARTED,
   SET_ALL_SLICES,
-  UPDATE_SLICE_NAME,
 } from '../actions/sliceEntities';
 import { t } from '../../locales';
 
@@ -18,17 +17,6 @@ export default function sliceEntitiesReducer(
   action,
 ) {
   const actionHandlers = {
-    [UPDATE_SLICE_NAME]() {
-      const updatedSlice = {
-        ...state.slices[action.key],
-        slice_name: action.sliceName,
-      };
-      const updatedSlices = {
-        ...state.slices,
-        [action.key]: updatedSlice,
-      };
-      return { ...state, slices: updatedSlices };
-    },
     [FETCH_ALL_SLICES_STARTED]() {
       return {
         ...state,
@@ -46,7 +34,7 @@ export default function sliceEntitiesReducer(
     [FETCH_ALL_SLICES_FAILED]() {
       const respJSON = action.error.responseJSON;
       const errorMessage =
-        t('Sorry, there was an error adding slices to this dashboard: ') +
+        t('Sorry, there was an error fetching slices: ') +
         (respJSON && respJSON.message)
           ? respJSON.message
           : action.error.responseText;
diff --git a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
index f48631f..7b8b5ce 100644
--- a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
+++ b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
@@ -5,14 +5,13 @@ export default function getEffectiveExtraFilters({
 }) {
   const immuneSlices = dashboardMetadata.filter_immune_slices || [];
 
-  const effectiveFilters = [];
-
   if (sliceId && immuneSlices.includes(sliceId)) {
     // The slice is immune to dashboard filters
-    return effectiveFilters;
+    return [];
   }
 
   // Build a list of fields the slice is immune to filters on
+  const effectiveFilters = [];
   let immuneToFields = [];
   if (
     sliceId &&
@@ -27,12 +26,13 @@ export default function getEffectiveExtraFilters({
       // Filters applied by the slice don't apply to itself
       return;
     }
-    Object.keys(filters[filteringSliceId]).forEach(field => {
+    const filtersFromSlice = filters[filteringSliceId];
+    Object.keys(filtersFromSlice).forEach(field => {
       if (!immuneToFields.includes(field)) {
         effectiveFilters.push({
           col: field,
           op: 'in',
-          val: filters[filteringSliceId][field],
+          val: filtersFromSlice[field],
         });
       }
     });
diff --git a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
index 17f7bb3..2fb5767 100644
--- a/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
+++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
@@ -14,11 +14,9 @@ export default function getFormDataWithExtraFilters({
 }) {
   // if dashboard metadata + filters have not changed, use cache if possible
   if (
-    cachedDashboardMetadataByChart[sliceId] &&
-    cachedDashboardMetadataByChart[sliceId] === dashboardMetadata &&
-    cachedFiltersByChart[sliceId] &&
-    cachedFiltersByChart[sliceId] === filters &&
-    cachedFormdataByChart[sliceId]
+    (cachedDashboardMetadataByChart[sliceId] || {}) === dashboardMetadata &&
+    (cachedFiltersByChart[sliceId] || {}) === filters &&
+    !!cachedFormdataByChart[sliceId]
   ) {
     return cachedFormdataByChart[sliceId];
   }
diff --git a/superset/assets/src/dashboard/util/componentTypes.js b/superset/assets/src/dashboard/util/componentTypes.js
index 2866898..b773417 100644
--- a/superset/assets/src/dashboard/util/componentTypes.js
+++ b/superset/assets/src/dashboard/util/componentTypes.js
@@ -1,7 +1,7 @@
 export const CHART_TYPE = 'DASHBOARD_CHART_TYPE';
 export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE';
+export const DASHBOARD_HEADER_TYPE = 'DASHBOARD_HEADER_TYPE';
 export const DASHBOARD_GRID_TYPE = 'DASHBOARD_GRID_TYPE';
-export const DASHBOARD_HEADER_TYPE = 'DASHBOARD_DASHBOARD_HEADER_TYPE';
 export const DASHBOARD_ROOT_TYPE = 'DASHBOARD_ROOT_TYPE';
 export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE';
 export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE';
@@ -14,8 +14,8 @@ export const TAB_TYPE = 'DASHBOARD_TAB_TYPE';
 export default {
   CHART_TYPE,
   COLUMN_TYPE,
-  DASHBOARD_GRID_TYPE,
   DASHBOARD_HEADER_TYPE,
+  DASHBOARD_GRID_TYPE,
   DASHBOARD_ROOT_TYPE,
   DIVIDER_TYPE,
   HEADER_TYPE,
diff --git a/superset/assets/src/dashboard/util/dropOverflowsParent.js b/superset/assets/src/dashboard/util/dropOverflowsParent.js
index bc7195f..328d8e3 100644
--- a/superset/assets/src/dashboard/util/dropOverflowsParent.js
+++ b/superset/assets/src/dashboard/util/dropOverflowsParent.js
@@ -1,14 +1,10 @@
 import { COLUMN_TYPE } from '../util/componentTypes';
-import {
-  GRID_COLUMN_COUNT,
-  NEW_COMPONENTS_SOURCE_ID,
-  GRID_MIN_COLUMN_COUNT,
-} from './constants';
+import { GRID_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID } from './constants';
 import findParentId from './findParentId';
 import getChildWidth from './getChildWidth';
 import newComponentFactory from './newComponentFactory';
 
-export default function doesChildOverflowParent(dropResult, components) {
+export default function doesChildOverflowParent(dropResult, layout) {
   const { source, destination, dragging } = dropResult;
 
   // moving a component within a container should never overflow
@@ -17,22 +13,33 @@ export default function doesChildOverflowParent(dropResult, components) {
   }
 
   const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
-  const grandparentId = findParentId({ childId: destination.id, components });
+  const grandparentId = findParentId({
+    childId: destination.id,
+    layout,
+  });
 
   const child = isNewComponent
     ? newComponentFactory(dragging.type)
-    : components[dragging.id] || {};
-  const parent = components[destination.id] || {};
-  const grandparent = components[grandparentId] || {};
-
-  const grandparentWidth =
-    (grandparent.meta && grandparent.meta.width) || GRID_COLUMN_COUNT;
-  const parentWidth = (parent.meta && parent.meta.width) || grandparentWidth;
-  const parentChildWidth =
-    parent.type === COLUMN_TYPE
-      ? (parent.meta && parent.meta.width) || GRID_MIN_COLUMN_COUNT
-      : getChildWidth({ id: destination.id, components });
+    : layout[dragging.id] || {};
+  const parent = layout[destination.id] || {};
+  const grandparent = layout[grandparentId] || {};
+
   const childWidth = (child.meta && child.meta.width) || 0;
 
-  return parentWidth - parentChildWidth < childWidth;
+  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;
 }
diff --git a/superset/assets/src/dashboard/util/findParentId.js b/superset/assets/src/dashboard/util/findParentId.js
index f84b0de..c2e285d 100644
--- a/superset/assets/src/dashboard/util/findParentId.js
+++ b/superset/assets/src/dashboard/util/findParentId.js
@@ -1,10 +1,10 @@
-export default function findParentId({ childId, components = {} }) {
+export default function findParentId({ childId, layout = {} }) {
   let parentId = null;
 
-  const ids = Object.keys(components);
+  const ids = Object.keys(layout);
   for (let i = 0; i < ids.length - 1; i += 1) {
     const id = ids[i];
-    const component = components[id] || {};
+    const component = layout[id] || {};
     if (
       id !== childId &&
       component.children &&
diff --git a/superset/assets/src/dashboard/util/getChartIdsFromLayout.js b/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
index f0963c1..9aebb61 100644
--- a/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
+++ b/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
@@ -1,7 +1,14 @@
+import { CHART_TYPE } from './componentTypes';
+
 export default function getChartIdsFromLayout(layout) {
-  return Object.values(layout).reduce((chartIds, value) => {
-    if (value && value.meta && value.meta.chartId) {
-      chartIds.push(value.meta.chartId);
+  return Object.values(layout).reduce((chartIds, currentComponent) => {
+    if (
+      currentComponent &&
+      currentComponent.type === CHART_TYPE &&
+      currentComponent.meta &&
+      currentComponent.meta.chartId
+    ) {
+      chartIds.push(currentComponent.meta.chartId);
     }
     return chartIds;
   }, []);
diff --git a/superset/assets/src/dashboard/util/getDragDropManager.js b/superset/assets/src/dashboard/util/getDragDropManager.js
new file mode 100644
index 0000000..1be8ecb
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getDragDropManager.js
@@ -0,0 +1,17 @@
+import { DragDropManager } from 'dnd-core';
+import HTML5Backend from 'react-dnd-html5-backend';
+
+let defaultManager;
+
+// we use this method to ensure that there is a singleton of the DragDropManager
+// within the app this seems to work fine, but in tests multiple are initialized
+// see this issue for more details https://github.com/react-dnd/react-dnd/issues/186
+// @TODO re-evaluate whether this is required when we move to jest
+// the alternative is simply using an HOC like:
+//  DragDropContext(HTML5Backend)(DashboardBuilder);
+export default function getDragDropManager() {
+  if (!defaultManager) {
+    defaultManager = new DragDropManager(HTML5Backend);
+  }
+  return defaultManager;
+}
diff --git a/superset/assets/src/dashboard/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
index a885c31..80bf69e 100644
--- a/superset/assets/src/dashboard/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -44,6 +44,7 @@ const parentMaxDepthLookup = {
 
   [DASHBOARD_GRID_TYPE]: {
     [CHART_TYPE]: depthOne,
+    [MARKDOWN_TYPE]: depthOne,
     [COLUMN_TYPE]: depthOne,
     [DIVIDER_TYPE]: depthOne,
     [HEADER_TYPE]: depthOne,
@@ -63,11 +64,12 @@ const parentMaxDepthLookup = {
 
   [TAB_TYPE]: {
     [CHART_TYPE]: depthTwo,
+    [MARKDOWN_TYPE]: depthTwo,
     [COLUMN_TYPE]: depthTwo,
     [DIVIDER_TYPE]: depthTwo,
     [HEADER_TYPE]: depthTwo,
     [ROW_TYPE]: depthTwo,
-    [TABS_TYPE]: depthTwo,
+    [TABS_TYPE]: rootDepth, // you cannot drop a Tabs within a Tab
   },
 
   [COLUMN_TYPE]: {
@@ -75,6 +77,7 @@ const parentMaxDepthLookup = {
     [HEADER_TYPE]: depthFive,
     [MARKDOWN_TYPE]: depthFive,
     [ROW_TYPE]: depthThree,
+    [DIVIDER_TYPE]: depthThree,
   },
 
   // these have no valid children
diff --git a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
index 7fe7f4e..8abc9b9 100644
--- a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
@@ -3,14 +3,13 @@ import newComponentFactory from './newComponentFactory';
 
 import { ROW_TYPE, TABS_TYPE, TAB_TYPE } from './componentTypes';
 
-export default function newEntitiesFromDrop({ dropResult, components }) {
+export default function newEntitiesFromDrop({ dropResult, layout }) {
   const { dragging, destination } = dropResult;
 
   const dragType = dragging.type;
-  const dragMeta = dragging.meta;
-  const dropEntity = components[destination.id];
+  const dropEntity = layout[destination.id];
   const dropType = dropEntity.type;
-  let newDropChild = newComponentFactory(dragType, dragMeta);
+  let newDropChild = newComponentFactory(dragType, dragging.meta);
   const wrapChildInRow = shouldWrapChildInRow({
     parentType: dropType,
     childType: dragType,
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index c8e1981..f07497c 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -66,9 +66,9 @@ export const slicePropShape = PropTypes.shape({
 });
 
 export const dashboardStatePropShape = PropTypes.shape({
-  sliceIds: PropTypes.object.isRequired,
+  sliceIds: PropTypes.arrayOf(PropTypes.number).isRequired,
   refresh: PropTypes.bool.isRequired,
-  filters: PropTypes.object,
+  filters: PropTypes.object.isRequired,
   expandedSlices: PropTypes.object,
   editMode: PropTypes.bool,
   showBuilderPane: PropTypes.bool,
diff --git a/superset/assets/src/logger.js b/superset/assets/src/logger.js
index c7823fc..65c81b5 100644
--- a/superset/assets/src/logger.js
+++ b/superset/assets/src/logger.js
@@ -18,8 +18,10 @@ export const Logger = {
   },
 
   append(eventName, eventBody) {
-    return handlers[eventName].length &&
-      handlers[eventName].forEach(handler => (handler(eventName, eventBody)));
+    return (
+      (handlers[eventName] || {}).length &&
+      handlers[eventName].forEach(handler => handler(eventName, eventBody))
+    );
   },
 
   end(log) {
@@ -28,8 +30,7 @@ export const Logger = {
 
     log.eventNames.forEach((eventName) => {
       if (handlers[eventName].length) {
-        const index = handlers[eventName]
-          .findIndex(handler => (handler === log.addEvent));
+        const index = handlers[eventName].findIndex(handler => handler === log.addEvent);
         handlers[eventName].splice(index, 1);
       }
     });
@@ -51,7 +52,7 @@ export const Logger = {
     }
     let url = '/superset/log/';
     if (requestPrams.length) {
-      url += '?' + requestPrams.map(([k, v]) => (k + '=' + v)).join('&');
+      url += '?' + requestPrams.map(([k, v]) => k + '=' + v).join('&');
     }
     const eventData = {};
     for (const eventName in events) {
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index 77ca84d..2add797 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -1457,6 +1457,10 @@ babylon@^6.15.0, babylon@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
 
+bail@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.3.tgz#63cfb9ddbac829b02a3128cd53224be78e6c21a3"
+
 balanced-match@^0.4.2:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -1621,6 +1625,10 @@ brace@^0.10.0:
   dependencies:
     w3c-blob "0.0.1"
 
+brace@^0.11.0:
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
+
 braces@^1.8.2:
   version "1.8.5"
   resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
@@ -1950,6 +1958,18 @@ change-emitter@^0.1.2:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
 
+character-entities-legacy@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c"
+
+character-entities@^1.0.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.2.tgz#58c8f371c0774ef0ba9b2aca5f00d8f100e6e363"
+
+character-reference-invalid@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz#21e421ad3d84055952dab4a43a04e73cd425d3ed"
+
 chardet@^0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@@ -2152,6 +2172,10 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
+collapse-white-space@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.4.tgz#ce05cf49e54c3277ae573036a26851ba430a0091"
+
 color-convert@^1.3.0, color-convert@^1.9.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
@@ -3600,7 +3624,7 @@ extend-shallow@^2.0.1:
   dependencies:
     is-extendable "^0.1.0"
 
-extend@~3.0.0, extend@~3.0.1:
+extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
@@ -4853,6 +4877,17 @@ is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
 
+is-alphabetical@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.2.tgz#1fa6e49213cb7885b75d15862fb3f3d96c884f41"
+
+is-alphanumerical@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz#1138e9ae5040158dc6ff76b820acd6b7a181fd40"
+  dependencies:
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -4863,7 +4898,7 @@ is-binary-path@^1.0.0:
   dependencies:
     binary-extensions "^1.0.0"
 
-is-buffer@^1.0.2:
+is-buffer@^1.0.2, is-buffer@^1.1.4:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
 
@@ -4897,6 +4932,10 @@ is-date-object@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
 
+is-decimal@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.2.tgz#894662d6a8709d307f3a276ca4339c8fa5dff0ff"
+
 is-dotfile@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -4937,6 +4976,10 @@ is-glob@^2.0.0, is-glob@^2.0.1:
   dependencies:
     is-extglob "^1.0.0"
 
+is-hexadecimal@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835"
+
 is-installed-globally@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
@@ -4993,7 +5036,7 @@ is-path-inside@^1.0.0:
   dependencies:
     path-is-inside "^1.0.1"
 
-is-plain-obj@^1.0.0:
+is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
 
@@ -5069,6 +5112,14 @@ is-utf8@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
 
+is-whitespace-character@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed"
+
+is-word-character@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.2.tgz#46a5dac3f2a1840898b91e576cd40d493f3ae553"
+
 is@~0.2.6:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562"
@@ -5906,6 +5957,10 @@ mapbox-gl@^0.44.2:
     vt-pbf "^3.0.1"
     webworkify "^1.5.0"
 
+markdown-escapes@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.2.tgz#e639cbde7b99c841c0bacc8a07982873b46d2122"
+
 material-colors@^1.2.1:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.5.tgz#5292593e6754cb1bcc2b98030e4e0d6a3afc9ea1"
@@ -7138,6 +7193,17 @@ parse-asn1@^5.0.0:
     evp_bytestokey "^1.0.0"
     pbkdf2 "^3.0.3"
 
+parse-entities@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.2.tgz#9eaf719b29dc3bd62246b4332009072e01527777"
+  dependencies:
+    character-entities "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    character-reference-invalid "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 parse-glob@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
@@ -7634,6 +7700,14 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, pr
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+prop-types@^15.6.1:
+  version "15.6.1"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
+  dependencies:
+    fbjs "^0.8.16"
+    loose-envify "^1.3.1"
+    object-assign "^4.1.1"
+
 proto-list@~1.2.1:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -7781,6 +7855,12 @@ qw@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
 
+raf@^3.3.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
+  dependencies:
+    performance-now "^2.1.0"
+
 randomatic@^1.1.3:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@@ -7828,16 +7908,14 @@ re-resizable@^4.3.1:
   version "4.4.8"
   resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-4.4.8.tgz#1c7eedfd9b9ed1f83b3adfa7a97cda76881e4e57"
 
-react-ace@^5.0.1:
-  version "5.2.2"
-  resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.2.2.tgz#2e35296531bcf3ba49f08ffb1ec482f8938a8d3b"
+react-ace@^5.10.0:
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.10.0.tgz#e328b37ac52759f700be5afdb86ada2f5ec84c5e"
   dependencies:
-    brace "^0.10.0"
+    brace "^0.11.0"
     lodash.get "^4.4.2"
     lodash.isequal "^4.1.1"
     prop-types "^15.5.8"
-    react "^15.6.2"
-    react-dom "^15.6.2"
 
 react-addons-css-transition-group@^15.6.0:
   version "15.6.2"
@@ -7981,6 +8059,16 @@ react-map-gl@^3.0.4:
     prop-types "^15.5.7"
     viewport-mercator-project "^4.0.1"
 
+react-markdown@^3.3.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.3.2.tgz#35d305e8a29b640717b9dac4658a1caeafd44c94"
+  dependencies:
+    prop-types "^15.6.1"
+    remark-parse "^5.0.0"
+    unified "^6.1.5"
+    unist-util-visit "^1.3.0"
+    xtend "^4.0.1"
+
 react-onclickoutside@^5.9.0:
   version "5.11.1"
   resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623"
@@ -8061,6 +8149,13 @@ react-split-pane@^0.1.63, react-split-pane@^0.1.66:
     prop-types "^15.5.10"
     react-style-proptype "^3.0.0"
 
+react-sticky@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/react-sticky/-/react-sticky-6.0.2.tgz#d301c1b5307649220dbc045fcbacd077885c5ede"
+  dependencies:
+    prop-types "^15.5.8"
+    raf "^3.3.0"
+
 react-style-proptype@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/react-style-proptype/-/react-style-proptype-3.0.0.tgz#89e0b646f266c656abb0f0dd8202dbd5036c31e6"
@@ -8456,6 +8551,26 @@ regjsparser@^0.1.4:
   dependencies:
     jsesc "~0.5.0"
 
+remark-parse@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95"
+  dependencies:
+    collapse-white-space "^1.0.2"
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-whitespace-character "^1.0.0"
+    is-word-character "^1.0.0"
+    markdown-escapes "^1.0.0"
+    parse-entities "^1.1.0"
+    repeat-string "^1.5.4"
+    state-toggle "^1.0.0"
+    trim "0.0.1"
+    trim-trailing-lines "^1.0.0"
+    unherit "^1.0.4"
+    unist-util-remove-position "^1.0.0"
+    vfile-location "^2.0.0"
+    xtend "^4.0.1"
+
 remove-trailing-separator@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -8464,7 +8579,7 @@ repeat-element@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
 
-repeat-string@^1.5.2:
+repeat-string@^1.5.2, repeat-string@^1.5.4:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
 
@@ -8480,6 +8595,10 @@ repeating@^2.0.0:
   dependencies:
     is-finite "^1.0.0"
 
+replace-ext@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
+
 request@2, request@^2.74.0:
   version "2.85.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
@@ -9229,6 +9348,10 @@ ssri@^5.2.4:
   dependencies:
     safe-buffer "^5.1.1"
 
+state-toggle@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.1.tgz#c3cb0974f40a6a0f8e905b96789eb41afa1cde3a"
+
 static-eval@~0.2.0:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-0.2.4.tgz#b7d34d838937b969f9641ca07d48f8ede263ea7b"
@@ -9641,6 +9764,18 @@ trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
 
+trim-trailing-lines@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz#e0ec0810fd3c3f1730516b45f49083caaf2774d9"
+
+trim@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
+
+trough@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.2.tgz#7f1663ec55c480139e2de5e486c6aef6cc24a535"
+
 tryit@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
@@ -9790,6 +9925,24 @@ unflowify@^1.0.0:
     flow-remove-types "^1.1.2"
     through "^2.3.8"
 
+unherit@^1.0.4:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c"
+  dependencies:
+    inherits "^2.0.1"
+    xtend "^4.0.1"
+
+unified@^6.1.5:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba"
+  dependencies:
+    bail "^1.0.0"
+    extend "^3.0.0"
+    is-plain-obj "^1.1.0"
+    trough "^1.0.0"
+    vfile "^2.0.0"
+    x-is-string "^0.1.0"
+
 uniq@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
@@ -9822,6 +9975,26 @@ unique-string@^1.0.0:
   dependencies:
     crypto-random-string "^1.0.0"
 
+unist-util-is@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.2.tgz#1193fa8f2bfbbb82150633f3a8d2eb9a1c1d55db"
+
+unist-util-remove-position@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz#86b5dad104d0bbfbeb1db5f5c92f3570575c12cb"
+  dependencies:
+    unist-util-visit "^1.1.0"
+
+unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6"
+
+unist-util-visit@^1.1.0, unist-util-visit@^1.3.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.3.1.tgz#c019ac9337a62486be58531bc27e7499ae7d55c7"
+  dependencies:
+    unist-util-is "^2.1.1"
+
 unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -9984,6 +10157,25 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+vfile-location@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.3.tgz#083ba80e50968e8d420be49dd1ea9a992131df77"
+
+vfile-message@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.0.1.tgz#51a2ccd8a6b97a7980bb34efb9ebde9632e93677"
+  dependencies:
+    unist-util-stringify-position "^1.1.1"
+
+vfile@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a"
+  dependencies:
+    is-buffer "^1.1.4"
+    replace-ext "1.0.0"
+    unist-util-stringify-position "^1.0.0"
+    vfile-message "^1.0.0"
+
 viewport-mercator-project@^4.0.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-4.1.1.tgz#92b0611fa0041041d2f3568da3529a8a846017d0"
@@ -10268,6 +10460,10 @@ write@^0.2.1:
   dependencies:
     mkdirp "^0.5.1"
 
+x-is-string@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
+
 xdg-basedir@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
diff --git a/superset/views/core.py b/superset/views/core.py
index b14050a..e9c1e00 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1611,7 +1611,7 @@ class Superset(BaseSupersetView):
             ):
                 slice_id = value.get('meta').get('chartId')
                 slice_ids.append(slice_id)
-                slice_id_to_name[slice_id] = value.get('meta').get('chartName')
+                slice_id_to_name[slice_id] = value.get('meta').get('sliceName')
 
         session = db.session()
         Slice = models.Slice  # noqa


[incubator-superset] 15/26: [dashboard v2] logging updates (#5087)

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

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

commit 69ab5f7af569dc2a5851526aaef8e68baa15e0cd
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Thu May 31 19:02:11 2018 -0700

    [dashboard v2] logging updates (#5087)
    
    * [dashboard v2] initial logging refactor
    
    * [dashboard v2] clean up logger
    
    * [logger] update explore with new log events, add refresh dashboard + refresh dashboard chart actions
    
    * [logging] add logger_spec.js, fix reducers/dashboardState_spec + gridComponents/Chart_spec
    
    * [dashboard v2][logging] refactor for bulk logging in python
    
    * [logging] tweak python, fix and remove dup start_offset entries
    
    * [dashboard v2][logging] add dashboard_first_load event
    
    * [dashboard v2][logging] add slice_ids to dashboard pane load event
    
    * [tests] fix npm test script
---
 superset/assets/package.json                       |   2 +-
 .../dashboard/components/Dashboard_spec.jsx        |   1 +
 .../components/gridComponents/Chart_spec.jsx       |  10 +-
 .../dashboard/reducers/dashboardState_spec.js      |  52 +++----
 superset/assets/spec/javascripts/logger_spec.js    | 143 +++++++++++++++++++
 superset/assets/src/chart/Chart.jsx                |  80 +++++------
 superset/assets/src/chart/chartAction.js           |  27 ++--
 .../assets/src/dashboard/actions/dashboardState.js |  28 ++--
 .../assets/src/dashboard/components/Dashboard.jsx  |  71 +++++++---
 .../src/dashboard/components/DashboardBuilder.jsx  |   2 -
 .../dashboard/components/SliceHeaderControls.jsx   |  50 ++++++-
 .../dashboard/components/gridComponents/Chart.jsx  |   9 +-
 superset/assets/src/dashboard/containers/Chart.jsx |   4 +-
 .../assets/src/dashboard/containers/Dashboard.jsx  |   8 +-
 .../assets/src/dashboard/containers/SliceAdder.jsx |  28 ++++
 .../src/dashboard/reducers/dashboardState.js       |  49 ++++---
 .../dashboard/util/logging/childChartsDidLoad.js   |  21 +++
 .../util/logging/findNonTabChildChartIds.js        |  45 ++++++
 .../util/logging/findTopLevelComponentIds.js       |  74 ++++++++++
 .../logging/getLoadStatsPerTopLevelComponent.js    |  26 ++++
 superset/assets/src/dashboard/util/propShapes.jsx  |  13 ++
 .../explore/components/ExploreViewContainer.jsx    | 121 ++++++++--------
 superset/assets/src/logger.js                      | 153 ++++++++++++++-------
 superset/models/core.py                            |  48 ++++---
 24 files changed, 768 insertions(+), 297 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 6d116a4..21abd17 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -8,7 +8,7 @@
     "test": "spec"
   },
   "scripts": {
-    "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/*_spec.*",
+    "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/**/*_spec.*",
     "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js --recursive spec/**/*_spec.*",
     "dev": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool eval-cheap-source-map",
     "dev-slow": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool inline-source-map",
diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
index 545b890..2094040 100644
--- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx
@@ -35,6 +35,7 @@ describe('Dashboard', () => {
     timeout: 60,
     userId: dashboardInfo.userId,
     impressionId: 'id',
+    loadStats: {},
   };
 
   function setup(overrideProps) {
diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
index 05756f4..5fff313 100644
--- a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx
@@ -35,9 +35,10 @@ describe('Chart', () => {
     refreshChart() {},
     toggleExpandSlice() {},
     addFilter() {},
-    removeFilter() {},
     editMode: false,
     isExpanded: false,
+    supersetCanExplore: false,
+    sliceCanEdit: false,
   };
 
   function setup(overrideProps) {
@@ -77,13 +78,6 @@ describe('Chart', () => {
     expect(addFilter.callCount).to.equal(1);
   });
 
-  it('should call removeFilter when ChartContainer calls removeFilter', () => {
-    const removeFilter = sinon.spy();
-    const wrapper = setup({ removeFilter });
-    wrapper.instance().removeFilter();
-    expect(removeFilter.callCount).to.equal(1);
-  });
-
   it('should return props.filters when its getFilters method is called', () => {
     const filters = { column: ['value'] };
     const wrapper = setup({ filters });
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
index 078019d..89c4ffe 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
@@ -3,11 +3,10 @@ import { expect } from 'chai';
 
 import {
   ADD_SLICE,
-  ADD_FILTER,
+  CHANGE_FILTER,
   ON_CHANGE,
   ON_SAVE,
   REMOVE_SLICE,
-  REMOVE_FILTER,
   SET_EDIT_MODE,
   SET_MAX_UNDO_HISTORY_EXCEEDED,
   SET_UNSAVED_CHANGES,
@@ -138,7 +137,7 @@ describe('dashboardState reducer', () => {
     });
   });
 
-  describe('add filter', () => {
+  describe('change filter', () => {
     it('should add a new filter if it does not exist', () => {
       expect(
         dashboardStateReducer(
@@ -147,7 +146,7 @@ describe('dashboardState reducer', () => {
             sliceIds: [1],
           },
           {
-            type: ADD_FILTER,
+            type: CHANGE_FILTER,
             chart: { id: 1, formData: { groupby: 'column' } },
             col: 'column',
             vals: ['b', 'a'],
@@ -172,7 +171,7 @@ describe('dashboardState reducer', () => {
             sliceIds: [1],
           },
           {
-            type: ADD_FILTER,
+            type: CHANGE_FILTER,
             chart: { id: 1, formData: { groupby: 'column' } },
             col: 'column',
             vals: ['b', 'a'],
@@ -197,7 +196,7 @@ describe('dashboardState reducer', () => {
             sliceIds: [1],
           },
           {
-            type: ADD_FILTER,
+            type: CHANGE_FILTER,
             chart: { id: 1, formData: { groupby: 'column' } },
             col: 'column',
             vals: ['b', 'a'],
@@ -211,29 +210,30 @@ describe('dashboardState reducer', () => {
         sliceIds: [1],
       });
     });
-  });
 
-  it('should remove a filter', () => {
-    expect(
-      dashboardStateReducer(
-        {
-          filters: {
-            1: {
-              column: ['a', 'b', 'c'],
+    it('should remove the filter if values are empty', () => {
+      expect(
+        dashboardStateReducer(
+          {
+            filters: {
+              1: { column: ['z'] },
             },
+            sliceIds: [1],
           },
-        },
-        {
-          type: REMOVE_FILTER,
-          sliceId: 1,
-          col: 'column',
-          vals: ['b', 'a'], // these are removed
-          refresh: true,
-        },
-      ),
-    ).to.deep.equal({
-      filters: { 1: { column: ['c'] } },
-      refresh: true,
+          {
+            type: CHANGE_FILTER,
+            chart: { id: 1, formData: { groupby: 'column' } },
+            col: 'column',
+            vals: [],
+            refresh: true,
+            merge: false,
+          },
+        ),
+      ).to.deep.equal({
+        filters: {},
+        refresh: true,
+        sliceIds: [1],
+      });
     });
   });
 });
diff --git a/superset/assets/spec/javascripts/logger_spec.js b/superset/assets/spec/javascripts/logger_spec.js
new file mode 100644
index 0000000..64580b4
--- /dev/null
+++ b/superset/assets/spec/javascripts/logger_spec.js
@@ -0,0 +1,143 @@
+import $ from 'jquery';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import { Logger, ActionLog } from '../../src/logger';
+
+describe('ActionLog', () => {
+  it('should be a constructor', () => {
+    const newLogger = new ActionLog({});
+    expect(newLogger instanceof ActionLog).to.equal(true);
+  });
+
+  it('should set the eventNames, impressionId, source, sourceId, and sendNow init parameters', () => {
+    const eventNames = [];
+    const impressionId = 'impressionId';
+    const source = 'source';
+    const sourceId = 'sourceId';
+    const sendNow = true;
+
+    const log = new ActionLog({ eventNames, impressionId, source, sourceId, sendNow });
+    expect(log.eventNames).to.equal(eventNames);
+    expect(log.impressionId).to.equal(impressionId);
+    expect(log.source).to.equal(source);
+    expect(log.sourceId).to.equal(sourceId);
+    expect(log.sendNow).to.equal(sendNow);
+  });
+
+  it('should set attributes with the setAttribute method', () => {
+    const log = new ActionLog({});
+    expect(log.test).to.equal(undefined);
+    log.setAttribute('test', 'testValue');
+    expect(log.test).to.equal('testValue');
+  });
+
+  it('should track added events', () => {
+    const log = new ActionLog({});
+    const eventName = 'myEventName';
+    const eventBody = { test: 'event' };
+    expect(log.events[eventName]).to.equal(undefined);
+
+    log.addEvent(eventName, eventBody);
+    expect(log.events[eventName]).to.have.length(1);
+    expect(log.events[eventName][0]).to.deep.include(eventBody);
+  });
+});
+
+describe('Logger', () => {
+  it('should add events when .append(eventName, eventBody) is called', () => {
+    const eventName = 'testEvent';
+    const eventBody = { test: 'event' };
+    const log = new ActionLog({ eventNames: [eventName] });
+    Logger.start(log);
+    Logger.append(eventName, eventBody);
+    expect(log.events[eventName]).to.have.length(1);
+    expect(log.events[eventName][0]).to.deep.include(eventBody);
+    Logger.end(log);
+  });
+
+  describe('.send()', () => {
+    beforeEach(() => {
+      sinon.spy($, 'ajax');
+    });
+    afterEach(() => {
+      $.ajax.restore();
+    });
+
+    const eventNames = ['test'];
+
+    function setup(overrides = {}) {
+      const log = new ActionLog({ eventNames, ...overrides });
+      return log;
+    }
+
+    it('should POST an event to /superset/log/ when called', () => {
+      const log = setup();
+      Logger.start(log);
+      Logger.append(eventNames[0], { test: 'event' });
+      expect(log.events[eventNames[0]]).to.have.length(1);
+      Logger.end(log);
+      expect($.ajax.calledOnce).to.equal(true);
+      const args = $.ajax.getCall(0).args[0];
+      expect(args.url).to.equal('/superset/log/');
+      expect(args.method).to.equal('POST');
+    });
+
+    it("should flush the log's events", () => {
+      const log = setup();
+      Logger.start(log);
+      Logger.append(eventNames[0], { test: 'event' });
+      const event = log.events[eventNames[0]][0];
+      expect(event).to.deep.include({ test: 'event' });
+      Logger.end(log);
+      expect(log.events).to.deep.equal({});
+    });
+
+    it('should include ts, start_offset, event_name, impression_id, source, and source_id in every event', () => {
+      const config = {
+        eventNames: ['event1', 'event2'],
+        impressionId: 'impress_me',
+        source: 'superset',
+        sourceId: 'lolz',
+      };
+      const log = setup(config);
+
+      Logger.start(log);
+      Logger.append('event1', { key: 'value' });
+      Logger.append('event2', { foo: 'bar' });
+      Logger.end(log);
+
+      const args = $.ajax.getCall(0).args[0];
+      const events = JSON.parse(args.data.events);
+
+      expect(events).to.have.length(2);
+      expect(events[0]).to.deep.include({
+        key: 'value',
+        event_name: 'event1',
+        impression_id: config.impressionId,
+        source: config.source,
+        source_id: config.sourceId,
+      });
+      expect(events[1]).to.deep.include({
+        foo: 'bar',
+        event_name: 'event2',
+        impression_id: config.impressionId,
+        source: config.source,
+        source_id: config.sourceId,
+      });
+      expect(typeof events[0].ts).to.equal('number');
+      expect(typeof events[1].ts).to.equal('number');
+      expect(typeof events[0].start_offset).to.equal('number');
+      expect(typeof events[1].start_offset).to.equal('number');
+    });
+
+    it('should send() a log immediately if .append() is called with sendNow=true', () => {
+      const log = setup();
+      Logger.start(log);
+      Logger.append(eventNames[0], { test: 'event' }, true);
+      expect($.ajax.calledOnce).to.equal(true);
+      Logger.end(log);
+    });
+  });
+});
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index 4a471e8..060249f 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -7,7 +7,7 @@ import { Tooltip } from 'react-bootstrap';
 import { d3format } from '../modules/utils';
 import ChartBody from './ChartBody';
 import Loading from '../components/Loading';
-import { Logger, LOG_ACTIONS_RENDER_EVENT } from '../logger';
+import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger';
 import StackTraceMessage from '../components/StackTraceMessage';
 import RefreshChartOverlay from '../components/RefreshChartOverlay';
 import visMap from '../visualizations';
@@ -42,7 +42,6 @@ const propTypes = {
   // dashboard callbacks
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  removeFilter: PropTypes.func,
   onQuery: PropTypes.func,
   onDismissRefreshOverlay: PropTypes.func,
 };
@@ -50,7 +49,6 @@ const propTypes = {
 const defaultProps = {
   addFilter: () => ({}),
   getFilters: () => ({}),
-  removeFilter: () => ({}),
 };
 
 class Chart extends React.PureComponent {
@@ -65,7 +63,6 @@ class Chart extends React.PureComponent {
     this.datasource = props.datasource;
     this.addFilter = this.addFilter.bind(this);
     this.getFilters = this.getFilters.bind(this);
-    this.removeFilter = this.removeFilter.bind(this);
     this.headerHeight = this.headerHeight.bind(this);
     this.height = this.height.bind(this);
     this.width = this.width.bind(this);
@@ -74,12 +71,7 @@ class Chart extends React.PureComponent {
   componentDidMount() {
     if (this.props.triggerQuery) {
       const { formData } = this.props;
-      this.props.actions.runQuery(
-        formData,
-        false,
-        this.props.timeout,
-        this.props.chartId,
-      );
+      this.props.actions.runQuery(formData, false, this.props.timeout, this.props.chartId);
     } else {
       // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data
       this.renderViz();
@@ -98,13 +90,12 @@ class Chart extends React.PureComponent {
     if (
       this.props.queryResponse &&
       ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
-      !this.props.queryResponse.error && (
-        prevProps.annotationData !== this.props.annotationData ||
+      !this.props.queryResponse.error &&
+      (prevProps.annotationData !== this.props.annotationData ||
         prevProps.queryResponse !== this.props.queryResponse ||
         prevProps.height !== this.props.height ||
         prevProps.width !== this.props.width ||
-        prevProps.lastRendered !== this.props.lastRendered
-      )
+        prevProps.lastRendered !== this.props.lastRendered)
     ) {
       this.renderViz();
     }
@@ -122,17 +113,14 @@ class Chart extends React.PureComponent {
     this.props.addFilter(col, vals, merge, refresh);
   }
 
-  removeFilter(col, vals, refresh = true) {
-    this.props.removeFilter(col, vals, refresh);
-  }
-
   clearError() {
     this.setState({ errorMsg: null });
   }
 
   width() {
-    return this.props.width ||
-      (this.container && this.container.el && this.container.el.offsetWidth);
+    return (
+      this.props.width || (this.container && this.container.el && this.container.el.offsetWidth)
+    );
   }
 
   headerHeight() {
@@ -140,8 +128,9 @@ class Chart extends React.PureComponent {
   }
 
   height() {
-    return this.props.height
-      || (this.container && this.container.el && this.container.el.offsetHeight);
+    return (
+      this.props.height || (this.container && this.container.el && this.container.el.offsetHeight)
+    );
   }
 
   d3format(col, number) {
@@ -200,7 +189,7 @@ class Chart extends React.PureComponent {
       if (chartStatus !== 'rendered') {
         this.props.actions.chartRenderingSucceeded(chartId);
       }
-      Logger.append(LOG_ACTIONS_RENDER_EVENT, {
+      Logger.append(LOG_ACTIONS_RENDER_CHART, {
         label: 'slice_' + chartId,
         vis_type: vizType,
         start_offset: renderStart,
@@ -222,36 +211,39 @@ class Chart extends React.PureComponent {
       <div className={`chart-container ${isLoading ? 'is-loading' : ''}`} style={containerStyles}>
         {this.renderTooltip()}
         {isLoading && <Loading size={75} />}
-        {this.props.chartAlert &&
+        {this.props.chartAlert && (
           <StackTraceMessage
             message={this.props.chartAlert}
             queryResponse={this.props.queryResponse}
-          />}
+          />
+        )}
 
         {!isLoading &&
           !this.props.chartAlert &&
           this.props.refreshOverlayVisible &&
           !this.props.errorMessage &&
-          this.container &&
-          <RefreshChartOverlay
-            height={this.height()}
-            width={this.width()}
-            onQuery={this.props.onQuery}
-            onDismiss={this.props.onDismissRefreshOverlay}
-          />}
+          this.container && (
+            <RefreshChartOverlay
+              height={this.height()}
+              width={this.width()}
+              onQuery={this.props.onQuery}
+              onDismiss={this.props.onDismissRefreshOverlay}
+            />
+          )}
 
-        {!isLoading && !this.props.chartAlert &&
-          <ChartBody
-            containerId={this.containerId}
-            vizType={this.props.vizType}
-            height={this.height}
-            width={this.width}
-            faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
-            ref={(inner) => {
-              this.container = inner;
-            }}
-          />
-        }
+        {!isLoading &&
+          !this.props.chartAlert && (
+            <ChartBody
+              containerId={this.containerId}
+              vizType={this.props.vizType}
+              height={this.height}
+              width={this.width}
+              faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
+              ref={(inner) => {
+                this.container = inner;
+              }}
+            />
+          )}
       </div>
     );
   }
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index 529d295..82c4250 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -1,10 +1,10 @@
 import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../explore/exploreUtils';
 import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
-import { Logger, LOG_ACTIONS_LOAD_EVENT } from '../logger';
+import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger';
 import { COMMON_ERR_MESSAGES } from '../common';
 import { t } from '../locales';
 
-const $ = window.$ = require('jquery');
+const $ = (window.$ = require('jquery'));
 
 export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
 export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
@@ -74,11 +74,13 @@ export function runAnnotationQuery(annotation, timeout = 60, formData = null, ke
     fd.time_grain_sqla = granularity;
     fd.granularity = granularity;
 
-    const sliceFormData = Object.keys(annotation.overrides)
-      .reduce((d, k) => ({
+    const sliceFormData = Object.keys(annotation.overrides).reduce(
+      (d, k) => ({
         ...d,
         [k]: annotation.overrides[k] || fd[k],
-      }), {});
+      }),
+      {},
+    );
     const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
     const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
     const queryRequest = $.ajax({
@@ -143,19 +145,22 @@ export function runQuery(formData, force = false, timeout = 60, key) {
     const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
       .then(() => queryRequest)
       .then((queryResponse) => {
-        Logger.append(LOG_ACTIONS_LOAD_EVENT, {
-          label: 'slice_' + key,
+        Logger.append(LOG_ACTIONS_LOAD_CHART, {
+          slice_id: 'slice_' + key,
           is_cached: queryResponse.is_cached,
+          force_refresh: force,
           row_count: queryResponse.rowcount,
           datasource: formData.datasource,
           start_offset: logStart,
           duration: Logger.getTimestamp() - logStart,
+          has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
+          viz_type: formData.viz_type,
         });
         return dispatch(chartUpdateSucceeded(queryResponse, key));
       })
       .catch((err) => {
-        Logger.append(LOG_ACTIONS_LOAD_EVENT, {
-          label: key,
+        Logger.append(LOG_ACTIONS_LOAD_CHART, {
+          slice_id: 'slice_' + key,
           has_err: true,
           datasource: formData.datasource,
           start_offset: logStart,
@@ -197,7 +202,5 @@ export function runQuery(formData, force = false, timeout = 60, key) {
 }
 
 export function refreshChart(chart, force, timeout) {
-  return dispatch => (
-    dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id))
-  );
+  return dispatch => dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id));
 }
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index aac4f98..688f5b0 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -7,6 +7,11 @@ import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
 import { applyDefaultFormData } from '../../explore/store';
 import { getAjaxErrorMsg } from '../../modules/utils';
+import {
+  Logger,
+  LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
+  LOG_ACTIONS_REFRESH_DASHBOARD,
+} from '../../logger';
 import { SAVE_TYPE_OVERWRITE } from '../util/constants';
 import { t } from '../../locales';
 
@@ -21,14 +26,16 @@ export function setUnsavedChanges(hasUnsavedChanges) {
   return { type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges } };
 }
 
-export const ADD_FILTER = 'ADD_FILTER';
-export function addFilter(chart, col, vals, merge = true, refresh = true) {
-  return { type: ADD_FILTER, chart, col, vals, merge, refresh };
-}
-
-export const REMOVE_FILTER = 'REMOVE_FILTER';
-export function removeFilter(sliceId, col, vals, refresh = true) {
-  return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
+export const CHANGE_FILTER = 'CHANGE_FILTER';
+export function changeFilter(chart, col, vals, merge = true, refresh = true) {
+  Logger.append(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, {
+    id: chart.id,
+    column: col,
+    value_count: Array.isArray(vals) ? vals.length : (vals && 1) || 0,
+    merge,
+    refresh,
+  });
+  return { type: CHANGE_FILTER, chart, col, vals, merge, refresh };
 }
 
 export const ADD_SLICE = 'ADD_SLICE';
@@ -130,6 +137,11 @@ export function saveDashboardRequest(data, id, saveType) {
 
 export function fetchCharts(chartList = [], force = false, interval = 0) {
   return (dispatch, getState) => {
+    Logger.append(LOG_ACTIONS_REFRESH_DASHBOARD, {
+      force,
+      interval,
+      chartCount: chartList.length,
+    });
     const timeout = getState().dashboardInfo.common.conf
       .SUPERSET_WEBSERVER_TIMEOUT;
     if (!interval) {
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 644ddf0..76f4b54 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -10,16 +10,19 @@ import {
   slicePropShape,
   dashboardInfoPropShape,
   dashboardStatePropShape,
+  loadStatsPropShape,
 } from '../util/propShapes';
 import { areObjectsEqual } from '../../reduxUtils';
 import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
 import {
   Logger,
   ActionLog,
-  LOG_ACTIONS_PAGE_LOAD,
-  LOG_ACTIONS_LOAD_EVENT,
-  LOG_ACTIONS_RENDER_EVENT,
+  DASHBOARD_EVENT_NAMES,
+  LOG_ACTIONS_MOUNT_DASHBOARD,
+  LOG_ACTIONS_LOAD_DASHBOARD_PANE,
+  LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
 } from '../../logger';
+
 import { t } from '../../locales';
 
 import '../stylesheets/index.less';
@@ -35,6 +38,7 @@ const propTypes = {
   charts: PropTypes.objectOf(chartPropShape).isRequired,
   slices: PropTypes.objectOf(slicePropShape).isRequired,
   datasources: PropTypes.object.isRequired,
+  loadStats: loadStatsPropShape.isRequired,
   layout: PropTypes.object.isRequired,
   impressionId: PropTypes.string.isRequired,
   initMessages: PropTypes.array,
@@ -65,28 +69,59 @@ class Dashboard extends React.PureComponent {
 
   constructor(props) {
     super(props);
-
-    this.firstLoad = true;
-    this.loadingLog = new ActionLog({
+    this.isFirstLoad = true;
+    this.actionLog = new ActionLog({
       impressionId: props.impressionId,
-      actionType: LOG_ACTIONS_PAGE_LOAD,
       source: 'dashboard',
       sourceId: props.dashboardInfo.id,
-      eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
+      eventNames: DASHBOARD_EVENT_NAMES,
     });
-    Logger.start(this.loadingLog);
+    Logger.start(this.actionLog);
+  }
+
+  componentDidMount() {
+    this.ts_mount = new Date().getTime();
+    Logger.append(LOG_ACTIONS_MOUNT_DASHBOARD);
   }
 
   componentWillReceiveProps(nextProps) {
-    if (
-      this.firstLoad &&
-      Object.values(nextProps.charts).every(
-        chart =>
-          ['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1,
-      )
-    ) {
-      Logger.end(this.loadingLog);
-      this.firstLoad = false;
+    if (!nextProps.dashboardState.editMode) {
+      // log pane loads
+      const loadedPaneIds = [];
+      const allPanesDidLoad = Object.entries(nextProps.loadStats).every(
+        ([paneId, stats]) => {
+          const { didLoad, minQueryStartTime, ...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,
+            });
+
+            if (!this.isFirstLoad) {
+              Logger.send(this.actionLog);
+            }
+          }
+          if (this.isFirstLoad && didLoad && stats.slice_ids.length > 0) {
+            loadedPaneIds.push(paneId);
+          }
+          return didLoad || stats.index !== 0;
+        },
+      );
+
+      if (allPanesDidLoad && this.isFirstLoad) {
+        Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
+          pane_ids: loadedPaneIds,
+          duration: new Date().getTime() - this.ts_mount,
+        });
+        Logger.send(this.actionLog);
+        this.isFirstLoad = false;
+      }
     }
 
     const currentChartIds = getChartIdsFromLayout(this.props.layout);
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 0f42f1b..30e2e78 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -159,8 +159,6 @@ class DashboardBuilder extends React.Component {
                   id={DASHBOARD_GRID_ID}
                   activeKey={tabIndex}
                   onSelect={this.handleChangeTab}
-                  // 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/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index 6729e57..7d25ab0 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -3,6 +3,12 @@ import PropTypes from 'prop-types';
 import cx from 'classnames';
 import moment from 'moment';
 import { Dropdown, MenuItem } from 'react-bootstrap';
+import {
+  Logger,
+  LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+  LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+  LOG_ACTIONS_REFRESH_CHART,
+} from '../../logger';
 
 import { t } from '../../locales';
 
@@ -42,22 +48,52 @@ const VerticalDotsTrigger = () => (
 class SliceHeaderControls extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.exportCSV = this.props.exportCSV.bind(this, this.props.slice.slice_id);
-    this.exploreChart = this.props.exploreChart.bind(
-      this,
-      this.props.slice.slice_id,
-    );
+    this.exportCSV = this.exportCSV.bind(this);
+    this.exploreChart = this.exploreChart.bind(this);
+    this.toggleControls = this.toggleControls.bind(this);
+    this.refreshChart = this.refreshChart.bind(this);
     this.toggleExpandSlice = this.props.toggleExpandSlice.bind(
       this,
       this.props.slice.slice_id,
     );
-    this.toggleControls = this.toggleControls.bind(this);
 
     this.state = {
       showControls: false,
     };
   }
 
+  exportCSV() {
+    this.props.exportCSV(this.props.slice.slice_id);
+    Logger.append(
+      LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+      {
+        slice_id: this.props.slice.slice_id,
+        is_cached: this.props.isCached,
+      },
+      true,
+    );
+  }
+
+  exploreChart() {
+    this.props.exploreChart(this.props.slice.slice_id);
+    Logger.append(
+      LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+      {
+        slice_id: this.props.slice.slice_id,
+        is_cached: this.props.isCached,
+      },
+      true,
+    );
+  }
+
+  refreshChart() {
+    this.props.forceRefresh(this.props.slice.slice_id);
+    Logger.append(LOG_ACTIONS_REFRESH_CHART, {
+      slice_id: this.props.slice.slice_id,
+      is_cached: this.props.isCached,
+    });
+  }
+
   toggleControls() {
     this.setState({
       showControls: !this.state.showControls,
@@ -84,7 +120,7 @@ class SliceHeaderControls extends React.PureComponent {
         </Dropdown.Toggle>
 
         <Dropdown.Menu>
-          <MenuItem onClick={this.props.forceRefresh}>
+          <MenuItem onClick={this.refreshChart}>
             {isCached && <span className="dot" />}
             {t('Force refresh')}
             {isCached && (
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index 1ace51d..39c1e81 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -26,7 +26,6 @@ const propTypes = {
   refreshChart: PropTypes.func.isRequired,
   toggleExpandSlice: PropTypes.func.isRequired,
   addFilter: PropTypes.func.isRequired,
-  removeFilter: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
   isExpanded: PropTypes.bool.isRequired,
   supersetCanExplore: PropTypes.bool.isRequired,
@@ -54,7 +53,6 @@ class Chart extends React.Component {
     this.exportCSV = this.exportCSV.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
     this.getFilters = this.getFilters.bind(this);
-    this.removeFilter = this.removeFilter.bind(this);
     this.resize = this.resize.bind(this);
     this.setDescriptionRef = this.setDescriptionRef.bind(this);
     this.setHeaderRef = this.setHeaderRef.bind(this);
@@ -140,10 +138,6 @@ class Chart extends React.Component {
     return this.props.refreshChart(this.props.chart, true, this.props.timeout);
   }
 
-  removeFilter(...args) {
-    this.props.removeFilter(this.props.id, ...args);
-  }
-
   render() {
     const {
       id,
@@ -161,6 +155,8 @@ class Chart extends React.Component {
       sliceCanEdit,
     } = this.props;
 
+    // this should never happen but prevents throwing in the case that a gridComponent
+    // references a chart that is not associated with the dashboard
     if (!chart || !slice) return null;
 
     const { width } = this.state;
@@ -224,7 +220,6 @@ class Chart extends React.Component {
             vizType={slice.viz_type}
             addFilter={this.addFilter}
             getFilters={this.getFilters}
-            removeFilter={this.removeFilter}
             annotationData={chart.annotationData}
             chartAlert={chart.chartAlert}
             chartStatus={chart.chartStatus}
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 4ad1270..5631a25 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -2,8 +2,7 @@ import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
 import {
-  addFilter,
-  removeFilter,
+  changeFilter as addFilter,
   toggleExpandSlice,
 } from '../actions/dashboardState';
 import { refreshChart } from '../../chart/chartAction';
@@ -54,7 +53,6 @@ function mapDispatchToProps(dispatch) {
       toggleExpandSlice,
       addFilter,
       refreshChart,
-      removeFilter,
     },
     dispatch,
   );
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
index bcf2ace..3252af3 100644
--- a/superset/assets/src/dashboard/containers/Dashboard.jsx
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -1,12 +1,14 @@
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
+import Dashboard from '../components/Dashboard';
+
 import {
   addSliceToDashboard,
   removeSliceFromDashboard,
 } from '../actions/dashboardState';
 import { runQuery } from '../../chart/chartAction';
-import Dashboard from '../components/Dashboard';
+import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent';
 
 function mapStateToProps({
   datasources,
@@ -28,6 +30,10 @@ function mapStateToProps({
     slices: sliceEntities.slices,
     layout: dashboardLayout.present,
     impressionId,
+    loadStats: getLoadStatsPerTopLevelComponent({
+      layout: dashboardLayout.present,
+      chartQueries: charts,
+    }),
   };
 }
 
diff --git a/superset/assets/src/dashboard/containers/SliceAdder.jsx b/superset/assets/src/dashboard/containers/SliceAdder.jsx
new file mode 100644
index 0000000..e3d931d
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/SliceAdder.jsx
@@ -0,0 +1,28 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { fetchAllSlices } from '../actions/sliceEntities';
+import SliceAdder from '../components/SliceAdder';
+
+function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
+  return {
+    userId: dashboardInfo.userId,
+    selectedSliceIds: dashboardState.sliceIds,
+    slices: sliceEntities.slices,
+    isLoading: sliceEntities.isLoading,
+    errorMessage: sliceEntities.errorMessage,
+    lastUpdated: sliceEntities.lastUpdated,
+    editMode: dashboardState.editMode,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators(
+    {
+      fetchAllSlices,
+    },
+    dispatch,
+  );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index c7f2277..410ecc0 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -1,11 +1,10 @@
 /* eslint-disable camelcase */
 import {
   ADD_SLICE,
-  ADD_FILTER,
+  CHANGE_FILTER,
   ON_CHANGE,
   ON_SAVE,
   REMOVE_SLICE,
-  REMOVE_FILTER,
   SET_EDIT_MODE,
   SET_MAX_UNDO_HISTORY_EXCEEDED,
   SET_UNSAVED_CHANGES,
@@ -86,15 +85,14 @@ export default function dashboardStateReducer(state = {}, action) {
       };
     },
 
-    // filters
-    [ADD_FILTER]() {
+    [CHANGE_FILTER]() {
       const hasSelectedFilter = state.sliceIds.includes(action.chart.id);
       if (!hasSelectedFilter) {
         return state;
       }
 
       let filters = state.filters;
-      const { chart, col, vals, merge, refresh } = action;
+      const { chart, col, vals: nextVals, merge, refresh } = action;
       const sliceId = chart.id;
       const filterKeys = [
         '__from',
@@ -110,33 +108,32 @@ export default function dashboardStateReducer(state = {}, action) {
       ) {
         let newFilter = {};
         if (!(sliceId in filters)) {
-          // Straight up set the filters if none existed for the slice
-          newFilter = { [col]: vals };
+          // if no filters existed for the slice, set them
+          newFilter = { [col]: nextVals };
         } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
-          newFilter = { ...filters[sliceId], [col]: vals };
-          // d3.merge pass in array of arrays while some value form filter components
-          // from and to filter box require string to be process and return
+          // If no filters exist for this column, or we are overwriting them
+          newFilter = { ...filters[sliceId], [col]: nextVals };
         } else if (filters[sliceId][col] instanceof Array) {
-          newFilter[col] = [...filters[sliceId][col], ...vals];
+          newFilter[col] = [...filters[sliceId][col], ...nextVals];
         } else {
-          newFilter[col] = [filters[sliceId][col], ...vals];
+          newFilter[col] = [filters[sliceId][col], ...nextVals];
         }
         filters = { ...filters, [sliceId]: newFilter };
-      }
-      return { ...state, filters, refresh };
-    },
-    [REMOVE_FILTER]() {
-      const { sliceId, col, vals, refresh } = action;
-      const excluded = new Set(vals);
 
-      let filters = state.filters;
-      // Have to be careful not to modify the dashboard state so that
-      // the render actually triggers
-      if (sliceId in state.filters && col in state.filters[sliceId]) {
-        const newFilter = filters[sliceId][col].filter(
-          val => !excluded.has(val),
-        );
-        filters = { ...filters, [sliceId]: { [col]: newFilter } };
+        // remove any empty filters so they don't pollute the logs
+        Object.keys(filters).forEach(chartId => {
+          Object.keys(filters[chartId]).forEach(column => {
+            if (
+              !filters[chartId][column] ||
+              filters[chartId][column].length === 0
+            ) {
+              delete filters[chartId][column];
+            }
+          });
+          if (Object.keys(filters[chartId]).length === 0) {
+            delete filters[chartId];
+          }
+        });
       }
       return { ...state, filters, refresh };
     },
diff --git a/superset/assets/src/dashboard/util/logging/childChartsDidLoad.js b/superset/assets/src/dashboard/util/logging/childChartsDidLoad.js
new file mode 100644
index 0000000..58b81f9
--- /dev/null
+++ b/superset/assets/src/dashboard/util/logging/childChartsDidLoad.js
@@ -0,0 +1,21 @@
+import findNonTabChildCharIds from './findNonTabChildChartIds';
+
+export default function childChartsDidLoad({ chartQueries, layout, id }) {
+  const chartIds = findNonTabChildCharIds({ id, layout });
+
+  let minQueryStartTime = Infinity;
+  const didLoad = chartIds.every(chartId => {
+    const query = chartQueries[chartId] || {};
+
+    // filterbox's don't re-render, don't use stale update time
+    if (query.formData && query.formData.viz_type !== 'filter_box') {
+      minQueryStartTime = Math.min(
+        query.chartUpdateStartTime,
+        minQueryStartTime,
+      );
+    }
+    return ['stopped', 'failed', 'rendered'].indexOf(query.chartStatus) > -1;
+  });
+
+  return { didLoad, minQueryStartTime };
+}
diff --git a/superset/assets/src/dashboard/util/logging/findNonTabChildChartIds.js b/superset/assets/src/dashboard/util/logging/findNonTabChildChartIds.js
new file mode 100644
index 0000000..a9a51f7
--- /dev/null
+++ b/superset/assets/src/dashboard/util/logging/findNonTabChildChartIds.js
@@ -0,0 +1,45 @@
+import { TABS_TYPE, CHART_TYPE } from '../componentTypes';
+
+// This function traverses the layout from the passed id, returning an array
+// of any child chartIds NOT nested within a Tabs component. These helps us identify
+// if the charts at a given "Tabs" level are loaded
+function findNonTabChildChartIds({ id, layout }) {
+  const chartIds = [];
+  function recurseFromNode(node) {
+    if (node && node.type === CHART_TYPE) {
+      if (node.meta && node.meta.chartId) {
+        chartIds.push(node.meta.chartId);
+      }
+    } else if (
+      node &&
+      node.type !== TABS_TYPE &&
+      node.children &&
+      node.children.length
+    ) {
+      node.children.forEach(childId => {
+        const child = layout[childId];
+        if (child) {
+          recurseFromNode(child);
+        }
+      });
+    }
+  }
+
+  recurseFromNode(layout[id]);
+
+  return chartIds;
+}
+
+// This method is called frequently, so cache results
+let cachedLayout;
+let cachedIdsLookup = {};
+export default function findNonTabChildChartIdsWithCache({ id, layout }) {
+  if (cachedLayout === layout && cachedIdsLookup[id]) {
+    return cachedIdsLookup[id];
+  } else if (layout !== cachedLayout) {
+    cachedLayout = layout;
+    cachedIdsLookup = {};
+  }
+  cachedIdsLookup[id] = findNonTabChildChartIds({ layout, id });
+  return cachedIdsLookup[id];
+}
diff --git a/superset/assets/src/dashboard/util/logging/findTopLevelComponentIds.js b/superset/assets/src/dashboard/util/logging/findTopLevelComponentIds.js
new file mode 100644
index 0000000..d274d7c
--- /dev/null
+++ b/superset/assets/src/dashboard/util/logging/findTopLevelComponentIds.js
@@ -0,0 +1,74 @@
+import { TAB_TYPE, DASHBOARD_GRID_TYPE } from '../componentTypes';
+import { DASHBOARD_ROOT_ID } from '../constants';
+import findNonTabChildChartIds from './findNonTabChildChartIds';
+
+// This function traverses the layout to identify top grid + tab level components
+// for which we track load times
+function findTopLevelComponentIds(layout) {
+  const topLevelNodes = [];
+
+  function recurseFromNode({
+    node,
+    index = null,
+    depth,
+    parentType = null,
+    parentId = null,
+  }) {
+    if (!node) return;
+
+    let nextParentType = parentType;
+    let nextParentId = parentId;
+    let nextDepth = depth;
+    if (node.type === TAB_TYPE || node.type === DASHBOARD_GRID_TYPE) {
+      const chartIds = findNonTabChildChartIds({
+        layout,
+        id: node.id,
+      });
+
+      topLevelNodes.push({
+        id: node.id,
+        type: node.type,
+        parent_type: parentType,
+        parent_id: parentId,
+        index,
+        depth,
+        slice_ids: chartIds,
+      });
+
+      nextParentId = node.id;
+      nextParentType = node.type;
+      nextDepth += 1;
+    }
+    if (node.children && node.children.length) {
+      node.children.forEach((childId, childIndex) => {
+        recurseFromNode({
+          node: layout[childId],
+          index: childIndex,
+          parentType: nextParentType,
+          parentId: nextParentId,
+          depth: nextDepth,
+        });
+      });
+    }
+  }
+
+  recurseFromNode({
+    node: layout[DASHBOARD_ROOT_ID],
+    depth: 0,
+  });
+
+  return topLevelNodes;
+}
+
+// This method is called frequently, so cache results
+let cachedLayout;
+let cachedTopLevelNodes;
+export default function findTopLevelComponentIdsWithCache(layout) {
+  if (layout === cachedLayout) {
+    return cachedTopLevelNodes;
+  }
+  cachedLayout = layout;
+  cachedTopLevelNodes = findTopLevelComponentIds(layout);
+
+  return cachedTopLevelNodes;
+}
diff --git a/superset/assets/src/dashboard/util/logging/getLoadStatsPerTopLevelComponent.js b/superset/assets/src/dashboard/util/logging/getLoadStatsPerTopLevelComponent.js
new file mode 100644
index 0000000..b503746
--- /dev/null
+++ b/superset/assets/src/dashboard/util/logging/getLoadStatsPerTopLevelComponent.js
@@ -0,0 +1,26 @@
+import findTopLevelComponentIds from './findTopLevelComponentIds';
+import childChartsDidLoad from './childChartsDidLoad';
+
+export default function getLoadStatsPerTopLevelComponent({
+  layout,
+  chartQueries,
+}) {
+  const topLevelComponents = findTopLevelComponentIds(layout);
+  const stats = {};
+  topLevelComponents.forEach(({ id, ...restStats }) => {
+    const { didLoad, minQueryStartTime } = childChartsDidLoad({
+      id,
+      layout,
+      chartQueries,
+    });
+
+    stats[id] = {
+      didLoad,
+      id,
+      minQueryStartTime,
+      ...restStats,
+    };
+  });
+
+  return stats;
+}
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index f07497c..1242d2b 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -84,3 +84,16 @@ export const dashboardInfoPropShape = PropTypes.shape({
   common: PropTypes.object,
   userId: PropTypes.string.isRequired,
 });
+
+export const loadStatsPropShape = PropTypes.objectOf(
+  PropTypes.shape({
+    didLoad: PropTypes.bool.isRequired,
+    minQueryStartTime: PropTypes.number.isRequired,
+    id: PropTypes.string.isRequired,
+    type: PropTypes.string.isRequired,
+    parent_id: PropTypes.string,
+    parent_type: PropTypes.string,
+    index: PropTypes.number.isRequired,
+    slice_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
+  }),
+);
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index a648464..3eada5c 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -15,8 +15,7 @@ import { chartPropShape } from '../../dashboard/util/propShapes';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
 import * as chartActions from '../../chart/chartAction';
-import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
-  LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
+import { Logger, ActionLog, EXPLORE_EVENT_NAMES, LOG_ACTIONS_MOUNT_EXPLORER } from '../../logger';
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
@@ -35,13 +34,11 @@ const propTypes = {
 class ExploreViewContainer extends React.Component {
   constructor(props) {
     super(props);
-    this.firstLoad = true;
     this.loadingLog = new ActionLog({
       impressionId: props.impressionId,
-      actionType: LOG_ACTIONS_PAGE_LOAD,
       source: 'slice',
       sourceId: props.slice ? props.slice.slice_id : 0,
-      eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
+      eventNames: EXPLORE_EVENT_NAMES,
     });
     Logger.start(this.loadingLog);
 
@@ -62,34 +59,37 @@ class ExploreViewContainer extends React.Component {
     window.addEventListener('resize', this.handleResize);
     window.addEventListener('popstate', this.handlePopstate);
     this.addHistory({ isReplace: true });
+    Logger.append(LOG_ACTIONS_MOUNT_EXPLORER);
   }
 
-  componentWillReceiveProps(np) {
-    if (this.firstLoad &&
-      ['rendered', 'failed', 'stopped'].indexOf(np.chart.chartStatus) > -1) {
-      Logger.end(this.loadingLog);
-      this.firstLoad = false;
+  componentWillReceiveProps(nextProps) {
+    const wasRendered =
+      ['rendered', 'failed', 'stopped'].indexOf(this.props.chart.chartStatus) > -1;
+    const isRendered = ['rendered', 'failed', 'stopped'].indexOf(nextProps.chart.chartStatus) > -1;
+    if (!wasRendered && isRendered) {
+      Logger.send(this.loadingLog);
     }
-    if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
+    if (nextProps.controls.viz_type.value !== this.props.controls.viz_type.value) {
       this.props.actions.resetControls();
       this.props.actions.triggerQuery(true, this.props.chart.id);
     }
     if (
-      np.controls.datasource && (
-        this.props.controls.datasource == null ||
-        np.controls.datasource.value !== this.props.controls.datasource.value
-      )
+      nextProps.controls.datasource &&
+      (this.props.controls.datasource == null ||
+        nextProps.controls.datasource.value !== this.props.controls.datasource.value)
     ) {
-      this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
+      this.props.actions.fetchDatasourceMetadata(nextProps.form_data.datasource, true);
     }
 
-    const changedControlKeys = this.findChangedControlKeys(this.props.controls, np.controls);
-    if (this.hasDisplayControlChanged(changedControlKeys, np.controls)) {
+    const changedControlKeys = this.findChangedControlKeys(this.props.controls, nextProps.controls);
+    if (this.hasDisplayControlChanged(changedControlKeys, nextProps.controls)) {
       this.props.actions.updateQueryFormData(
-        getFormDataFromControls(np.controls), this.props.chart.id);
+        getFormDataFromControls(nextProps.controls),
+        this.props.chart.id,
+      );
       this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.id);
     }
-    if (this.hasQueryControlChanged(changedControlKeys, np.controls)) {
+    if (this.hasQueryControlChanged(changedControlKeys, nextProps.controls)) {
       this.setState({ chartIsStale: true, refreshOverlayVisible: true });
     }
   }
@@ -139,26 +139,31 @@ class ExploreViewContainer extends React.Component {
   }
 
   findChangedControlKeys(prevControls, currentControls) {
-    return Object.keys(currentControls).filter(key => (
-      typeof prevControls[key] !== 'undefined' &&
-      !areObjectsEqual(currentControls[key].value, prevControls[key].value)
-    ));
+    return Object.keys(currentControls).filter(
+      key =>
+        typeof prevControls[key] !== 'undefined' &&
+        !areObjectsEqual(currentControls[key].value, prevControls[key].value),
+    );
   }
 
   hasDisplayControlChanged(changedControlKeys, currentControls) {
-    return changedControlKeys.some(key => (currentControls[key].renderTrigger));
+    return changedControlKeys.some(key => currentControls[key].renderTrigger);
   }
 
   hasQueryControlChanged(changedControlKeys, currentControls) {
-    return changedControlKeys.some(key => (
-      !currentControls[key].renderTrigger && !currentControls[key].dontRefreshOnChange
-    ));
+    return changedControlKeys.some(
+      key => !currentControls[key].renderTrigger && !currentControls[key].dontRefreshOnChange,
+    );
   }
 
   triggerQueryIfNeeded() {
     if (this.props.chart.triggerQuery && !this.hasErrors()) {
-      this.props.actions.runQuery(this.props.form_data, false,
-        this.props.timeout, this.props.chart.id);
+      this.props.actions.runQuery(
+        this.props.form_data,
+        false,
+        this.props.timeout,
+        this.props.chart.id,
+      );
     }
   }
 
@@ -166,15 +171,9 @@ class ExploreViewContainer extends React.Component {
     const { payload } = getExploreUrlAndPayload({ formData: this.props.form_data });
     const longUrl = getExploreLongUrl(this.props.form_data);
     if (isReplace) {
-      history.replaceState(
-        payload,
-        title,
-        longUrl);
+      history.replaceState(payload, title, longUrl);
     } else {
-      history.pushState(
-        payload,
-        title,
-        longUrl);
+      history.pushState(payload, title, longUrl);
     }
 
     // it seems some browsers don't support pushState title attribute
@@ -194,12 +193,7 @@ class ExploreViewContainer extends React.Component {
     const formData = history.state;
     if (formData && Object.keys(formData).length) {
       this.props.actions.setExploreControls(formData);
-      this.props.actions.runQuery(
-        formData,
-        false,
-        this.props.timeout,
-        this.props.chart.id,
-      );
+      this.props.actions.runQuery(formData, false, this.props.timeout, this.props.chart.id);
     }
   }
 
@@ -209,7 +203,8 @@ class ExploreViewContainer extends React.Component {
   hasErrors() {
     const ctrls = this.props.controls;
     return Object.keys(ctrls).some(
-      k => ctrls[k].validationErrors && ctrls[k].validationErrors.length > 0);
+      k => ctrls[k].validationErrors && ctrls[k].validationErrors.length > 0,
+    );
   }
   renderErrorMessage() {
     // Returns an error message as a node if any errors are in the store
@@ -227,9 +222,7 @@ class ExploreViewContainer extends React.Component {
     }
     let errorMessage;
     if (errors.length > 0) {
-      errorMessage = (
-        <div style={{ textAlign: 'left' }}>{errors}</div>
-      );
+      errorMessage = <div style={{ textAlign: 'left' }}>{errors}</div>;
     }
     return errorMessage;
   }
@@ -244,7 +237,8 @@ class ExploreViewContainer extends React.Component {
         addHistory={this.addHistory}
         onQuery={this.onQuery.bind(this)}
         onDismissRefreshOverlay={this.onDismissRefreshOverlay.bind(this)}
-      />);
+      />
+    );
   }
 
   render() {
@@ -260,13 +254,13 @@ class ExploreViewContainer extends React.Component {
           overflow: 'hidden',
         }}
       >
-        {this.state.showModal &&
-        <SaveModal
-          onHide={this.toggleModal.bind(this)}
-          actions={this.props.actions}
-          form_data={this.props.form_data}
-        />
-      }
+        {this.state.showModal && (
+          <SaveModal
+            onHide={this.toggleModal.bind(this)}
+            actions={this.props.actions}
+            form_data={this.props.form_data}
+          />
+        )}
         <div className="row">
           <div className="col-sm-4">
             <QueryAndSaveBtns
@@ -287,9 +281,7 @@ class ExploreViewContainer extends React.Component {
               isDatasourceMetaLoading={this.props.isDatasourceMetaLoading}
             />
           </div>
-          <div className="col-sm-8">
-            {this.renderChartContainer()}
-          </div>
+          <div className="col-sm-8">{this.renderChartContainer()}</div>
         </div>
       </div>
     );
@@ -301,12 +293,11 @@ ExploreViewContainer.propTypes = propTypes;
 function mapStateToProps({ explore, charts, impressionId }) {
   const form_data = getFormDataFromControls(explore.controls);
   // fill in additional params stored in form_data but not used by control
-  Object.keys(explore.rawFormData)
-    .forEach((key) => {
-      if (form_data[key] === undefined) {
-        form_data[key] = explore.rawFormData[key];
-      }
-    });
+  Object.keys(explore.rawFormData).forEach((key) => {
+    if (form_data[key] === undefined) {
+      form_data[key] = explore.rawFormData[key];
+    }
+  });
   const chartKey = Object.keys(charts)[0];
   const chart = charts[chartKey];
   return {
diff --git a/superset/assets/src/logger.js b/superset/assets/src/logger.js
index 65c81b5..06059b2 100644
--- a/superset/assets/src/logger.js
+++ b/superset/assets/src/logger.js
@@ -1,64 +1,61 @@
 import $ from 'jquery';
 
-export const LOG_ACTIONS_PAGE_LOAD = 'page_load_perf';
-export const LOG_ACTIONS_LOAD_EVENT = 'load_events';
-export const LOG_ACTIONS_RENDER_EVENT = 'render_events';
-
-const handlers = {};
+// This creates an association between an eventName and the ActionLog instance so that
+// Logger.append calls do not have to know about the appropriate ActionLog instance
+const addEventHandlers = {};
 
 export const Logger = {
   start(log) {
-    log.setAttribute('startAt', new Date().getTime() - this.getTimestamp());
+    // create a handler to handle adding each event type
     log.eventNames.forEach((eventName) => {
-      if (!handlers[eventName]) {
-        handlers[eventName] = [];
+      if (!addEventHandlers[eventName]) {
+        addEventHandlers[eventName] = log.addEvent.bind(log);
+      } else {
+        console.warn(`Duplicate event handler for event '${eventName}'`);
       }
-      handlers[eventName].push(log.addEvent.bind(log));
     });
   },
 
-  append(eventName, eventBody) {
-    return (
-      (handlers[eventName] || {}).length &&
-      handlers[eventName].forEach(handler => handler(eventName, eventBody))
-    );
+  append(eventName, eventBody, sendNow) {
+    if (addEventHandlers[eventName]) {
+      addEventHandlers[eventName](eventName, eventBody, sendNow);
+    } else {
+      console.warn(`No event handler for event '${eventName}'`);
+    }
   },
 
   end(log) {
-    log.setAttribute('duration', new Date().getTime() - log.startAt);
     this.send(log);
 
+    // remove handlers
     log.eventNames.forEach((eventName) => {
-      if (handlers[eventName].length) {
-        const index = handlers[eventName].findIndex(handler => handler === log.addEvent);
-        handlers[eventName].splice(index, 1);
+      if (addEventHandlers[eventName]) {
+        delete addEventHandlers[eventName];
       }
     });
   },
 
   send(log) {
-    const { impressionId, actionType, source, sourceId, events, startAt, duration } = log;
-    const requestPrams = [];
-    requestPrams.push(['impression_id', impressionId]);
-    switch (source) {
-      case 'dashboard':
-        requestPrams.push(['dashboard_id', sourceId]);
-        break;
-      case 'slice':
-        requestPrams.push(['slice_id', sourceId]);
-        break;
-      default:
-        break;
-    }
+    const { impressionId, source, sourceId, events } = log;
     let url = '/superset/log/';
-    if (requestPrams.length) {
-      url += '?' + requestPrams.map(([k, v]) => k + '=' + v).join('&');
+
+    // backend logs treat these request params as first-class citizens
+    if (source === 'dashboard') {
+      url += `?dashboard_id=${sourceId}`;
+    } else if (source === 'slice') {
+      url += `?slice_id=${sourceId}`;
     }
-    const eventData = {};
+
+    const eventData = [];
     for (const eventName in events) {
-      eventData[eventName] = [];
       events[eventName].forEach((event) => {
-        eventData[eventName].push(event);
+        eventData.push({
+          source,
+          source_id: sourceId,
+          event_name: eventName,
+          impression_id: impressionId,
+          ...event,
+        });
       });
     }
 
@@ -67,30 +64,28 @@ export const Logger = {
       method: 'POST',
       dataType: 'json',
       data: {
-        source: 'client',
-        type: actionType,
-        started_time: startAt,
-        duration,
+        explode: 'events',
         events: JSON.stringify(eventData),
       },
     });
+
+    // flush events for this logger
+    log.events = {}; // eslint-disable-line no-param-reassign
   },
 
+  // note that this returns ms since page load, NOT ms since epoc
   getTimestamp() {
     return Math.round(window.performance.now());
   },
 };
 
 export class ActionLog {
-  constructor({ impressionId, actionType, source, sourceId, eventNames, sendNow }) {
+  constructor({ impressionId, source, sourceId, sendNow, eventNames }) {
     this.impressionId = impressionId;
     this.source = source;
     this.sourceId = sourceId;
-    this.actionType = actionType;
     this.eventNames = eventNames;
     this.sendNow = sendNow || false;
-    this.startAt = 0;
-    this.duration = 0;
     this.events = {};
 
     this.addEvent = this.addEvent.bind(this);
@@ -100,16 +95,68 @@ export class ActionLog {
     this[name] = value;
   }
 
-  addEvent(eventName, eventBody) {
-    if (!this.events[eventName]) {
-      this.events[eventName] = [];
-    }
-    this.events[eventName].push(eventBody);
+  addEvent(eventName, eventBody, sendNow) {
+    if (sendNow) {
+      Logger.send({
+        ...this,
+        // overwrite events so that Logger.send doesn't clear this.events
+        events: {
+          [eventName]: [
+            {
+              ts: new Date().getTime(),
+              start_offset: Logger.getTimestamp(),
+              ...eventBody,
+            },
+          ],
+        },
+      });
+    } else {
+      this.events[eventName] = this.events[eventName] || [];
 
-    if (this.sendNow) {
-      this.setAttribute('duration', new Date().getTime() - this.startAt);
-      Logger.send(this);
-      this.events = {};
+      this.events[eventName].push({
+        ts: new Date().getTime(),
+        start_offset: Logger.getTimestamp(),
+        ...eventBody,
+      });
+
+      if (this.sendNow) {
+        Logger.send(this);
+      }
     }
   }
 }
+
+// Log event types ------------------------------------------------------------
+export const LOG_ACTIONS_MOUNT_DASHBOARD = 'mount_dashboard';
+export const LOG_ACTIONS_MOUNT_EXPLORER = 'mount_explorer';
+
+export const LOG_ACTIONS_FIRST_DASHBOARD_LOAD = 'first_dashboard_load';
+export const LOG_ACTIONS_LOAD_DASHBOARD_PANE = 'load_dashboard_pane';
+export const LOG_ACTIONS_LOAD_CHART = 'load_chart_data';
+export const LOG_ACTIONS_RENDER_CHART = 'render_chart';
+export const LOG_ACTIONS_REFRESH_CHART = 'force_refresh_chart';
+
+export const LOG_ACTIONS_REFRESH_DASHBOARD = 'force_refresh_dashboard';
+export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart';
+export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART = 'export_csv_dashboard_chart';
+export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter';
+
+export const DASHBOARD_EVENT_NAMES = [
+  LOG_ACTIONS_MOUNT_DASHBOARD,
+  LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
+  LOG_ACTIONS_LOAD_DASHBOARD_PANE,
+  LOG_ACTIONS_LOAD_CHART,
+  LOG_ACTIONS_RENDER_CHART,
+  LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+  LOG_ACTIONS_REFRESH_CHART,
+  LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+  LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
+  LOG_ACTIONS_REFRESH_DASHBOARD,
+];
+
+export const EXPLORE_EVENT_NAMES = [
+  LOG_ACTIONS_MOUNT_EXPLORER,
+  LOG_ACTIONS_LOAD_CHART,
+  LOG_ACTIONS_RENDER_CHART,
+  LOG_ACTIONS_REFRESH_CHART,
+];
diff --git a/superset/models/core.py b/superset/models/core.py
index 0ef3331..3b663af 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -883,16 +883,18 @@ class Log(Model):
         """Decorator to log user actions"""
         @functools.wraps(f)
         def wrapper(*args, **kwargs):
-            start_dttm = datetime.now()
             user_id = None
             if g.user:
                 user_id = g.user.get_id()
             d = request.form.to_dict() or {}
+
             # request parameters can overwrite post body
             request_params = request.args.to_dict()
             d.update(request_params)
             d.update(kwargs)
+
             slice_id = d.get('slice_id')
+            dashboard_id = d.get('dashboard_id')
 
             try:
                 slice_id = int(
@@ -900,26 +902,40 @@ class Log(Model):
             except (ValueError, TypeError):
                 slice_id = 0
 
-            params = ''
-            try:
-                params = json.dumps(d)
-            except Exception:
-                pass
             stats_logger.incr(f.__name__)
+            start_dttm = datetime.now()
             value = f(*args, **kwargs)
+            duration_ms = (datetime.now() - start_dttm).total_seconds() * 1000
+
+            # bulk insert
+            try:
+                explode_by = d.get('explode')
+                records = json.loads(d.get(explode_by))
+            except Exception:
+                records = [d]
+
+            referrer = request.referrer[:1000] if request.referrer else None
+            logs = []
+            for record in records:
+                try:
+                    json_string = json.dumps(record)
+                except Exception:
+                    json_string = None
+                log = cls(
+                    action=f.__name__,
+                    json=json_string,
+                    dashboard_id=dashboard_id,
+                    slice_id=slice_id,
+                    duration_ms=duration_ms,
+                    referrer=referrer,
+                    user_id=user_id)
+                logs.append(log)
+
             sesh = db.session()
-            log = cls(
-                action=f.__name__,
-                json=params,
-                dashboard_id=d.get('dashboard_id'),
-                slice_id=slice_id,
-                duration_ms=(
-                    datetime.now() - start_dttm).total_seconds() * 1000,
-                referrer=request.referrer[:1000] if request.referrer else None,
-                user_id=user_id)
-            sesh.add(log)
+            sesh.bulk_save_objects(logs)
             sesh.commit()
             return value
+
         return wrapper
 
 


[incubator-superset] 05/26: [dashboard builder] git mv to src/ post-rebase

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

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

commit 996b0837873c0ff94ba56c50fded146bcdfc8074
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Wed Apr 18 17:04:34 2018 -0700

    [dashboard builder] git mv to src/ post-rebase
---
 superset/assets/{javascripts => src}/dashboard/v2/.eslintrc               | 0
 .../assets/{javascripts => src}/dashboard/v2/actions/dashboardLayout.js   | 0
 superset/assets/{javascripts => src}/dashboard/v2/actions/editMode.js     | 0
 .../assets/{javascripts => src}/dashboard/v2/actions/messageToasts.js     | 0
 .../{javascripts => src}/dashboard/v2/components/BuilderComponentPane.jsx | 0
 .../assets/{javascripts => src}/dashboard/v2/components/Dashboard.jsx     | 0
 .../{javascripts => src}/dashboard/v2/components/DashboardBuilder.jsx     | 0
 .../assets/{javascripts => src}/dashboard/v2/components/DashboardGrid.jsx | 0
 .../{javascripts => src}/dashboard/v2/components/DashboardHeader.jsx      | 0
 .../dashboard/v2/components/DeleteComponentButton.jsx                     | 0
 .../assets/{javascripts => src}/dashboard/v2/components/IconButton.jsx    | 0
 .../{javascripts => src}/dashboard/v2/components/StaticDashboard.jsx      | 0
 superset/assets/{javascripts => src}/dashboard/v2/components/Toast.jsx    | 0
 .../{javascripts => src}/dashboard/v2/components/ToastPresenter.jsx       | 0
 .../{javascripts => src}/dashboard/v2/components/dnd/DragDroppable.jsx    | 0
 .../{javascripts => src}/dashboard/v2/components/dnd/DragHandle.jsx       | 0
 .../dashboard/v2/components/dnd/dragDroppableConfig.js                    | 0
 .../assets/{javascripts => src}/dashboard/v2/components/dnd/handleDrop.js | 0
 .../{javascripts => src}/dashboard/v2/components/dnd/handleHover.js       | 0
 .../{javascripts => src}/dashboard/v2/components/gridComponents/Chart.jsx | 0
 .../dashboard/v2/components/gridComponents/Column.jsx                     | 0
 .../dashboard/v2/components/gridComponents/Divider.jsx                    | 0
 .../dashboard/v2/components/gridComponents/Header.jsx                     | 0
 .../{javascripts => src}/dashboard/v2/components/gridComponents/Row.jsx   | 0
 .../{javascripts => src}/dashboard/v2/components/gridComponents/Tab.jsx   | 0
 .../{javascripts => src}/dashboard/v2/components/gridComponents/Tabs.jsx  | 0
 .../{javascripts => src}/dashboard/v2/components/gridComponents/index.js  | 0
 .../dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx  | 0
 .../dashboard/v2/components/gridComponents/new/NewChart.jsx               | 0
 .../dashboard/v2/components/gridComponents/new/NewColumn.jsx              | 0
 .../dashboard/v2/components/gridComponents/new/NewDivider.jsx             | 0
 .../dashboard/v2/components/gridComponents/new/NewHeader.jsx              | 0
 .../dashboard/v2/components/gridComponents/new/NewRow.jsx                 | 0
 .../dashboard/v2/components/gridComponents/new/NewTabs.jsx                | 0
 .../dashboard/v2/components/menu/BackgroundStyleDropdown.jsx              | 0
 .../{javascripts => src}/dashboard/v2/components/menu/HoverMenu.jsx       | 0
 .../{javascripts => src}/dashboard/v2/components/menu/PopoverDropdown.jsx | 0
 .../{javascripts => src}/dashboard/v2/components/menu/WithPopoverMenu.jsx | 0
 .../dashboard/v2/components/resizable/ResizableContainer.jsx              | 0
 .../dashboard/v2/components/resizable/ResizableHandle.jsx                 | 0
 .../{javascripts => src}/dashboard/v2/containers/DashboardBuilder.jsx     | 0
 .../{javascripts => src}/dashboard/v2/containers/DashboardComponent.jsx   | 0
 .../assets/{javascripts => src}/dashboard/v2/containers/DashboardGrid.jsx | 0
 .../{javascripts => src}/dashboard/v2/containers/DashboardHeader.jsx      | 0
 .../{javascripts => src}/dashboard/v2/containers/ToastPresenter.jsx       | 0
 .../{javascripts => src}/dashboard/v2/fixtures/emptyDashboardLayout.js    | 0
 .../assets/{javascripts => src}/dashboard/v2/reducers/dashboardLayout.js  | 0
 superset/assets/{javascripts => src}/dashboard/v2/reducers/editMode.js    | 0
 superset/assets/{javascripts => src}/dashboard/v2/reducers/index.js       | 0
 .../assets/{javascripts => src}/dashboard/v2/reducers/messageToasts.js    | 0
 .../assets/{javascripts => src}/dashboard/v2/stylesheets/builder.less     | 0
 .../assets/{javascripts => src}/dashboard/v2/stylesheets/buttons.less     | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/chart.less   | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/column.less  | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/divider.less | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/header.less  | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/index.less   | 0
 .../dashboard/v2/stylesheets/components/new-component.less                | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/row.less     | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/components/tabs.less    | 0
 superset/assets/{javascripts => src}/dashboard/v2/stylesheets/dnd.less    | 0
 superset/assets/{javascripts => src}/dashboard/v2/stylesheets/grid.less   | 0
 .../assets/{javascripts => src}/dashboard/v2/stylesheets/hover-menu.less  | 0
 superset/assets/{javascripts => src}/dashboard/v2/stylesheets/index.less  | 0
 .../{javascripts => src}/dashboard/v2/stylesheets/popover-menu.less       | 0
 .../assets/{javascripts => src}/dashboard/v2/stylesheets/resizable.less   | 0
 superset/assets/{javascripts => src}/dashboard/v2/stylesheets/toast.less  | 0
 .../assets/{javascripts => src}/dashboard/v2/stylesheets/variables.less   | 0
 .../{javascripts => src}/dashboard/v2/util/backgroundStyleOptions.js      | 0
 .../assets/{javascripts => src}/dashboard/v2/util/componentIsResizable.js | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/componentTypes.js  | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/constants.js       | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/dnd-reorder.js     | 0
 .../assets/{javascripts => src}/dashboard/v2/util/dropOverflowsParent.js  | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/findParentId.js    | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/getChildWidth.js   | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/getDropPosition.js | 0
 .../assets/{javascripts => src}/dashboard/v2/util/headerStyleOptions.js   | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/isValidChild.js    | 0
 .../assets/{javascripts => src}/dashboard/v2/util/newComponentFactory.js  | 0
 .../assets/{javascripts => src}/dashboard/v2/util/newEntitiesFromDrop.js  | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/propShapes.jsx     | 0
 superset/assets/{javascripts => src}/dashboard/v2/util/resizableConfig.js | 0
 .../assets/{javascripts => src}/dashboard/v2/util/shouldWrapChildInRow.js | 0
 84 files changed, 0 insertions(+), 0 deletions(-)

diff --git a/superset/assets/javascripts/dashboard/v2/.eslintrc b/superset/assets/src/dashboard/v2/.eslintrc
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/.eslintrc
rename to superset/assets/src/dashboard/v2/.eslintrc
diff --git a/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js b/superset/assets/src/dashboard/v2/actions/dashboardLayout.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
rename to superset/assets/src/dashboard/v2/actions/dashboardLayout.js
diff --git a/superset/assets/javascripts/dashboard/v2/actions/editMode.js b/superset/assets/src/dashboard/v2/actions/editMode.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/actions/editMode.js
rename to superset/assets/src/dashboard/v2/actions/editMode.js
diff --git a/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js b/superset/assets/src/dashboard/v2/actions/messageToasts.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
rename to superset/assets/src/dashboard/v2/actions/messageToasts.js
diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
rename to superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx b/superset/assets/src/dashboard/v2/components/Dashboard.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
rename to superset/assets/src/dashboard/v2/components/Dashboard.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
rename to superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
rename to superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
rename to superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/DeleteComponentButton.jsx
rename to superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx b/superset/assets/src/dashboard/v2/components/IconButton.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
rename to superset/assets/src/dashboard/v2/components/IconButton.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx b/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/StaticDashboard.jsx
rename to superset/assets/src/dashboard/v2/components/StaticDashboard.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/Toast.jsx b/superset/assets/src/dashboard/v2/components/Toast.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/Toast.jsx
rename to superset/assets/src/dashboard/v2/components/Toast.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx b/superset/assets/src/dashboard/v2/components/ToastPresenter.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
rename to superset/assets/src/dashboard/v2/components/ToastPresenter.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
rename to superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/dnd/DragHandle.jsx
rename to superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
rename to superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
rename to superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js b/superset/assets/src/dashboard/v2/components/dnd/handleHover.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
rename to superset/assets/src/dashboard/v2/components/dnd/handleHover.js
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/index.js
rename to superset/assets/src/dashboard/v2/components/gridComponents/index.js
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewChart.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewColumn.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewDivider.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewHeader.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewRow.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/gridComponents/new/NewTabs.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx b/superset/assets/src/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
rename to superset/assets/src/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx b/superset/assets/src/dashboard/v2/components/menu/HoverMenu.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/menu/HoverMenu.jsx
rename to superset/assets/src/dashboard/v2/components/menu/HoverMenu.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx b/superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/menu/PopoverDropdown.jsx
rename to superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
rename to superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
rename to superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx b/superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/components/resizable/ResizableHandle.jsx
rename to superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
rename to superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
rename to superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
rename to superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
rename to superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx b/superset/assets/src/dashboard/v2/containers/ToastPresenter.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
rename to superset/assets/src/dashboard/v2/containers/ToastPresenter.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js b/superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js
rename to superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js b/superset/assets/src/dashboard/v2/reducers/dashboardLayout.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
rename to superset/assets/src/dashboard/v2/reducers/dashboardLayout.js
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/editMode.js b/superset/assets/src/dashboard/v2/reducers/editMode.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/reducers/editMode.js
rename to superset/assets/src/dashboard/v2/reducers/editMode.js
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/src/dashboard/v2/reducers/index.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/reducers/index.js
rename to superset/assets/src/dashboard/v2/reducers/index.js
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js b/superset/assets/src/dashboard/v2/reducers/messageToasts.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
rename to superset/assets/src/dashboard/v2/reducers/messageToasts.js
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less b/superset/assets/src/dashboard/v2/stylesheets/builder.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
rename to superset/assets/src/dashboard/v2/stylesheets/builder.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less b/superset/assets/src/dashboard/v2/stylesheets/buttons.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
rename to superset/assets/src/dashboard/v2/stylesheets/buttons.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/chart.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/src/dashboard/v2/stylesheets/components/column.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/column.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less b/superset/assets/src/dashboard/v2/stylesheets/components/divider.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/divider.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less b/superset/assets/src/dashboard/v2/stylesheets/components/header.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/header.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less b/superset/assets/src/dashboard/v2/stylesheets/components/index.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/index.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/index.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/src/dashboard/v2/stylesheets/components/new-component.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/new-component.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/src/dashboard/v2/stylesheets/components/row.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/row.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less b/superset/assets/src/dashboard/v2/stylesheets/components/tabs.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
rename to superset/assets/src/dashboard/v2/stylesheets/components/tabs.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less b/superset/assets/src/dashboard/v2/stylesheets/dnd.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
rename to superset/assets/src/dashboard/v2/stylesheets/dnd.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less b/superset/assets/src/dashboard/v2/stylesheets/grid.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
rename to superset/assets/src/dashboard/v2/stylesheets/grid.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less b/superset/assets/src/dashboard/v2/stylesheets/hover-menu.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
rename to superset/assets/src/dashboard/v2/stylesheets/hover-menu.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/src/dashboard/v2/stylesheets/index.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/index.less
rename to superset/assets/src/dashboard/v2/stylesheets/index.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/src/dashboard/v2/stylesheets/popover-menu.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
rename to superset/assets/src/dashboard/v2/stylesheets/popover-menu.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less b/superset/assets/src/dashboard/v2/stylesheets/resizable.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
rename to superset/assets/src/dashboard/v2/stylesheets/resizable.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less b/superset/assets/src/dashboard/v2/stylesheets/toast.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
rename to superset/assets/src/dashboard/v2/stylesheets/toast.less
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less b/superset/assets/src/dashboard/v2/stylesheets/variables.less
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
rename to superset/assets/src/dashboard/v2/stylesheets/variables.less
diff --git a/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js b/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js
rename to superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js b/superset/assets/src/dashboard/v2/util/componentIsResizable.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/componentIsResizable.js
rename to superset/assets/src/dashboard/v2/util/componentIsResizable.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/src/dashboard/v2/util/componentTypes.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/componentTypes.js
rename to superset/assets/src/dashboard/v2/util/componentTypes.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/src/dashboard/v2/util/constants.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/constants.js
rename to superset/assets/src/dashboard/v2/util/constants.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/src/dashboard/v2/util/dnd-reorder.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
rename to superset/assets/src/dashboard/v2/util/dnd-reorder.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js b/superset/assets/src/dashboard/v2/util/dropOverflowsParent.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js
rename to superset/assets/src/dashboard/v2/util/dropOverflowsParent.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/findParentId.js b/superset/assets/src/dashboard/v2/util/findParentId.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/findParentId.js
rename to superset/assets/src/dashboard/v2/util/findParentId.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js b/superset/assets/src/dashboard/v2/util/getChildWidth.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
rename to superset/assets/src/dashboard/v2/util/getChildWidth.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js b/superset/assets/src/dashboard/v2/util/getDropPosition.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
rename to superset/assets/src/dashboard/v2/util/getDropPosition.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js b/superset/assets/src/dashboard/v2/util/headerStyleOptions.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/headerStyleOptions.js
rename to superset/assets/src/dashboard/v2/util/headerStyleOptions.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/src/dashboard/v2/util/isValidChild.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/isValidChild.js
rename to superset/assets/src/dashboard/v2/util/isValidChild.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
rename to superset/assets/src/dashboard/v2/util/newComponentFactory.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
rename to superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/src/dashboard/v2/util/propShapes.jsx
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
rename to superset/assets/src/dashboard/v2/util/propShapes.jsx
diff --git a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js b/superset/assets/src/dashboard/v2/util/resizableConfig.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
rename to superset/assets/src/dashboard/v2/util/resizableConfig.js
diff --git a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/src/dashboard/v2/util/shouldWrapChildInRow.js
similarity index 100%
rename from superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
rename to superset/assets/src/dashboard/v2/util/shouldWrapChildInRow.js


[incubator-superset] 26/26: [dashboard v2] remove webpack-cli, fresh yarn.lock post-rebase

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

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

commit 00ff7780d38e162d004d8d486106d113701850ee
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Thu Jun 21 17:53:20 2018 -0700

    [dashboard v2] remove webpack-cli, fresh yarn.lock post-rebase
---
 superset/assets/package.json |    3 +-
 superset/assets/yarn.lock    | 4602 +++++++++++++++++++-----------------------
 2 files changed, 2032 insertions(+), 2573 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index c68e490..6a39354 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -124,8 +124,7 @@
     "supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
     "underscore": "^1.8.3",
     "urijs": "^1.18.10",
-    "viewport-mercator-project": "^5.0.0",
-    "webpack-cli": "^2.1.4"
+    "viewport-mercator-project": "^5.0.0"
   },
   "devDependencies": {
     "babel-cli": "^6.14.0",
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index 9d1a39b..37ee898 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -2,27 +2,27 @@
 # yarn lockfile v1
 
 
-"@data-ui/event-flow@^0.0.8":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@data-ui/event-flow/-/event-flow-0.0.8.tgz#237ef225e3085fae9e47bbb1ae11b6fab27eaede"
-  dependencies:
-    "@data-ui/forms" "0.0.4"
-    "@data-ui/radial-chart" "0.0.8"
-    "@data-ui/theme" "0.0.9"
-    "@vx/axis" "0.0.120"
-    "@vx/bounds" "0.0.129"
-    "@vx/clip-path" "0.0.126"
-    "@vx/glyph" "0.0.126"
-    "@vx/gradient" "0.0.120"
-    "@vx/grid" "0.0.120"
-    "@vx/group" "0.0.120"
-    "@vx/legend" "0.0.121"
-    "@vx/pattern" "0.0.120"
-    "@vx/point" "0.0.112"
-    "@vx/responsive" "0.0.120"
-    "@vx/scale" "0.0.121"
-    "@vx/shape" "0.0.120"
-    "@vx/tooltip" "0.0.126"
+"@data-ui/event-flow@^0.0.54":
+  version "0.0.54"
+  resolved "https://registry.yarnpkg.com/@data-ui/event-flow/-/event-flow-0.0.54.tgz#bb03e1fd2b5634248655b8df9d3c6c38a747e65e"
+  dependencies:
+    "@data-ui/forms" "0.0.50"
+    "@data-ui/radial-chart" "0.0.54"
+    "@data-ui/theme" "0.0.48"
+    "@vx/axis" "0.0.140"
+    "@vx/bounds" "0.0.140"
+    "@vx/clip-path" "0.0.140"
+    "@vx/glyph" "0.0.140"
+    "@vx/gradient" "0.0.140"
+    "@vx/grid" "0.0.140"
+    "@vx/group" "0.0.140"
+    "@vx/legend" "0.0.140"
+    "@vx/pattern" "0.0.140"
+    "@vx/point" "0.0.136"
+    "@vx/responsive" "0.0.140"
+    "@vx/scale" "0.0.140"
+    "@vx/shape" "0.0.140"
+    "@vx/tooltip" "0.0.140"
     aphrodite "^1.2.0"
     d3-array "^1.2.0"
     d3-format "^1.2.0"
@@ -35,28 +35,29 @@
     react-with-styles-interface-aphrodite "^1.2.0"
     recompose "^0.23.5"
 
-"@data-ui/forms@0.0.4":
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/@data-ui/forms/-/forms-0.0.4.tgz#3c1efc55904289e4bffc6536c5f89b3b18f39e4d"
+"@data-ui/forms@0.0.50":
+  version "0.0.50"
+  resolved "https://registry.yarnpkg.com/@data-ui/forms/-/forms-0.0.50.tgz#c55a699ee4b7cf44ff263d30784299c38d939932"
   dependencies:
     prop-types "^15.5.10"
     react-select "^1.0.0-rc.5"
 
-"@data-ui/radial-chart@0.0.8":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@data-ui/radial-chart/-/radial-chart-0.0.8.tgz#44c0d789a3981c0e9e90f9dbb5e0cbaaafcea09d"
-  dependencies:
-    "@data-ui/theme" "0.0.9"
-    "@vx/event" "0.0.127"
-    "@vx/group" "0.0.127"
-    "@vx/scale" "0.0.127"
-    "@vx/shape" "0.0.131"
-    "@vx/tooltip" "0.0.134"
+"@data-ui/radial-chart@0.0.54":
+  version "0.0.54"
+  resolved "https://registry.yarnpkg.com/@data-ui/radial-chart/-/radial-chart-0.0.54.tgz#0d28b07681d9b6027d9ac23b729241827d513001"
+  dependencies:
+    "@data-ui/shared" "0.0.54"
+    "@data-ui/theme" "0.0.48"
+    "@vx/event" "0.0.140"
+    "@vx/group" "0.0.140"
+    "@vx/scale" "0.0.140"
+    "@vx/shape" "0.0.140"
+    "@vx/tooltip" "0.0.140"
     prop-types "^15.5.10"
 
-"@data-ui/shared@0.0.49":
-  version "0.0.49"
-  resolved "https://registry.yarnpkg.com/@data-ui/shared/-/shared-0.0.49.tgz#cea1fff1434e54a2dcc2d5f9612300738a466a19"
+"@data-ui/shared@0.0.54":
+  version "0.0.54"
+  resolved "https://registry.yarnpkg.com/@data-ui/shared/-/shared-0.0.54.tgz#2fb0d6dee90dac20bf8f3c2913c6850a8223d59b"
   dependencies:
     "@data-ui/theme" "0.0.48"
     "@vx/event" "0.0.143"
@@ -66,11 +67,11 @@
     d3-array "^1.2.1"
     prop-types "^15.5.10"
 
-"@data-ui/sparkline@^0.0.49":
-  version "0.0.49"
-  resolved "https://registry.yarnpkg.com/@data-ui/sparkline/-/sparkline-0.0.49.tgz#5ae28769289af7f8a1bffc6b2ac5299f1fb9e846"
+"@data-ui/sparkline@^0.0.54":
+  version "0.0.54"
+  resolved "https://registry.yarnpkg.com/@data-ui/sparkline/-/sparkline-0.0.54.tgz#ce3d166d9e0b239a0ba02f3894cb9e8c84171cef"
   dependencies:
-    "@data-ui/shared" "0.0.49"
+    "@data-ui/shared" "0.0.54"
     "@data-ui/theme" "0.0.8"
     "@vx/axis" "0.0.140"
     "@vx/curve" "0.0.140"
@@ -94,9 +95,31 @@
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/@data-ui/theme/-/theme-0.0.8.tgz#3116723d04b99f65c7750f81a500e9608b4837c3"
 
-"@data-ui/theme@0.0.9":
-  version "0.0.9"
-  resolved "https://registry.yarnpkg.com/@data-ui/theme/-/theme-0.0.9.tgz#a9d66b20d74018009c129eed4326ca4ef3b83127"
+"@deck.gl/core@^5.3.1":
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/@deck.gl/core/-/core-5.3.1.tgz#acfc1e5fefd3b12e9142419b0aeb77c07885626c"
+  dependencies:
+    luma.gl "^5.3.0"
+    math.gl "^1.2.1"
+    mjolnir.js "^1.0.0"
+    probe.gl "^1.0.0"
+    seer "^0.2.4"
+    viewport-mercator-project "^5.1.0"
+
+"@deck.gl/layers@^5.3.2":
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/@deck.gl/layers/-/layers-5.3.2.tgz#c76b9a7890305a5d6a0fdd56bd0d0d68d4046f6f"
+  dependencies:
+    "@deck.gl/core" "^5.3.1"
+    d3-hexbin "^0.2.1"
+    earcut "^2.0.6"
+
+"@deck.gl/react@^5.3.1":
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/@deck.gl/react/-/react-5.3.1.tgz#0c16fac59061924eb3509dea06c837bcef8044f2"
+  dependencies:
+    "@deck.gl/core" "^5.3.1"
+    prop-types "^15.6.0"
 
 "@mapbox/geojson-area@0.2.2":
   version "0.2.2"
@@ -108,15 +131,19 @@
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/@mapbox/gl-matrix/-/gl-matrix-0.0.1.tgz#e5126aab4d64c36b81c7a97d0ae0dddde5773d2b"
 
-"@mapbox/mapbox-gl-supported@^1.3.0":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.3.0.tgz#89daee16845400ea1c76e084bdfab2971e552a9c"
+"@mapbox/jsonlint-lines-primitives@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.1.tgz#bc4c1593e2ec2371e2771c518068d6eab8eeae58"
+
+"@mapbox/mapbox-gl-supported@^1.3.1":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.4.0.tgz#36946b22944fe2cfa43cfafd5ef36fdb54a069e4"
 
 "@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0":
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2"
 
-"@mapbox/shelf-pack@^3.0.0", "@mapbox/shelf-pack@^3.1.0":
+"@mapbox/shelf-pack@^3.1.0":
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/@mapbox/shelf-pack/-/shelf-pack-3.1.0.tgz#1edea9c0bf6715b217171ba60646c201af520f6a"
 
@@ -128,9 +155,9 @@
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e"
 
-"@mapbox/vector-tile@^1.3.0":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.0.tgz#c495f972525befccefcd838f45ffa37ef3b70fe8"
+"@mapbox/vector-tile@^1.3.1":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666"
   dependencies:
     "@mapbox/point-geometry" "~0.1.0"
 
@@ -138,18 +165,15 @@
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.0.0.tgz#c1de4293081424da3ac30c23afa850af1019bb54"
 
-"@types/react@>=15":
-  version "16.0.12"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.12.tgz#63f460337e83b24549db744aa2f033121d6d55db"
-
-"@vx/axis@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/axis/-/axis-0.0.120.tgz#5dd5841d03e992990491b20ce918b242f9061d4e"
+"@sinonjs/formatio@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
   dependencies:
-    "@vx/group" "0.0.120"
-    "@vx/point" "0.0.112"
-    "@vx/shape" "0.0.120"
-    classnames "^2.2.5"
+    samsam "1.3.0"
+
+"@types/d3@3.5.38":
+  version "3.5.38"
+  resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.38.tgz#76f8f2e9159ae562965b2fa0e6fbee1aa643a1bc"
 
 "@vx/axis@0.0.140":
   version "0.0.140"
@@ -161,12 +185,11 @@
     classnames "^2.2.5"
     prop-types "15.5.10"
 
-"@vx/bounds@0.0.129":
-  version "0.0.129"
-  resolved "https://registry.yarnpkg.com/@vx/bounds/-/bounds-0.0.129.tgz#7ecbdfaa3f5041e3f87c851bf4981547e2d19ffe"
+"@vx/bounds@0.0.140":
+  version "0.0.140"
+  resolved "https://registry.yarnpkg.com/@vx/bounds/-/bounds-0.0.140.tgz#4ede9766aabb41b791a4fbf4c27fcc19ed83f910"
   dependencies:
     prop-types "^15.5.10"
-    react-dom "^15.0.0 || 15.x"
 
 "@vx/bounds@0.0.147":
   version "0.0.147"
@@ -174,21 +197,9 @@
   dependencies:
     prop-types "^15.5.10"
 
-"@vx/clip-path@0.0.126":
-  version "0.0.126"
-  resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.126.tgz#58f595dbd5b7fbb27f6a98bac9885a3d701e976e"
-
-"@vx/curve@0.0.112":
-  version "0.0.112"
-  resolved "https://registry.yarnpkg.com/@vx/curve/-/curve-0.0.112.tgz#8e4880a7ae8902bbd5b337ad1b0719bdfb28df3d"
-  dependencies:
-    d3-shape "^1.0.6"
-
-"@vx/curve@0.0.127":
-  version "0.0.127"
-  resolved "https://registry.yarnpkg.com/@vx/curve/-/curve-0.0.127.tgz#f05ea9871a97e1e1d59129b847f216751ffb31c3"
-  dependencies:
-    d3-shape "^1.0.6"
+"@vx/clip-path@0.0.140":
+  version "0.0.140"
+  resolved "https://registry.yarnpkg.com/@vx/clip-path/-/clip-path-0.0.140.tgz#b2623d004dd5c3c8a6afe8d060de59df51472d94"
 
 "@vx/curve@0.0.140":
   version "0.0.140"
@@ -202,11 +213,11 @@
   dependencies:
     d3-shape "^1.0.6"
 
-"@vx/event@0.0.127":
-  version "0.0.127"
-  resolved "https://registry.yarnpkg.com/@vx/event/-/event-0.0.127.tgz#8b0079a63cf0aeefd884123dd99ee18efef0af69"
+"@vx/event@0.0.140":
+  version "0.0.140"
+  resolved "https://registry.yarnpkg.com/@vx/event/-/event-0.0.140.tgz#658ec4de92cd61df40b883296168d4e0824015bf"
   dependencies:
-    "@vx/point" "0.0.127"
+    "@vx/point" "0.0.136"
 
 "@vx/event@0.0.143":
   version "0.0.143"
@@ -214,14 +225,6 @@
   dependencies:
     "@vx/point" "0.0.143"
 
-"@vx/glyph@0.0.126":
-  version "0.0.126"
-  resolved "https://registry.yarnpkg.com/@vx/glyph/-/glyph-0.0.126.tgz#9ebf0924aa15098b6e4b8c44bfbdfbb8c8a2c658"
-  dependencies:
-    "@vx/group" "0.0.126"
-    classnames "^2.2.5"
-    d3-shape "^1.2.0"
-
 "@vx/glyph@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/glyph/-/glyph-0.0.140.tgz#f8323f82aee22192b675bef25789bbb7d74691ba"
@@ -230,12 +233,6 @@
     classnames "^2.2.5"
     d3-shape "^1.2.0"
 
-"@vx/gradient@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/gradient/-/gradient-0.0.120.tgz#eeac03390d543bcba354d5d5de169ec5f1945c2d"
-  dependencies:
-    classnames "^2.2.5"
-
 "@vx/gradient@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/gradient/-/gradient-0.0.140.tgz#56b421016cbae0dcb00190cfffb9e860a28febf4"
@@ -243,31 +240,13 @@
     classnames "^2.2.5"
     prop-types "^15.5.7"
 
-"@vx/grid@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/grid/-/grid-0.0.120.tgz#3471d5cc8a4bef32082ff476ef73fc04db29335e"
-  dependencies:
-    "@vx/group" "0.0.120"
-    "@vx/point" "0.0.112"
-    "@vx/shape" "0.0.120"
-    classnames "^2.2.5"
-
-"@vx/group@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/group/-/group-0.0.120.tgz#0cc8bc8f40e371585c8c523d98c41cc62684aab9"
-  dependencies:
-    classnames "^2.2.5"
-
-"@vx/group@0.0.126":
-  version "0.0.126"
-  resolved "https://registry.yarnpkg.com/@vx/group/-/group-0.0.126.tgz#5610ce74d0f118db28287806315ae479f313479c"
-  dependencies:
-    classnames "^2.2.5"
-
-"@vx/group@0.0.127":
-  version "0.0.127"
-  resolved "https://registry.yarnpkg.com/@vx/group/-/group-0.0.127.tgz#d607c957119cdb787aa709818532f40e7e04eb8f"
+"@vx/grid@0.0.140":
+  version "0.0.140"
+  resolved "https://registry.yarnpkg.com/@vx/grid/-/grid-0.0.140.tgz#9dfd3071bc5d90d4b457dd55d7f795699233b230"
   dependencies:
+    "@vx/group" "0.0.140"
+    "@vx/point" "0.0.136"
+    "@vx/shape" "0.0.140"
     classnames "^2.2.5"
 
 "@vx/group@0.0.140":
@@ -282,18 +261,11 @@
   dependencies:
     classnames "^2.2.5"
 
-"@vx/legend@0.0.121":
-  version "0.0.121"
-  resolved "https://registry.yarnpkg.com/@vx/legend/-/legend-0.0.121.tgz#0c8cd6860f45af6cbc524ec6526256d2951cca34"
-  dependencies:
-    "@vx/group" "0.0.120"
-    classnames "^2.2.5"
-    prop-types "^15.5.10"
-
-"@vx/pattern@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/pattern/-/pattern-0.0.120.tgz#cff57b6279c9d6b097a20df3562b8b81c17bdb35"
+"@vx/legend@0.0.140":
+  version "0.0.140"
+  resolved "https://registry.yarnpkg.com/@vx/legend/-/legend-0.0.140.tgz#4062c27d6bc9c4d607309d77eff12b844727ae99"
   dependencies:
+    "@vx/group" "0.0.140"
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
@@ -304,14 +276,6 @@
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
-"@vx/point@0.0.112":
-  version "0.0.112"
-  resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.112.tgz#cd5b0740268bb432550902ad5e00261bad641cef"
-
-"@vx/point@0.0.127":
-  version "0.0.127"
-  resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.127.tgz#51ef7f648488feed758a6ec1cc8c71319602e3e7"
-
 "@vx/point@0.0.136":
   version "0.0.136"
   resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.136.tgz#93b325b4b95c9d5b96df740f4204017f57396559"
@@ -320,12 +284,6 @@
   version "0.0.143"
   resolved "https://registry.yarnpkg.com/@vx/point/-/point-0.0.143.tgz#7b6dfa611175ee1b74e3c392072589a79dadf265"
 
-"@vx/responsive@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/responsive/-/responsive-0.0.120.tgz#31c021f213796570787cdd2d2ddb90c4c9444e39"
-  dependencies:
-    lodash "^4.0.8"
-
 "@vx/responsive@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/responsive/-/responsive-0.0.140.tgz#c73ec68b9e89a181605f1ac9ecc09f35216779a8"
@@ -339,46 +297,12 @@
     lodash "^4.0.8"
     resize-observer-polyfill "1.5.0"
 
-"@vx/scale@0.0.121":
-  version "0.0.121"
-  resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.121.tgz#5f49ea2060469ded0bf0e3ef5a5bb1416b81180e"
-  dependencies:
-    d3-scale "^1.0.5"
-
-"@vx/scale@0.0.127":
-  version "0.0.127"
-  resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.127.tgz#2f81530c89b1ad837be387aaccebedd507f16549"
-  dependencies:
-    d3-scale "^1.0.5"
-
 "@vx/scale@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.140.tgz#1eb087d11d0000b250c2cdc4061b9e2212edb10d"
   dependencies:
     d3-scale "^1.0.5"
 
-"@vx/shape@0.0.120":
-  version "0.0.120"
-  resolved "https://registry.yarnpkg.com/@vx/shape/-/shape-0.0.120.tgz#53d5457ec58e298bed507baff015fed2507ac741"
-  dependencies:
-    "@vx/curve" "0.0.112"
-    "@vx/group" "0.0.120"
-    "@vx/point" "0.0.112"
-    classnames "^2.2.5"
-    d3-shape "^1.2.0"
-    prop-types "^15.5.10"
-
-"@vx/shape@0.0.131":
-  version "0.0.131"
-  resolved "https://registry.yarnpkg.com/@vx/shape/-/shape-0.0.131.tgz#38585e93319c9f958d317485b0b6520ff295f179"
-  dependencies:
-    "@vx/curve" "0.0.127"
-    "@vx/group" "0.0.127"
-    "@vx/point" "0.0.127"
-    classnames "^2.2.5"
-    d3-shape "^1.2.0"
-    prop-types "^15.5.10"
-
 "@vx/shape@0.0.140":
   version "0.0.140"
   resolved "https://registry.yarnpkg.com/@vx/shape/-/shape-0.0.140.tgz#6a282d5fdf3a5752b6e938bb3debe983e89ff6d3"
@@ -401,19 +325,11 @@
     d3-shape "^1.2.0"
     prop-types "^15.5.10"
 
-"@vx/tooltip@0.0.126":
-  version "0.0.126"
-  resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.126.tgz#bcbd48bddf614585b11dff4d5b75ba35a42df68c"
-  dependencies:
-    classnames "^2.2.5"
-    prop-types "^15.5.10"
-    recompose "^0.23.5"
-
-"@vx/tooltip@0.0.134":
-  version "0.0.134"
-  resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.134.tgz#8337f0876a98b3eec8e9636b4694126789eba60c"
+"@vx/tooltip@0.0.140":
+  version "0.0.140"
+  resolved "https://registry.yarnpkg.com/@vx/tooltip/-/tooltip-0.0.140.tgz#c5c8306272877c1bbd4e8b478ea5291f1019ffe3"
   dependencies:
-    "@vx/bounds" "0.0.129"
+    "@vx/bounds" "0.0.140"
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
@@ -426,21 +342,17 @@
     prop-types "^15.5.10"
 
 JSONStream@^1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.3.tgz#27b4b8fbbfeab4e71bcf551e7f27be8d952239bf"
   dependencies:
     jsonparse "^1.2.0"
     through ">=2.2.7 <3"
 
-"JSV@>= 4.0.x":
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
-
 abab@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
 
-abbrev@1, abbrev@^1.0.7, abbrev@~1.1.1:
+abbrev@1, abbrev@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
 
@@ -460,54 +372,44 @@ acorn-globals@^3.1.0:
   dependencies:
     acorn "^4.0.4"
 
-acorn-jsx@^3.0.0, acorn-jsx@^3.0.1:
+acorn-jsx@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
   dependencies:
     acorn "^3.0.4"
 
-acorn-object-spread@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/acorn-object-spread/-/acorn-object-spread-1.0.0.tgz#48ead0f4a8eb16995a17a0db9ffc6acaada4ba68"
-  dependencies:
-    acorn "^3.1.0"
-
-acorn@^3.0.4, acorn@^3.1.0, acorn@^3.3.0:
+acorn@^3.0.4:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
-acorn@^4.0.0, acorn@^4.0.3, acorn@^4.0.4:
+acorn@^4.0.3, acorn@^4.0.4:
   version "4.0.13"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
 
-acorn@^5.0.0:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7"
-
-acorn@^5.5.0:
-  version "5.5.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
+acorn@^5.0.0, acorn@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
 
-agent-base@4, agent-base@^4.1.0:
+agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce"
   dependencies:
     es6-promisify "^5.0.0"
 
-agentkeepalive@^3.3.0:
+agentkeepalive@^3.4.1:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c"
   dependencies:
     humanize-ms "^1.2.1"
 
-ajv-keywords@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
-
 ajv-keywords@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
 
+ajv-keywords@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
+
 ajv@^4.9.1:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
@@ -515,16 +417,7 @@ ajv@^4.9.1:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5:
-  version "5.2.3"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
-  dependencies:
-    co "^4.6.0"
-    fast-deep-equal "^1.0.0"
-    json-schema-traverse "^0.3.0"
-    json-stable-stringify "^1.0.1"
-
-ajv@^5.2.3, ajv@^5.3.0:
+ajv@^5.0.0, ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
   version "5.5.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
   dependencies:
@@ -533,6 +426,15 @@ ajv@^5.2.3, ajv@^5.3.0:
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
 
+ajv@^6.1.0:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.1.tgz#88ebc1263c7133937d108b80c5572e64e1d9322d"
+  dependencies:
+    fast-deep-equal "^2.0.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.1"
+
 align-text@^0.1.1, align-text@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -555,10 +457,6 @@ ansi-align@^2.0.0:
   dependencies:
     string-width "^2.0.0"
 
-ansi-escapes@^1.1.0, ansi-escapes@^1.3.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
-
 ansi-escapes@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
@@ -575,12 +473,6 @@ ansi-styles@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
 
-ansi-styles@^3.1.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
-  dependencies:
-    color-convert "^1.9.0"
-
 ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -591,14 +483,14 @@ ansi-styles@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
 
-ansicolors@^0.3.2, ansicolors@~0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
-
 ansicolors@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
 
+ansicolors@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
+
 ansistyles@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
@@ -610,19 +502,26 @@ anymatch@^1.3.0:
     micromatch "^2.1.5"
     normalize-path "^2.0.0"
 
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
 aphrodite@^1.2.0:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/aphrodite/-/aphrodite-1.2.4.tgz#5dc1622aa6f1b02c775e1f1ed850df08839e203c"
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/aphrodite/-/aphrodite-1.2.5.tgz#8358c36c80bb03aee9b97165aaa70186225b4983"
   dependencies:
     asap "^2.0.3"
     inline-style-prefixer "^3.0.1"
     string-hash "^1.1.3"
 
-append-transform@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+append-transform@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab"
   dependencies:
-    default-require-extensions "^1.0.0"
+    default-require-extensions "^2.0.0"
 
 application-config-path@^0.1.0:
   version "0.1.0"
@@ -639,32 +538,37 @@ aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
 
+"aproba@^1.1.2 || 2":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+
 aproba@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0"
 
-archy@^1.0.0, archy@~1.0.0:
+archy@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
 
 are-we-there-yet@~1.1.2:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
   dependencies:
     delegates "^1.0.0"
     readable-stream "^2.0.6"
 
 argparse@^1.0.7:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
   dependencies:
     sprintf-js "~1.0.2"
 
 aria-query@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.0.tgz#4af10a1e61573ddea0cf3b99b51c52c05b424d24"
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.1.tgz#26cbb5aff64144b0a825be1846e0b16cfa00b11e"
   dependencies:
     ast-types-flow "0.0.7"
+    commander "^2.11.0"
 
 arr-diff@^2.0.0:
   version "2.0.0"
@@ -672,10 +576,18 @@ arr-diff@^2.0.0:
   dependencies:
     arr-flatten "^1.0.1"
 
-arr-flatten@^1.0.1:
+arr-diff@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
 
+arr-union@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+
 array-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
@@ -712,6 +624,10 @@ array-unique@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
 
+array-unique@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+
 arrify@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -721,8 +637,8 @@ asap@^2.0.0, asap@^2.0.3, asap@^2.0.6, asap@~2.0.3, asap@~2.0.5:
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
 
 asn1.js@^4.0.0:
-  version "4.9.1"
-  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
   dependencies:
     bn.js "^4.0.0"
     inherits "^2.0.1"
@@ -755,8 +671,12 @@ assert@^1.1.1:
     util "0.10.3"
 
 assertion-error@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+
+assign-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
 
 ast-types-flow@0.0.7:
   version "0.0.7"
@@ -766,28 +686,28 @@ async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
 
-async@1.x, async@^1.4.0, async@~1.5:
+async@1.x, async@^1.4.0:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
 async@^2.1.2, async@^2.1.4, async@^2.4.1:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
   dependencies:
-    lodash "^4.14.0"
+    lodash "^4.17.10"
 
 async@~0.2.7, async@~0.2.9:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
-async@~0.9.0:
-  version "0.9.2"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
-
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
 
+atob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
+
 autoprefixer@^6.3.1:
   version "6.7.7"
   resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
@@ -812,8 +732,8 @@ aws-sign@~0.3.0:
   resolved "https://registry.yarnpkg.com/aws-sign/-/aws-sign-0.3.0.tgz#3d81ca69b474b1e16518728b51c24ff0bbedc6e9"
 
 aws4@^1.2.1, aws4@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
 
 axobject-query@^0.1.0:
   version "0.1.0"
@@ -842,7 +762,7 @@ babel-cli@^6.14.0:
   optionalDependencies:
     chokidar "^1.6.1"
 
-babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
   dependencies:
@@ -851,8 +771,8 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
     js-tokens "^3.0.2"
 
 babel-core@^6.10.4, babel-core@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+  version "6.26.3"
+  resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
   dependencies:
     babel-code-frame "^6.26.0"
     babel-generator "^6.26.0"
@@ -864,19 +784,19 @@ babel-core@^6.10.4, babel-core@^6.26.0:
     babel-traverse "^6.26.0"
     babel-types "^6.26.0"
     babylon "^6.18.0"
-    convert-source-map "^1.5.0"
-    debug "^2.6.8"
+    convert-source-map "^1.5.1"
+    debug "^2.6.9"
     json5 "^0.5.1"
     lodash "^4.17.4"
     minimatch "^3.0.4"
     path-is-absolute "^1.0.1"
-    private "^0.1.7"
+    private "^0.1.8"
     slash "^1.0.0"
-    source-map "^0.5.6"
+    source-map "^0.5.7"
 
 babel-generator@^6.18.0, babel-generator@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
+  version "6.26.1"
+  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
   dependencies:
     babel-messages "^6.23.0"
     babel-runtime "^6.26.0"
@@ -884,7 +804,7 @@ babel-generator@^6.18.0, babel-generator@^6.26.0:
     detect-indent "^4.0.0"
     jsesc "^1.3.0"
     lodash "^4.17.4"
-    source-map "^0.5.6"
+    source-map "^0.5.7"
     trim-right "^1.0.1"
 
 babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
@@ -1018,8 +938,8 @@ babel-istanbul@^0.12.2:
     wordwrap "1.0.x"
 
 babel-loader@^7.0.0:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126"
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
   dependencies:
     find-cache-dir "^1.0.0"
     loader-utils "^1.0.2"
@@ -1038,8 +958,8 @@ babel-plugin-check-es2015-constants@^6.22.0:
     babel-runtime "^6.22.0"
 
 babel-plugin-css-modules-transform@^1.1.0:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/babel-plugin-css-modules-transform/-/babel-plugin-css-modules-transform-1.2.7.tgz#56bcc8ee2665bed8fe59dd8733767911b905bae3"
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-css-modules-transform/-/babel-plugin-css-modules-transform-1.6.1.tgz#5af9483bd62d09af18eeebdc7e6c4370e5125eed"
   dependencies:
     css-modules-require-hook "^4.0.6"
     mkdirp "^0.5.1"
@@ -1160,9 +1080,9 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015
     babel-runtime "^6.22.0"
     babel-template "^6.24.1"
 
-babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.26.2:
+  version "6.26.2"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
   dependencies:
     babel-plugin-transform-strict-mode "^6.24.1"
     babel-runtime "^6.26.0"
@@ -1256,6 +1176,13 @@ babel-plugin-transform-es3-property-literals@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
+babel-plugin-transform-es5-property-mutators@^6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es5-property-mutators/-/babel-plugin-transform-es5-property-mutators-6.24.1.tgz#0b9a24f4e2ff18c33603d24a0d438dc9793b0a13"
+  dependencies:
+    babel-helper-define-map "^6.24.1"
+    babel-runtime "^6.22.0"
+
 babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1:
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
@@ -1277,7 +1204,7 @@ babel-plugin-transform-jscript@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-object-rest-spread@^6.23.0:
+babel-plugin-transform-object-rest-spread@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
   dependencies:
@@ -1334,24 +1261,25 @@ babel-polyfill@^6.23.0, babel-polyfill@^6.26.0:
     regenerator-runtime "^0.10.5"
 
 babel-preset-airbnb@^2.1.1:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-airbnb/-/babel-preset-airbnb-2.4.0.tgz#1b1476f3fafd3c7abc22fa97f932f9e021301450"
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/babel-preset-airbnb/-/babel-preset-airbnb-2.5.1.tgz#17479ff3707a93a9e06d91e77ae213168ce06fa1"
   dependencies:
     babel-plugin-syntax-trailing-function-commas "^6.22.0"
-    babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+    babel-plugin-transform-es2015-modules-commonjs "^6.26.2"
     babel-plugin-transform-es2015-template-literals "^6.22.0"
     babel-plugin-transform-es3-member-expression-literals "^6.22.0"
     babel-plugin-transform-es3-property-literals "^6.22.0"
+    babel-plugin-transform-es5-property-mutators "^6.24.1"
     babel-plugin-transform-exponentiation-operator "^6.24.1"
     babel-plugin-transform-jscript "^6.22.0"
-    babel-plugin-transform-object-rest-spread "^6.23.0"
-    babel-preset-env "^1.5.2"
+    babel-plugin-transform-object-rest-spread "^6.26.0"
+    babel-preset-env "^1.7.0"
     babel-preset-react "^6.24.1"
-    object.assign "^4.0.4"
+    object.assign "^4.1.0"
 
-babel-preset-env@^1.5.2:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4"
+babel-preset-env@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
   dependencies:
     babel-plugin-check-es2015-constants "^6.22.0"
     babel-plugin-syntax-trailing-function-commas "^6.22.0"
@@ -1380,7 +1308,7 @@ babel-preset-env@^1.5.2:
     babel-plugin-transform-es2015-unicode-regex "^6.22.0"
     babel-plugin-transform-exponentiation-operator "^6.22.0"
     babel-plugin-transform-regenerator "^6.22.0"
-    browserslist "^2.1.2"
+    browserslist "^3.2.6"
     invariant "^2.2.2"
     semver "^5.3.0"
 
@@ -1453,7 +1381,7 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26
     lodash "^4.17.4"
     to-fast-properties "^1.0.3"
 
-babylon@^6.15.0, babylon@^6.18.0:
+babylon@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
 
@@ -1474,8 +1402,20 @@ base64-js@0.0.2:
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.2.tgz#024f0f72afa25b75f9c0ee73cd4f55ec1bed9784"
 
 base64-js@^1.0.2:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
+
+base@^0.11.1:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+  dependencies:
+    cache-base "^1.0.1"
+    class-utils "^0.3.5"
+    component-emitter "^1.2.1"
+    define-property "^1.0.0"
+    isobject "^3.0.1"
+    mixin-deep "^1.2.0"
+    pascalcase "^0.1.1"
 
 bcrypt-pbkdf@^1.0.0:
   version "1.0.1"
@@ -1487,7 +1427,7 @@ big.js@^3.1.3:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
 
-bin-links@^1.1.0:
+bin-links@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.2.tgz#fb74bd54bae6b7befc6c6221f25322ac830d9757"
   dependencies:
@@ -1498,8 +1438,8 @@ bin-links@^1.1.0:
     write-file-atomic "^2.3.0"
 
 binary-extensions@^1.0.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
 
 bl@~0.9.4:
   version "0.9.5"
@@ -1547,27 +1487,15 @@ boom@2.x.x:
   dependencies:
     hoek "2.x.x"
 
-boom@4.x.x:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
-  dependencies:
-    hoek "4.x.x"
-
-boom@5.x.x:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
-  dependencies:
-    hoek "4.x.x"
+bootstrap-slider@9.9.0:
+  version "9.9.0"
+  resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-9.9.0.tgz#4e14ecc6401901da1ddf7681aa24e33b00dadce8"
 
 bootstrap-slider@^10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-10.0.0.tgz#d4edd3a10af03197d020e3792d32ea6d37cb3b28"
-
-bootstrap-slider@^9.8.0:
-  version "9.10.0"
-  resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-9.10.0.tgz#1103d6bc00cfbfa8cfc9a2599ab518c55643da3f"
+  version "10.0.2"
+  resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-10.0.2.tgz#442d84293aea248a31b1a3f976ac3737a7362a62"
 
-bootstrap@^3.3.6, bootstrap@^3.3.7:
+bootstrap@^3.3.6:
   version "3.3.7"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
 
@@ -1578,27 +1506,9 @@ bops@0.0.6:
     base64-js "0.0.2"
     to-utf8 "0.0.1"
 
-bops@~0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/bops/-/bops-0.1.1.tgz#062e02a8daa801fa10f2e5dbe6740cff801fe17e"
-  dependencies:
-    base64-js "0.0.2"
-    to-utf8 "0.0.1"
-
 bowser@^1.2.0, bowser@^1.7.3:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.7.3.tgz#504bdb43118ca8db9cbbadf28fd60f265af96e4f"
-
-boxen@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.3.1.tgz#a7d898243ae622f7abb6bb604d740a76c6a5461b"
-  dependencies:
-    chalk "^1.1.1"
-    filled-array "^1.0.0"
-    object-assign "^4.0.1"
-    repeating "^2.0.0"
-    string-width "^1.0.1"
-    widest-line "^1.0.0"
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.3.tgz#6643ae4d783f31683f6d23156976b74183862162"
 
 boxen@^1.2.1:
   version "1.3.0"
@@ -1613,19 +1523,13 @@ boxen@^1.2.1:
     widest-line "^2.0.0"
 
 brace-expansion@^1.1.7:
-  version "1.1.8"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
   dependencies:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-brace@^0.10.0:
-  version "0.10.0"
-  resolved "https://registry.yarnpkg.com/brace/-/brace-0.10.0.tgz#edef4eb9b0928ba1ee5f717ffc157749a6dd5d76"
-  dependencies:
-    w3c-blob "0.0.1"
-
-brace@^0.11.0:
+brace@^0.11.0, brace@^0.11.1:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
 
@@ -1637,17 +1541,32 @@ braces@^1.8.2:
     preserve "^0.2.0"
     repeat-element "^1.1.2"
 
+braces@^2.3.0, braces@^2.3.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+  dependencies:
+    arr-flatten "^1.1.0"
+    array-unique "^0.3.2"
+    extend-shallow "^2.0.1"
+    fill-range "^4.0.0"
+    isobject "^3.0.1"
+    repeat-element "^1.1.2"
+    snapdragon "^0.8.1"
+    snapdragon-node "^2.0.1"
+    split-string "^3.0.2"
+    to-regex "^3.0.1"
+
 brcast@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/brcast/-/brcast-2.0.2.tgz#2db16de44140e418dc37fab10beec0369e78dcef"
 
-brfs@^1.3.0, brfs@^1.4.0, brfs@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.4.3.tgz#db675d6f5e923e6df087fca5859c9090aaed3216"
+brfs@^1.3.0, brfs@^1.4.3, brfs@^1.4.4:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3"
   dependencies:
     quote-stream "^1.0.1"
     resolve "^1.1.5"
-    static-module "^1.1.0"
+    static-module "^2.2.0"
     through2 "^2.0.0"
 
 brorand@^1.0.1:
@@ -1659,8 +1578,8 @@ browser-stdout@1.3.0:
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
 
 browserify-aes@^1.0.0, browserify-aes@^1.0.4:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.8.tgz#c8fa3b1b7585bb7ba77c5560b60996ddec6d5309"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
   dependencies:
     buffer-xor "^1.0.3"
     cipher-base "^1.0.0"
@@ -1670,25 +1589,21 @@ browserify-aes@^1.0.0, browserify-aes@^1.0.4:
     safe-buffer "^5.0.1"
 
 browserify-cipher@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
   dependencies:
     browserify-aes "^1.0.4"
     browserify-des "^1.0.0"
     evp_bytestokey "^1.0.0"
 
 browserify-des@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c"
   dependencies:
     cipher-base "^1.0.1"
     des.js "^1.0.0"
     inherits "^2.0.1"
 
-browserify-package-json@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea"
-
 browserify-rsa@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
@@ -1708,11 +1623,11 @@ browserify-sign@^4.0.0:
     inherits "^2.0.1"
     parse-asn1 "^5.0.0"
 
-browserify-zlib@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+browserify-zlib@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
   dependencies:
-    pako "~0.2.0"
+    pako "~1.0.5"
 
 browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
   version "1.7.7"
@@ -1721,39 +1636,20 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
     caniuse-db "^1.0.30000639"
     electron-to-chromium "^1.2.7"
 
-browserslist@^2.1.2:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8"
-  dependencies:
-    caniuse-lite "^1.0.30000718"
-    electron-to-chromium "^1.3.18"
-
-buble@^0.15.1:
-  version "0.15.2"
-  resolved "https://registry.yarnpkg.com/buble/-/buble-0.15.2.tgz#547fc47483f8e5e8176d82aa5ebccb183b02d613"
+browserslist@^3.2.6:
+  version "3.2.8"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
   dependencies:
-    acorn "^3.3.0"
-    acorn-jsx "^3.0.1"
-    acorn-object-spread "^1.0.0"
-    chalk "^1.1.3"
-    magic-string "^0.14.0"
-    minimist "^1.2.0"
-    os-homedir "^1.0.1"
-
-bubleify@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/bubleify/-/bubleify-0.7.0.tgz#d08ea642ffd085ff8711c8843f57072f0d5eb8f6"
-  dependencies:
-    buble "^0.15.1"
-    object-assign "^4.0.1"
+    caniuse-lite "^1.0.30000844"
+    electron-to-chromium "^1.3.47"
 
 buffer-equal@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
 
 buffer-from@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
 
 buffer-shims@^1.0.0:
   version "1.0.0"
@@ -1771,7 +1667,7 @@ buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
-builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+builtin-modules@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
 
@@ -1791,7 +1687,11 @@ byline@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
 
-cacache@^10.0.0, cacache@^10.0.4:
+byte-size@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.3.tgz#b7c095efc68eadf82985fccd9a2df43a74fa2ccd"
+
+cacache@^10.0.4:
   version "10.0.4"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
   dependencies:
@@ -1809,37 +1709,43 @@ cacache@^10.0.0, cacache@^10.0.4:
     unique-filename "^1.1.0"
     y18n "^4.0.0"
 
-cacache@^10.0.1:
-  version "10.0.2"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.2.tgz#105a93a162bbedf3a25da42e1939ed99ffb145f8"
+cacache@^11.0.1, cacache@^11.0.2:
+  version "11.0.2"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.0.2.tgz#ff30541a05302200108a759e660e30786f788764"
   dependencies:
-    bluebird "^3.5.0"
+    bluebird "^3.5.1"
     chownr "^1.0.1"
+    figgy-pudding "^3.1.0"
     glob "^7.1.2"
     graceful-fs "^4.1.11"
-    lru-cache "^4.1.1"
-    mississippi "^1.3.0"
+    lru-cache "^4.1.2"
+    mississippi "^3.0.0"
     mkdirp "^0.5.1"
     move-concurrently "^1.0.1"
     promise-inflight "^1.0.1"
-    rimraf "^2.6.1"
-    ssri "^5.0.0"
+    rimraf "^2.6.2"
+    ssri "^6.0.0"
     unique-filename "^1.1.0"
-    y18n "^3.2.1"
+    y18n "^4.0.0"
+
+cache-base@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+  dependencies:
+    collection-visit "^1.0.0"
+    component-emitter "^1.2.1"
+    get-value "^2.0.6"
+    has-value "^1.0.0"
+    isobject "^3.0.1"
+    set-value "^2.0.0"
+    to-object-path "^0.3.0"
+    union-value "^1.0.0"
+    unset-value "^1.0.0"
 
 call-limit@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea"
 
-call-matcher@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/call-matcher/-/call-matcher-1.0.1.tgz#5134d077984f712a54dad3cbf62de28dce416ca8"
-  dependencies:
-    core-js "^2.0.0"
-    deep-equal "^1.0.0"
-    espurify "^1.6.0"
-    estraverse "^4.0.0"
-
 caller-path@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1872,12 +1778,12 @@ caniuse-api@^1.5.2:
     lodash.uniq "^4.5.0"
 
 caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
-  version "1.0.30000743"
-  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000743.tgz#bc8df2a257cf91ba024322266295af3ded852306"
+  version "1.0.30000856"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000856.tgz#fbebb99abe15a5654fc7747ebb5315bdfde3358f"
 
-caniuse-lite@^1.0.30000718:
-  version "1.0.30000743"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000743.tgz#f4f5c6750676ff8f6144ea40456c3729d5341769"
+caniuse-lite@^1.0.30000844:
+  version "1.0.30000856"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz#ecc16978135a6f219b138991eb62009d25ee8daa"
 
 capture-stack-trace@^1.0.0:
   version "1.0.0"
@@ -1920,7 +1826,7 @@ chain-function@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
 
-chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -1930,22 +1836,14 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.0.1:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
   dependencies:
     ansi-styles "^3.2.1"
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
-  dependencies:
-    ansi-styles "^3.1.0"
-    escape-string-regexp "^1.0.5"
-    supports-color "^4.0.0"
-
 chalk@~0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
@@ -2003,7 +1901,7 @@ cheerio@^0.22.0:
     lodash.reject "^4.4.0"
     lodash.some "^4.4.0"
 
-chokidar@^1.6.1, chokidar@^1.7.0:
+chokidar@^1.6.1:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
   dependencies:
@@ -2018,6 +1916,25 @@ chokidar@^1.6.1, chokidar@^1.7.0:
   optionalDependencies:
     fsevents "^1.0.0"
 
+chokidar@^2.0.2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
+  dependencies:
+    anymatch "^2.0.0"
+    async-each "^1.0.0"
+    braces "^2.3.0"
+    glob-parent "^3.1.0"
+    inherits "^2.0.1"
+    is-binary-path "^1.0.0"
+    is-glob "^4.0.0"
+    lodash.debounce "^4.0.8"
+    normalize-path "^2.1.1"
+    path-is-absolute "^1.0.0"
+    readdirp "^2.0.0"
+    upath "^1.0.5"
+  optionalDependencies:
+    fsevents "^1.2.2"
+
 chownr@^1.0.1, chownr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
@@ -2026,9 +1943,11 @@ ci-info@^1.0.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2"
 
-cidr-regex@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
+cidr-regex@^2.0.8:
+  version "2.0.9"
+  resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-2.0.9.tgz#9c17bb2b18e15af07f7d0c3b994b961d687ed1c9"
+  dependencies:
+    ip-regex "^2.1.0"
 
 cint@^8.2.1:
   version "8.2.1"
@@ -2051,17 +1970,22 @@ clap@^1.0.9:
   dependencies:
     chalk "^1.1.3"
 
-classnames@2.x, classnames@^2.1.2:
+class-utils@^0.3.5:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+  dependencies:
+    arr-union "^3.1.0"
+    define-property "^0.2.5"
+    isobject "^3.0.0"
+    static-extend "^0.1.1"
+
+classnames@2.x, classnames@^2.1.2, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
   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"
-
 clean-webpack-plugin@^0.1.16:
-  version "0.1.17"
-  resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-0.1.17.tgz#71c57242e6d47204d46f809413176e7bed28ec49"
+  version "0.1.19"
+  resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-0.1.19.tgz#ceda8bb96b00fe168e9b080272960d20fdcadd6d"
   dependencies:
     rimraf "^2.6.1"
 
@@ -2069,11 +1993,12 @@ cli-boxes@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
 
-cli-cursor@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+cli-columns@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e"
   dependencies:
-    restore-cursor "^1.0.1"
+    string-width "^2.0.0"
+    strip-ansi "^3.0.1"
 
 cli-cursor@^2.1.0:
   version "2.1.0"
@@ -2081,7 +2006,7 @@ cli-cursor@^2.1.0:
   dependencies:
     restore-cursor "^2.0.0"
 
-cli-table2@~0.2.0:
+cli-table2@^0.2.0, cli-table2@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
   dependencies:
@@ -2100,19 +2025,13 @@ cli-width@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
 
-clite@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/clite/-/clite-0.3.0.tgz#e7fcbc8cc5bd3e7f8b84ed48db12e9474cc73441"
+clipboard@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.1.tgz#a12481e1c13d8a50f5f036b0560fe5d16d74e46a"
   dependencies:
-    abbrev "^1.0.7"
-    debug "^2.2.0"
-    es6-promise "^3.1.2"
-    lodash.defaults "^4.0.1"
-    lodash.defaultsdeep "^4.3.1"
-    lodash.mergewith "^4.3.1"
-    then-fs "^2.0.0"
-    update-notifier "^0.6.0"
-    yargs "^4.3.2"
+    good-listener "^1.2.2"
+    select "^1.1.2"
+    tiny-emitter "^2.0.0"
 
 cliui@^2.1.0:
   version "2.1.0"
@@ -2131,25 +2050,16 @@ cliui@^3.2.0:
     wrap-ansi "^2.0.0"
 
 cliui@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
   dependencies:
     string-width "^2.1.1"
     strip-ansi "^4.0.0"
     wrap-ansi "^2.0.0"
 
-clone-deep@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8"
-  dependencies:
-    for-own "^1.0.0"
-    is-plain-object "^2.0.1"
-    kind-of "^3.2.2"
-    shallow-clone "^0.1.2"
-
 clone@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
 
 clone@^2.1.1:
   version "2.1.1"
@@ -2180,13 +2090,24 @@ collapse-white-space@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.4.tgz#ce05cf49e54c3277ae573036a26851ba430a0091"
 
+collection-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+  dependencies:
+    map-visit "^1.0.0"
+    object-visit "^1.0.0"
+
 color-convert@^1.3.0, color-convert@^1.9.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+  version "1.9.2"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147"
   dependencies:
-    color-name "^1.1.1"
+    color-name "1.1.1"
 
-color-name@^1.0.0, color-name@^1.1.1:
+color-name@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
+
+color-name@^1.0.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
 
@@ -2221,8 +2142,8 @@ colors@1.0.3:
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
 colors@^1.1.2:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794"
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e"
 
 colors@~1.1.2:
   version "1.1.2"
@@ -2235,9 +2156,9 @@ columnify@~1.5.4:
     strip-ansi "^3.0.0"
     wcwidth "^1.0.0"
 
-combined-stream@^1.0.5, combined-stream@~1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
   dependencies:
     delayed-stream "~1.0.0"
 
@@ -2247,6 +2168,12 @@ combined-stream@~0.0.4:
   dependencies:
     delayed-stream "0.0.5"
 
+comma-separated-tokens@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz#b13793131d9ea2d2431cf5b507ddec258f0ce0db"
+  dependencies:
+    trim "0.0.1"
+
 commander@2.9.0:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
@@ -2254,8 +2181,8 @@ commander@2.9.0:
     graceful-readlink ">= 1.0.0"
 
 commander@^2.11.0, commander@^2.9.0:
-  version "2.11.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+  version "2.15.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
 
 commander@~2.13.0:
   version "2.13.0"
@@ -2265,23 +2192,23 @@ commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
 
+compare-versions@^3.1.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.3.0.tgz#af93ea705a96943f622ab309578b9b90586f39c3"
+
 complex.js@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373"
 
+component-emitter@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 
-concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@~1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
-  dependencies:
-    inherits "^2.0.3"
-    readable-stream "^2.2.2"
-    typedarray "^0.0.6"
-
-concat-stream@^1.6.0:
+concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@^1.6.0, concat-stream@~1.6.0:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
   dependencies:
@@ -2303,36 +2230,9 @@ config-chain@~1.1.11:
     ini "^1.3.4"
     proto-list "~1.2.1"
 
-configstore@^1.0.0, configstore@^1.2.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021"
-  dependencies:
-    graceful-fs "^4.1.2"
-    mkdirp "^0.5.0"
-    object-assign "^4.0.1"
-    os-tmpdir "^1.0.0"
-    osenv "^0.1.0"
-    uuid "^2.0.1"
-    write-file-atomic "^1.1.2"
-    xdg-basedir "^2.0.0"
-
-configstore@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1"
-  dependencies:
-    dot-prop "^3.0.0"
-    graceful-fs "^4.1.2"
-    mkdirp "^0.5.0"
-    object-assign "^4.0.1"
-    os-tmpdir "^1.0.0"
-    osenv "^0.1.0"
-    uuid "^2.0.1"
-    write-file-atomic "^1.1.2"
-    xdg-basedir "^2.0.0"
-
 configstore@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
   dependencies:
     dot-prop "^4.1.0"
     graceful-fs "^4.1.2"
@@ -2347,7 +2247,7 @@ console-browserify@^1.1.0:
   dependencies:
     date-now "^0.1.4"
 
-console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
 
@@ -2360,12 +2260,12 @@ contains-path@^0.1.0:
   resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
 
 content-type-parser@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
 
-convert-source-map@^1.1.1, convert-source-map@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+convert-source-map@^1.5.0, convert-source-map@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
 
 cookie-jar@~0.3.0:
   version "0.3.0"
@@ -2382,43 +2282,48 @@ copy-concurrently@^1.0.0:
     rimraf "^2.5.4"
     run-queue "^1.0.0"
 
+copy-descriptor@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+
 core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
 
-core-js@^2.0.0, core-js@^2.4.0, core-js@^2.5.0:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
+core-js@^2.4.0, core-js@^2.5.0:
+  version "2.5.7"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
 
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
 
 create-ecdh@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
   dependencies:
     bn.js "^4.1.0"
     elliptic "^6.0.0"
 
-create-error-class@^3.0.0, create-error-class@^3.0.1:
+create-error-class@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
   dependencies:
     capture-stack-trace "^1.0.0"
 
 create-hash@^1.1.0, create-hash@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
   dependencies:
     cipher-base "^1.0.1"
     inherits "^2.0.1"
-    ripemd160 "^2.0.0"
+    md5.js "^1.3.4"
+    ripemd160 "^2.0.1"
     sha.js "^2.4.0"
 
 create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
   dependencies:
     cipher-base "^1.0.3"
     create-hash "^1.1.0"
@@ -2427,9 +2332,9 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
-create-react-class@^15.5.2, create-react-class@^15.5.x, create-react-class@^15.6.0:
-  version "15.6.2"
-  resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a"
+create-react-class@^15.5.2, create-react-class@^15.6.0:
+  version "15.6.3"
+  resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
   dependencies:
     fbjs "^0.8.9"
     loose-envify "^1.3.1"
@@ -2459,15 +2364,9 @@ cryptiles@2.x.x:
   dependencies:
     boom "2.x.x"
 
-cryptiles@3.x.x:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
-  dependencies:
-    boom "5.x.x"
-
 crypto-browserify@^3.11.0:
-  version "3.11.1"
-  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
   dependencies:
     browserify-cipher "^1.0.0"
     browserify-sign "^4.0.0"
@@ -2479,6 +2378,7 @@ crypto-browserify@^3.11.0:
     pbkdf2 "^3.0.3"
     public-encrypt "^4.0.0"
     randombytes "^2.0.0"
+    randomfill "^1.0.3"
 
 crypto-random-string@^1.0.0:
   version "1.0.0"
@@ -2489,33 +2389,34 @@ css-color-names@0.0.4:
   resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
 
 css-in-js-utils@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.0.tgz#5af1dd70f4b06b331f48d22a3d86e0786c0b9435"
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99"
   dependencies:
     hyphenate-style-name "^1.0.2"
+    isobject "^3.0.1"
 
 css-loader@^0.28.0:
-  version "0.28.7"
-  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.7.tgz#5f2ee989dd32edd907717f953317656160999c1b"
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
   dependencies:
-    babel-code-frame "^6.11.0"
+    babel-code-frame "^6.26.0"
     css-selector-tokenizer "^0.7.0"
-    cssnano ">=2.6.1 <4"
+    cssnano "^3.10.0"
     icss-utils "^2.1.0"
     loader-utils "^1.0.2"
     lodash.camelcase "^4.3.0"
-    object-assign "^4.0.1"
+    object-assign "^4.1.1"
     postcss "^5.0.6"
-    postcss-modules-extract-imports "^1.0.0"
-    postcss-modules-local-by-default "^1.0.1"
-    postcss-modules-scope "^1.0.0"
-    postcss-modules-values "^1.1.0"
+    postcss-modules-extract-imports "^1.2.0"
+    postcss-modules-local-by-default "^1.2.0"
+    postcss-modules-scope "^1.1.0"
+    postcss-modules-values "^1.3.0"
     postcss-value-parser "^3.3.0"
     source-list-map "^2.0.0"
 
 css-modules-require-hook@^4.0.6:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/css-modules-require-hook/-/css-modules-require-hook-4.2.2.tgz#99bb95e5ce9cf49060eed74997c7f7a04bf221eb"
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/css-modules-require-hook/-/css-modules-require-hook-4.2.3.tgz#6792ca412b15e23e6f9be6a07dcef7f577ff904d"
   dependencies:
     debug "^2.2.0"
     generic-names "^1.0.1"
@@ -2559,7 +2460,7 @@ cssesc@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
 
-"cssnano@>=2.6.1 <4":
+cssnano@^3.10.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
   dependencies:
@@ -2626,20 +2527,20 @@ d3-array@1, d3-array@^1.2.0, d3-array@^1.2.1:
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
 
 d3-cloud@^1.2.1:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/d3-cloud/-/d3-cloud-1.2.4.tgz#3e169403adab74fdb0c867638d7f0bb8e6ae71ff"
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/d3-cloud/-/d3-cloud-1.2.5.tgz#3e91564f2d27fba47fcc7d812eb5081ea24c603d"
   dependencies:
-    d3-dispatch "1"
+    d3-dispatch "^1.0.3"
 
-d3-collection@1:
+d3-collection@1, d3-collection@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
 
 d3-color@1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
 
-d3-dispatch@1:
+d3-dispatch@1, d3-dispatch@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
 
@@ -2655,8 +2556,8 @@ d3-ease@1:
   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
 
 d3-format@1, d3-format@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.0.tgz#6b480baa886885d4651dc248a8f4ac9da16db07a"
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11"
 
 d3-geo-projection@0.2:
   version "0.2.16"
@@ -2669,12 +2570,12 @@ d3-hexbin@^0.2.1:
   resolved "https://registry.yarnpkg.com/d3-hexbin/-/d3-hexbin-0.2.2.tgz#9c5837dacfd471ab05337a9e91ef10bfc4f98831"
 
 d3-hierarchy@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#842c1372090f870b7ea013ebae5c0c8d9f56229c"
 
 d3-interpolate@1:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.5.tgz#69e099ff39214716e563c9aec3ea9d1ea4b8a79f"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41"
   dependencies:
     d3-color "1"
 
@@ -2699,8 +2600,8 @@ d3-sankey@^0.4.2:
     d3-interpolate "1"
 
 d3-scale@^1.0.5:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.6.tgz#bce19da80d3a0cf422c9543ae3322086220b34ed"
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
   dependencies:
     d3-array "^1.2.0"
     d3-collection "1"
@@ -2710,9 +2611,9 @@ d3-scale@^1.0.5:
     d3-time "1"
     d3-time-format "2"
 
-d3-selection@1, d3-selection@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.1.0.tgz#1998684896488f839ca0372123da34f1d318809c"
+d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
 
 d3-shape@^1.0.6, d3-shape@^1.2.0:
   version "1.2.0"
@@ -2725,28 +2626,29 @@ d3-svg-legend@^1.x:
   resolved "https://registry.yarnpkg.com/d3-svg-legend/-/d3-svg-legend-1.13.0.tgz#6217478c9add9d62cb333617e1961311a41a4db3"
 
 d3-time-format@2:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.0.5.tgz#9d7780204f7c9119c9170b1a56db4de9a8af972e"
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
   dependencies:
     d3-time "1"
 
 d3-time@1:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.7.tgz#94caf6edbb7879bb809d0d1f7572bc48482f7270"
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
 
 d3-timer@1:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
 
-d3-tip@^0.6.7:
-  version "0.6.8"
-  resolved "https://registry.yarnpkg.com/d3-tip/-/d3-tip-0.6.8.tgz#e5b4491ae8983fde646ea49008ff542a033c0a2c"
+d3-tip@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/d3-tip/-/d3-tip-0.9.1.tgz#84e6d331c4e6650d80c5228a07e41820609ab64b"
   dependencies:
-    d3 "^3.5.5"
+    d3-collection "^1.0.4"
+    d3-selection "^1.3.0"
 
 d3-transition@1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.0.tgz#cfc85c74e5239324290546623572990560c3966f"
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
   dependencies:
     d3-color "1"
     d3-dispatch "1"
@@ -2756,8 +2658,8 @@ d3-transition@1:
     d3-timer "1"
 
 d3-zoom@^1.3.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.6.0.tgz#eb645b07fd0c37acc8b36b88476b781ed277b40e"
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
   dependencies:
     d3-dispatch "1"
     d3-drag "1"
@@ -2765,7 +2667,7 @@ d3-zoom@^1.3.0:
     d3-selection "1"
     d3-transition "1"
 
-d3@3, d3@^3.5.17, d3@^3.5.5, d3@^3.5.6:
+d3@3, d3@^3.5.17, d3@^3.5.6:
   version "3.5.17"
   resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
 
@@ -2786,22 +2688,23 @@ dashdash@^1.12.0:
     assert-plus "^1.0.0"
 
 datamaps@^0.5.8:
-  version "0.5.8"
-  resolved "https://registry.yarnpkg.com/datamaps/-/datamaps-0.5.8.tgz#d3ab9e86759529bffb98e82b4a43b5267f49d183"
+  version "0.5.9"
+  resolved "https://registry.yarnpkg.com/datamaps/-/datamaps-0.5.9.tgz#2a775473aaab29b55025208b2245e840ecfd4fe1"
   dependencies:
+    "@types/d3" "3.5.38"
     d3 "^3.5.6"
     topojson "^1.6.19"
 
 datatables.net-bs@^1.10.15:
-  version "1.10.16"
-  resolved "https://registry.yarnpkg.com/datatables.net-bs/-/datatables.net-bs-1.10.16.tgz#b0854f5b374f713ae3db4156c7cea8a760c3de76"
+  version "1.10.18"
+  resolved "https://registry.yarnpkg.com/datatables.net-bs/-/datatables.net-bs-1.10.18.tgz#72c9ebe926f9189f891d4c474a629defc99753e6"
   dependencies:
-    datatables.net "1.10.16"
+    datatables.net "1.10.18"
     jquery ">=1.7"
 
-datatables.net@1.10.16:
-  version "1.10.16"
-  resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.16.tgz#4b052d1082824261b68eed9d22741b711d3d2469"
+datatables.net@1.10.18:
+  version "1.10.18"
+  resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.18.tgz#b6f045aa533101bcd33714e5338da8f905d3ef09"
   dependencies:
     jquery ">=1.7"
 
@@ -2821,7 +2724,7 @@ debug@3.1.0, debug@^3.1.0:
   dependencies:
     ms "2.0.0"
 
-debug@^2.1.2, debug@^2.2.0, debug@^2.6.3, debug@^2.6.8:
+debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -2840,18 +2743,12 @@ decimal.js@9.0.1:
   resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-9.0.1.tgz#1cc8b228177da7ab6498c1cc06eb130a290e6e1e"
 
 deck.gl@^5.1.4:
-  version "5.1.4"
-  resolved "https://registry.yarnpkg.com/deck.gl/-/deck.gl-5.1.4.tgz#1adb33798ec91abb2a1d164adfff4cdb1a1a888d"
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/deck.gl/-/deck.gl-5.3.2.tgz#2297d820fb8fb02eab95ac6a03261a689500bc11"
   dependencies:
-    d3-hexbin "^0.2.1"
-    earcut "^2.0.6"
-    lodash.flattendeep "^4.4.0"
-    luma.gl "^5.1.4"
-    math.gl "^1.0.0"
-    mjolnir.js "^1.0.0"
-    prop-types "^15.6.0"
-    seer "^0.2.4"
-    viewport-mercator-project "^5.0.0"
+    "@deck.gl/core" "^5.3.1"
+    "@deck.gl/layers" "^5.3.2"
+    "@deck.gl/react" "^5.3.1"
 
 decode-uri-component@^0.2.0:
   version "0.2.0"
@@ -2863,13 +2760,13 @@ deep-eql@^3.0.0:
   dependencies:
     type-detect "^4.0.0"
 
-deep-equal@^1.0.0, deep-equal@^1.0.1:
+deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
 
-deep-extend@~0.4.0:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+deep-extend@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
 
 deep-is@~0.1.3:
   version "0.1.3"
@@ -2879,11 +2776,11 @@ deepmerge@^1.3.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
 
-default-require-extensions@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+default-require-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"
   dependencies:
-    strip-bom "^2.0.0"
+    strip-bom "^3.0.0"
 
 defaults@^1.0.3:
   version "1.0.3"
@@ -2898,6 +2795,25 @@ define-properties@^1.1.2:
     foreach "^2.0.5"
     object-keys "^1.0.8"
 
+define-property@^0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+  dependencies:
+    is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+  dependencies:
+    is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+  dependencies:
+    is-descriptor "^1.0.2"
+    isobject "^3.0.1"
+
 defined@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -2922,6 +2838,10 @@ delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
 
+delegate@^3.1.2:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -2943,6 +2863,10 @@ detect-indent@~5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
 
+detect-libc@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
 detect-newline@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
@@ -2959,12 +2883,12 @@ diff@3.2.0:
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
 
 diff@^3.1.0:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
 
 diffie-hellman@^5.0.0:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
   dependencies:
     bn.js "^4.1.0"
     miller-rabin "^4.0.0"
@@ -2975,8 +2899,8 @@ disposables@^1.0.1:
   resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.2.tgz#36c6a674475f55a2d6913567a601444e487b4b6e"
 
 distributions@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/distributions/-/distributions-1.0.0.tgz#16466e676df7f311929941d3d7f02010466671a9"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/distributions/-/distributions-1.1.0.tgz#012973634ef6dd595a525ab1b397d217131c0ea5"
   dependencies:
     mathfn "^1.0.0"
 
@@ -2996,13 +2920,6 @@ doctrine@1.5.0:
     esutils "^2.0.2"
     isarray "^1.0.0"
 
-doctrine@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
-  dependencies:
-    esutils "^2.0.2"
-    isarray "^1.0.0"
-
 doctrine@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -3010,8 +2927,8 @@ doctrine@^2.1.0:
     esutils "^2.0.2"
 
 "dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.2.0, dom-helpers@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
 
 dom-serializer@0, dom-serializer@~0.1.0:
   version "0.1.0"
@@ -3021,8 +2938,8 @@ dom-serializer@0, dom-serializer@~0.1.0:
     entities "~1.1.1"
 
 domain-browser@^1.1.1:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
 
 domelementtype@1, domelementtype@^1.3.0:
   version "1.3.0"
@@ -3033,14 +2950,14 @@ domelementtype@~1.1.1:
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
 
 domhandler@^2.3.0:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
   dependencies:
     domelementtype "1"
 
 dompurify@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-1.0.3.tgz#3f2f6ecb6ecd27599a506b410ff47d6eb90fd05d"
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-1.0.5.tgz#844ebcbf8465c8cb724291e63dbaf90ad525551c"
 
 domutils@1.5.1:
   version "1.5.1"
@@ -3050,18 +2967,12 @@ domutils@1.5.1:
     domelementtype "1"
 
 domutils@^1.5.1:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
   dependencies:
     dom-serializer "0"
     domelementtype "1"
 
-dot-prop@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
-  dependencies:
-    is-obj "^1.0.0"
-
 dot-prop@^4.1.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@@ -3072,44 +2983,31 @@ dotenv@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
 
-duplexer2@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
-  dependencies:
-    readable-stream "^2.0.2"
-
 duplexer2@~0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db"
   dependencies:
     readable-stream "~1.1.9"
 
+duplexer2@~0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
+  dependencies:
+    readable-stream "^2.0.2"
+
 duplexer3@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
 
-duplexify@^3.2.0:
-  version "3.5.4"
-  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4"
-  dependencies:
-    end-of-stream "^1.0.0"
-    inherits "^2.0.1"
-    readable-stream "^2.0.0"
-    stream-shift "^1.0.0"
-
-duplexify@^3.4.2, duplexify@^3.5.3:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e"
+duplexify@^3.4.2, duplexify@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410"
   dependencies:
     end-of-stream "^1.0.0"
     inherits "^2.0.1"
     readable-stream "^2.0.0"
     stream-shift "^1.0.0"
 
-earcut@^2.0.3:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.1.1.tgz#157634e5f3ebb42224e475016e86a5b6ce556b45"
-
 earcut@^2.0.6, earcut@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.1.3.tgz#ca579545f351941af7c3d0df49c9f7d34af99b0c"
@@ -3124,9 +3022,9 @@ editor@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742"
 
-electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18:
-  version "1.3.24"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6"
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.47:
+  version "1.3.50"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.50.tgz#7438b76f92b41b919f3fbdd350fbd0757dacddf7"
 
 elliptic@^6.0.0:
   version "6.4.0"
@@ -3140,10 +3038,6 @@ elliptic@^6.0.0:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.0"
 
-email-validator@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-1.1.1.tgz#b07f3be7bac1dc099bc43e75f6ae399f552d5a80"
-
 emoji-regex@^6.1.0:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
@@ -3196,33 +3090,21 @@ err-code@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
 
-errno@^0.1.1, errno@^0.1.3:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
-  dependencies:
-    prr "~0.0.0"
-
-errno@^0.1.4:
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026"
-  dependencies:
-    prr "~1.0.1"
-
-errno@~0.1.7:
+errno@^0.1.1, errno@^0.1.3, errno@~0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
   dependencies:
     prr "~1.0.1"
 
 error-ex@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
   dependencies:
     is-arrayish "^0.2.1"
 
 es-abstract@^1.6.1, es-abstract@^1.7.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
   dependencies:
     es-to-primitive "^1.1.1"
     function-bind "^1.1.1"
@@ -3238,20 +3120,21 @@ es-to-primitive@^1.1.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.1"
 
-es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
-  version "0.10.30"
-  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939"
+es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
+  version "0.10.45"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.45.tgz#0bfdf7b473da5919d5adf3bd25ceb754fccc3653"
   dependencies:
-    es6-iterator "2"
-    es6-symbol "~3.1"
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.1"
+    next-tick "1"
 
-es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
+es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
   dependencies:
     d "1"
-    es5-ext "^0.10.14"
-    es6-symbol "^3.1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
 
 es6-map@^0.1.3:
   version "0.1.5"
@@ -3264,11 +3147,7 @@ es6-map@^0.1.3:
     es6-symbol "~3.1.1"
     event-emitter "~0.3.5"
 
-es6-promise@^3.0.2, es6-promise@^3.1.2:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
-
-es6-promise@^4.0.3, es6-promise@^4.1.1:
+es6-promise@^4.0.3:
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
 
@@ -3288,7 +3167,7 @@ es6-set@~0.1.5:
     es6-symbol "3.1.1"
     event-emitter "~0.3.5"
 
-es6-symbol@3.1.1, es6-symbol@^3.0.2, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+es6-symbol@3.1.1, es6-symbol@^3.0.2, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
   dependencies:
@@ -3304,9 +3183,13 @@ es6-weak-map@^2.0.1:
     es6-iterator "^2.0.1"
     es6-symbol "^3.1.1"
 
+es6bindall@^0.0.9:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/es6bindall/-/es6bindall-0.0.9.tgz#71e00afa69f8dd59ac5ac898a0d31c978df817d5"
+
 escape-latex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.0.0.tgz#74b9e94d8c178645704c33791e95a4155b59718f"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.1.0.tgz#c0a94a51eb8c73c3a67a95cc90fbb626cef54539"
 
 escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
   version "1.0.5"
@@ -3323,35 +3206,27 @@ escodegen@1.8.x:
   optionalDependencies:
     source-map "~0.2.0"
 
-escodegen@^1.6.1:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852"
+escodegen@^1.6.1, escodegen@^1.8.1:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.10.0.tgz#f647395de22519fbd0d928ffcf1d17e0dec2603e"
   dependencies:
     esprima "^3.1.3"
     estraverse "^4.2.0"
     esutils "^2.0.2"
     optionator "^0.8.1"
   optionalDependencies:
-    source-map "~0.5.6"
-
-escodegen@~0.0.24:
-  version "0.0.28"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-0.0.28.tgz#0e4ff1715f328775d6cab51ac44a406cd7abffd3"
-  dependencies:
-    esprima "~1.0.2"
-    estraverse "~1.3.0"
-  optionalDependencies:
-    source-map ">= 0.1.2"
+    source-map "~0.6.1"
 
-escodegen@~1.3.2:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.3.3.tgz#f024016f5a88e046fd12005055e939802e6c5f23"
+escodegen@~1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
   dependencies:
-    esprima "~1.1.1"
-    estraverse "~1.5.0"
-    esutils "~1.0.0"
+    esprima "^3.1.3"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
   optionalDependencies:
-    source-map "~0.1.33"
+    source-map "~0.6.1"
 
 escope@^3.6.0:
   version "3.6.0"
@@ -3381,33 +3256,33 @@ eslint-config-prettier@^2.9.0:
     get-stdin "^5.0.1"
 
 eslint-import-resolver-node@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
   dependencies:
-    debug "^2.6.8"
-    resolve "^1.2.0"
+    debug "^2.6.9"
+    resolve "^1.5.0"
 
-eslint-module-utils@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+eslint-module-utils@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
   dependencies:
     debug "^2.6.8"
     pkg-dir "^1.0.0"
 
 eslint-plugin-import@^2.2.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz#21de33380b9efb55f5ef6d2e210ec0e07e7fa69f"
+  version "2.12.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz#dad31781292d6664b25317fd049d2e2b2f02205d"
   dependencies:
-    builtin-modules "^1.1.1"
     contains-path "^0.1.0"
     debug "^2.6.8"
     doctrine "1.5.0"
     eslint-import-resolver-node "^0.3.1"
-    eslint-module-utils "^2.1.1"
+    eslint-module-utils "^2.2.0"
     has "^1.0.1"
-    lodash.cond "^4.3.0"
+    lodash "^4.17.4"
     minimatch "^3.0.3"
     read-pkg-up "^2.0.0"
+    resolve "^1.6.0"
 
 eslint-plugin-jsx-a11y@^5.1.1:
   version "5.1.1"
@@ -3429,13 +3304,13 @@ eslint-plugin-prettier@^2.6.0:
     jest-docblock "^21.0.0"
 
 eslint-plugin-react@^7.0.1:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz#300a95861b9729c087d362dd64abcc351a74364a"
+  version "7.9.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.9.1.tgz#101aadd15e7c7b431ed025303ac7b421a8e3dc15"
   dependencies:
-    doctrine "^2.0.0"
-    has "^1.0.1"
-    jsx-ast-utils "^2.0.0"
-    prop-types "^15.5.10"
+    doctrine "^2.1.0"
+    has "^1.0.2"
+    jsx-ast-utils "^2.0.1"
+    prop-types "^15.6.1"
 
 eslint-restricted-globals@^0.1.1:
   version "0.1.1"
@@ -3514,32 +3389,21 @@ esprima@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
 
-esprima@~1.0.2, esprima@~1.0.4:
+esprima@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad"
 
-esprima@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.1.1.tgz#5b6f1547f4d102e670e140c509be6771d6aeb549"
-
-espurify@^1.3.0, espurify@^1.6.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.7.0.tgz#1c5cf6cbccc32e6f639380bd4f991fab9ba9d226"
-  dependencies:
-    core-js "^2.0.0"
-
 esquery@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
   dependencies:
     estraverse "^4.0.0"
 
 esrecurse@^4.1.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
   dependencies:
     estraverse "^4.1.0"
-    object-assign "^4.0.1"
 
 estraverse@^1.9.1:
   version "1.9.3"
@@ -3549,22 +3413,10 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
 
-estraverse@~1.3.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.3.2.tgz#37c2b893ef13d723f276d878d60d8535152a6c42"
-
-estraverse@~1.5.0:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.5.1.tgz#867a3e8e58a9f84618afb6c2ddbcd916b7cbaf71"
-
 esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
 
-esutils@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.0.0.tgz#8151d358e20c8acc7fb745e7472c0025fe496570"
-
 event-emitter@~0.3.5:
   version "0.3.5"
   resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
@@ -3599,16 +3451,24 @@ 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"
-
 expand-brackets@^0.1.4:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
   dependencies:
     is-posix-bracket "^0.1.0"
 
+expand-brackets@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+  dependencies:
+    debug "^2.3.3"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    posix-character-classes "^0.1.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
 expand-range@^1.8.1:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
@@ -3619,12 +3479,12 @@ expect.js@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1"
 
-exports-loader@^0.6.3:
-  version "0.6.4"
-  resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.6.4.tgz#d70fc6121975b35fc12830cf52754be2740fc886"
+exports-loader@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.7.0.tgz#84881c784dea6036b8e1cd1dac3da9b6409e21a5"
   dependencies:
-    loader-utils "^1.0.2"
-    source-map "0.5.x"
+    loader-utils "^1.1.0"
+    source-map "0.5.0"
 
 extend-shallow@^2.0.1:
   version "2.0.1"
@@ -3632,6 +3492,13 @@ extend-shallow@^2.0.1:
   dependencies:
     is-extendable "^0.1.0"
 
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+  dependencies:
+    assign-symbols "^1.0.0"
+    is-extendable "^1.0.1"
+
 extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
@@ -3641,8 +3508,8 @@ extent@0.2.0:
   resolved "https://registry.yarnpkg.com/extent/-/extent-0.2.0.tgz#efad0869682d3628bdbeeb140c1f4d0023e6bec4"
 
 external-editor@^2.0.4:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
   dependencies:
     chardet "^0.4.0"
     iconv-lite "^0.4.17"
@@ -3654,19 +3521,36 @@ extglob@^0.3.1:
   dependencies:
     is-extglob "^1.0.0"
 
-extract-text-webpack-plugin@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612"
+extglob@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+  dependencies:
+    array-unique "^0.3.2"
+    define-property "^1.0.0"
+    expand-brackets "^2.1.4"
+    extend-shallow "^2.0.1"
+    fragment-cache "^0.2.1"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extract-text-webpack-plugin@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7"
   dependencies:
     async "^2.4.1"
     loader-utils "^1.1.0"
     schema-utils "^0.3.0"
     webpack-sources "^1.0.1"
 
-extsprintf@1.3.0, extsprintf@^1.2.0:
+extsprintf@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
 
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
 falafel@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.1.0.tgz#96bb17761daba94f46d001738b3cedf3a67fe06c"
@@ -3677,8 +3561,12 @@ falafel@^2.1.0:
     object-keys "^1.0.6"
 
 fast-deep-equal@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
+
+fast-deep-equal@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
 
 fast-diff@^1.0.1, fast-diff@^1.1.1:
   version "1.1.2"
@@ -3693,12 +3581,12 @@ fast-levenshtein@~2.0.4:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
 
 fast-memoize@^2.2.7:
-  version "2.2.8"
-  resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.2.8.tgz#d7f899f31d037b12d9db4281912f9018575720b1"
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.4.0.tgz#2f79eca41c41112b0b70cf53ac3940e206574648"
 
 fastdom@^1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/fastdom/-/fastdom-1.0.6.tgz#0fa5866238e1e8bea35dc14e31cdf34547f0db73"
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/fastdom/-/fastdom-1.0.8.tgz#10f9d36998fd6efae30e529597d788e750c9febb"
   dependencies:
     strictdom "^1.0.1"
 
@@ -3706,9 +3594,15 @@ fastparse@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
 
-fbjs@^0.8.1, fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9:
-  version "0.8.16"
-  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
+fault@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
+  dependencies:
+    format "^0.2.2"
+
+fbjs@^0.8.1, fbjs@^0.8.12, fbjs@^0.8.4, fbjs@^0.8.9:
+  version "0.8.17"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
   dependencies:
     core-js "^1.0.0"
     isomorphic-fetch "^2.1.1"
@@ -3716,14 +3610,11 @@ fbjs@^0.8.1, fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9:
     object-assign "^4.1.0"
     promise "^7.1.1"
     setimmediate "^1.0.5"
-    ua-parser-js "^0.7.9"
+    ua-parser-js "^0.7.18"
 
-figures@^1.3.5:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
-  dependencies:
-    escape-string-regexp "^1.0.5"
-    object-assign "^4.1.0"
+figgy-pudding@^3.0.0, figgy-pudding@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.1.0.tgz#a77ed2284175976c424b390b298569e9df86dd1e"
 
 figures@^2.0.0:
   version "2.0.0"
@@ -3756,18 +3647,23 @@ fileset@^2.0.2:
     minimatch "^3.0.3"
 
 fill-range@^2.1.0:
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
   dependencies:
     is-number "^2.1.0"
     isobject "^2.0.0"
-    randomatic "^1.1.3"
+    randomatic "^3.0.0"
     repeat-element "^1.1.2"
     repeat-string "^1.5.2"
 
-filled-array@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84"
+fill-range@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+    to-regex-range "^2.1.0"
 
 find-cache-dir@^1.0.0:
   version "1.0.0"
@@ -3807,25 +3703,14 @@ flatten@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
 
-flow-remove-types@^1.1.2:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-1.2.1.tgz#58e261bf8b842bd234c86cafb982a1213aff0edb"
-  dependencies:
-    babylon "^6.15.0"
-    vlq "^0.2.1"
-
 flush-write-stream@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
   dependencies:
     inherits "^2.0.1"
     readable-stream "^2.0.4"
 
-for-in@^0.1.3:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
-
-for-in@^1.0.1:
+for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
 
@@ -3835,12 +3720,6 @@ for-own@^0.1.4:
   dependencies:
     for-in "^1.0.1"
 
-for-own@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
-  dependencies:
-    for-in "^1.0.1"
-
 foreach@^2.0.5, foreach@~2.0.1:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
@@ -3878,23 +3757,27 @@ form-data@~2.1.1:
     mime-types "^2.1.12"
 
 form-data@~2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
   dependencies:
     asynckit "^0.4.0"
-    combined-stream "^1.0.5"
+    combined-stream "1.0.6"
     mime-types "^2.1.12"
 
-formatio@1.2.0, formatio@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
-  dependencies:
-    samsam "1.x"
+format@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
 
 fraction.js@4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.4.tgz#04e567110718adf7b52974a10434ab4c67a5183e"
 
+fragment-cache@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+  dependencies:
+    map-cache "^0.2.2"
+
 from2@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/from2/-/from2-1.3.0.tgz#88413baaa5f9a597cfde9221d86986cd3c061dfd"
@@ -3926,8 +3809,8 @@ fs-minipass@^1.2.5:
     minipass "^2.2.1"
 
 fs-readdir-recursive@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
 
 fs-vacuum@^1.2.10, fs-vacuum@~1.2.10, fs-vacuum@~1.2.9:
   version "1.2.10"
@@ -3950,14 +3833,14 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
-fsevents@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
+fsevents@^1.0.0, fsevents@^1.2.2:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
   dependencies:
-    nan "^2.3.0"
-    node-pre-gyp "^0.6.36"
+    nan "^2.9.2"
+    node-pre-gyp "^0.10.0"
 
-fstream-ignore@^1.0.0, fstream-ignore@^1.0.5:
+fstream-ignore@^1.0.0:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
   dependencies:
@@ -3972,7 +3855,7 @@ fstream-npm@~1.2.0:
     fstream-ignore "^1.0.0"
     inherits "2"
 
-fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2, fstream@~1.0.10:
+fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10:
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
   dependencies:
@@ -3981,16 +3864,16 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2, fstream@~1.0.10:
     mkdirp ">=0.5 0"
     rimraf "2"
 
-function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
+function-bind@^1.1.0, function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 
 function.prototype.name@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.3.tgz#0099ae5572e9dd6f03c97d023fd92bcc5e639eac"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
   dependencies:
     define-properties "^1.1.2"
-    function-bind "^1.1.0"
+    function-bind "^1.1.1"
     is-callable "^1.1.3"
 
 functional-red-black-tree@^1.0.1:
@@ -3998,8 +3881,8 @@ functional-red-black-tree@^1.0.1:
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
 
 fuse.js@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4"
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.1.tgz#6320cb94ce56ec9755c89ade775bcdbb0358d425"
 
 gauge@~2.6.0:
   version "2.6.0"
@@ -4039,8 +3922,8 @@ generate-object-property@^1.1.0:
     is-property "^1.0.0"
 
 generic-names@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-1.0.2.tgz#e25b7feceb5b5a8f28f5f972a7ccfe57e562adcd"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-1.0.3.tgz#2d786a121aee508876796939e8e3bff836c20917"
   dependencies:
     loader-utils "^0.2.16"
 
@@ -4061,12 +3944,6 @@ gentle-fs@^2.0.0, gentle-fs@^2.0.1:
     read-cmd-shim "^1.0.1"
     slide "^1.1.6"
 
-geojson-area@0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/geojson-area/-/geojson-area-0.1.0.tgz#d48d807082cfadf4a78df1349be50f38bf1894ae"
-  dependencies:
-    wgs84 "0.0.0"
-
 geojson-coords@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/geojson-coords/-/geojson-coords-0.0.0.tgz#dcdba86612da6be6b9511f16f38e90fcccb6dd75"
@@ -4095,29 +3972,18 @@ geojson-normalize@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/geojson-normalize/-/geojson-normalize-0.0.0.tgz#2dbc3678cd1b31b8179e876bda70cd120dde35c0"
 
-geojson-rewind@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/geojson-rewind/-/geojson-rewind-0.1.0.tgz#57022a054b196660d755354fe5d26684d90cd019"
-  dependencies:
-    concat-stream "~1.2.1"
-    geojson-area "0.1.0"
-    minimist "0.0.5"
-
 geojson-rewind@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/geojson-rewind/-/geojson-rewind-0.3.0.tgz#d5c35025fa708910e2da1a97fc23a2e2478a876a"
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/geojson-rewind/-/geojson-rewind-0.3.1.tgz#22240797c847cc2f0c1d313e4aa0c915afa7f29d"
   dependencies:
     "@mapbox/geojson-area" "0.2.2"
     concat-stream "~1.6.0"
     minimist "1.2.0"
+    sharkdown "^0.1.0"
 
-geojson-vt@^2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-2.4.0.tgz#3c1cf44493f35eb4d2c70c95da6550de66072c05"
-
-geojson-vt@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.0.0.tgz#a24cae5488ab4897e86ca0e4bf0d9760d628ae0a"
+geojson-vt@^3.1.0:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.1.3.tgz#f5185ea3b476832008bd824039bccdf14951f2fb"
 
 geolib@^2.0.24:
   version "2.0.24"
@@ -4139,6 +4005,10 @@ get-stream@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
 
+get-value@^2.0.3, get-value@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+
 getpass@^0.1.1:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -4163,8 +4033,8 @@ ghauth@3.0.0:
     xtend "~4.0.0"
 
 github-changes@^1.0.4:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/github-changes/-/github-changes-1.1.0.tgz#d4e8b98bc37b32fe272ae00171416d6cdf6bffee"
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/github-changes/-/github-changes-1.1.2.tgz#0411c1995dd73541a3b0b9cd341073613d02120b"
   dependencies:
     bluebird "1.0.3"
     ghauth "3.0.0"
@@ -4174,7 +4044,7 @@ github-changes@^1.0.4:
     moment-timezone "0.5.5"
     nomnom "1.6.2"
     parse-link-header "0.1.0"
-    semver "2.2.1"
+    semver "5.4.1"
 
 github-commit-stream@0.1.0:
   version "0.1.0"
@@ -4194,8 +4064,8 @@ gl-mat3@^1.0.0:
   resolved "https://registry.yarnpkg.com/gl-mat3/-/gl-mat3-1.0.0.tgz#89633219ca429379a16b9185d95d41713453b912"
 
 gl-mat4@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/gl-mat4/-/gl-mat4-1.1.4.tgz#1e895b55892e56a896867abd837d38f37a178086"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gl-mat4/-/gl-mat4-1.2.0.tgz#49d8a7636b70aa00819216635f4a3fd3f4669b26"
 
 gl-quat@^1.0.0:
   version "1.0.0"
@@ -4206,12 +4076,12 @@ gl-quat@^1.0.0:
     gl-vec4 "^1.0.0"
 
 gl-vec2@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/gl-vec2/-/gl-vec2-1.0.0.tgz#77fce6ae9612856d6c8b621cd261cd8281b9c637"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gl-vec2/-/gl-vec2-1.2.0.tgz#b0af95d2a582e3ad818446a1800093fc60b8b212"
 
 gl-vec3@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/gl-vec3/-/gl-vec3-1.0.3.tgz#110fd897d0729f6398307381567d0944941bf22b"
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/gl-vec3/-/gl-vec3-1.1.3.tgz#a47c62f918774a06cbed1b65bcd0288ecbb03826"
 
 gl-vec4@^1.0.0, gl-vec4@^1.0.1:
   version "1.0.1"
@@ -4250,6 +4120,13 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
+glob-parent@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+  dependencies:
+    is-glob "^3.1.0"
+    path-dirname "^1.0.0"
+
 glob-to-regexp@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
@@ -4300,8 +4177,8 @@ global-dirs@^0.1.0:
     ini "^1.3.4"
 
 globals@^11.0.1:
-  version "11.4.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-11.4.0.tgz#b85c793349561c16076a3c13549238a27945f1bc"
+  version "11.7.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
 
 globals@^9.18.0:
   version "9.18.0"
@@ -4318,40 +4195,11 @@ globby@^5.0.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-got@^3.2.0:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
-  dependencies:
-    duplexify "^3.2.0"
-    infinity-agent "^2.0.0"
-    is-redirect "^1.0.0"
-    is-stream "^1.0.0"
-    lowercase-keys "^1.0.0"
-    nested-error-stacks "^1.0.0"
-    object-assign "^3.0.0"
-    prepend-http "^1.0.0"
-    read-all-stream "^3.0.0"
-    timed-out "^2.0.0"
-
-got@^5.0.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
+good-listener@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
   dependencies:
-    create-error-class "^3.0.1"
-    duplexer2 "^0.1.4"
-    is-redirect "^1.0.0"
-    is-retry-allowed "^1.0.0"
-    is-stream "^1.0.0"
-    lowercase-keys "^1.0.0"
-    node-status-codes "^1.0.0"
-    object-assign "^4.0.1"
-    parse-json "^2.1.0"
-    pinkie-promise "^2.0.0"
-    read-all-stream "^3.0.0"
-    readable-stream "^2.0.5"
-    timed-out "^3.0.0"
-    unzip-response "^1.0.2"
-    url-parse-lax "^1.0.0"
+    delegate "^3.1.2"
 
 got@^6.7.1:
   version "6.7.1"
@@ -4377,12 +4225,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6,
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
 
-graphlib@^2.1.1:
-  version "2.1.5"
-  resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.5.tgz#6afe1afcc5148555ec799e499056795bd6938c87"
-  dependencies:
-    lodash "^4.11.1"
-
 gray-matter@^3.0.8:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-3.1.1.tgz#101f80d9e69eeca6765cdce437705b18f40876ac"
@@ -4405,8 +4247,8 @@ hammerjs@^2.0.8:
   resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
 
 handlebars@^4.0.1, handlebars@^4.0.3:
-  version "4.0.10"
-  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
   dependencies:
     async "^1.4.0"
     optimist "^0.6.1"
@@ -4467,27 +4309,46 @@ has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
 
+has-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
 has-unicode@^2.0.0, has-unicode@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
 
-has@^1.0.0, has@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+has-value@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
   dependencies:
-    function-bind "^1.0.2"
+    get-value "^2.0.3"
+    has-values "^0.1.4"
+    isobject "^2.0.0"
 
-hasbin@^1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/hasbin/-/hasbin-1.2.3.tgz#78c5926893c80215c2b568ae1fd3fcab7a2696b0"
+has-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
   dependencies:
-    async "~1.5"
+    get-value "^2.0.6"
+    has-values "^1.0.0"
+    isobject "^3.0.0"
 
-hash-base@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+has-values@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+
+has-values@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
   dependencies:
-    inherits "^2.0.1"
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+has@^1.0.1, has@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  dependencies:
+    function-bind "^1.1.1"
 
 hash-base@^3.0.0:
   version "3.0.4"
@@ -4497,20 +4358,25 @@ hash-base@^3.0.0:
     safe-buffer "^5.0.1"
 
 hash.js@^1.0.0, hash.js@^1.0.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.4.tgz#8b50e1f35d51bd01e5ed9ece4dbe3549ccfa0a3c"
   dependencies:
     inherits "^2.0.3"
     minimalistic-assert "^1.0.0"
 
-hawk@3.1.3, hawk@~3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+hast-util-parse-selector@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.1.1.tgz#fc06985272f5d204a25187f002bb916521e74f3a"
+
+hastscript@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-3.1.0.tgz#66628ba6d7f1ad07d9277dd09028aba7f4934599"
   dependencies:
-    boom "2.x.x"
-    cryptiles "2.x.x"
-    hoek "2.x.x"
-    sntp "1.x.x"
+    camelcase "^3.0.0"
+    comma-separated-tokens "^1.0.0"
+    hast-util-parse-selector "^2.0.0"
+    property-information "^3.0.0"
+    space-separated-tokens "^1.0.0"
 
 hawk@~0.13.0:
   version "0.13.1"
@@ -4521,14 +4387,14 @@ hawk@~0.13.0:
     hoek "0.8.x"
     sntp "0.2.x"
 
-hawk@~6.0.2:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
+hawk@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
   dependencies:
-    boom "4.x.x"
-    cryptiles "3.x.x"
-    hoek "4.x.x"
-    sntp "2.x.x"
+    boom "2.x.x"
+    cryptiles "2.x.x"
+    hoek "2.x.x"
+    sntp "1.x.x"
 
 he@1.1.1:
   version "1.1.1"
@@ -4558,21 +4424,13 @@ hoek@2.x.x:
   version "2.16.3"
   resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
 
-hoek@4.x.x:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
-
 hoist-non-react-statics@^1.0.0, hoist-non-react-statics@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
 
-hoist-non-react-statics@^2.1.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
-
-hoist-non-react-statics@^2.2.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
+hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.5.0:
+  version "2.5.5"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
 
 home-or-tmp@^2.0.0:
   version "2.0.0"
@@ -4581,11 +4439,7 @@ home-or-tmp@^2.0.0:
     os-homedir "^1.0.0"
     os-tmpdir "^1.0.1"
 
-hosted-git-info@^2.1.4:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
-
-hosted-git-info@^2.1.5, hosted-git-info@^2.4.2, hosted-git-info@^2.5.0, hosted-git-info@^2.6.0:
+hosted-git-info@^2.1.4, hosted-git-info@^2.1.5, hosted-git-info@^2.4.2, hosted-git-info@^2.6.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
 
@@ -4598,18 +4452,18 @@ html-comment-regex@^1.1.0:
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
 
 html-element-attributes@^1.0.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-1.3.0.tgz#f06ebdfce22de979db82020265cac541fb17d4fc"
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-1.3.1.tgz#9fa6a2e37e6b61790a303e87ddbbb9746e8c035f"
 
 html-encoding-sniffer@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
   dependencies:
     whatwg-encoding "^1.0.1"
 
 html-tag-names@^1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.2.tgz#f65168964c5a9c82675efda882875dcb2a875c22"
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.3.tgz#f81f75e59d626cb8a958a19e58f90c1d69707b82"
 
 htmlparser2@^3.9.1:
   version "3.9.2"
@@ -4622,11 +4476,11 @@ htmlparser2@^3.9.1:
     inherits "^2.0.1"
     readable-stream "^2.0.2"
 
-http-cache-semantics@^3.8.0:
+http-cache-semantics@^3.8.1:
   version "3.8.1"
   resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
 
-http-proxy-agent@^2.0.0:
+http-proxy-agent@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
   dependencies:
@@ -4657,11 +4511,11 @@ http-signature@~1.2.0:
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
-https-browserify@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+https-browserify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
 
-https-proxy-agent@^2.1.0:
+https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
   dependencies:
@@ -4689,14 +4543,16 @@ iconv-lite@0.2:
   version "0.2.11"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8"
 
-iconv-lite@0.4.13:
-  version "0.4.13"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
-
-iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+iconv-lite@0.4.19:
   version "0.4.19"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
 
+iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+  version "0.4.23"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
 icss-replace-symbols@^1.0.2, icss-replace-symbols@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -4714,13 +4570,17 @@ icss-utils@^3.0.1:
     postcss "^6.0.2"
 
 ieee754@^1.1.4, ieee754@^1.1.6:
-  version "1.1.8"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
 
 iferr@^0.1.5, iferr@~0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
 
+iferr@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/iferr/-/iferr-1.0.2.tgz#e9fde49a9da06dc4a4194c6c9ed6d08305037a6d"
+
 ignore-styles@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/ignore-styles/-/ignore-styles-5.0.1.tgz#b49ef2274bdafcd8a4880a966bfe38d1a0bf4671"
@@ -4732,14 +4592,14 @@ ignore-walk@^3.0.1:
     minimatch "^3.0.4"
 
 ignore@^3.3.3:
-  version "3.3.7"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
 
 image-size@~0.5.0:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
 
-immutable@*, immutable@^3.8.1, immutable@^3.8.2:
+immutable@^3.8.1, immutable@^3.8.2:
   version "3.8.2"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
 
@@ -4766,10 +4626,6 @@ indexof@0.0.1, indexof@~0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
 
-infinity-agent@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216"
-
 inflight@^1.0.4, inflight@~1.0.5, inflight@~1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -4777,7 +4633,7 @@ inflight@^1.0.4, inflight@~1.0.5, inflight@~1.0.6:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 
@@ -4785,14 +4641,10 @@ inherits@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
 
-ini@1.x.x, ini@^1.3.4, ini@^1.3.5, ini@~1.3.4:
+ini@^1.3.4, ini@^1.3.5, ini@~1.3.0, ini@~1.3.4:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
-ini@~1.3.0:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
-
 init-package-json@^1.10.3:
   version "1.10.3"
   resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe"
@@ -4826,24 +4678,6 @@ inline-style-prefixer@^3.0.1, inline-style-prefixer@^3.0.6:
     bowser "^1.7.3"
     css-in-js-utils "^2.0.0"
 
-inquirer@1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.0.3.tgz#ebe3a0948571bcc46ccccbe2f9bcec251e984bd0"
-  dependencies:
-    ansi-escapes "^1.1.0"
-    chalk "^1.0.0"
-    cli-cursor "^1.0.1"
-    cli-width "^2.0.0"
-    figures "^1.3.5"
-    lodash "^4.3.0"
-    mute-stream "0.0.6"
-    pinkie-promise "^2.0.0"
-    run-async "^2.2.0"
-    rx "^4.1.0"
-    string-width "^1.0.1"
-    strip-ansi "^3.0.0"
-    through "^2.3.6"
-
 inquirer@^3.0.6:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
@@ -4864,12 +4698,12 @@ inquirer@^3.0.6:
     through "^2.3.6"
 
 interpret@^1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
 
 invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   dependencies:
     loose-envify "^1.0.0"
 
@@ -4877,7 +4711,11 @@ invert-kv@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
 
-ip@^1.1.4:
+ip-regex@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+
+ip@^1.1.4, ip@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
 
@@ -4885,6 +4723,18 @@ is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
 
+is-accessor-descriptor@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+  dependencies:
+    kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+  dependencies:
+    kind-of "^6.0.0"
+
 is-alphabetical@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.2.tgz#1fa6e49213cb7885b75d15862fb3f3d96c884f41"
@@ -4906,14 +4756,10 @@ is-binary-path@^1.0.0:
   dependencies:
     binary-extensions "^1.0.0"
 
-is-buffer@^1.0.2, is-buffer@^1.1.4:
+is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
 
-is-buffer@^1.1.5, is-buffer@~1.1.1:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
-
 is-builtin-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
@@ -4930,11 +4776,23 @@ is-ci@^1.0.10:
   dependencies:
     ci-info "^1.0.0"
 
-is-cidr@~1.0.0:
+is-cidr@^2.0.5:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-2.0.6.tgz#4b01c9693d8e18399dacd18a4f3d60ea5871ac60"
+  dependencies:
+    cidr-regex "^2.0.8"
+
+is-data-descriptor@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+  dependencies:
+    kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc"
+  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
   dependencies:
-    cidr-regex "1.0.6"
+    kind-of "^6.0.0"
 
 is-date-object@^1.0.1:
   version "1.0.1"
@@ -4944,6 +4802,22 @@ is-decimal@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.2.tgz#894662d6a8709d307f3a276ca4339c8fa5dff0ff"
 
+is-descriptor@^0.1.0:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+  dependencies:
+    is-accessor-descriptor "^0.1.6"
+    is-data-descriptor "^0.1.4"
+    kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+  dependencies:
+    is-accessor-descriptor "^1.0.0"
+    is-data-descriptor "^1.0.0"
+    kind-of "^6.0.2"
+
 is-dotfile@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -4958,10 +4832,20 @@ is-extendable@^0.1.0, is-extendable@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
 
+is-extendable@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  dependencies:
+    is-plain-object "^2.0.4"
+
 is-extglob@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
 
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+
 is-finite@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
@@ -4984,9 +4868,21 @@ is-glob@^2.0.0, is-glob@^2.0.1:
   dependencies:
     is-extglob "^1.0.0"
 
-is-hexadecimal@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835"
+is-glob@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+  dependencies:
+    is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-hexadecimal@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835"
 
 is-installed-globally@^0.1.0:
   version "0.1.0"
@@ -4995,12 +4891,17 @@ is-installed-globally@^0.1.0:
     global-dirs "^0.1.0"
     is-path-inside "^1.0.0"
 
+is-my-ip-valid@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
+
 is-my-json-valid@^2.12.4:
-  version "2.16.1"
-  resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11"
+  version "2.17.2"
+  resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
   dependencies:
     generate-function "^2.0.0"
     generate-object-property "^1.1.0"
+    is-my-ip-valid "^1.0.0"
     jsonpointer "^4.0.0"
     xtend "^4.0.0"
 
@@ -5020,6 +4921,10 @@ is-number@^3.0.0:
   dependencies:
     kind-of "^3.0.2"
 
+is-number@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
 is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -5028,19 +4933,25 @@ is-object@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7"
 
+is-odd@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
+  dependencies:
+    is-number "^4.0.0"
+
 is-path-cwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
 
 is-path-in-cwd@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
   dependencies:
     is-path-inside "^1.0.0"
 
 is-path-inside@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
   dependencies:
     path-is-inside "^1.0.1"
 
@@ -5048,7 +4959,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
 
-is-plain-object@^2.0.1:
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
   dependencies:
@@ -5081,10 +4992,8 @@ is-regex@^1.0.4:
     has "^1.0.1"
 
 is-resolvable@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
-  dependencies:
-    tryit "^1.0.1"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
 
 is-retina@^1.0.3:
   version "1.0.3"
@@ -5116,14 +5025,14 @@ is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
 
-is-utf8@^0.2.0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
-
 is-whitespace-character@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed"
 
+is-windows@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+
 is-word-character@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.2.tgz#46a5dac3f2a1840898b91e576cd40d493f3ae553"
@@ -5150,7 +5059,7 @@ isobject@^2.0.0:
   dependencies:
     isarray "1.0.0"
 
-isobject@^3.0.1:
+isobject@^3.0.0, isobject@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
 
@@ -5166,65 +5075,66 @@ isstream@~0.1.2:
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
 istanbul-api@^1.0.0-alpha:
-  version "1.1.14"
-  resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680"
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.1.tgz#4c3b05d18c0016d1022e079b98dc82c40f488954"
   dependencies:
     async "^2.1.4"
+    compare-versions "^3.1.0"
     fileset "^2.0.2"
-    istanbul-lib-coverage "^1.1.1"
-    istanbul-lib-hook "^1.0.7"
-    istanbul-lib-instrument "^1.8.0"
-    istanbul-lib-report "^1.1.1"
-    istanbul-lib-source-maps "^1.2.1"
-    istanbul-reports "^1.1.2"
+    istanbul-lib-coverage "^1.2.0"
+    istanbul-lib-hook "^1.2.0"
+    istanbul-lib-instrument "^1.10.1"
+    istanbul-lib-report "^1.1.4"
+    istanbul-lib-source-maps "^1.2.4"
+    istanbul-reports "^1.3.0"
     js-yaml "^3.7.0"
     mkdirp "^0.5.1"
     once "^1.4.0"
 
-istanbul-lib-coverage@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+istanbul-lib-coverage@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
 
-istanbul-lib-hook@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc"
+istanbul-lib-hook@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz#f614ec45287b2a8fc4f07f5660af787575601805"
   dependencies:
-    append-transform "^0.4.0"
+    append-transform "^1.0.0"
 
-istanbul-lib-instrument@^1.8.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.8.0.tgz#66f6c9421cc9ec4704f76f2db084ba9078a2b532"
+istanbul-lib-instrument@^1.10.1:
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
   dependencies:
     babel-generator "^6.18.0"
     babel-template "^6.16.0"
     babel-traverse "^6.18.0"
     babel-types "^6.18.0"
     babylon "^6.18.0"
-    istanbul-lib-coverage "^1.1.1"
+    istanbul-lib-coverage "^1.2.0"
     semver "^5.3.0"
 
-istanbul-lib-report@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9"
+istanbul-lib-report@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz#e886cdf505c4ebbd8e099e4396a90d0a28e2acb5"
   dependencies:
-    istanbul-lib-coverage "^1.1.1"
+    istanbul-lib-coverage "^1.2.0"
     mkdirp "^0.5.1"
     path-parse "^1.0.5"
     supports-color "^3.1.2"
 
-istanbul-lib-source-maps@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c"
+istanbul-lib-source-maps@^1.2.4:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz#ffe6be4e7ab86d3603e4290d54990b14506fc9b1"
   dependencies:
-    debug "^2.6.3"
-    istanbul-lib-coverage "^1.1.1"
+    debug "^3.1.0"
+    istanbul-lib-coverage "^1.2.0"
     mkdirp "^0.5.1"
     rimraf "^2.6.1"
     source-map "^0.5.3"
 
-istanbul-reports@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.2.tgz#0fb2e3f6aa9922bd3ce45d05d8ab4d5e8e07bd4f"
+istanbul-reports@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.3.0.tgz#2f322e81e1d9520767597dca3c20a0cce89a3554"
   dependencies:
     handlebars "^4.0.3"
 
@@ -5262,12 +5172,12 @@ jquery@3.1.1:
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.1.1.tgz#347c1c21c7e004115e0a4da32cece041fad3c8a3"
 
 jquery@>=1.7:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
 
 js-base64@^2.1.9:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
+  version "2.4.5"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92"
 
 js-search@^1.3.1:
   version "1.4.2"
@@ -5277,16 +5187,9 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
 
-js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.7.0:
-  version "3.10.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
-  dependencies:
-    argparse "^1.0.7"
-    esprima "^4.0.0"
-
-js-yaml@^3.5.3, js-yaml@^3.6.1, js-yaml@^3.9.1:
-  version "3.11.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
+js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.6.1, js-yaml@^3.7.0, js-yaml@^3.9.1:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
   dependencies:
     argparse "^1.0.7"
     esprima "^4.0.0"
@@ -5338,14 +5241,10 @@ json-loader@^0.5.4:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
 
-json-parse-better-errors@^1.0.0:
+json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
 
-json-parse-better-errors@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a"
-
 json-parse-helpfulerror@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz#13f14ce02eed4e981297b64eb9e3b932e2dd13dc"
@@ -5356,6 +5255,10 @@ json-schema-traverse@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
 
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+
 json-schema@0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -5396,13 +5299,6 @@ jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
 
-jsonlint-lines-primitives@~1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/jsonlint-lines-primitives/-/jsonlint-lines-primitives-1.6.0.tgz#bb89f60c8b9b612fd913ddaa236649b840d86611"
-  dependencies:
-    JSV ">= 4.0.x"
-    nomnom ">= 1.5.x"
-
 jsonparse@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -5424,31 +5320,25 @@ jsx-ast-utils@^1.4.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
 
-jsx-ast-utils@^2.0.0:
+jsx-ast-utils@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
   dependencies:
     array-includes "^3.0.3"
 
-just-extend@^1.1.22:
-  version "1.1.22"
-  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.22.tgz#3330af756cab6a542700c64b2e4e4aa062d52fff"
+just-extend@^1.1.27:
+  version "1.1.27"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905"
 
 kdbush@^1.0.0, kdbush@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-1.0.1.tgz#3cbd03e9dead9c0f6f66ccdb96450e5cecc640e0"
 
 keycode@^2.1.2:
-  version "2.1.9"
-  resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"
-
-kind-of@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
-  dependencies:
-    is-buffer "^1.0.2"
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
 
-kind-of@^3.0.2, kind-of@^3.2.2:
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
   dependencies:
@@ -5460,38 +5350,26 @@ kind-of@^4.0.0:
   dependencies:
     is-buffer "^1.1.5"
 
-kind-of@^5.0.2:
+kind-of@^5.0.0, kind-of@^5.0.2:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
 
+kind-of@^6.0.0, kind-of@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
 klaw@^1.0.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
   optionalDependencies:
     graceful-fs "^4.1.9"
 
-latest-version@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb"
-  dependencies:
-    package-json "^1.0.0"
-
-latest-version@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b"
-  dependencies:
-    package-json "^2.0.0"
-
 latest-version@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
   dependencies:
     package-json "^4.0.0"
 
-lazy-cache@^0.2.3:
-  version "0.2.7"
-  resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
-
 lazy-cache@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
@@ -5507,16 +5385,16 @@ lcid@^1.0.0:
     invert-kv "^1.0.0"
 
 less-loader@^4.0.3:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.0.5.tgz#ae155a7406cac6acd293d785587fcff0f478c4dd"
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.1.0.tgz#2c1352c5b09a4f84101490274fd51674de41363e"
   dependencies:
     clone "^2.1.1"
     loader-utils "^1.1.0"
-    pify "^2.3.0"
+    pify "^3.0.0"
 
 less@^2.6.1:
-  version "2.7.2"
-  resolved "https://registry.yarnpkg.com/less/-/less-2.7.2.tgz#368d6cc73e1fb03981183280918743c5dcf9b3df"
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/less/-/less-2.7.3.tgz#cc1260f51c900a9ec0d91fb6998139e02507b63b"
   optionalDependencies:
     errno "^0.1.1"
     graceful-fs "^4.1.2"
@@ -5524,7 +5402,7 @@ less@^2.6.1:
     mime "^1.2.11"
     mkdirp "^0.5.0"
     promise "^7.1.1"
-    request "^2.72.0"
+    request "2.81.0"
     source-map "^0.5.3"
 
 levn@^0.3.0, levn@~0.3.0:
@@ -5534,25 +5412,32 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
-libcipm@^1.6.0:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/libcipm/-/libcipm-1.6.2.tgz#5a9d83b8606b9733cfff016ad9b37d3b8198ae09"
+libcipm@^1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/libcipm/-/libcipm-1.6.3.tgz#dc4052d710941547782d85bbdb3c77eedec733ff"
   dependencies:
-    bin-links "^1.1.0"
+    bin-links "^1.1.2"
     bluebird "^3.5.1"
     find-npm-prefix "^1.0.2"
     graceful-fs "^4.1.11"
-    lock-verify "^2.0.0"
-    npm-lifecycle "^2.0.0"
+    lock-verify "^2.0.2"
+    npm-lifecycle "^2.0.3"
     npm-logical-tree "^1.2.1"
-    npm-package-arg "^6.0.0"
-    pacote "^7.5.1"
+    npm-package-arg "^6.1.0"
+    pacote "^8.1.6"
     protoduck "^5.0.0"
-    read-package-json "^2.0.12"
+    read-package-json "^2.0.13"
     rimraf "^2.6.2"
-    worker-farm "^1.5.4"
+    worker-farm "^1.6.0"
+
+libnpmhook@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/libnpmhook/-/libnpmhook-4.0.1.tgz#63641654de772cbeb96a88527a7fd5456ec3c2d7"
+  dependencies:
+    figgy-pudding "^3.1.0"
+    npm-registry-fetch "^3.0.0"
 
-libnpx@^10.0.1:
+libnpx@^10.2.0:
   version "10.2.0"
   resolved "https://registry.yarnpkg.com/libnpx/-/libnpx-10.2.0.tgz#1bf4a1c9f36081f64935eb014041da10855e3102"
   dependencies:
@@ -5565,16 +5450,6 @@ libnpx@^10.0.1:
     y18n "^4.0.0"
     yargs "^11.0.0"
 
-load-json-file@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^2.2.0"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-    strip-bom "^2.0.0"
-
 load-json-file@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
@@ -5612,26 +5487,22 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
-lock-verify@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.1.tgz#6d671eea60b459c6048b3b26b62959208be67682"
+lock-verify@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.2.tgz#148e4f85974915c9e3c34d694b7de9ecb18ee7a8"
   dependencies:
-    npm-package-arg "^5.1.2"
+    npm-package-arg "^5.1.2 || 6"
     semver "^5.4.1"
 
-lockfile@~1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79"
-
-lockfile@~1.0.3:
+lockfile@^1.0.4, lockfile@~1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609"
   dependencies:
     signal-exit "^3.0.2"
 
-lodash-es@^4.2.0, lodash-es@^4.2.1:
-  version "4.17.4"
-  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
+lodash-es@^4.17.5, lodash-es@^4.2.1:
+  version "4.17.10"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
 
 lodash._baseassign@^3.0.0:
   version "3.2.0"
@@ -5671,10 +5542,6 @@ lodash._root@~3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
 
-lodash.assign@^4.0.3, lodash.assign@^4.0.6:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
-
 lodash.assignin@^4.0.9:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
@@ -5687,14 +5554,10 @@ lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
 
-lodash.clonedeep@^4.3.0, lodash.clonedeep@^4.3.1, lodash.clonedeep@~4.5.0:
+lodash.clonedeep@~4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
 
-lodash.cond@^4.3.0:
-  version "4.5.2"
-  resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
-
 lodash.create@3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
@@ -5703,14 +5566,14 @@ lodash.create@3.1.1:
     lodash._basecreate "^3.0.0"
     lodash._isiterateecall "^3.0.0"
 
+lodash.debounce@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+
 lodash.defaults@^4.0.1:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
 
-lodash.defaultsdeep@^4.3.1:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz#bec1024f85b1bd96cbea405b23c14ad6443a6f81"
-
 lodash.filter@^4.4.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
@@ -5719,10 +5582,6 @@ lodash.flatten@^4.2.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
 
-lodash.flattendeep@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
-
 lodash.foreach@^4.3.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
@@ -5743,6 +5602,10 @@ 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"
 
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
 lodash.keys@^3.0.0:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -5760,12 +5623,8 @@ lodash.memoize@^4.1.2:
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
 
 lodash.merge@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
-
-lodash.mergewith@^4.3.1:
   version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
 
 lodash.pick@^4.2.1:
   version "4.4.0"
@@ -5807,21 +5666,13 @@ lodash@3.x, lodash@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.0.1, lodash@^4.0.8, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
-  version "4.17.4"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-
-lodash@^4.11.1, lodash@^4.15.0:
-  version "4.17.5"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+"lodash@>=3.5 <5", lodash@^4.0.1, lodash@^4.0.8, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
+  version "4.17.10"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
-lolex@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
-
-lolex@^2.1.3:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.1.3.tgz#53f893bbe88c80378156240e127126b905c83087"
+lolex@^2.2.0, lolex@^2.3.2:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.0.tgz#9c087a69ec440e39d3f796767cf1b2cdc43d5ea5"
 
 longest@^1.0.1:
   version "1.0.1"
@@ -5834,124 +5685,107 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3
     js-tokens "^3.0.0"
 
 lowercase-keys@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
 
 lowlight@~1.9.1:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.1.tgz#ed7c3dffc36f8c1f263735c0fe0c907847c11250"
+  version "1.9.2"
+  resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.2.tgz#0b9127e3cec2c3021b7795dd81005c709a42fdd1"
   dependencies:
+    fault "^1.0.2"
     highlight.js "~9.12.0"
 
-lru-cache@^4.0.0, lru-cache@~4.1.1:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f"
-  dependencies:
-    pseudomap "^1.0.2"
-    yallist "^2.1.2"
-
-lru-cache@^4.0.1, lru-cache@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
   dependencies:
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
-luma.gl@^5.1.4:
-  version "5.1.6"
-  resolved "https://registry.yarnpkg.com/luma.gl/-/luma.gl-5.1.6.tgz#36ce71dae2f25dd10a5e4ea72fbd1829d1d8da41"
+luma.gl@^5.1.4, luma.gl@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/luma.gl/-/luma.gl-5.3.0.tgz#a93b2f34489d8230eb6d8c871335800d9b83ee67"
   dependencies:
-    math.gl "^1.0.0"
+    math.gl "^1.1.0"
+    probe.gl "^1.0.0"
     seer "^0.2.4"
     webgl-debug "^2.0.0"
 
-macaddress@^0.2.8:
-  version "0.2.8"
-  resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
-
-magic-string@^0.14.0:
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.14.0.tgz#57224aef1701caeed273b17a39a956e72b172462"
+magic-string@^0.22.4:
+  version "0.22.5"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
   dependencies:
-    vlq "^0.2.1"
+    vlq "^0.2.2"
 
 make-dir@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
   dependencies:
-    pify "^2.3.0"
+    pify "^3.0.0"
 
-make-fetch-happen@^2.5.0, make-fetch-happen@^2.6.0:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38"
+"make-fetch-happen@^2.5.0 || 3 || 4", make-fetch-happen@^4.0.0, make-fetch-happen@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz#141497cb878f243ba93136c83d8aba12c216c083"
+  dependencies:
+    agentkeepalive "^3.4.1"
+    cacache "^11.0.1"
+    http-cache-semantics "^3.8.1"
+    http-proxy-agent "^2.1.0"
+    https-proxy-agent "^2.2.1"
+    lru-cache "^4.1.2"
+    mississippi "^3.0.0"
+    node-fetch-npm "^2.0.2"
+    promise-retry "^1.1.1"
+    socks-proxy-agent "^4.0.0"
+    ssri "^6.0.0"
+
+make-fetch-happen@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-3.0.0.tgz#7b661d2372fc4710ab5cc8e1fa3c290eea69a961"
   dependencies:
-    agentkeepalive "^3.3.0"
-    cacache "^10.0.0"
-    http-cache-semantics "^3.8.0"
-    http-proxy-agent "^2.0.0"
-    https-proxy-agent "^2.1.0"
-    lru-cache "^4.1.1"
-    mississippi "^1.2.0"
+    agentkeepalive "^3.4.1"
+    cacache "^10.0.4"
+    http-cache-semantics "^3.8.1"
+    http-proxy-agent "^2.1.0"
+    https-proxy-agent "^2.2.0"
+    lru-cache "^4.1.2"
+    mississippi "^3.0.0"
     node-fetch-npm "^2.0.2"
     promise-retry "^1.1.1"
     socks-proxy-agent "^3.0.1"
-    ssri "^5.0.0"
+    ssri "^5.2.4"
 
-mapbox-gl-supported@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/mapbox-gl-supported/-/mapbox-gl-supported-1.2.0.tgz#cbd34df894206cadda9a33c8d9a4609f26bb1989"
+map-cache@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
 
-mapbox-gl@0.38.0:
-  version "0.38.0"
-  resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-0.38.0.tgz#64731bb55eabdaa520270815175fcf31e5a3cd80"
+map-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
   dependencies:
-    "@mapbox/gl-matrix" "^0.0.1"
-    "@mapbox/shelf-pack" "^3.0.0"
-    "@mapbox/unitbezier" "^0.0.0"
-    "@mapbox/whoots-js" "^3.0.0"
-    brfs "^1.4.0"
-    bubleify "^0.7.0"
-    earcut "^2.0.3"
-    geojson-rewind "^0.1.0"
-    geojson-vt "^2.4.0"
-    grid-index "^1.0.0"
-    mapbox-gl-supported "^1.2.0"
-    package-json-versionify "^1.0.2"
-    pbf "^1.3.2"
-    point-geometry "^0.0.0"
-    quickselect "^1.0.0"
-    supercluster "^2.0.1"
-    through2 "^2.0.3"
-    tinyqueue "^1.1.0"
-    unassertify "^2.0.0"
-    unflowify "^1.0.0"
-    vector-tile "^1.3.0"
-    vt-pbf "^2.0.2"
-    webworkify "^1.4.0"
+    object-visit "^1.0.0"
 
-mapbox-gl@^0.44.2:
-  version "0.44.2"
-  resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-0.44.2.tgz#8c118ba8c5c15b054272644f30877309db0f8ee2"
+mapbox-gl@0.45, mapbox-gl@^0.45.0:
+  version "0.45.0"
+  resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-0.45.0.tgz#af71cc824f0d7e51ccd5c505eaae411bc0910ccd"
   dependencies:
     "@mapbox/gl-matrix" "^0.0.1"
-    "@mapbox/mapbox-gl-supported" "^1.3.0"
+    "@mapbox/jsonlint-lines-primitives" "^2.0.1"
+    "@mapbox/mapbox-gl-supported" "^1.3.1"
     "@mapbox/point-geometry" "^0.1.0"
     "@mapbox/shelf-pack" "^3.1.0"
     "@mapbox/tiny-sdf" "^1.1.0"
     "@mapbox/unitbezier" "^0.0.0"
-    "@mapbox/vector-tile" "^1.3.0"
+    "@mapbox/vector-tile" "^1.3.1"
     "@mapbox/whoots-js" "^3.0.0"
-    brfs "^1.4.0"
-    bubleify "^0.7.0"
+    brfs "^1.4.4"
     csscolorparser "~1.0.2"
     earcut "^2.1.3"
     geojson-rewind "^0.3.0"
-    geojson-vt "^3.0.0"
+    geojson-vt "^3.1.0"
     gray-matter "^3.0.8"
     grid-index "^1.0.0"
-    jsonlint-lines-primitives "~1.6.0"
     minimist "0.0.8"
-    package-json-versionify "^1.0.2"
     pbf "^3.0.5"
     quickselect "^1.0.0"
     rw "^1.3.3"
@@ -5960,27 +5794,29 @@ mapbox-gl@^0.44.2:
     supercluster "^2.3.0"
     through2 "^2.0.3"
     tinyqueue "^1.1.0"
-    unassertify "^2.0.0"
-    unflowify "^1.0.0"
     vt-pbf "^3.0.1"
-    webworkify "^1.5.0"
 
 markdown-escapes@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.2.tgz#e639cbde7b99c841c0bacc8a07982873b46d2122"
 
 material-colors@^1.2.1:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.5.tgz#5292593e6754cb1bcc2b98030e4e0d6a3afc9ea1"
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
 
 math-expression-evaluator@^1.2.14:
   version "1.2.17"
   resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
 
-math.gl@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/math.gl/-/math.gl-1.0.3.tgz#89e00cb4452b997a71e77c79bc4c26e732d1d4b8"
+math-random@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
+
+math.gl@^1.1.0, math.gl@^1.1.3, math.gl@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/math.gl/-/math.gl-1.2.1.tgz#3c7da0ae4f3383116c24bc183533cc4d7b8065a9"
   dependencies:
+    gl-mat3 "^1.0.0"
     gl-mat4 "^1.1.4"
     gl-quat "^1.0.0"
     gl-vec2 "^1.0.0"
@@ -5988,8 +5824,8 @@ math.gl@^1.0.0:
     gl-vec4 "^1.0.1"
 
 mathfn@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/mathfn/-/mathfn-1.0.0.tgz#eaa60f2aa82d67c7949018748b74b70603880fae"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mathfn/-/mathfn-1.0.1.tgz#650a0b183b0102debe94e42a807dc6b4d2cc57bd"
 
 mathjs@^3.20.2:
   version "3.20.2"
@@ -6036,6 +5872,12 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
+merge-source-map@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f"
+  dependencies:
+    source-map "^0.5.6"
+
 micromatch@^2.1.5:
   version "2.3.11"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@@ -6054,6 +5896,24 @@ micromatch@^2.1.5:
     parse-glob "^3.0.4"
     regex-cache "^0.4.2"
 
+micromatch@^3.1.4:
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    braces "^2.3.1"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    extglob "^2.0.4"
+    fragment-cache "^0.2.1"
+    kind-of "^6.0.2"
+    nanomatch "^1.2.9"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.2"
+
 miller-rabin@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -6061,31 +5921,17 @@ miller-rabin@^4.0.0:
     bn.js "^4.0.0"
     brorand "^1.0.1"
 
-mime-db@~1.30.0:
-  version "1.30.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
-
 mime-db@~1.33.0:
   version "1.33.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
 
-mime-types@^2.1.11:
+mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
   version "2.1.18"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
   dependencies:
     mime-db "~1.33.0"
 
-mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
-  version "2.1.17"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
-  dependencies:
-    mime-db "~1.30.0"
-
-mime@^1.2.11:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
-
-mime@^1.4.1:
+mime@^1.2.11, mime@^1.4.1:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
 
@@ -6094,18 +5940,18 @@ mime@~1.2.2, mime@~1.2.9:
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10"
 
 mimic-fn@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
 
 minimalistic-assert@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
 
 minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
 
-"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
@@ -6119,7 +5965,7 @@ minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
-minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
+minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
@@ -6127,11 +5973,11 @@ minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
 
-minipass@^2.2.1, minipass@^2.2.4:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40"
+minipass@^2.2.1, minipass@^2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
   dependencies:
-    safe-buffer "^5.1.1"
+    safe-buffer "^5.1.2"
     yallist "^3.0.0"
 
 minizlib@^1.1.0:
@@ -6140,36 +5986,6 @@ minizlib@^1.1.0:
   dependencies:
     minipass "^2.2.1"
 
-mississippi@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e"
-  dependencies:
-    concat-stream "^1.5.0"
-    duplexify "^3.4.2"
-    end-of-stream "^1.1.0"
-    flush-write-stream "^1.0.0"
-    from2 "^2.1.0"
-    parallel-transform "^1.1.0"
-    pump "^1.0.0"
-    pumpify "^1.3.3"
-    stream-each "^1.1.0"
-    through2 "^2.0.0"
-
-mississippi@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.0.tgz#d201583eb12327e3c5c1642a404a9cacf94e34f5"
-  dependencies:
-    concat-stream "^1.5.0"
-    duplexify "^3.4.2"
-    end-of-stream "^1.1.0"
-    flush-write-stream "^1.0.0"
-    from2 "^2.1.0"
-    parallel-transform "^1.1.0"
-    pump "^1.0.0"
-    pumpify "^1.3.3"
-    stream-each "^1.1.0"
-    through2 "^2.0.0"
-
 mississippi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -6200,16 +6016,16 @@ mississippi@^3.0.0:
     stream-each "^1.1.0"
     through2 "^2.0.0"
 
-mixin-object@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
+mixin-deep@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
   dependencies:
-    for-in "^0.1.3"
-    is-extendable "^0.1.1"
+    for-in "^1.0.2"
+    is-extendable "^1.0.1"
 
-mjolnir.js@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/mjolnir.js/-/mjolnir.js-1.0.0.tgz#881eaac896f2c2693bc39f5127b4fabe99ff56fb"
+mjolnir.js@^1.0.0, mjolnir.js@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/mjolnir.js/-/mjolnir.js-1.2.1.tgz#ab3237afc5fbbc8aa4bafc965b10735c4bf8328c"
   dependencies:
     hammerjs "^2.0.8"
 
@@ -6242,17 +6058,13 @@ moment-timezone@0.5.5:
   dependencies:
     moment ">= 2.6.0"
 
-"moment@>= 2.6.0":
-  version "2.18.1"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
-
-moment@^2.20.1:
-  version "2.21.0"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a"
+"moment@>= 2.6.0", moment@^2.20.1:
+  version "2.22.2"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
 
 mousetrap@^1.6.1:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9"
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.2.tgz#caadd9cf886db0986fb2fee59a82f6bd37527587"
 
 move-concurrently@^1.0.1:
   version "1.0.1"
@@ -6281,69 +6093,66 @@ multi-glob@^1.0.1:
     glob "5.x"
     lodash "3.x"
 
-multi-stage-sourcemap@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/multi-stage-sourcemap/-/multi-stage-sourcemap-0.2.1.tgz#b09fc8586eaa17f81d575c4ad02e0f7a3f6b1105"
-  dependencies:
-    source-map "^0.1.34"
-
 mustache@^2.2.1:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0"
 
-mute-stream@0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db"
-
 mute-stream@0.0.7, mute-stream@~0.0.4:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
 
-nan@^2.3.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
+nan@^2.9.2:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
 
 nanoid@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-0.2.2.tgz#e2ebc6ad3db5e0454fd8124d30ca39b06555fe56"
 
-native-promise-only@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
+nanomatch@^1.2.9:
+  version "1.2.9"
+  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    fragment-cache "^0.2.1"
+    is-odd "^2.0.0"
+    is-windows "^1.0.2"
+    kind-of "^6.0.2"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
 
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
 
-nconf@^0.7.2:
-  version "0.7.2"
-  resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.7.2.tgz#a05fdf22dc01c378dd5c4df27f2dc90b9aa8bb00"
-  dependencies:
-    async "~0.9.0"
-    ini "1.x.x"
-    yargs "~3.15.0"
-
-needle@^2.0.1:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.0.tgz#f14efc69cee1024b72c8b21c7bdf94a731dc12fa"
+needle@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
   dependencies:
     debug "^2.1.2"
     iconv-lite "^0.4.4"
     sax "^1.2.4"
 
-nested-error-stacks@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf"
-  dependencies:
-    inherits "~2.0.1"
+neo-async@^2.5.0:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee"
 
-nise@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-1.1.1.tgz#1faa07147f3bf2465d4dbedc0e4a84048f081041"
+next-tick@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+
+nise@^1.2.0:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.2.tgz#a9a3800e3994994af9e452333d549d60f72b8e8c"
   dependencies:
-    formatio "^1.2.0"
-    just-extend "^1.1.22"
-    lolex "^1.6.0"
+    "@sinonjs/formatio" "^2.0.0"
+    just-extend "^1.1.27"
+    lolex "^2.3.2"
     path-to-regexp "^1.7.0"
     text-encoding "^0.6.4"
 
@@ -6370,18 +6179,17 @@ node-fetch@^1.0.1:
     is-stream "^1.0.1"
 
 node-gyp@^3.6.2:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.7.0.tgz#789478e8f6c45e277aa014f3e28f958f286f9203"
   dependencies:
     fstream "^1.0.0"
     glob "^7.0.3"
     graceful-fs "^4.1.2"
-    minimatch "^3.0.2"
     mkdirp "^0.5.0"
     nopt "2 || 3"
     npmlog "0 || 1 || 2 || 3 || 4"
     osenv "0"
-    request "2"
+    request ">=2.9.0 <2.82.0"
     rimraf "2"
     semver "~5.3.0"
     tar "^2.0.0"
@@ -6407,51 +6215,47 @@ node-gyp@~3.4.0:
     which "1"
 
 node-libs-browser@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
   dependencies:
     assert "^1.1.1"
-    browserify-zlib "^0.1.4"
+    browserify-zlib "^0.2.0"
     buffer "^4.3.0"
     console-browserify "^1.1.0"
     constants-browserify "^1.0.0"
     crypto-browserify "^3.11.0"
     domain-browser "^1.1.1"
     events "^1.0.0"
-    https-browserify "0.0.1"
-    os-browserify "^0.2.0"
+    https-browserify "^1.0.0"
+    os-browserify "^0.3.0"
     path-browserify "0.0.0"
-    process "^0.11.0"
+    process "^0.11.10"
     punycode "^1.2.4"
     querystring-es3 "^0.2.0"
-    readable-stream "^2.0.5"
+    readable-stream "^2.3.3"
     stream-browserify "^2.0.1"
-    stream-http "^2.3.1"
-    string_decoder "^0.10.25"
-    timers-browserify "^2.0.2"
+    stream-http "^2.7.2"
+    string_decoder "^1.0.0"
+    timers-browserify "^2.0.4"
     tty-browserify "0.0.0"
     url "^0.11.0"
     util "^0.10.3"
     vm-browserify "0.0.4"
 
-node-pre-gyp@^0.6.36:
-  version "0.6.38"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d"
+node-pre-gyp@^0.10.0:
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
   dependencies:
-    hawk "3.1.3"
+    detect-libc "^1.0.2"
     mkdirp "^0.5.1"
+    needle "^2.2.0"
     nopt "^4.0.1"
+    npm-packlist "^1.1.6"
     npmlog "^4.0.2"
     rc "^1.1.7"
-    request "2.81.0"
     rimraf "^2.6.1"
     semver "^5.3.0"
-    tar "^2.2.1"
-    tar-pack "^3.4.0"
-
-node-status-codes@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
+    tar "^4"
 
 node-uuid@~1.4.0, node-uuid@~1.4.7:
   version "1.4.8"
@@ -6464,7 +6268,7 @@ nomnom@1.6.2:
     colors "0.5.x"
     underscore "~1.4.4"
 
-nomnom@1.8.1, "nomnom@>= 1.5.x":
+nomnom@1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
   dependencies:
@@ -6506,7 +6310,7 @@ normalize-package-data@~2.3.5:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
-normalize-path@^2.0.0, normalize-path@^2.0.1:
+normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
   dependencies:
@@ -6525,6 +6329,13 @@ normalize-url@^1.4.0:
     query-string "^4.1.0"
     sort-keys "^1.0.0"
 
+npm-audit-report@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.2.1.tgz#14813e9551f0f33088e7acc442e83ea6d627ef13"
+  dependencies:
+    cli-table2 "^0.2.0"
+    console-control-strings "^1.1.0"
+
 npm-bundled@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
@@ -6534,8 +6345,8 @@ npm-cache-filename@~1.0.2:
   resolved "https://registry.yarnpkg.com/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11"
 
 npm-check-updates@^2.14.0:
-  version "2.14.1"
-  resolved "https://registry.yarnpkg.com/npm-check-updates/-/npm-check-updates-2.14.1.tgz#783b2a67b422407fed9c3b0187ea127a23928440"
+  version "2.14.2"
+  resolved "https://registry.yarnpkg.com/npm-check-updates/-/npm-check-updates-2.14.2.tgz#1adb0d5fa48be8a6243cf335c7a2d6bbb0356e87"
   dependencies:
     bluebird "^3.4.3"
     chalk "^1.1.3"
@@ -6553,7 +6364,6 @@ npm-check-updates@^2.14.0:
     rc-config-loader "^2.0.1"
     semver "^5.3.0"
     semver-utils "^1.1.1"
-    snyk "^1.25.1"
     spawn-please "^0.3.0"
     update-notifier "^2.2.0"
 
@@ -6563,9 +6373,9 @@ npm-install-checks@~3.0.0:
   dependencies:
     semver "^2.3.0 || 3.x || 4 || 5"
 
-npm-lifecycle@^2.0.0, npm-lifecycle@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.0.1.tgz#897313f05ed24db8e28d99fa8b42c31b625e6237"
+npm-lifecycle@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.0.3.tgz#696bedf1143371163e9cc16fe872357e25d8d90e"
   dependencies:
     byline "^5.0.0"
     graceful-fs "^4.1.11"
@@ -6587,7 +6397,7 @@ npm-logical-tree@^1.2.1:
     hosted-git-info "^2.1.5"
     semver "^5.1.0"
 
-"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0:
+"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^5.1.2 || 6", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1"
   dependencies:
@@ -6596,7 +6406,7 @@ npm-logical-tree@^1.2.1:
     semver "^5.5.0"
     validate-npm-package-name "^3.0.0"
 
-"npm-package-arg@^4.0.0 || ^5.0.0", npm-package-arg@^5.1.2:
+"npm-package-arg@^4.0.0 || ^5.0.0":
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-5.1.2.tgz#fb18d17bb61e60900d6312619919bd753755ab37"
   dependencies:
@@ -6605,16 +6415,7 @@ npm-logical-tree@^1.2.1:
     semver "^5.1.0"
     validate-npm-package-name "^3.0.0"
 
-npm-package-arg@~6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.0.0.tgz#8cce04b49d3f9faec3f56b0fe5f4391aeb9d2fac"
-  dependencies:
-    hosted-git-info "^2.5.0"
-    osenv "^0.1.4"
-    semver "^5.4.1"
-    validate-npm-package-name "^3.0.0"
-
-npm-packlist@^1.1.10, npm-packlist@~1.1.10:
+npm-packlist@^1.1.10, npm-packlist@^1.1.6, npm-packlist@~1.1.10:
   version "1.1.10"
   resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
   dependencies:
@@ -6629,11 +6430,11 @@ npm-pick-manifest@^2.1.0:
     semver "^5.4.1"
 
 npm-profile@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.1.tgz#65a1018340f14399a086b5d0a9bd0d13145d8e57"
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.2.tgz#58d568f1b56ef769602fd0aed8c43fa0e0de0f57"
   dependencies:
-    aproba "^1.1.2"
-    make-fetch-happen "^2.5.0"
+    aproba "^1.1.2 || 2"
+    make-fetch-happen "^2.5.0 || 3 || 4"
 
 npm-registry-client@^8.5.1:
   version "8.5.1"
@@ -6669,6 +6470,27 @@ npm-registry-client@~7.2.1:
   optionalDependencies:
     npmlog "~2.0.0 || ~3.1.0"
 
+npm-registry-fetch@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-1.1.1.tgz#710bc5947d9ee2c549375072dab6d5d17baf2eb2"
+  dependencies:
+    bluebird "^3.5.1"
+    figgy-pudding "^3.0.0"
+    lru-cache "^4.1.2"
+    make-fetch-happen "^3.0.0"
+    npm-package-arg "^6.0.0"
+    safe-buffer "^5.1.1"
+
+npm-registry-fetch@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-3.1.1.tgz#e96bae698afdd45d4a01aca29e881fc0bc55206c"
+  dependencies:
+    bluebird "^3.5.1"
+    figgy-pudding "^3.1.0"
+    lru-cache "^4.1.2"
+    make-fetch-happen "^4.0.0"
+    npm-package-arg "^6.0.0"
+
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -6759,9 +6581,9 @@ npm@^3, npm@^3.10.6:
     wrappy "~1.0.2"
     write-file-atomic "~1.2.0"
 
-npm@^5.7.1:
-  version "5.8.0"
-  resolved "https://registry.yarnpkg.com/npm/-/npm-5.8.0.tgz#5e4bfb8c2e7ada01dd41ec0555d13dd0f446ddb2"
+npm@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/npm/-/npm-6.1.0.tgz#d685cdfc1a812fb063f031af09bed7a5a68eedf1"
   dependencies:
     JSONStream "^1.3.2"
     abbrev "~1.1.1"
@@ -6770,11 +6592,13 @@ npm@^5.7.1:
     ansistyles "~0.1.3"
     aproba "~1.2.0"
     archy "~1.0.0"
-    bin-links "^1.1.0"
+    bin-links "^1.1.2"
     bluebird "~3.5.1"
-    cacache "^10.0.4"
+    byte-size "^4.0.3"
+    cacache "^11.0.2"
     call-limit "~1.1.0"
     chownr "~1.0.1"
+    cli-columns "^3.1.2"
     cli-table2 "~0.2.0"
     cmd-shim "~2.0.2"
     columnify "~1.5.4"
@@ -6783,6 +6607,7 @@ npm@^5.7.1:
     detect-newline "^2.1.0"
     dezalgo "~1.0.3"
     editor "~1.0.0"
+    figgy-pudding "^3.1.0"
     find-npm-prefix "^1.0.2"
     fs-vacuum "~1.2.10"
     fs-write-stream-atomic "~1.0.10"
@@ -6791,75 +6616,83 @@ npm@^5.7.1:
     graceful-fs "~4.1.11"
     has-unicode "~2.0.1"
     hosted-git-info "^2.6.0"
-    iferr "~0.1.5"
+    iferr "^1.0.0"
     inflight "~1.0.6"
     inherits "~2.0.3"
     ini "^1.3.5"
     init-package-json "^1.10.3"
-    is-cidr "~1.0.0"
-    json-parse-better-errors "^1.0.1"
+    is-cidr "^2.0.5"
+    json-parse-better-errors "^1.0.2"
     lazy-property "~1.0.0"
-    libcipm "^1.6.0"
-    libnpx "^10.0.1"
-    lockfile "~1.0.3"
+    libcipm "^1.6.2"
+    libnpmhook "^4.0.1"
+    libnpx "^10.2.0"
+    lock-verify "^2.0.2"
+    lockfile "^1.0.4"
     lodash._baseuniq "~4.6.0"
     lodash.clonedeep "~4.5.0"
     lodash.union "~4.6.0"
     lodash.uniq "~4.5.0"
     lodash.without "~4.4.0"
-    lru-cache "~4.1.1"
+    lru-cache "^4.1.3"
     meant "~1.0.1"
     mississippi "^3.0.0"
     mkdirp "~0.5.1"
     move-concurrently "^1.0.1"
+    node-gyp "^3.6.2"
     nopt "~4.0.1"
     normalize-package-data "~2.4.0"
+    npm-audit-report "^1.2.1"
     npm-cache-filename "~1.0.2"
     npm-install-checks "~3.0.0"
-    npm-lifecycle "^2.0.1"
-    npm-package-arg "~6.0.0"
+    npm-lifecycle "^2.0.3"
+    npm-package-arg "^6.1.0"
     npm-packlist "~1.1.10"
+    npm-pick-manifest "^2.1.0"
     npm-profile "^3.0.1"
     npm-registry-client "^8.5.1"
+    npm-registry-fetch "^1.1.0"
     npm-user-validate "~1.0.0"
     npmlog "~4.1.2"
     once "~1.4.0"
     opener "~1.4.3"
     osenv "^0.1.5"
-    pacote "^7.6.1"
+    pacote "^8.1.5"
     path-is-inside "~1.0.2"
     promise-inflight "~1.0.1"
-    qrcode-terminal "~0.11.0"
-    query-string "^5.1.0"
+    qrcode-terminal "^0.12.0"
+    query-string "^6.1.0"
     qw "~1.0.1"
     read "~1.0.7"
     read-cmd-shim "~1.0.1"
     read-installed "~4.0.3"
     read-package-json "^2.0.13"
-    read-package-tree "~5.1.6"
-    readable-stream "^2.3.5"
-    request "~2.83.0"
-    retry "~0.10.1"
+    read-package-tree "^5.2.1"
+    readable-stream "^2.3.6"
+    request "^2.86.0"
+    retry "^0.12.0"
     rimraf "~2.6.2"
-    safe-buffer "~5.1.1"
+    safe-buffer "^5.1.2"
     semver "^5.5.0"
     sha "~2.0.1"
     slide "~1.1.6"
     sorted-object "~2.0.1"
     sorted-union-stream "~2.1.3"
-    ssri "^5.2.4"
+    ssri "^6.0.0"
     strip-ansi "~4.0.0"
-    tar "^4.4.0"
+    tar "^4.4.1"
     text-table "~0.2.0"
+    tiny-relative-date "^1.3.0"
     uid-number "0.0.6"
     umask "~1.1.0"
     unique-filename "~1.1.0"
     unpipe "~1.0.0"
-    update-notifier "~2.3.0"
+    update-notifier "^2.5.0"
     uuid "^3.2.1"
+    validate-npm-package-license "^3.0.3"
     validate-npm-package-name "~3.0.0"
     which "~1.3.0"
-    worker-farm "^1.5.4"
+    worker-farm "^1.6.0"
     wrappy "~1.0.2"
     write-file-atomic "^2.3.0"
 
@@ -6916,8 +6749,8 @@ nvd3@1.8.6:
   resolved "https://registry.yarnpkg.com/nvd3/-/nvd3-1.8.6.tgz#2d3eba74bf33363b5101ebf1d093c59a53ae73c4"
 
 "nwmatcher@>= 1.3.9 < 2.0.0":
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.2.tgz#c5e545ab40d22a56b0326531c4beaed7a888b3ea"
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
 
 oauth-sign@~0.3.0:
   version "0.3.0"
@@ -6935,17 +6768,25 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
 
-object-inspect@~0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-0.4.0.tgz#f5157c116c1455b243b06ee97703392c5ad89fec"
+object-copy@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+  dependencies:
+    copy-descriptor "^0.1.0"
+    define-property "^0.2.5"
+    kind-of "^3.0.3"
+
+object-inspect@~1.4.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.4.1.tgz#37ffb10e71adaf3748d05f713b4c9452f402cbc4"
 
 object-is@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
 
-object-keys@^1.0.10, object-keys@^1.0.6, object-keys@^1.0.8, object-keys@^1.0.9:
-  version "1.0.11"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+object-keys@^1.0.11, object-keys@^1.0.6, object-keys@^1.0.8, object-keys@^1.0.9:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
 
 object-keys@~0.2.0:
   version "0.2.0"
@@ -6955,17 +6796,20 @@ object-keys@~0.2.0:
     indexof "~0.0.1"
     is "~0.2.6"
 
-object-keys@~0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
+object-visit@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+  dependencies:
+    isobject "^3.0.0"
 
-object.assign@^4.0.4:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc"
+object.assign@^4.0.4, object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
   dependencies:
     define-properties "^1.1.2"
-    function-bind "^1.1.0"
-    object-keys "^1.0.10"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
 
 object.entries@^1.0.4:
   version "1.0.4"
@@ -6987,6 +6831,12 @@ object.omit@^2.0.0:
     for-own "^0.1.4"
     is-extendable "^0.1.1"
 
+object.pick@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+  dependencies:
+    isobject "^3.0.1"
+
 object.values@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a"
@@ -7002,20 +6852,12 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0, once@~1.4.0:
   dependencies:
     wrappy "1"
 
-onetime@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
-
 onetime@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
   dependencies:
     mimic-fn "^1.0.0"
 
-open@^0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc"
-
 opener@~1.4.2, opener@~1.4.3:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
@@ -7044,20 +6886,14 @@ optionator@^0.8.1, optionator@^0.8.2:
     type-check "~0.3.2"
     wordwrap "~1.0.0"
 
-os-browserify@^0.2.0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+os-browserify@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
 
-os-homedir@^1.0.0, os-homedir@^1.0.1:
+os-homedir@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
 
-os-locale@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
-  dependencies:
-    lcid "^1.0.0"
-
 os-locale@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
@@ -7066,37 +6902,17 @@ os-locale@^2.0.0:
     lcid "^1.0.0"
     mem "^1.1.0"
 
-os-name@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/os-name/-/os-name-1.0.3.tgz#1b379f64835af7c5a7f498b357cb95215c159edf"
-  dependencies:
-    osx-release "^1.0.0"
-    win-release "^1.0.0"
-
 os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
-osenv@0, osenv@^0.1.0, osenv@^0.1.5, osenv@~0.1.3:
+osenv@0, osenv@^0.1.4, osenv@^0.1.5, osenv@~0.1.3:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
   dependencies:
     os-homedir "^1.0.0"
     os-tmpdir "^1.0.0"
 
-osenv@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
-osx-release@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/osx-release/-/osx-release-1.1.0.tgz#f217911a28136949af1bf9308b241e2737d3cd6c"
-  dependencies:
-    minimist "^1.1.0"
-
 output-file-sync@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76"
@@ -7110,8 +6926,10 @@ p-finally@^1.0.0:
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
 
 p-limit@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+  dependencies:
+    p-try "^1.0.0"
 
 p-locate@^2.0.0:
   version "2.0.0"
@@ -7119,27 +6937,9 @@ p-locate@^2.0.0:
   dependencies:
     p-limit "^1.1.0"
 
-package-json-versionify@^1.0.2:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17"
-  dependencies:
-    browserify-package-json "^1.0.0"
-
-package-json@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0"
-  dependencies:
-    got "^3.2.0"
-    registry-url "^3.0.0"
-
-package-json@^2.0.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb"
-  dependencies:
-    got "^5.0.0"
-    registry-auth-token "^3.0.1"
-    registry-url "^3.0.3"
-    semver "^5.1.0"
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
 
 package-json@^4.0.0:
   version "4.0.1"
@@ -7150,21 +6950,22 @@ package-json@^4.0.0:
     registry-url "^3.0.3"
     semver "^5.1.0"
 
-pacote@^7.5.1, pacote@^7.6.1:
-  version "7.6.1"
-  resolved "https://registry.yarnpkg.com/pacote/-/pacote-7.6.1.tgz#d44621c89a5a61f173989b60236757728387c094"
+pacote@^8.1.5, pacote@^8.1.6:
+  version "8.1.6"
+  resolved "https://registry.yarnpkg.com/pacote/-/pacote-8.1.6.tgz#8e647564d38156367e7a9dc47a79ca1ab278d46e"
   dependencies:
     bluebird "^3.5.1"
-    cacache "^10.0.4"
+    cacache "^11.0.2"
     get-stream "^3.0.0"
     glob "^7.1.2"
-    lru-cache "^4.1.1"
-    make-fetch-happen "^2.6.0"
+    lru-cache "^4.1.3"
+    make-fetch-happen "^4.0.1"
     minimatch "^3.0.4"
+    minipass "^2.3.3"
     mississippi "^3.0.0"
     mkdirp "^0.5.1"
     normalize-package-data "^2.4.0"
-    npm-package-arg "^6.0.0"
+    npm-package-arg "^6.1.0"
     npm-packlist "^1.1.10"
     npm-pick-manifest "^2.1.0"
     osenv "^0.1.5"
@@ -7172,16 +6973,16 @@ pacote@^7.5.1, pacote@^7.6.1:
     promise-retry "^1.1.1"
     protoduck "^5.0.0"
     rimraf "^2.6.2"
-    safe-buffer "^5.1.1"
+    safe-buffer "^5.1.2"
     semver "^5.5.0"
-    ssri "^5.2.4"
-    tar "^4.4.0"
+    ssri "^6.0.0"
+    tar "^4.4.3"
     unique-filename "^1.1.0"
     which "^1.3.0"
 
-pako@~0.2.0:
-  version "0.2.9"
-  resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+pako@~1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
 
 parallel-transform@^1.1.0:
   version "1.1.0"
@@ -7192,8 +6993,8 @@ parallel-transform@^1.1.0:
     readable-stream "^2.1.5"
 
 parse-asn1@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
   dependencies:
     asn1.js "^4.0.0"
     browserify-aes "^1.0.0"
@@ -7225,7 +7026,7 @@ parse-iso-duration@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-iso-duration/-/parse-iso-duration-1.0.0.tgz#b923ab898a8ff8f42bdc9ee5db6e22808c48a864"
 
-parse-json@^2.1.0, parse-json@^2.2.0:
+parse-json@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
   dependencies:
@@ -7241,6 +7042,10 @@ parse5@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
 
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
 path-array@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-array/-/path-array-1.0.1.tgz#7e2f0f35f07a2015122b868b7eac0eb2c4fec271"
@@ -7251,6 +7056,10 @@ path-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
 
+path-dirname@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+
 path-exists@^2.0.0, path-exists@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
@@ -7283,14 +7092,6 @@ path-to-regexp@^1.7.0:
   dependencies:
     isarray "0.0.1"
 
-path-type@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
-  dependencies:
-    graceful-fs "^4.1.2"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-
 path-type@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@@ -7301,13 +7102,6 @@ pathval@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
 
-pbf@^1.3.2:
-  version "1.3.7"
-  resolved "https://registry.yarnpkg.com/pbf/-/pbf-1.3.7.tgz#1e3d047ba3cbe8086ae854a25503ab4537d4335d"
-  dependencies:
-    ieee754 "^1.1.6"
-    resolve-protobuf-schema "^2.0.0"
-
 pbf@^3.0.5:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.1.0.tgz#f70004badcb281761eabb1e76c92f179f08189e9"
@@ -7316,8 +7110,8 @@ pbf@^3.0.5:
     resolve-protobuf-schema "^2.0.0"
 
 pbkdf2@^3.0.3:
-  version "3.0.14"
-  resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+  version "3.0.16"
+  resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
   dependencies:
     create-hash "^1.1.2"
     create-hmac "^1.1.4"
@@ -7333,10 +7127,14 @@ performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
 
-pify@^2.0.0, pify@^2.3.0:
+pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
 
+pify@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
 pinkie-promise@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
@@ -7370,9 +7168,9 @@ po2json@^0.4.5:
     gettext-parser "1.1.0"
     nomnom "1.8.1"
 
-point-geometry@0.0.0, point-geometry@^0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/point-geometry/-/point-geometry-0.0.0.tgz#6fcbcad7a803b6418247dd6e49c2853c584daff7"
+posix-character-classes@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
 
 postcss-calc@^5.2.0:
   version "5.3.1"
@@ -7429,11 +7227,10 @@ postcss-discard-unused@^2.2.1:
     uniqs "^2.0.0"
 
 postcss-filter-plugins@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c"
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz#82245fdf82337041645e477114d8e593aa18b8ec"
   dependencies:
     postcss "^5.0.4"
-    uniqid "^4.0.0"
 
 postcss-merge-idents@^2.1.5:
   version "2.1.7"
@@ -7497,12 +7294,18 @@ postcss-minify-selectors@^2.0.4:
     postcss-selector-parser "^2.0.0"
 
 postcss-modules-extract-imports@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz#b614c9720be6816eaee35fb3a5faa1dba6a05ddb"
+  dependencies:
+    postcss "^6.0.1"
+
+postcss-modules-extract-imports@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85"
   dependencies:
     postcss "^6.0.1"
 
-postcss-modules-local-by-default@^1.0.1:
+postcss-modules-local-by-default@^1.0.1, postcss-modules-local-by-default@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
   dependencies:
@@ -7517,14 +7320,14 @@ postcss-modules-resolve-imports@^1.3.0:
     icss-utils "^3.0.1"
     minimist "^1.2.0"
 
-postcss-modules-scope@^1.0.0:
+postcss-modules-scope@^1.0.0, postcss-modules-scope@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
   dependencies:
     css-selector-tokenizer "^0.7.0"
     postcss "^6.0.1"
 
-postcss-modules-values@^1.1.0, postcss-modules-values@^1.1.1:
+postcss-modules-values@^1.1.1, postcss-modules-values@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
   dependencies:
@@ -7621,12 +7424,12 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
     supports-color "^3.2.3"
 
 postcss@^6.0.1, postcss@^6.0.2:
-  version "6.0.13"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.13.tgz#b9ecab4ee00c89db3ec931145bd9590bbf3f125f"
+  version "6.0.23"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
   dependencies:
-    chalk "^2.1.0"
+    chalk "^2.4.1"
     source-map "^0.6.1"
-    supports-color "^4.4.0"
+    supports-color "^5.4.0"
 
 prelude-ls@~1.1.2:
   version "1.1.2"
@@ -7641,12 +7444,30 @@ preserve@^0.2.0:
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
 
 prettier@^1.12.1:
-  version "1.12.1"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
+  version "1.13.5"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.5.tgz#7ae2076998c8edce79d63834e9b7b09fead6bfd0"
 
-private@^0.1.6, private@^0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+prismjs@^1.8.4:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9"
+  optionalDependencies:
+    clipboard "^2.0.0"
+
+prismjs@~1.14.0:
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.14.0.tgz#bbccfdb8be5d850d26453933cb50122ca0362ae0"
+  optionalDependencies:
+    clipboard "^2.0.0"
+
+private@^0.1.6, private@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+probe.gl@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/probe.gl/-/probe.gl-1.0.2.tgz#3b0bdf015ff07dba6582badc6993890cfa5219e6"
+  dependencies:
+    babel-runtime "^6.11.6"
 
 process-nextick-args@~1.0.6:
   version "1.0.7"
@@ -7656,7 +7477,7 @@ process-nextick-args@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
 
-process@^0.11.0:
+process@^0.11.10:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
 
@@ -7675,7 +7496,7 @@ promise-retry@^1.1.1:
     err-code "^1.0.0"
     retry "^0.10.0"
 
-"promise@>=3.2 <8", promise@^7.1.1:
+promise@^7.1.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
   dependencies:
@@ -7688,9 +7509,10 @@ promzard@^0.3.0:
     read "1"
 
 prop-types-extra@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.0.1.tgz#a57bd4810e82d27a3ff4317ecc1b4ad005f79a82"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.0.tgz#32609910ea2dcf190366bacd3490d5a6412a605f"
   dependencies:
+    react-is "^16.3.2"
     warning "^3.0.0"
 
 prop-types@15.5.10:
@@ -7700,21 +7522,16 @@ prop-types@15.5.10:
     fbjs "^0.8.9"
     loose-envify "^1.3.1"
 
-prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0:
-  version "15.6.0"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
+prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1:
+  version "15.6.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
   dependencies:
-    fbjs "^0.8.16"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
-prop-types@^15.6.1:
-  version "15.6.1"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
-  dependencies:
-    fbjs "^0.8.16"
-    loose-envify "^1.3.1"
-    object-assign "^4.1.1"
+property-information@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/property-information/-/property-information-3.2.0.tgz#fd1483c8fbac61808f5fe359e7693a1f48a58331"
 
 proto-list@~1.2.1:
   version "1.2.4"
@@ -7730,14 +7547,6 @@ protoduck@^5.0.0:
   dependencies:
     genfun "^4.0.1"
 
-proxy-from-env@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
-
-prr@~0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
-
 prr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -7746,9 +7555,13 @@ pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
 
+psl@^1.1.24:
+  version "1.1.28"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.28.tgz#4fb6ceb08a1e2214d4fd4de0ca22dae13740bc7b"
+
 public-encrypt@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994"
   dependencies:
     bn.js "^4.1.0"
     browserify-rsa "^4.0.0"
@@ -7756,13 +7569,6 @@ public-encrypt@^4.0.0:
     parse-asn1 "^5.0.0"
     randombytes "^2.0.1"
 
-pump@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
-  dependencies:
-    end-of-stream "^1.1.0"
-    once "^1.3.1"
-
 pump@^2.0.0, pump@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@@ -7778,10 +7584,10 @@ pump@^3.0.0:
     once "^1.3.1"
 
 pumpify@^1.3.3:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb"
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
   dependencies:
-    duplexify "^3.5.3"
+    duplexify "^3.6.0"
     inherits "^2.0.3"
     pump "^2.0.0"
 
@@ -7793,13 +7599,17 @@ punycode@^1.2.4, punycode@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
 
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+
 q@^1.1.2:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
 
-qrcode-terminal@~0.11.0:
-  version "0.11.0"
-  resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e"
+qrcode-terminal@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
 
 qs@~0.6.0:
   version "0.6.6"
@@ -7814,8 +7624,8 @@ qs@~6.4.0:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
 
 qs@~6.5.1:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 
 query-string@^4.1.0, query-string@^4.2.2:
   version "4.3.4"
@@ -7824,13 +7634,12 @@ query-string@^4.1.0, query-string@^4.2.2:
     object-assign "^4.1.0"
     strict-uri-encode "^1.0.0"
 
-query-string@^5.1.0:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
+query-string@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.1.0.tgz#01e7d69f6a0940dac67a937d6c6325647aa4532a"
   dependencies:
     decode-uri-component "^0.2.0"
-    object-assign "^4.1.0"
-    strict-uri-encode "^1.0.0"
+    strict-uri-encode "^2.0.0"
 
 querystring-es3@^0.2.0:
   version "0.2.1"
@@ -7841,10 +7650,10 @@ querystring@0.2.0:
   resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
 
 quickselect@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.0.0.tgz#02630818f9aae4ecab26f0103f98d061c17c58f3"
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2"
 
-quote-stream@^1.0.1:
+quote-stream@^1.0.1, quote-stream@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2"
   dependencies:
@@ -7852,13 +7661,6 @@ quote-stream@^1.0.1:
     minimist "^1.1.3"
     through2 "^2.0.0"
 
-quote-stream@~0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-0.0.0.tgz#cde29e94c409b16e19dc7098b89b6658f9721d3b"
-  dependencies:
-    minimist "0.0.8"
-    through2 "~0.4.1"
-
 qw@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
@@ -7869,17 +7671,25 @@ raf@^3.3.0:
   dependencies:
     performance-now "^2.1.0"
 
-randomatic@^1.1.3:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+randomatic@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923"
   dependencies:
-    is-number "^3.0.0"
-    kind-of "^4.0.0"
+    is-number "^4.0.0"
+    kind-of "^6.0.0"
+    math-random "^1.0.1"
 
-randombytes@^2.0.0, randombytes@^2.0.1:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+  dependencies:
+    safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
   dependencies:
+    randombytes "^2.0.5"
     safe-buffer "^5.1.0"
 
 rc-config-loader@^2.0.1:
@@ -7894,27 +7704,18 @@ rc-config-loader@^2.0.1:
     path-exists "^2.1.0"
     require-from-string "^2.0.1"
 
-rc@^1.0.1, rc@^1.1.6:
-  version "1.2.6"
-  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092"
-  dependencies:
-    deep-extend "~0.4.0"
-    ini "~1.3.0"
-    minimist "^1.2.0"
-    strip-json-comments "~2.0.1"
-
-rc@^1.1.7:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
+rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
   dependencies:
-    deep-extend "~0.4.0"
+    deep-extend "^0.6.0"
     ini "~1.3.0"
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
 re-resizable@^4.3.1:
-  version "4.4.8"
-  resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-4.4.8.tgz#1c7eedfd9b9ed1f83b3adfa7a97cda76881e4e57"
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-4.5.1.tgz#4688e8311ea4b70d558f7aebb3168a25926e62d9"
 
 react-ace@^5.10.0:
   version "5.10.0"
@@ -7951,17 +7752,14 @@ react-alert@^2.3.0:
     nanoid "^0.2.2"
     react-transition-group "^1.1.2"
 
-react-bootstrap-slider@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/react-bootstrap-slider/-/react-bootstrap-slider-2.0.1.tgz#2a4a9014f97ba997f6c62a3c758d88e1059a19b0"
+react-bootstrap-slider@2.1.5:
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/react-bootstrap-slider/-/react-bootstrap-slider-2.1.5.tgz#2f79e57b69ddf2b5bd23310bddbd2de0c6bdfef3"
   dependencies:
-    bootstrap "^3.3.7"
-    bootstrap-slider "^9.8.0"
-    prop-types "^15.5.10"
-    react "^15.6.1"
-    react-dom "^15.6.1"
+    bootstrap-slider "9.9.0"
+    es6bindall "^0.0.9"
 
-react-bootstrap-table@^4.0.2:
+react-bootstrap-table@^4.3.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/react-bootstrap-table/-/react-bootstrap-table-4.3.1.tgz#f704be55b7f6bf0557d2fc5bec6d25fd307d0cde"
   dependencies:
@@ -7986,8 +7784,8 @@ react-bootstrap@^0.31.5:
     warning "^3.0.0"
 
 react-color@^2.13.8:
-  version "2.13.8"
-  resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.13.8.tgz#bcc58f79a722b9bfc37c402e68cd18f26970aee4"
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.14.1.tgz#db8ad4f45d81e74896fc2e1c99508927c6d084e0"
   dependencies:
     lodash "^4.0.1"
     material-colors "^1.2.1"
@@ -7995,14 +7793,14 @@ react-color@^2.13.8:
     reactcss "^1.2.0"
     tinycolor2 "^1.4.1"
 
-react-datetime@2.9.0:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.9.0.tgz#9ec80060cbb8e5c5d8f98f0acebb6f4712ce449a"
+react-datetime@2.14.0:
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.14.0.tgz#c7859c5b765275d7980f1cca27c03a727ff9ccef"
   dependencies:
-    "@types/react" ">=15"
+    create-react-class "^15.5.2"
     object-assign "^3.0.0"
     prop-types "^15.5.7"
-    react-onclickoutside "^5.9.0"
+    react-onclickoutside "^6.5.0"
 
 react-dnd-html5-backend@^2.5.4:
   version "2.6.0"
@@ -8021,7 +7819,7 @@ react-dnd@^2.5.4:
     lodash "^4.2.0"
     prop-types "^15.5.10"
 
-"react-dom@^15.0.0 || 15.x", react-dom@^15.6.1, react-dom@^15.6.2:
+react-dom@^15.6.2:
   version "15.6.2"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
   dependencies:
@@ -8030,20 +7828,13 @@ react-dnd@^2.5.4:
     object-assign "^4.1.0"
     prop-types "^15.5.10"
 
-react-draggable@3.x:
+react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3":
   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"
-  dependencies:
-    classnames "^2.2.5"
-    prop-types "^15.5.10"
-
 react-gravatar@^2.6.1:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/react-gravatar/-/react-gravatar-2.6.3.tgz#5407eb6ac87e830e2a34deb760d2a4c404eb1dac"
@@ -8052,9 +7843,9 @@ 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"
+react-grid-layout@0.16.6:
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.6.tgz#9b2407a2b946c2260ebaf66f13b556e1da4efeb2"
   dependencies:
     classnames "2.x"
     lodash.isequal "^4.0.0"
@@ -8063,43 +7854,41 @@ react-grid-layout@0.16.5:
     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"
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.4.2.tgz#0d2ccf134fc79b2d3543837dc1591d32b7b903f9"
   dependencies:
     html-element-attributes "^1.0.0"
 
-react-input-autosize@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.0.1.tgz#e92190497b4026c2780ad0f2fd703c835ba03e33"
-  dependencies:
-    create-react-class "^15.5.2"
-    prop-types "^15.5.8"
-
 react-input-autosize@^2.1.2:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
   dependencies:
     prop-types "^15.5.8"
 
-react-lifecycles-compat@^3.0.0:
+react-is@^16.3.2:
+  version "16.4.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
+
+react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
   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"
+  version "3.2.10"
+  resolved "https://registry.yarnpkg.com/react-map-gl/-/react-map-gl-3.2.10.tgz#30ce9a26be4aea659c4fef981962ceb6feebe28a"
   dependencies:
     babel-runtime "^6.23.0"
     bowser "^1.2.0"
-    hammerjs "^2.0.8"
-    immutable "*"
-    mapbox-gl "0.38.0"
+    immutable "^3.8.2"
+    mapbox-gl "0.45"
+    math.gl "^1.1.3"
+    mjolnir.js "^1.2.1"
     prop-types "^15.5.7"
-    viewport-mercator-project "^4.0.1"
+    viewport-mercator-project "^5.1.0"
 
 react-markdown@^3.3.0:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.3.2.tgz#35d305e8a29b640717b9dac4658a1caeafd44c94"
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.3.4.tgz#4002599ad133c649923a49aedc06b2f3af2016b6"
   dependencies:
     prop-types "^15.6.1"
     remark-parse "^5.0.0"
@@ -8116,11 +7905,9 @@ react-modal@^3.1.7:
     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"
-  dependencies:
-    create-react-class "^15.5.x"
+react-onclickoutside@^6.5.0:
+  version "6.7.1"
+  resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93"
 
 react-overlays@^0.7.4:
   version "0.7.4"
@@ -8133,15 +7920,15 @@ react-overlays@^0.7.4:
     warning "^3.0.0"
 
 react-redux@^5.0.2:
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946"
+  version "5.0.7"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
   dependencies:
-    hoist-non-react-statics "^2.2.1"
+    hoist-non-react-statics "^2.5.0"
     invariant "^2.0.0"
-    lodash "^4.2.0"
-    lodash-es "^4.2.0"
+    lodash "^4.17.5"
+    lodash-es "^4.17.5"
     loose-envify "^1.1.0"
-    prop-types "^15.5.10"
+    prop-types "^15.6.0"
 
 react-resizable@1.x, react-resizable@^1.3.3:
   version "1.7.5"
@@ -8169,7 +7956,7 @@ react-select-fast-filter-options@^0.2.1:
   dependencies:
     js-search "^1.3.1"
 
-react-select@1.2.1, react-select@^1.0.0-beta14:
+react-select@1.2.1, react-select@^1.0.0-rc.2, react-select@^1.0.0-rc.5:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.1.tgz#a2fe58a569eb14dcaa6543816260b97e538120d1"
   dependencies:
@@ -8177,26 +7964,17 @@ react-select@1.2.1, react-select@^1.0.0-beta14:
     prop-types "^15.5.8"
     react-input-autosize "^2.1.2"
 
-react-select@^1.0.0-rc.5:
-  version "1.0.0-rc.10"
-  resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.0.0-rc.10.tgz#f137346250f9255c979fbfa21860899928772350"
-  dependencies:
-    classnames "^2.2.4"
-    prop-types "^15.5.8"
-    react-input-autosize "^2.0.1"
-
-react-sortable-hoc@^0.6.7:
-  version "0.6.8"
-  resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.6.8.tgz#b08562f570d7c41f6e393fca52879d2ebb9118e9"
+react-sortable-hoc@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.8.3.tgz#8537e8ab8d6bad6829885755a0f847817ed78648"
   dependencies:
     babel-runtime "^6.11.6"
     invariant "^2.2.1"
-    lodash "^4.12.0"
     prop-types "^15.5.7"
 
 react-split-pane@^0.1.63, react-split-pane@^0.1.66:
-  version "0.1.66"
-  resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.66.tgz#369085dd07ec1237bda123e73813dcc7dc6502c1"
+  version "0.1.77"
+  resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.77.tgz#f0c8cd18d076bbac900248dcf6dbcec02d5340db"
   dependencies:
     inline-style-prefixer "^3.0.6"
     prop-types "^15.5.10"
@@ -8210,18 +7988,20 @@ react-sticky@^6.0.2:
     raf "^3.3.0"
 
 react-style-proptype@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/react-style-proptype/-/react-style-proptype-3.0.0.tgz#89e0b646f266c656abb0f0dd8202dbd5036c31e6"
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/react-style-proptype/-/react-style-proptype-3.2.1.tgz#7cfeb9b87ec7ab9dcbde9715170ed10c11fb86aa"
   dependencies:
     prop-types "^15.5.4"
 
-react-syntax-highlighter@^5.7.0:
-  version "5.7.0"
-  resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-5.7.0.tgz#3cbe81a53fd0f91e34f789c229e5274411054b37"
+react-syntax-highlighter@^7.0.4:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-7.0.4.tgz#38d0cabada32fd4a7d08db4b7fbdb33565c9420d"
   dependencies:
     babel-runtime "^6.18.0"
     highlight.js "~9.12.0"
     lowlight "~1.9.1"
+    prismjs "^1.8.4"
+    refractor "^2.4.1"
 
 react-test-renderer@^15.6.2:
   version "15.6.2"
@@ -8240,22 +8020,24 @@ react-transition-group@^1.1.2, react-transition-group@^1.2.0:
     prop-types "^15.5.6"
     warning "^3.0.0"
 
-react-virtualized-select@2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/react-virtualized-select/-/react-virtualized-select-2.4.0.tgz#8fcddb11e9cae98a012d3d09121d67249ead0b33"
+react-virtualized-select@^2.4.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/react-virtualized-select/-/react-virtualized-select-2.4.2.tgz#ac58bf363c5dca40282ce12c793d6044d2cb8dfe"
   dependencies:
     babel-runtime "^6.11.6"
-    react-select "^1.0.0-beta14"
+    react-select "^1.0.0-rc.2"
     react-virtualized "^8.0.5"
 
-react-virtualized@9.3.0:
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.3.0.tgz#b2ecb5aa936e8e199929ac98508decd4b6b0f71c"
+react-virtualized@9.19.1:
+  version "9.19.1"
+  resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.19.1.tgz#84b53253df2d9df61c85ce037141edccc70a73fd"
   dependencies:
-    babel-runtime "^6.11.6"
+    babel-runtime "^6.26.0"
     classnames "^2.2.3"
     dom-helpers "^2.4.0 || ^3.0.0"
     loose-envify "^1.3.0"
+    prop-types "^15.6.0"
+    react-lifecycles-compat "^3.0.4"
 
 react-virtualized@^8.0.5:
   version "8.11.4"
@@ -8282,7 +8064,7 @@ react-with-styles@^1.3.0:
     hoist-non-react-statics "^1.2.0"
     prop-types "^15.5.8"
 
-react@^15.6.1, react@^15.6.2:
+react@^15.6.2:
   version "15.6.2"
   resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
   dependencies:
@@ -8292,9 +8074,9 @@ react@^15.6.1, react@^15.6.2:
     object-assign "^4.1.0"
     prop-types "^15.5.10"
 
-reactable@^0.14.1:
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/reactable/-/reactable-0.14.1.tgz#2ab9895e3df6da2498625de46d3691592d18c93b"
+reactable@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/reactable/-/reactable-1.0.2.tgz#67a579fee3af68b991b5f04df921a4a40ece0b72"
 
 reactcss@^1.2.0:
   version "1.2.3"
@@ -8302,13 +8084,6 @@ reactcss@^1.2.0:
   dependencies:
     lodash "^4.0.1"
 
-read-all-stream@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa"
-  dependencies:
-    pinkie-promise "^2.0.0"
-    readable-stream "^2.0.0"
-
 read-cmd-shim@^1.0.1, read-cmd-shim@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz#2d5d157786a37c055d22077c32c53f8329e91c7b"
@@ -8328,7 +8103,7 @@ read-installed@~4.0.3:
   optionalDependencies:
     graceful-fs "^4.1.2"
 
-"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.12, read-package-json@^2.0.13, read-package-json@~2.0.4:
+"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13, read-package-json@~2.0.4:
   version "2.0.13"
   resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a"
   dependencies:
@@ -8339,9 +8114,9 @@ read-installed@~4.0.3:
   optionalDependencies:
     graceful-fs "^4.1.2"
 
-read-package-tree@~5.1.5, read-package-tree@~5.1.6:
-  version "5.1.6"
-  resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.1.6.tgz#4f03e83d0486856fb60d97c94882841c2a7b1b7a"
+read-package-tree@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.2.1.tgz#6218b187d6fac82289ce4387bbbaf8eef536ad63"
   dependencies:
     debuglog "^1.0.1"
     dezalgo "^1.0.0"
@@ -8349,12 +8124,15 @@ read-package-tree@~5.1.5, read-package-tree@~5.1.6:
     read-package-json "^2.0.0"
     readdir-scoped-modules "^1.0.0"
 
-read-pkg-up@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+read-package-tree@~5.1.5:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.1.6.tgz#4f03e83d0486856fb60d97c94882841c2a7b1b7a"
   dependencies:
-    find-up "^1.0.0"
-    read-pkg "^1.0.0"
+    debuglog "^1.0.1"
+    dezalgo "^1.0.0"
+    once "^1.3.0"
+    read-package-json "^2.0.0"
+    readdir-scoped-modules "^1.0.0"
 
 read-pkg-up@^2.0.0:
   version "2.0.0"
@@ -8363,14 +8141,6 @@ read-pkg-up@^2.0.0:
     find-up "^2.0.0"
     read-pkg "^2.0.0"
 
-read-pkg@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
-  dependencies:
-    load-json-file "^1.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^1.0.0"
-
 read-pkg@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
@@ -8385,19 +8155,19 @@ read@1, read@~1.0.1, read@~1.0.5, read@~1.0.7:
   dependencies:
     mute-stream "~0.0.4"
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.3:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.3"
     isarray "~1.0.0"
-    process-nextick-args "~1.0.6"
+    process-nextick-args "~2.0.0"
     safe-buffer "~5.1.1"
-    string_decoder "~1.0.3"
+    string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
-"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.26, readable-stream@~1.0.27-1:
+"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.26:
   version "1.0.34"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
   dependencies:
@@ -8406,18 +8176,6 @@ read@1, read@~1.0.1, read@~1.0.5, read@~1.0.7:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^2.3.5:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.3"
-    isarray "~1.0.0"
-    process-nextick-args "~2.0.0"
-    safe-buffer "~5.1.1"
-    string_decoder "~1.1.1"
-    util-deprecate "~1.0.1"
-
 readable-stream@~1.1.0, readable-stream@~1.1.10, readable-stream@~1.1.9:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -8484,12 +8242,6 @@ recompose@^0.23.5:
     hoist-non-react-statics "^1.0.0"
     symbol-observable "^1.0.4"
 
-recursive-readdir@^2.2.1:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
-  dependencies:
-    minimatch "3.0.4"
-
 redeyed@~0.4.0:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-0.4.4.tgz#37e990a6f2b21b2a11c2e6a48fd4135698cba97f"
@@ -8515,12 +8267,14 @@ redux-localstorage@^0.4.1:
   resolved "https://registry.yarnpkg.com/redux-localstorage/-/redux-localstorage-0.4.1.tgz#faf6d719c581397294d811473ffcedee065c933c"
 
 redux-mock-store@^1.2.3:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.3.0.tgz#6edfef0d2332f20576381069d6d889a6d0a4451c"
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d"
+  dependencies:
+    lodash.isplainobject "^4.0.6"
 
 redux-thunk@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
 
 redux-undo@^1.0.0-beta9-9-7:
   version "1.0.0-beta9-9-7"
@@ -8535,17 +8289,24 @@ redux@^3.5.2, redux@^3.7.1:
     loose-envify "^1.1.0"
     symbol-observable "^1.0.3"
 
+refractor@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.4.1.tgz#067654311ed1618fc2dd76e9263c8cf05ab6298b"
+  dependencies:
+    hastscript "^3.1.0"
+    prismjs "~1.14.0"
+
 regenerate@^1.2.1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
 
 regenerator-runtime@^0.10.5:
   version "0.10.5"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
 
 regenerator-runtime@^0.11.0:
-  version "0.11.0"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
 
 regenerator-transform@^0.10.0:
   version "0.10.1"
@@ -8561,6 +8322,13 @@ regex-cache@^0.4.2:
   dependencies:
     is-equal-shallow "^0.1.3"
 
+regex-not@^1.0.0, regex-not@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+  dependencies:
+    extend-shallow "^3.0.2"
+    safe-regex "^1.1.0"
+
 regexpp@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab"
@@ -8588,7 +8356,7 @@ registry-auth-token@^3.0.1:
     rc "^1.1.6"
     safe-buffer "^5.0.1"
 
-registry-url@^3.0.0, registry-url@^3.0.3:
+registry-url@^3.0.3:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
   dependencies:
@@ -8632,16 +8400,10 @@ repeat-element@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
 
-repeat-string@^1.5.2, repeat-string@^1.5.4:
+repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
 
-repeating@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac"
-  dependencies:
-    is-finite "^1.0.0"
-
 repeating@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
@@ -8652,9 +8414,9 @@ replace-ext@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
 
-request@2, request@^2.74.0:
-  version "2.85.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
+request@2, request@^2.74.0, request@^2.79.0, request@^2.86.0:
+  version "2.87.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
   dependencies:
     aws-sign2 "~0.7.0"
     aws4 "^1.6.0"
@@ -8664,7 +8426,6 @@ request@2, request@^2.74.0:
     forever-agent "~0.6.1"
     form-data "~2.3.1"
     har-validator "~5.0.3"
-    hawk "~6.0.2"
     http-signature "~1.2.0"
     is-typedarray "~1.0.0"
     isstream "~0.1.2"
@@ -8674,12 +8435,11 @@ request@2, request@^2.74.0:
     performance-now "^2.1.0"
     qs "~6.5.1"
     safe-buffer "^5.1.1"
-    stringstream "~0.0.5"
     tough-cookie "~2.3.3"
     tunnel-agent "^0.6.0"
     uuid "^3.1.0"
 
-request@2.81.0:
+request@2.81.0, "request@>=2.9.0 <2.82.0":
   version "2.81.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
   dependencies:
@@ -8706,33 +8466,6 @@ request@2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
-request@^2.72.0, request@^2.79.0, request@~2.83.0:
-  version "2.83.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.6.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.5"
-    extend "~3.0.1"
-    forever-agent "~0.6.1"
-    form-data "~2.3.1"
-    har-validator "~5.0.3"
-    hawk "~6.0.2"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.17"
-    oauth-sign "~0.8.2"
-    performance-now "^2.1.0"
-    qs "~6.5.1"
-    safe-buffer "^5.1.1"
-    stringstream "~0.0.5"
-    tough-cookie "~2.3.3"
-    tunnel-agent "^0.6.0"
-    uuid "^3.1.0"
-
 request@~2.22.0:
   version "2.22.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.22.0.tgz#b883a769cc4a909571eb5004b344c43cf7e51592"
@@ -8781,8 +8514,8 @@ require-directory@^2.1.1:
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
 
 require-from-string@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff"
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
 
 require-main-filename@^1.0.1:
   version "1.0.1"
@@ -8813,18 +8546,15 @@ resolve-protobuf-schema@^2.0.0:
   dependencies:
     protocol-buffers-schema "^2.0.2"
 
-resolve@^1.1.0, resolve@^1.1.5, resolve@^1.2.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
-  dependencies:
-    path-parse "^1.0.5"
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
 
-restore-cursor@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+resolve@^1.1.0, resolve@^1.1.5, resolve@^1.5.0, resolve@^1.6.0:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
   dependencies:
-    exit-hook "^1.0.0"
-    onetime "^1.0.0"
+    path-parse "^1.0.5"
 
 restore-cursor@^2.0.0:
   version "2.0.0"
@@ -8833,17 +8563,25 @@ restore-cursor@^2.0.0:
     onetime "^2.0.0"
     signal-exit "^3.0.2"
 
-retry@^0.10.0, retry@~0.10.0, retry@~0.10.1:
+ret@~0.1.10:
+  version "0.1.15"
+  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+retry@^0.10.0, retry@~0.10.0:
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
 
+retry@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+
 right-align@^0.1.1:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -8856,10 +8594,10 @@ rimraf@~2.5.4:
     glob "^7.0.5"
 
 ripemd160@^2.0.0, ripemd160@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
   dependencies:
-    hash-base "^2.0.0"
+    hash-base "^3.0.0"
     inherits "^2.0.1"
 
 run-async@^2.2.0:
@@ -8892,19 +8630,25 @@ rx-lite@*, rx-lite@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
 
-rx@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+safe-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+  dependencies:
+    ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
 
-samsam@1.x, samsam@^1.1.3:
+samsam@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
 
-sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
+sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
 
@@ -8914,12 +8658,12 @@ schema-utils@^0.3.0:
   dependencies:
     ajv "^5.0.0"
 
-schema-utils@^0.4.2:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.3.tgz#e2a594d3395834d5e15da22b48be13517859458e"
+schema-utils@^0.4.5:
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"
   dependencies:
-    ajv "^5.0.0"
-    ajv-keywords "^2.1.0"
+    ajv "^6.1.0"
+    ajv-keywords "^3.1.0"
 
 seed-random@2.2.0:
   version "2.2.0"
@@ -8937,6 +8681,10 @@ seer@^0.2.4:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/seer/-/seer-0.2.4.tgz#6b8a81d09bfe6b3b3ad0268971a65e7f7405135c"
 
+select@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+
 semver-diff@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@@ -8944,21 +8692,17 @@ semver-diff@^2.0.0:
     semver "^5.0.3"
 
 semver-utils@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/semver-utils/-/semver-utils-1.1.1.tgz#27d92fec34d27cfa42707d3b40d025ae9855f2df"
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/semver-utils/-/semver-utils-1.1.2.tgz#197d758a0a28c3d3a009338cfbcc1211bccd76d4"
 
-"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0:
+"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
 
-"semver@2 || 3 || 4 || 5", semver@^5.3.0:
+semver@5.4.1:
   version "5.4.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
 
-semver@2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-2.2.1.tgz#7941182b3ffcc580bff1c17942acdf7951c0d213"
-
 semver@^4.1.0:
   version "4.3.6"
   resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
@@ -8968,8 +8712,8 @@ semver@~5.3.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
 
 serialize-javascript@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005"
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe"
 
 set-blocking@^2.0.0, set-blocking@~2.0.0:
   version "2.0.0"
@@ -8979,13 +8723,31 @@ set-immediate-shim@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
 
+set-value@^0.4.3:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.1"
+    to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
 setimmediate@^1.0.4, setimmediate@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
 
 sha.js@^2.4.0, sha.js@^2.4.8:
-  version "2.4.9"
-  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
   dependencies:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
@@ -8997,15 +8759,6 @@ sha@~2.0.1:
     graceful-fs "^4.1.2"
     readable-stream "^2.0.2"
 
-shallow-clone@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
-  dependencies:
-    is-extendable "^0.1.1"
-    kind-of "^2.0.1"
-    lazy-cache "^0.2.3"
-    mixin-object "^2.0.1"
-
 shallow-copy@~0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
@@ -9018,7 +8771,7 @@ shapefile@0.3:
     iconv-lite "0.2"
     optimist "0.3"
 
-sharkdown@~0.1.0:
+sharkdown@^0.1.0, sharkdown@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/sharkdown/-/sharkdown-0.1.0.tgz#61d4fe529e75d02442127cc9234362265099214f"
   dependencies:
@@ -9053,20 +8806,17 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 
-sinon@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.0.1.tgz#e46146a8a8420f837bdba32e2965bd1fe43d5b05"
+sinon@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.5.0.tgz#427ae312a337d3c516804ce2754e8c0d5028cb04"
   dependencies:
+    "@sinonjs/formatio" "^2.0.0"
     diff "^3.1.0"
-    formatio "1.2.0"
     lodash.get "^4.4.2"
-    lolex "^2.1.3"
-    native-promise-only "^0.8.1"
-    nise "^1.1.1"
-    path-to-regexp "^1.7.0"
-    samsam "^1.1.3"
-    text-encoding "0.6.4"
-    type-detect "^4.0.0"
+    lolex "^2.2.0"
+    nise "^1.2.0"
+    supports-color "^5.1.0"
+    type-detect "^4.0.5"
 
 slash@^1.0.0:
   version "1.0.0"
@@ -9086,176 +8836,48 @@ smart-buffer@^1.0.13:
   version "1.1.15"
   resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
 
-sntp@0.2.x:
-  version "0.2.4"
-  resolved "https://registry.yarnpkg.com/sntp/-/sntp-0.2.4.tgz#fb885f18b0f3aad189f824862536bceeec750900"
-  dependencies:
-    hoek "0.9.x"
-
-sntp@1.x.x:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
-  dependencies:
-    hoek "2.x.x"
-
-sntp@2.x.x:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
-  dependencies:
-    hoek "4.x.x"
-
-snyk-config@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-1.0.1.tgz#f27aec2498b24027ac719214026521591111508f"
-  dependencies:
-    debug "^2.2.0"
-    nconf "^0.7.2"
-    path-is-absolute "^1.0.0"
-
-snyk-go-plugin@1.4.5:
-  version "1.4.5"
-  resolved "https://registry.yarnpkg.com/snyk-go-plugin/-/snyk-go-plugin-1.4.5.tgz#bf462656caade0603970b68e756f4b389c3aeaaa"
-  dependencies:
-    graphlib "^2.1.1"
-    toml "^2.3.2"
-
-snyk-gradle-plugin@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-1.2.0.tgz#ef5aea5d132905cbf0315c72d9d96b24aa4a75dd"
-  dependencies:
-    clone-deep "^0.3.0"
-
-snyk-module@1.8.1, snyk-module@^1.6.0, snyk-module@^1.8.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/snyk-module/-/snyk-module-1.8.1.tgz#31d5080fb1c0dfd6fa8567dd34a523fd02bf1fca"
-  dependencies:
-    debug "^2.2.0"
-    hosted-git-info "^2.1.4"
-
-snyk-mvn-plugin@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-1.1.1.tgz#15c13131a368dde487763de93557ad5fb9572ffe"
-
-snyk-nuget-plugin@1.3.9:
-  version "1.3.9"
-  resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.3.9.tgz#bcdc503eafe9f3eeb4024b756ded4d0c3b265d12"
-  dependencies:
-    debug "^3.1.0"
-    es6-promise "^4.1.1"
-    xml2js "^0.4.17"
-    zip "^1.2.0"
-
-snyk-php-plugin@1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/snyk-php-plugin/-/snyk-php-plugin-1.3.2.tgz#51c19171dee0cd35158a7aa835fe02a97dc84ab8"
-  dependencies:
-    debug "^3.1.0"
-
-snyk-policy@^1.10.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/snyk-policy/-/snyk-policy-1.10.2.tgz#2a7bf0f07c7b811b9dda93cf9bbb10dc992dd7bc"
-  dependencies:
-    debug "^2.2.0"
-    email-validator "^1.1.1"
-    es6-promise "^3.1.2"
-    js-yaml "^3.5.3"
-    lodash.clonedeep "^4.3.1"
-    semver "^5.1.0"
-    snyk-module "^1.8.1"
-    snyk-resolve "^1.0.0"
-    snyk-try-require "^1.1.1"
-    then-fs "^2.0.0"
-
-snyk-python-plugin@1.5.7:
-  version "1.5.7"
-  resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.5.7.tgz#fe45da46b59becec6e41f34023948246778ebc3e"
+smart-buffer@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.1.tgz#07ea1ca8d4db24eb4cac86537d7d18995221ace3"
 
-snyk-resolve-deps@1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/snyk-resolve-deps/-/snyk-resolve-deps-1.7.0.tgz#13743a058437dff890baaf437c333c966a743cb6"
+snapdragon-node@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
   dependencies:
-    abbrev "^1.0.7"
-    ansicolors "^0.3.2"
-    clite "^0.3.0"
-    debug "^2.2.0"
-    es6-promise "^3.0.2"
-    lodash "^4.0.0"
-    lru-cache "^4.0.0"
-    minimist "^1.2.0"
-    semver "^5.1.0"
-    snyk-module "^1.6.0"
-    snyk-resolve "^1.0.0"
-    snyk-tree "^1.0.0"
-    snyk-try-require "^1.1.1"
-    then-fs "^2.0.0"
+    define-property "^1.0.0"
+    isobject "^3.0.0"
+    snapdragon-util "^3.0.1"
 
-snyk-resolve@1.0.0, snyk-resolve@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/snyk-resolve/-/snyk-resolve-1.0.0.tgz#bbe9196d37f57c39251e6be75ccdd5b2097e99a2"
+snapdragon-util@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
   dependencies:
-    debug "^2.2.0"
-    then-fs "^2.0.0"
+    kind-of "^3.2.0"
 
-snyk-sbt-plugin@1.2.5:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/snyk-sbt-plugin/-/snyk-sbt-plugin-1.2.5.tgz#e86a3b4e727d206f7e41154b0dd2019b16102360"
+snapdragon@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
   dependencies:
+    base "^0.11.1"
     debug "^2.2.0"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    map-cache "^0.2.2"
+    source-map "^0.5.6"
+    source-map-resolve "^0.5.0"
+    use "^3.1.0"
 
-snyk-tree@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/snyk-tree/-/snyk-tree-1.0.0.tgz#0fb73176dbf32e782f19100294160448f9111cc8"
-  dependencies:
-    archy "^1.0.0"
-
-snyk-try-require@^1.1.1, snyk-try-require@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/snyk-try-require/-/snyk-try-require-1.2.0.tgz#30fc2b11c07064591ee35780c826be91312f2144"
+sntp@0.2.x:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/sntp/-/sntp-0.2.4.tgz#fb885f18b0f3aad189f824862536bceeec750900"
   dependencies:
-    debug "^2.2.0"
-    es6-promise "^3.1.2"
-    lodash.clonedeep "^4.3.0"
-    lru-cache "^4.0.0"
-    then-fs "^2.0.0"
+    hoek "0.9.x"
 
-snyk@^1.25.1:
-  version "1.70.3"
-  resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.70.3.tgz#53c98949d27bf57905eeb7beed94d360a24fff55"
+sntp@1.x.x:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
   dependencies:
-    abbrev "^1.0.7"
-    ansi-escapes "^1.3.0"
-    chalk "^1.1.1"
-    configstore "^1.2.0"
-    debug "^3.1.0"
-    es6-promise "^3.0.2"
-    hasbin "^1.2.3"
-    inquirer "1.0.3"
-    needle "^2.0.1"
-    open "^0.0.5"
-    os-name "^1.0.3"
-    proxy-from-env "^1.0.0"
-    recursive-readdir "^2.2.1"
-    semver "^5.1.0"
-    snyk-config "1.0.1"
-    snyk-go-plugin "1.4.5"
-    snyk-gradle-plugin "1.2.0"
-    snyk-module "1.8.1"
-    snyk-mvn-plugin "1.1.1"
-    snyk-nuget-plugin "1.3.9"
-    snyk-php-plugin "1.3.2"
-    snyk-policy "^1.10.2"
-    snyk-python-plugin "1.5.7"
-    snyk-resolve "1.0.0"
-    snyk-resolve-deps "1.7.0"
-    snyk-sbt-plugin "1.2.5"
-    snyk-tree "^1.0.0"
-    snyk-try-require "^1.2.0"
-    tempfile "^1.1.1"
-    then-fs "^2.0.0"
-    undefsafe "0.0.3"
-    update-notifier "^0.5.0"
-    url "^0.11.0"
-    uuid "^3.0.1"
+    hoek "2.x.x"
 
 socks-proxy-agent@^3.0.1:
   version "3.0.1"
@@ -9264,6 +8886,13 @@ socks-proxy-agent@^3.0.1:
     agent-base "^4.1.0"
     socks "^1.1.10"
 
+socks-proxy-agent@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473"
+  dependencies:
+    agent-base "~4.2.0"
+    socks "~2.2.0"
+
 socks@^1.1.10:
   version "1.1.10"
   resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a"
@@ -9271,6 +8900,13 @@ socks@^1.1.10:
     ip "^1.1.4"
     smart-buffer "^1.0.13"
 
+socks@~2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.0.tgz#144985b3331ced3ab5ccbee640ab7cb7d43fdd1f"
+  dependencies:
+    ip "^1.1.5"
+    smart-buffer "^4.0.1"
+
 sort-asc@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9"
@@ -9307,55 +8943,87 @@ source-list-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
 
+source-map-resolve@^0.5.0:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+  dependencies:
+    atob "^2.1.1"
+    decode-uri-component "^0.2.0"
+    resolve-url "^0.2.1"
+    source-map-url "^0.4.0"
+    urix "^0.1.0"
+
 source-map-support@^0.4.15:
   version "0.4.18"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
   dependencies:
     source-map "^0.5.6"
 
+source-map-url@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+
 source-map@0.4.x, source-map@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6:
+source-map@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.0.tgz#0fe96503ac86a5adb5de63f4e412ae4872cdbe86"
+
+source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
 
-"source-map@>= 0.1.2", source-map@^0.6.1, source-map@~0.6.1:
+source-map@^0.6.1, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
-source-map@^0.1.34, source-map@~0.1.33:
-  version "0.1.43"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
-  dependencies:
-    amdefine ">=0.0.4"
-
 source-map@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
   dependencies:
     amdefine ">=0.0.4"
 
+space-separated-tokens@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412"
+  dependencies:
+    trim "0.0.1"
+
 spawn-please@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/spawn-please/-/spawn-please-0.3.0.tgz#db338ec4cff63abc69f1d0e08cee9eb8bebd9d11"
 
-spdx-correct@~1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+spdx-correct@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"
   dependencies:
-    spdx-license-ids "^1.0.2"
+    spdx-expression-parse "^3.0.0"
+    spdx-license-ids "^3.0.0"
 
-spdx-expression-parse@~1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+spdx-exceptions@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9"
 
-spdx-license-ids@^1.0.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+spdx-expression-parse@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
+
+split-string@^3.0.1, split-string@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+  dependencies:
+    extend-shallow "^3.0.0"
 
 split@~0.2.10:
   version "0.2.10"
@@ -9376,56 +9044,65 @@ srcdoc-polyfill@^1.0.0:
   resolved "https://registry.yarnpkg.com/srcdoc-polyfill/-/srcdoc-polyfill-1.0.0.tgz#81b6d79131f33231ea0f205c9236be90e9aca718"
 
 sshpk@^1.7.0:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+  version "1.14.2"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
   dependencies:
     asn1 "~0.2.3"
     assert-plus "^1.0.0"
     dashdash "^1.12.0"
     getpass "^0.1.1"
+    safer-buffer "^2.0.2"
   optionalDependencies:
     bcrypt-pbkdf "^1.0.0"
     ecc-jsbn "~0.1.1"
     jsbn "~0.1.0"
     tweetnacl "~0.14.0"
 
-ssri@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.1.0.tgz#2cbf1df36b74d0fc91fcf89640a4b3e1d10b1899"
-  dependencies:
-    safe-buffer "^5.1.0"
-
 ssri@^5.2.4:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
   dependencies:
     safe-buffer "^5.1.1"
 
+ssri@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.0.tgz#fc21bfc90e03275ac3e23d5a42e38b8a1cbc130d"
+
 state-toggle@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.1.tgz#c3cb0974f40a6a0f8e905b96789eb41afa1cde3a"
 
-static-eval@~0.2.0:
-  version "0.2.4"
-  resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-0.2.4.tgz#b7d34d838937b969f9641ca07d48f8ede263ea7b"
+static-eval@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.0.tgz#0e821f8926847def7b4b50cda5d55c04a9b13864"
   dependencies:
-    escodegen "~0.0.24"
+    escodegen "^1.8.1"
 
-static-module@^1.1.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/static-module/-/static-module-1.5.0.tgz#27da9883c41a8cd09236f842f0c1ebc6edf63d86"
+static-extend@^0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+  dependencies:
+    define-property "^0.2.5"
+    object-copy "^0.1.0"
+
+static-module@^2.2.0:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/static-module/-/static-module-2.2.5.tgz#bd40abceae33da6b7afb84a0e4329ff8852bfbbf"
   dependencies:
     concat-stream "~1.6.0"
-    duplexer2 "~0.0.2"
-    escodegen "~1.3.2"
+    convert-source-map "^1.5.1"
+    duplexer2 "~0.1.4"
+    escodegen "~1.9.0"
     falafel "^2.1.0"
-    has "^1.0.0"
-    object-inspect "~0.4.0"
-    quote-stream "~0.0.0"
-    readable-stream "~1.0.27-1"
+    has "^1.0.1"
+    magic-string "^0.22.4"
+    merge-source-map "1.0.4"
+    object-inspect "~1.4.0"
+    quote-stream "~1.0.2"
+    readable-stream "~2.3.3"
     shallow-copy "~0.0.1"
-    static-eval "~0.2.0"
-    through2 "~0.4.1"
+    static-eval "^2.0.0"
+    through2 "~2.0.3"
 
 stream-browserify@^2.0.1:
   version "2.0.1"
@@ -9441,13 +9118,13 @@ stream-each@^1.1.0:
     end-of-stream "^1.1.0"
     stream-shift "^1.0.0"
 
-stream-http@^2.3.1:
-  version "2.7.2"
-  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
+stream-http@^2.7.2:
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
   dependencies:
     builtin-status-codes "^3.0.0"
     inherits "^2.0.1"
-    readable-stream "^2.2.6"
+    readable-stream "^2.3.6"
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
@@ -9472,6 +9149,10 @@ strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
 
+strict-uri-encode@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
+
 strictdom@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/strictdom/-/strictdom-1.0.1.tgz#189de91649f73d44d59b8432efa68ef9d2659460"
@@ -9480,13 +9161,7 @@ string-hash@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
 
-string-length@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
-  dependencies:
-    strip-ansi "^3.0.0"
-
-string-width@^1.0.1, string-width@^1.0.2:
+string-width@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
   dependencies:
@@ -9494,32 +9169,26 @@ string-width@^1.0.1, string-width@^1.0.2:
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
-string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   dependencies:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
-string_decoder@^0.10.25, string_decoder@~0.10.x:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-
-string_decoder@~1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
-  dependencies:
-    safe-buffer "~5.1.0"
-
-string_decoder@~1.1.1:
+string_decoder@^1.0.0, string_decoder@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
   dependencies:
     safe-buffer "~5.1.0"
 
-stringstream@~0.0.4, stringstream@~0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+string_decoder@~0.10.x:
+  version "0.10.31"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+stringstream@~0.0.4:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
 
 strip-ansi@^3.0.0, strip-ansi@^3.0.1, strip-ansi@~3.0.1:
   version "3.0.1"
@@ -9541,12 +9210,6 @@ strip-bom-string@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
 
-strip-bom@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
-  dependencies:
-    is-utf8 "^0.2.0"
-
 strip-bom@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -9559,14 +9222,14 @@ strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
-style-loader@^0.18.2:
-  version "0.18.2"
-  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.18.2.tgz#cc31459afbcd6d80b7220ee54b291a9fd66ff5eb"
+style-loader@^0.21.0:
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852"
   dependencies:
-    loader-utils "^1.0.2"
-    schema-utils "^0.3.0"
+    loader-utils "^1.1.0"
+    schema-utils "^0.4.5"
 
-supercluster@^2.0.1, supercluster@^2.3.0:
+supercluster@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-2.3.0.tgz#87ab56081bbea9a1d724df5351ee9e8c3af2f48b"
   dependencies:
@@ -9594,15 +9257,15 @@ supports-color@^3.1.2, supports-color@^3.2.3:
   dependencies:
     has-flag "^1.0.0"
 
-supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
+supports-color@^4.2.1:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
   dependencies:
     has-flag "^2.0.0"
 
-supports-color@^5.3.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
+supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0:
+  version "5.4.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
   dependencies:
     has-flag "^3.0.0"
 
@@ -9623,8 +9286,8 @@ svgo@^0.7.0:
     whet.extend "~0.9.9"
 
 symbol-observable@^1.0.3, symbol-observable@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
 
 symbol-tree@^3.2.1:
   version "3.2.2"
@@ -9645,20 +9308,11 @@ tapable@^0.2.7:
   version "0.2.8"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
 
-tar-pack@^3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
-  dependencies:
-    debug "^2.2.0"
-    fstream "^1.0.10"
-    fstream-ignore "^1.0.5"
-    once "^1.3.3"
-    readable-stream "^2.1.4"
-    rimraf "^2.5.1"
-    tar "^2.2.1"
-    uid-number "^0.0.6"
+tapable@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
 
-tar@^2.0.0, tar@^2.2.1, tar@~2.2.1:
+tar@^2.0.0, tar@~2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
   dependencies:
@@ -9666,32 +9320,25 @@ tar@^2.0.0, tar@^2.2.1, tar@~2.2.1:
     fstream "^1.0.2"
     inherits "2"
 
-tar@^4.4.0:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749"
+tar@^4, tar@^4.4.1, tar@^4.4.3:
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd"
   dependencies:
     chownr "^1.0.1"
     fs-minipass "^1.2.5"
-    minipass "^2.2.4"
+    minipass "^2.3.3"
     minizlib "^1.1.0"
     mkdirp "^0.5.0"
-    safe-buffer "^5.1.1"
+    safe-buffer "^5.1.2"
     yallist "^3.0.2"
 
-tempfile@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2"
-  dependencies:
-    os-tmpdir "^1.0.0"
-    uuid "^2.0.1"
-
 term-size@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
   dependencies:
     execa "^0.7.0"
 
-text-encoding@0.6.4, text-encoding@^0.6.4:
+text-encoding@^0.6.4:
   version "0.6.4"
   resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
 
@@ -9699,26 +9346,13 @@ text-table@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
 
-then-fs@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/then-fs/-/then-fs-2.0.0.tgz#72f792dd9d31705a91ae19ebfcf8b3f968c81da2"
-  dependencies:
-    promise ">=3.2 <8"
-
-through2@^2.0.0, through2@^2.0.3:
+through2@^2.0.0, through2@^2.0.3, through2@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
   dependencies:
     readable-stream "^2.1.5"
     xtend "~4.0.1"
 
-through2@~0.4.1:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b"
-  dependencies:
-    readable-stream "~1.0.17"
-    xtend "~2.1.1"
-
 through2@~0.6.3:
   version "0.6.5"
   resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
@@ -9726,39 +9360,35 @@ through2@~0.6.3:
     readable-stream ">=1.0.33-1 <1.1.0-0"
     xtend ">=4.0.0 <4.1.0-0"
 
-through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.7, through@^2.3.8, through@~2.3.4:
+through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3.4:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
 
-timed-out@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a"
-
-timed-out@^3.0.0:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217"
-
 timed-out@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
 
-timers-browserify@^2.0.2:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
+timers-browserify@^2.0.4:
+  version "2.0.10"
+  resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
   dependencies:
     setimmediate "^1.0.4"
 
-tiny-emitter@2.0.2:
+tiny-emitter@2.0.2, tiny-emitter@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
 
+tiny-relative-date@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
+
 tinycolor2@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
 
 tinyqueue@^1.1.0:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.2.tgz#947229e5e4197aba988acd27751dcc582e6728ff"
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d"
 
 tmp@^0.0.33:
   version "0.0.33"
@@ -9774,14 +9404,32 @@ to-fast-properties@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
 
+to-object-path@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+  dependencies:
+    kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+  dependencies:
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+  dependencies:
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    regex-not "^1.0.2"
+    safe-regex "^1.1.0"
+
 to-utf8@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/to-utf8/-/to-utf8-0.0.1.tgz#d17aea72ff2fba39b9e43601be7b3ff72e089852"
 
-toml@^2.3.2:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.3.tgz#8d683d729577cb286231dfc7a8affe58d31728fb"
-
 topojson@^1.6.19:
   version "1.6.27"
   resolved "https://registry.yarnpkg.com/topojson/-/topojson-1.6.27.tgz#adbe33a67e2f1673d338df12644ad20fc20b42ed"
@@ -9793,9 +9441,16 @@ topojson@^1.6.19:
     rw "1"
     shapefile "0.3"
 
-tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
+tough-cookie@^2.3.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.2.tgz#aa9133154518b494efab98a58247bfc38818c00c"
+  dependencies:
+    psl "^1.1.24"
+    punycode "^1.4.1"
+
+tough-cookie@~2.3.0, tough-cookie@~2.3.3:
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
   dependencies:
     punycode "^1.4.1"
 
@@ -9829,10 +9484,6 @@ trough@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.2.tgz#7f1663ec55c480139e2de5e486c6aef6cc24a535"
 
-tryit@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
-
 tty-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -9861,9 +9512,9 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-detect@^4.0.0:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea"
+type-detect@^4.0.0, type-detect@^4.0.5:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
 
 typed-function@0.10.7:
   version "0.10.7"
@@ -9873,13 +9524,13 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
-ua-parser-js@^0.7.9:
-  version "0.7.14"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca"
+ua-parser-js@^0.7.18:
+  version "0.7.18"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
 
 uglify-es@^3.3.4:
-  version "3.3.8"
-  resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.8.tgz#f2c68e6cff0d0f9dc9577e4da207151c2e753b7e"
+  version "3.3.9"
+  resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
   dependencies:
     commander "~2.13.0"
     source-map "~0.6.1"
@@ -9906,19 +9557,19 @@ uglifyjs-webpack-plugin@^0.4.6:
     webpack-sources "^1.0.1"
 
 uglifyjs-webpack-plugin@^1.1.0:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.1.6.tgz#f4ba8449edcf17835c18ba6ae99b9d610857fb19"
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.6.tgz#f4bb44f02431e82b301d8d4624330a6a35729381"
   dependencies:
-    cacache "^10.0.1"
+    cacache "^10.0.4"
     find-cache-dir "^1.0.0"
-    schema-utils "^0.4.2"
+    schema-utils "^0.4.5"
     serialize-javascript "^1.4.0"
     source-map "^0.6.1"
     uglify-es "^3.3.4"
     webpack-sources "^1.1.0"
     worker-farm "^1.5.2"
 
-uid-number@0.0.6, uid-number@^0.0.6:
+uid-number@0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
@@ -9926,42 +9577,15 @@ umask@^1.1.0, umask@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
 
-unassert@^1.3.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/unassert/-/unassert-1.5.1.tgz#cbc88ec387417c5a5e4c02d3cd07be98bd75ff76"
-  dependencies:
-    acorn "^4.0.0"
-    call-matcher "^1.0.1"
-    deep-equal "^1.0.0"
-    espurify "^1.3.0"
-    estraverse "^4.1.0"
-    esutils "^2.0.2"
-    object-assign "^4.1.0"
-
-unassertify@^2.0.0:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/unassertify/-/unassertify-2.0.4.tgz#b3ca2ba5f29b4836e35a6dd77e5b20f6dbbf8e52"
-  dependencies:
-    acorn "^4.0.0"
-    convert-source-map "^1.1.1"
-    escodegen "^1.6.1"
-    multi-stage-sourcemap "^0.2.1"
-    through "^2.3.7"
-    unassert "^1.3.1"
-
 uncontrollable@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-4.1.0.tgz#e0358291252e1865222d90939b19f2f49f81c1a9"
   dependencies:
     invariant "^2.1.0"
 
-undefsafe@0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f"
-
 underscore@^1.8.3:
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
 
 underscore@~1.4.4:
   version "1.4.4"
@@ -9971,13 +9595,6 @@ underscore@~1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
 
-unflowify@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/unflowify/-/unflowify-1.0.1.tgz#a2ea0d25c0affcc46955e6473575f7c5a1f4a696"
-  dependencies:
-    flow-remove-types "^1.1.2"
-    through "^2.3.8"
-
 unherit@^1.0.4:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c"
@@ -9996,16 +9613,19 @@ unified@^6.1.5:
     vfile "^2.0.0"
     x-is-string "^0.1.0"
 
+union-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+  dependencies:
+    arr-union "^3.1.0"
+    get-value "^2.0.6"
+    is-extendable "^0.1.1"
+    set-value "^0.4.3"
+
 uniq@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
 
-uniqid@^4.0.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1"
-  dependencies:
-    macaddress "^0.2.8"
-
 uniqs@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
@@ -10052,52 +9672,22 @@ unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 
-unzip-response@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe"
+unset-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+  dependencies:
+    has-value "^0.3.1"
+    isobject "^3.0.0"
 
 unzip-response@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
 
-update-notifier@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc"
-  dependencies:
-    chalk "^1.0.0"
-    configstore "^1.0.0"
-    is-npm "^1.0.0"
-    latest-version "^1.0.0"
-    repeating "^1.1.2"
-    semver-diff "^2.0.0"
-    string-length "^1.0.0"
-
-update-notifier@^0.6.0:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.6.3.tgz#776dec8daa13e962a341e8a1d98354306b67ae08"
-  dependencies:
-    boxen "^0.3.1"
-    chalk "^1.0.0"
-    configstore "^2.0.0"
-    is-npm "^1.0.0"
-    latest-version "^2.0.0"
-    semver-diff "^2.0.0"
-
-update-notifier@^2.2.0, update-notifier@~2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451"
-  dependencies:
-    boxen "^1.2.1"
-    chalk "^2.0.1"
-    configstore "^3.0.0"
-    import-lazy "^2.1.0"
-    is-installed-globally "^0.1.0"
-    is-npm "^1.0.0"
-    latest-version "^3.0.0"
-    semver-diff "^2.0.0"
-    xdg-basedir "^3.0.0"
+upath@^1.0.5:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
 
-update-notifier@^2.3.0:
+update-notifier@^2.2.0, update-notifier@^2.3.0, update-notifier@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
   dependencies:
@@ -10112,9 +9702,19 @@ update-notifier@^2.3.0:
     semver-diff "^2.0.0"
     xdg-basedir "^3.0.0"
 
+uri-js@^4.2.1:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  dependencies:
+    punycode "^2.1.0"
+
 urijs@^1.18.10:
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.0.tgz#d8aa284d0e7469703a6988ad045c4cbfdf08ada0"
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a"
+
+urix@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
 
 url-loader@^0.6.2:
   version "0.6.2"
@@ -10137,6 +9737,12 @@ url@^0.11.0:
     punycode "1.3.2"
     querystring "0.2.0"
 
+use@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
+  dependencies:
+    kind-of "^6.0.2"
+
 user-home@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
@@ -10149,21 +9755,19 @@ util-extend@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f"
 
-util@0.10.3, util@^0.10.3:
+util@0.10.3:
   version "0.10.3"
   resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
   dependencies:
     inherits "2.0.1"
 
-uuid@^2.0.1:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
-
-uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+util@^0.10.3:
+  version "0.10.4"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+  dependencies:
+    inherits "2.0.3"
 
-uuid@^3.2.1:
+uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
 
@@ -10173,12 +9777,12 @@ v8flags@^2.1.1:
   dependencies:
     user-home "^1.1.1"
 
-validate-npm-package-license@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
   dependencies:
-    spdx-correct "~1.0.0"
-    spdx-expression-parse "~1.0.0"
+    spdx-correct "^3.0.0"
+    spdx-expression-parse "^3.0.0"
 
 validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0:
   version "3.0.0"
@@ -10192,15 +9796,9 @@ validate-npm-package-name@~2.2.2:
   dependencies:
     builtins "0.0.7"
 
-vector-tile@^1.1.3, vector-tile@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/vector-tile/-/vector-tile-1.3.0.tgz#06d516a83b063f04c82ef539cf1bb1aebeb696b4"
-  dependencies:
-    point-geometry "0.0.0"
-
 vendors@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801"
 
 verror@1.10.0:
   version "1.10.0"
@@ -10229,22 +9827,13 @@ vfile@^2.0.0:
     unist-util-stringify-position "^1.0.0"
     vfile-message "^1.0.0"
 
-viewport-mercator-project@^4.0.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-4.1.1.tgz#92b0611fa0041041d2f3568da3529a8a846017d0"
-  dependencies:
-    gl-mat4 "^1.1.4"
-    gl-vec2 "^1.0.0"
-    gl-vec3 "^1.0.3"
-    gl-vec4 "^1.0.1"
-
-viewport-mercator-project@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-5.0.0.tgz#d18709dad652581108a6dd58f320409491d40982"
+viewport-mercator-project@^5.0.0, viewport-mercator-project@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-5.1.0.tgz#68bc5586988c2808d1456e1eff950d424eccec16"
   dependencies:
-    math.gl "^1.0.0"
+    math.gl "^1.1.0"
 
-vlq@^0.2.1:
+vlq@^0.2.2:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
 
@@ -10254,26 +9843,14 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
-vt-pbf@^2.0.2:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-2.1.4.tgz#b5df7c3f9706156e0b9881a99dcb05635740b522"
-  dependencies:
-    pbf "^1.3.2"
-    point-geometry "0.0.0"
-    vector-tile "^1.1.3"
-
 vt-pbf@^3.0.1:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.0.tgz#d7e63f585b362cbff6b84fcd052c159112e0acc7"
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.1.tgz#b0f627e39a10ce91d943b898ed2363d21899fb82"
   dependencies:
     "@mapbox/point-geometry" "0.1.0"
-    "@mapbox/vector-tile" "^1.3.0"
+    "@mapbox/vector-tile" "^1.3.1"
     pbf "^3.0.5"
 
-w3c-blob@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8"
-
 warning@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
@@ -10281,12 +9858,12 @@ warning@^3.0.0:
     loose-envify "^1.0.0"
 
 watchpack@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
   dependencies:
-    async "^2.1.2"
-    chokidar "^1.7.0"
+    chokidar "^2.0.2"
     graceful-fs "^4.1.2"
+    neo-async "^2.5.0"
 
 wcwidth@^1.0.0:
   version "1.0.1"
@@ -10306,35 +9883,29 @@ webidl-conversions@^4.0.0:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
 
-webpack-manifest-plugin@1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz#5ea8ee5756359ddc1d98814324fe43496349a7d4"
+webpack-manifest-plugin@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.0.3.tgz#b42c5b08a0319cedb3ec45d9375a9ecee0acf5eb"
   dependencies:
     fs-extra "^0.30.0"
     lodash ">=3.5 <5"
+    tapable "^1.0.0"
 
-webpack-sources@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
-  dependencies:
-    source-list-map "^2.0.0"
-    source-map "~0.5.3"
-
-webpack-sources@^1.1.0:
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
   dependencies:
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
-webpack@^3.8.1:
-  version "3.10.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.10.0.tgz#5291b875078cf2abf42bdd23afe3f8f96c17d725"
+webpack@^3.10.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.12.0.tgz#3f9e34360370602fcf639e97939db486f4ec0d74"
   dependencies:
     acorn "^5.0.0"
     acorn-dynamic-import "^2.0.0"
-    ajv "^5.1.5"
-    ajv-keywords "^2.0.0"
+    ajv "^6.1.0"
+    ajv-keywords "^3.1.0"
     async "^2.1.2"
     enhanced-resolve "^3.4.0"
     escope "^3.6.0"
@@ -10354,31 +9925,23 @@ webpack@^3.8.1:
     webpack-sources "^1.0.1"
     yargs "^8.0.2"
 
-webworkify-webpack@2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/webworkify-webpack/-/webworkify-webpack-2.1.0.tgz#2cebf41846807388c0312238dc3c3f6356ce22a9"
-
-webworkify@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.4.0.tgz#71245d1e34cacf54e426bd955f8cc6ee12d024c2"
-
-webworkify@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
+webworkify-webpack@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/webworkify-webpack/-/webworkify-webpack-2.1.2.tgz#319dd5d73b3137974a4ae815b7388d0ad28b7276"
 
 wgs84@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/wgs84/-/wgs84-0.0.0.tgz#34fdc555917b6e57cf2a282ed043710c049cdc76"
 
 whatwg-encoding@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3"
   dependencies:
-    iconv-lite "0.4.13"
+    iconv-lite "0.4.19"
 
 whatwg-fetch@>=0.10.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
 
 whatwg-url@^4.3.0:
   version "4.8.0"
@@ -10391,17 +9954,13 @@ whet.extend@~0.9.9:
   version "0.9.9"
   resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
 
-which-module@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
-
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
 
 which@1, which@^1.1.1, which@^1.2.9, which@^1.3.0, which@~1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   dependencies:
     isexe "^2.0.0"
 
@@ -10412,16 +9971,10 @@ which@1.2.x, which@~1.2.11:
     isexe "^2.0.0"
 
 wide-align@^1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
-  dependencies:
-    string-width "^1.0.2"
-
-widest-line@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c"
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
   dependencies:
-    string-width "^1.0.1"
+    string-width "^1.0.2 || 2"
 
 widest-line@^2.0.0:
   version "2.0.0"
@@ -10429,24 +9982,10 @@ widest-line@^2.0.0:
   dependencies:
     string-width "^2.1.1"
 
-win-release@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/win-release/-/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209"
-  dependencies:
-    semver "^5.0.1"
-
 window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
-window-size@^0.1.1:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
-
-window-size@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
-
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
@@ -10459,14 +9998,7 @@ wordwrap@~0.0.2:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
 
-worker-farm@^1.5.2:
-  version "1.5.2"
-  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"
-  dependencies:
-    errno "^0.1.4"
-    xtend "^4.0.1"
-
-worker-farm@^1.5.4:
+worker-farm@^1.5.2, worker-farm@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
   dependencies:
@@ -10483,14 +10015,6 @@ wrappy@1, wrappy@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 
-write-file-atomic@^1.1.2:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
-  dependencies:
-    graceful-fs "^4.1.11"
-    imurmurhash "^0.1.4"
-    slide "^1.1.5"
-
 write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
@@ -10517,12 +10041,6 @@ x-is-string@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
 
-xdg-basedir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
-  dependencies:
-    os-homedir "^1.0.0"
-
 xdg-basedir@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
@@ -10531,17 +10049,6 @@ xml-name-validator@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
 
-xml2js@^0.4.17:
-  version "0.4.19"
-  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
-  dependencies:
-    sax ">=0.6.0"
-    xmlbuilder "~9.0.1"
-
-xmlbuilder@~9.0.1:
-  version "9.0.7"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
-
 "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -10553,12 +10060,6 @@ xtend@~2.0.5:
     is-object "~0.1.2"
     object-keys "~0.2.0"
 
-xtend@~2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
-  dependencies:
-    object-keys "~0.4.0"
-
 y18n@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
@@ -10575,13 +10076,6 @@ yallist@^3.0.0, yallist@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
 
-yargs-parser@^2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4"
-  dependencies:
-    camelcase "^3.0.0"
-    lodash.assign "^4.0.6"
-
 yargs-parser@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
@@ -10611,25 +10105,6 @@ yargs@^11.0.0:
     y18n "^3.2.1"
     yargs-parser "^9.0.2"
 
-yargs@^4.3.2:
-  version "4.8.1"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0"
-  dependencies:
-    cliui "^3.2.0"
-    decamelize "^1.1.1"
-    get-caller-file "^1.0.1"
-    lodash.assign "^4.0.3"
-    os-locale "^1.4.0"
-    read-pkg-up "^1.0.1"
-    require-directory "^2.1.1"
-    require-main-filename "^1.0.1"
-    set-blocking "^2.0.0"
-    string-width "^1.0.1"
-    which-module "^1.0.0"
-    window-size "^0.2.0"
-    y18n "^3.2.1"
-    yargs-parser "^2.4.1"
-
 yargs@^8.0.2:
   version "8.0.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
@@ -10656,18 +10131,3 @@ yargs@~3.10.0:
     cliui "^2.1.0"
     decamelize "^1.0.0"
     window-size "0.1.0"
-
-yargs@~3.15.0:
-  version "3.15.0"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.15.0.tgz#3d9446ef21fb3791b3985690662e4b9683c7f181"
-  dependencies:
-    camelcase "^1.0.2"
-    cliui "^2.1.0"
-    decamelize "^1.0.0"
-    window-size "^0.1.1"
-
-zip@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/zip/-/zip-1.2.0.tgz#ad0ad42265309be42eb56fc86194e17c24e66a9c"
-  dependencies:
-    bops "~0.1.1"


[incubator-superset] 22/26: [dash builder fix] combine markdown and slice name, slice picker height (#5165)

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

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

commit 086034199843c927be621bc7f9461922cb2c71f0
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Fri Jun 15 11:10:01 2018 -0700

    [dash builder fix] combine markdown and slice name, slice picker height (#5165)
    
    * combine markdown code and markdown slice name
    
    * allow dynamic height for slice picker cell
    
    * add word break for long datasource name
---
 .../assets/src/dashboard/actions/sliceEntities.js  |  4 +-
 .../assets/src/dashboard/components/SliceAdder.jsx | 44 +++++++++++++++-------
 .../src/dashboard/reducers/getInitialState.js      |  1 +
 .../dashboard/stylesheets/builder-sidepane.less    | 11 +++---
 .../src/dashboard/util/dashboardLayoutConverter.js | 10 ++---
 superset/models/core.py                            |  1 +
 superset/models/helpers.py                         |  2 +-
 7 files changed, 47 insertions(+), 26 deletions(-)

diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index b635ea0..516a514 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -56,7 +56,9 @@ export function fetchAllSlices(userId) {
                 description: slice.description,
                 description_markdown: slice.description_markeddown,
                 viz_type: slice.viz_type,
-                modified: slice.modified,
+                modified: slice.modified
+                  ? slice.modified.replace(/<[^>]*>/g, '')
+                  : '',
               };
             }
           });
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 9e68278..ed652c0 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -2,7 +2,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
-import { List } from 'react-virtualized';
+import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
 import SearchInput, { createFilter } from 'react-search-input';
 
 import AddSliceCard from './AddSliceCard';
@@ -42,6 +42,12 @@ const KEYS_TO_SORT = [
 const MARGIN_BOTTOM = 16;
 const SIDEPANE_HEADER_HEIGHT = 55;
 const SLICE_ADDER_CONTROL_HEIGHT = 64;
+const DEFAULT_CELL_HEIGHT = 136;
+
+const cache = new CellMeasurerCache({
+  defaultHeight: DEFAULT_CELL_HEIGHT,
+  fixedWidth: true,
+});
 
 class SliceAdder extends React.Component {
   static sortByComparator(attr) {
@@ -133,7 +139,7 @@ class SliceAdder extends React.Component {
     });
   }
 
-  rowRenderer({ key, index, style }) {
+  rowRenderer({ key, index, style, parent }) {
     const { filteredSlices, selectedSliceIdsSet } = this.state;
     const cellData = filteredSlices[index];
     const isSelected = selectedSliceIdsSet.has(cellData.slice_id);
@@ -160,19 +166,28 @@ class SliceAdder extends React.Component {
         // we must use a custom drag preview within the List because
         // it does not seem to work within a fixed-position container
         useEmptyDragPreview
+        // List library expect style props here
+        // actual style should be applied to nested AddSliceCard component
+        style={{}}
       >
         {({ dragSourceRef }) => (
-          <AddSliceCard
-            innerRef={dragSourceRef}
-            style={style}
-            sliceName={cellData.slice_name}
-            lastModified={
-              cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : ''
-            }
-            visType={cellData.viz_type}
-            datasourceLink={cellData.datasource_link}
-            isSelected={isSelected}
-          />
+          <CellMeasurer
+            cache={cache}
+            columnIndex={0}
+            key={key}
+            parent={parent}
+            rowIndex={index}
+          >
+            <AddSliceCard
+              innerRef={dragSourceRef}
+              style={style}
+              sliceName={cellData.slice_name}
+              lastModified={cellData.modified}
+              visType={cellData.viz_type}
+              datasourceLink={cellData.datasource_link}
+              isSelected={isSelected}
+            />
+          </CellMeasurer>
         )}
       </DragDroppable>
     );
@@ -223,7 +238,8 @@ class SliceAdder extends React.Component {
               width={376}
               height={slicesListHeight}
               rowCount={this.state.filteredSlices.length}
-              rowHeight={136}
+              deferredMeasurementCache={cache}
+              rowHeight={cache.rowHeight}
               rowRenderer={this.rowRenderer}
               searchTerm={this.state.searchTerm}
               sortBy={this.state.sortBy}
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index 7378c7b..e1cb6ba 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -93,6 +93,7 @@ export default function(bootstrapData) {
         datasource: slice.form_data.datasource,
         description: slice.description,
         description_markeddown: slice.description_markeddown,
+        modified: slice.modified ? slice.modified.replace(/<[^>]*>/g, '') : '',
       };
 
       sliceIds.add(key);
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index bbcb7e1..6250243 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -76,7 +76,6 @@
     .chart-card {
       border: 1px solid @gray-light;
       font-weight: 200;
-      height: 120px;
       padding: 16px;
       margin: 16px;
       position: relative;
@@ -88,6 +87,7 @@
     }
 
     .card-title {
+      margin-right: 60px;
       margin-bottom: 8px;
       font-weight: 800;
     }
@@ -97,10 +97,12 @@
       flex-direction: column;
 
       .item {
-        height: 18px;
+        span {
+          word-break: break-all;
 
-        span:first-child {
-          font-weight: 400;
+          &:first-child {
+            font-weight: 400;
+          }
         }
       }
     }
@@ -118,7 +120,6 @@
       text-transform: uppercase;
       position: absolute;
       padding: 4px 8px;
-      position: absolute;
       top: 32px;
       right: 32px;
       pointer-events: none;
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index c6c124b..c1d855c 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -88,13 +88,13 @@ function getChartHolder(item) {
     Math.round(size_y / GRID_RATIO * 100 / ROW_HEIGHT),
   );
   if (code !== undefined) {
-    let markdownContent = '';
-    if (slice_name) {
-      markdownContent = `##### **${slice_name}**\n`;
-    }
+    let markdownContent = ' '; // white-space markdown
     if (code) {
-      markdownContent += code;
+      markdownContent = code;
+    } else if (slice_name.trim()) {
+      markdownContent = `##### ${slice_name}`;
     }
+
     return {
       type: MARKDOWN_TYPE,
       id: `DASHBOARD_MARKDOWN_TYPE-${generateId()}`,
diff --git a/superset/models/core.py b/superset/models/core.py
index faf0a6f..02109e7 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -189,6 +189,7 @@ class Slice(Model, AuditMixinNullable, ImportMixin):
             'slice_id': self.id,
             'slice_name': self.slice_name,
             'slice_url': self.slice_url,
+            'modified': self.modified(),
         }
 
     @property
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index dd2a13b..4b2976a 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -280,7 +280,7 @@ class AuditMixinNullable(AuditMixin):
         return Markup(
             '<span class="no-wrap">{}</span>'.format(self.changed_on))
 
-    @renders('changed_on')
+    @renders('modified')
     def modified(self):
         s = humanize.naturaltime(datetime.now() - self.changed_on)
         return Markup('<span class="no-wrap">{}</span>'.format(s))


[incubator-superset] 16/26: Fix: update slices list when add/remove multiple slices (#5138)

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

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

commit a61f6f7d0483fd80c7d272e93f5c97a01d935e4e
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue Jun 5 10:40:56 2018 -0700

    Fix: update slices list when add/remove multiple slices (#5138)
---
 superset/assets/src/dashboard/components/Dashboard.jsx | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 76f4b54..62bcbb5 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -129,16 +129,24 @@ class Dashboard extends React.PureComponent {
 
     if (currentChartIds.length < nextChartIds.length) {
       // adding new chart
-      const newChartId = nextChartIds.find(
+      const newChartIds = nextChartIds.filter(
         key => currentChartIds.indexOf(key) === -1,
       );
-      this.props.actions.addSliceToDashboard(newChartId);
+      if (newChartIds.length) {
+        newChartIds.forEach(newChartId =>
+          this.props.actions.addSliceToDashboard(newChartId),
+        );
+      }
     } else if (currentChartIds.length > nextChartIds.length) {
       // remove chart
-      const removedChartId = currentChartIds.find(
+      const removedChartIds = currentChartIds.filter(
         key => nextChartIds.indexOf(key) === -1,
       );
-      this.props.actions.removeSliceFromDashboard(removedChartId);
+      if (removedChartIds.length) {
+        removedChartIds.forEach(removedChartId =>
+          this.props.actions.removeSliceFromDashboard(removedChartId),
+        );
+      }
     }
   }
 


[incubator-superset] 06/26: Dashboard builder rebased + linted (#4849)

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

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

commit 5ff440b101ae2c3ee41e184fea2c594fb8d2bfec
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Thu Apr 19 15:27:23 2018 -0700

    Dashboard builder rebased + linted (#4849)
    
    * define dashboard redux state
    
    * update dashboard state reducer
    
    * dashboard layout converter + grid render
    
    * builder pane + slice adder
    
    * Dashboard header + slice header controls
    
    * fix linting
    
    * 2nd code review comments
---
 superset/assets/package.json                       |   1 +
 .../assets/spec/javascripts/chart/Chart_spec.jsx   |   2 +-
 .../spec/javascripts/dashboard/Dashboard_spec.jsx  |  96 ++++--
 .../assets/spec/javascripts/dashboard/fixtures.jsx |  10 +-
 .../spec/javascripts/dashboard/reducers_spec.js    |  23 +-
 superset/assets/src/chart/Chart.jsx                |  38 +--
 superset/assets/src/chart/ChartContainer.jsx       |   2 +-
 superset/assets/src/chart/chartAction.js           |  14 +-
 superset/assets/src/chart/chartReducer.js          |  35 +--
 superset/assets/src/dashboard/actions.js           | 127 --------
 .../assets/src/dashboard/actions/dashboardState.js | 166 ++++++++++
 .../assets/src/dashboard/actions/datasources.js    |  35 +++
 .../assets/src/dashboard/actions/sliceEntities.js  |  93 ++++++
 .../src/dashboard/components/ActionMenuItem.jsx    |  45 +++
 .../assets/src/dashboard/components/Controls.jsx   | 133 ++------
 .../assets/src/dashboard/components/Dashboard.jsx  | 301 +++++++-----------
 .../dashboard/components/DashboardContainer.jsx    |  52 +++-
 .../assets/src/dashboard/components/GridCell.jsx   |  49 +--
 .../assets/src/dashboard/components/GridLayout.jsx | 210 +++++--------
 .../assets/src/dashboard/components/Header.jsx     | 164 ++++++----
 .../dashboard/components/RefreshIntervalModal.jsx  |   7 +-
 .../assets/src/dashboard/components/SaveModal.jsx  |  34 +-
 .../assets/src/dashboard/components/SliceAdder.jsx | 341 ++++++++++-----------
 .../dashboard/components/SliceAdderContainer.jsx   |  25 ++
 .../src/dashboard/components/SliceHeader.jsx       | 123 ++------
 .../dashboard/components/SliceHeaderControls.jsx   | 106 +++++++
 superset/assets/src/dashboard/index.jsx            |  28 +-
 superset/assets/src/dashboard/reducers.js          | 214 -------------
 .../src/dashboard/reducers/dashboardState.js       | 128 ++++++++
 .../assets/src/dashboard/reducers/datasources.js   |  17 +
 .../src/dashboard/reducers/getInitialState.js      | 109 +++++++
 superset/assets/src/dashboard/reducers/index.js    |  22 ++
 .../assets/src/dashboard/reducers/sliceEntities.js |  62 ++++
 .../assets/src/dashboard/util/dashboardHelper.js   |   9 +
 .../src/dashboard/util/dashboardLayoutConverter.js | 322 +++++++++++++++++++
 .../src/dashboard/v2/actions/messageToasts.js      |   1 -
 .../v2/components/BuilderComponentPane.jsx         |  58 +++-
 .../dashboard/v2/components/DashboardBuilder.jsx   |  11 +-
 .../src/dashboard/v2/components/DashboardGrid.jsx  |   3 +-
 .../dashboard/v2/components/DashboardHeader.jsx    |   2 +-
 .../v2/components/dnd/dragDroppableConfig.js       |   1 +
 .../src/dashboard/v2/components/dnd/handleDrop.js  |   1 +
 .../gridComponents/{Chart.jsx => ChartHolder.jsx}  |  11 +-
 .../v2/components/gridComponents/Column.jsx        |  29 +-
 .../dashboard/v2/components/gridComponents/Row.jsx |  29 +-
 .../v2/components/gridComponents/index.js          |   6 +-
 .../dashboard/v2/containers/DashboardBuilder.jsx   |   6 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  13 +-
 .../src/dashboard/v2/containers/DashboardGrid.jsx  |   9 +-
 .../dashboard/v2/containers/DashboardHeader.jsx    |  43 ++-
 superset/assets/src/dashboard/v2/reducers/index.js |  11 +-
 .../dashboard/v2/stylesheets/builder-sidepane.less | 103 +++++++
 .../src/dashboard/v2/stylesheets/builder.less      |   3 +-
 .../dashboard/v2/stylesheets/components/chart.less |   3 +-
 superset/assets/src/dashboard/v2/util/constants.js |   2 +-
 .../src/dashboard/v2/util/newComponentFactory.js   |   3 +-
 .../src/dashboard/v2/util/newEntitiesFromDrop.js   |   3 +-
 .../assets/src/dashboard/v2/util/propShapes.jsx    |  50 ++-
 .../src/explore/components/ExploreChartHeader.jsx  |   6 +-
 .../src/explore/components/ExploreChartPanel.jsx   |   6 +-
 .../explore/components/ExploreViewContainer.jsx    |  16 +-
 superset/assets/src/explore/exploreUtils.js        |   2 +-
 superset/assets/src/explore/index.jsx              |   2 +-
 superset/assets/src/explore/reducers/index.js      |   5 +-
 superset/assets/src/modules/utils.js               |   1 -
 superset/assets/src/visualizations/table.css       |   9 +-
 .../stylesheets/{dashboard.css => dashboard.less}  | 116 +++----
 superset/assets/stylesheets/superset.less          |  16 +-
 superset/models/core.py                            |  13 +-
 superset/templates/superset/dashboard.html         |   1 +
 superset/views/core.py                             |  27 +-
 71 files changed, 2299 insertions(+), 1465 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 75f9504..d20dad7 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -102,6 +102,7 @@
     "react-map-gl": "^3.0.4",
     "react-redux": "^5.0.2",
     "react-resizable": "^1.3.3",
+    "react-search-input": "^0.11.3",
     "react-select": "1.2.1",
     "react-select-fast-filter-options": "^0.2.1",
     "react-sortable-hoc": "^0.8.3",
diff --git a/superset/assets/spec/javascripts/chart/Chart_spec.jsx b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
index b766d9f..29a2941 100644
--- a/superset/assets/spec/javascripts/chart/Chart_spec.jsx
+++ b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
@@ -20,7 +20,7 @@ describe('Chart', () => {
   };
   const mockedProps = {
     ...chart,
-    chartKey: 'slice_223',
+    id: 223,
     containerId: 'slice-container-223',
     datasource: {},
     formData: {},
diff --git a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
index c6e94d8..f4def13 100644
--- a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
@@ -1,61 +1,68 @@
+/* eslint camelcase: 0 */
 import React from 'react';
 import { shallow } from 'enzyme';
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 import sinon from 'sinon';
 
-import * as dashboardActions from '../../../src/dashboard/actions';
+import * as sliceActions from '../../../src/dashboard/actions/sliceEntities';
+import * as dashboardActions from '../../../src/dashboard/actions/dashboardState';
 import * as chartActions from '../../../src/chart/chartAction';
 import Dashboard from '../../../src/dashboard/components/Dashboard';
-import { defaultFilters, dashboard, charts } from './fixtures';
+import { defaultFilters, dashboardState, dashboardInfo, dashboardLayout,
+  charts, datasources, sliceEntities } from './fixtures';
 
 describe('Dashboard', () => {
   const mockedProps = {
-    actions: { ...chartActions, ...dashboardActions },
+    actions: { ...chartActions, ...dashboardActions, ...sliceActions },
     initMessages: [],
-    dashboard: dashboard.dashboard,
-    slices: charts,
-    filters: dashboard.filters,
-    datasources: dashboard.datasources,
-    refresh: false,
+    dashboardState,
+    dashboardInfo,
+    charts,
+    slices: sliceEntities.slices,
+    datasources,
+    layout: dashboardLayout.present,
     timeout: 60,
-    isStarred: false,
-    userId: dashboard.userId,
+    userId: dashboardInfo.userId,
   };
 
   it('should render', () => {
     const wrapper = shallow(<Dashboard {...mockedProps} />);
     expect(wrapper.find('#dashboard-container')).to.have.length(1);
-    expect(wrapper.instance().getAllSlices()).to.have.length(3);
+    expect(wrapper.instance().getAllCharts()).to.have.length(3);
   });
 
   it('should handle metadata default_filters', () => {
     const wrapper = shallow(<Dashboard {...mockedProps} />);
-    expect(wrapper.instance().props.filters).deep.equal(defaultFilters);
+    expect(wrapper.instance().props.dashboardState.filters).deep.equal(defaultFilters);
   });
 
   describe('getFormDataExtra', () => {
     let wrapper;
-    let selectedSlice;
+    let selectedChart;
     beforeEach(() => {
       wrapper = shallow(<Dashboard {...mockedProps} />);
-      selectedSlice = wrapper.instance().props.dashboard.slices[1];
+      selectedChart = charts[248];
     });
 
     it('should carry default_filters', () => {
-      const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
+      const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
       expect(extraFilters[0]).to.deep.equal({ col: 'region', op: 'in', val: [] });
       expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['United States'] });
     });
 
     it('should carry updated filter', () => {
-      wrapper.setProps({
+      const newState = {
+        ...wrapper.props('dashboardState'),
         filters: {
           256: { region: [] },
           257: { country_name: ['France'] },
         },
+      };
+      wrapper.setProps({
+        dashboardState: newState,
       });
-      const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
+      const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
       expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['France'] });
     });
   });
@@ -65,7 +72,7 @@ describe('Dashboard', () => {
     let spy;
     beforeEach(() => {
       wrapper = shallow(<Dashboard {...mockedProps} />);
-      spy = sinon.spy(wrapper.instance(), 'fetchSlices');
+      spy = sinon.spy(mockedProps.actions, 'runQuery');
     });
     afterEach(() => {
       spy.restore();
@@ -75,13 +82,13 @@ describe('Dashboard', () => {
       const filterKey = Object.keys(defaultFilters)[1];
       wrapper.instance().refreshExcept(filterKey);
       expect(spy.callCount).to.equal(1);
-      expect(spy.getCall(0).args[0].length).to.equal(1);
+      const slice_id = spy.getCall(0).args[0].slice_id;
+      expect(slice_id).to.equal(248);
     });
 
     it('should refresh all slices', () => {
       wrapper.instance().refreshExcept();
-      expect(spy.callCount).to.equal(1);
-      expect(spy.getCall(0).args[0].length).to.equal(3);
+      expect(spy.callCount).to.equal(3);
     });
   });
 
@@ -94,7 +101,7 @@ describe('Dashboard', () => {
       wrapper = shallow(<Dashboard {...mockedProps} />);
       prevProp = wrapper.instance().props;
       refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
-      fetchSlicesStub = sinon.stub(wrapper.instance(), 'fetchSlices');
+      fetchSlicesStub = sinon.stub(mockedProps.actions, 'fetchCharts');
     });
     afterEach(() => {
       fetchSlicesStub.restore();
@@ -106,48 +113,63 @@ describe('Dashboard', () => {
         refreshExceptSpy.reset();
       });
       it('no change', () => {
-        wrapper.setProps({
-          refresh: true,
+        const newState = {
+          ...wrapper.props('dashboardState'),
           filters: {
             256: { region: [] },
             257: { country_name: ['United States'] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(0);
       });
 
       it('remove filter', () => {
-        wrapper.setProps({
+        const newState = {
+          ...wrapper.props('dashboardState'),
           refresh: true,
           filters: {
             256: { region: [] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(1);
       });
 
       it('change filter', () => {
-        wrapper.setProps({
+        const newState = {
+          ...wrapper.props('dashboardState'),
           refresh: true,
           filters: {
             256: { region: [] },
             257: { country_name: ['Canada'] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(1);
       });
 
       it('add filter', () => {
-        wrapper.setProps({
+        const newState = {
+          ...wrapper.props('dashboardState'),
           refresh: true,
           filters: {
             256: { region: [] },
             257: { country_name: ['Canada'] },
             258: { another_filter: ['new'] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(1);
@@ -155,28 +177,36 @@ describe('Dashboard', () => {
     });
 
     it('should refresh if refresh flag is true', () => {
-      wrapper.setProps({
+      const newState = {
+        ...wrapper.props('dashboardState'),
         refresh: true,
         filters: {
           256: { region: ['Asian'] },
         },
+      };
+      wrapper.setProps({
+        dashboardState: newState,
       });
       wrapper.instance().componentDidUpdate(prevProp);
-      const fetchArgs = fetchSlicesStub.lastCall.args[0];
-      expect(fetchArgs).to.have.length(2);
+      expect(refreshExceptSpy.callCount).to.equal(1);
+      expect(refreshExceptSpy.lastCall.args[0]).to.equal('256');
     });
 
     it('should not refresh filter_immune_slices', () => {
-      wrapper.setProps({
+      const newState = {
+        ...wrapper.props('dashboardState'),
         refresh: true,
         filters: {
           256: { region: [] },
           257: { country_name: ['Canada'] },
         },
+      };
+      wrapper.setProps({
+        dashboardState: newState,
       });
       wrapper.instance().componentDidUpdate(prevProp);
-      const fetchArgs = fetchSlicesStub.lastCall.args[0];
-      expect(fetchArgs).to.have.length(1);
+      expect(refreshExceptSpy.callCount).to.equal(1);
+      expect(refreshExceptSpy.lastCall.args[0]).to.equal('257');
     });
   });
 });
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
index 371b02c..1565ccd 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
@@ -1,4 +1,4 @@
-import { getInitialState } from '../../../src/dashboard/reducers';
+import getInitialState from '../../../src/dashboard/reducers/getInitialState';
 
 export const defaultFilters = {
   256: { region: [] },
@@ -118,7 +118,6 @@ export const slice = {
   slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
 };
 
-const datasources = {};
 const mockDashboardData = {
   css: '',
   dash_edit_perm: true,
@@ -152,10 +151,13 @@ const mockDashboardData = {
   slices: [regionFilter, slice, countryFilter],
   standalone_mode: false,
 };
-export const { dashboard, charts } = getInitialState({
+export const {
+  dashboardState, dashboardInfo,
+  charts, datasources, sliceEntities,
+  dashboardLayout } = getInitialState({
   common: {},
   dashboard_data: mockDashboardData,
-  datasources,
+  datasources: {},
   user_id: '1',
 });
 
diff --git a/superset/assets/spec/javascripts/dashboard/reducers_spec.js b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
index 6421fec..580a574 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
@@ -1,20 +1,23 @@
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 
-import { dashboard as reducers } from '../../../src/dashboard/reducers';
-import * as actions from '../../../src/dashboard/actions';
-import { defaultFilters, dashboard as initState } from './fixtures';
+import reducers from '../../../src/dashboard/reducers/dashboardState';
+import * as actions from '../../../src/dashboard/actions/dashboardState';
+import { defaultFilters, dashboardState as initState } from './fixtures';
 
 describe('Dashboard reducers', () => {
+  it('should initialized', () => {
+    expect(initState.sliceIds.size).to.equal(3);
+  });
+
   it('should remove slice', () => {
     const action = {
       type: actions.REMOVE_SLICE,
-      slice: initState.dashboard.slices[1],
+      sliceId: 248,
     };
-    expect(initState.dashboard.slices).to.have.length(3);
 
-    const { dashboard, filters, refresh } = reducers(initState, action);
-    expect(dashboard.slices).to.have.length(2);
+    const { sliceIds, filters, refresh } = reducers(initState, action);
+    expect(sliceIds.size).to.be.equal(2);
     expect(filters).to.deep.equal(defaultFilters);
     expect(refresh).to.equal(false);
   });
@@ -22,13 +25,13 @@ describe('Dashboard reducers', () => {
   it('should remove filter slice', () => {
     const action = {
       type: actions.REMOVE_SLICE,
-      slice: initState.dashboard.slices[0],
+      sliceId: 256,
     };
     const initFilters = Object.keys(initState.filters);
     expect(initFilters).to.have.length(2);
 
-    const { dashboard, filters, refresh } = reducers(initState, action);
-    expect(dashboard.slices).to.have.length(2);
+    const { sliceIds, filters, refresh } = reducers(initState, action);
+    expect(sliceIds.size).to.equal(2);
     expect(Object.keys(filters)).to.have.length(1);
     expect(refresh).to.equal(true);
   });
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index f223174..e9f7c63 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -17,7 +17,7 @@ import './chart.css';
 const propTypes = {
   annotationData: PropTypes.object,
   actions: PropTypes.object,
-  chartKey: PropTypes.string.isRequired,
+  chartId: PropTypes.number.isRequired,
   containerId: PropTypes.string.isRequired,
   datasource: PropTypes.object.isRequired,
   formData: PropTypes.object.isRequired,
@@ -42,7 +42,6 @@ const propTypes = {
   // dashboard callbacks
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
   onQuery: PropTypes.func,
   onDismissRefreshOverlay: PropTypes.func,
@@ -51,7 +50,6 @@ const propTypes = {
 const defaultProps = {
   addFilter: () => ({}),
   getFilters: () => ({}),
-  clearFilter: () => ({}),
   removeFilter: () => ({}),
 };
 
@@ -67,7 +65,6 @@ class Chart extends React.PureComponent {
     this.datasource = props.datasource;
     this.addFilter = this.addFilter.bind(this);
     this.getFilters = this.getFilters.bind(this);
-    this.clearFilter = this.clearFilter.bind(this);
     this.removeFilter = this.removeFilter.bind(this);
     this.headerHeight = this.headerHeight.bind(this);
     this.height = this.height.bind(this);
@@ -78,7 +75,7 @@ class Chart extends React.PureComponent {
     if (this.props.triggerQuery) {
       this.props.actions.runQuery(this.props.formData, false,
         this.props.timeout,
-        this.props.chartKey,
+        this.props.chartId,
       );
     }
   }
@@ -92,15 +89,14 @@ class Chart extends React.PureComponent {
   }
 
   componentDidUpdate(prevProps) {
-    if (
-        this.props.queryResponse &&
-        ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
-        !this.props.queryResponse.error && (
-        prevProps.annotationData !== this.props.annotationData ||
-        prevProps.queryResponse !== this.props.queryResponse ||
-        prevProps.height !== this.props.height ||
-        prevProps.width !== this.props.width ||
-        prevProps.lastRendered !== this.props.lastRendered)
+    if (this.props.queryResponse &&
+      ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
+      !this.props.queryResponse.error && (
+      prevProps.annotationData !== this.props.annotationData ||
+      prevProps.queryResponse !== this.props.queryResponse ||
+      prevProps.height !== this.props.height ||
+      prevProps.width !== this.props.width ||
+      prevProps.lastRendered !== this.props.lastRendered)
     ) {
       this.renderViz();
     }
@@ -118,10 +114,6 @@ class Chart extends React.PureComponent {
     this.props.addFilter(col, vals, merge, refresh);
   }
 
-  clearFilter() {
-    this.props.clearFilter();
-  }
-
   removeFilter(col, vals, refresh = true) {
     this.props.removeFilter(col, vals, refresh);
   }
@@ -150,7 +142,7 @@ class Chart extends React.PureComponent {
   }
 
   error(e) {
-    this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+    this.props.actions.chartRenderingFailed(e, this.props.chartId);
   }
 
   verboseMetricName(metric) {
@@ -198,21 +190,21 @@ class Chart extends React.PureComponent {
       // [re]rendering the visualization
       viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_EVENT, {
-        label: this.props.chartKey,
+        label: 'slice_' + this.props.chartId,
         vis_type: this.props.vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(this.props.chartKey);
+      this.props.actions.chartRenderingSucceeded(this.props.chartId);
     } catch (e) {
-      this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+      this.props.actions.chartRenderingFailed(e, this.props.chartId);
     }
   }
 
   render() {
     const isLoading = this.props.chartStatus === 'loading';
     return (
-      <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
+      <div className={`${isLoading ? 'is-loading' : ''}`}>
         {this.renderTooltip()}
         {isLoading &&
           <Loading size={25} />
diff --git a/superset/assets/src/chart/ChartContainer.jsx b/superset/assets/src/chart/ChartContainer.jsx
index b731412..e3cb1f9 100644
--- a/superset/assets/src/chart/ChartContainer.jsx
+++ b/superset/assets/src/chart/ChartContainer.jsx
@@ -5,7 +5,7 @@ import * as Actions from './chartAction';
 import Chart from './Chart';
 
 function mapStateToProps({ charts }, ownProps) {
-  const chart = charts[ownProps.chartKey];
+  const chart = charts[ownProps.chartId];
   return {
     annotationData: chart.annotationData,
     chartAlert: chart.chartAlert,
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index cb24f65..33f49d1 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -117,6 +117,11 @@ export function updateQueryFormData(value, key) {
   return { type: UPDATE_QUERY_FORM_DATA, value, key };
 }
 
+export const ADD_CHART = 'ADD_CHART';
+export function addChart(chart, key) {
+  return { type: ADD_CHART, chart, key };
+}
+
 export const RUN_QUERY = 'RUN_QUERY';
 export function runQuery(formData, force = false, timeout = 60, key) {
   return (dispatch) => {
@@ -139,7 +144,7 @@ export function runQuery(formData, force = false, timeout = 60, key) {
       .then(() => queryRequest)
       .then((queryResponse) => {
         Logger.append(LOG_ACTIONS_LOAD_EVENT, {
-          label: key,
+          label: 'slice_' + key,
           is_cached: queryResponse.is_cached,
           row_count: queryResponse.rowcount,
           datasource: formData.datasource,
@@ -190,3 +195,10 @@ export function runQuery(formData, force = false, timeout = 60, key) {
     ]);
   };
 }
+
+export function refreshChart(chart, force, timeout) {
+  return dispatch => (
+    dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id))
+  );
+}
+
diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js
index f68a5b8..d57959a 100644
--- a/superset/assets/src/chart/chartReducer.js
+++ b/superset/assets/src/chart/chartReducer.js
@@ -1,25 +1,10 @@
 /* eslint camelcase: 0 */
-import PropTypes from 'prop-types';
-
 import { now } from '../modules/dates';
 import * as actions from './chartAction';
 import { t } from '../locales';
 
-export const chartPropType = {
-  chartKey: PropTypes.string.isRequired,
-  chartAlert: PropTypes.string,
-  chartStatus: PropTypes.string,
-  chartUpdateEndTime: PropTypes.number,
-  chartUpdateStartTime: PropTypes.number,
-  latestQueryFormData: PropTypes.object,
-  queryRequest: PropTypes.object,
-  queryResponse: PropTypes.object,
-  triggerQuery: PropTypes.bool,
-  lastRendered: PropTypes.number,
-};
-
 export const chart = {
-  chartKey: '',
+  id: 0,
   chartAlert: null,
   chartStatus: 'loading',
   chartUpdateEndTime: null,
@@ -33,6 +18,12 @@ export const chart = {
 
 export default function chartReducer(charts = {}, action) {
   const actionHandlers = {
+    [actions.ADD_CHART]() {
+      return {
+        ...chart,
+        ...action.chart,
+      };
+    },
     [actions.CHART_UPDATE_SUCCEEDED](state) {
       return { ...state,
         chartStatus: 'success',
@@ -70,12 +61,12 @@ export default function chartReducer(charts = {}, action) {
       return { ...state,
         chartStatus: 'failed',
         chartAlert: (
-            `${t('Query timeout')} - ` +
-            t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
-            t('Perhaps your data has grown, your database is under unusual load, ' +
-                'or you are simply querying a data source that is too large ' +
-                'to be processed within the timeout range. ' +
-                'If that is the case, we recommend that you summarize your data further.')),
+          `${t('Query timeout')} - ` +
+          t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
+          t('Perhaps your data has grown, your database is under unusual load, ' +
+            'or you are simply querying a data source that is too large ' +
+            'to be processed within the timeout range. ' +
+            'If that is the case, we recommend that you summarize your data further.')),
       };
     },
     [actions.CHART_UPDATE_FAILED](state) {
diff --git a/superset/assets/src/dashboard/actions.js b/superset/assets/src/dashboard/actions.js
deleted file mode 100644
index c7f1a6a..0000000
--- a/superset/assets/src/dashboard/actions.js
+++ /dev/null
@@ -1,127 +0,0 @@
-/* global notify */
-import $ from 'jquery';
-import { getExploreUrlAndPayload } from '../explore/exploreUtils';
-
-export const ADD_FILTER = 'ADD_FILTER';
-export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
-  return { type: ADD_FILTER, sliceId, col, vals, merge, refresh };
-}
-
-export const CLEAR_FILTER = 'CLEAR_FILTER';
-export function clearFilter(sliceId) {
-  return { type: CLEAR_FILTER, sliceId };
-}
-
-export const REMOVE_FILTER = 'REMOVE_FILTER';
-export function removeFilter(sliceId, col, vals, refresh = true) {
-  return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
-}
-
-export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT';
-export function updateDashboardLayout(layout) {
-  return { type: UPDATE_DASHBOARD_LAYOUT, layout };
-}
-
-export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
-export function updateDashboardTitle(title) {
-  return { type: UPDATE_DASHBOARD_TITLE, title };
-}
-
-export function addSlicesToDashboard(dashboardId, sliceIds) {
-  return () => (
-    $.ajax({
-      type: 'POST',
-      url: `/superset/add_slices/${dashboardId}/`,
-      data: {
-        data: JSON.stringify({ slice_ids: sliceIds }),
-      },
-    })
-      .done(() => {
-        // Refresh page to allow for slices to re-render
-        window.location.reload();
-      })
-  );
-}
-
-export const REMOVE_SLICE = 'REMOVE_SLICE';
-export function removeSlice(slice) {
-  return { type: REMOVE_SLICE, slice };
-}
-
-export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
-export function updateSliceName(slice, sliceName) {
-  return { type: UPDATE_SLICE_NAME, slice, sliceName };
-}
-export function saveSlice(slice, sliceName) {
-  const oldName = slice.slice_name;
-  return (dispatch) => {
-    const sliceParams = {};
-    sliceParams.slice_id = slice.slice_id;
-    sliceParams.action = 'overwrite';
-    sliceParams.slice_name = sliceName;
-
-    const { url, payload } = getExploreUrlAndPayload({
-      formData: slice.form_data,
-      endpointType: 'base',
-      force: false,
-      curUrl: null,
-      requestParams: sliceParams,
-    });
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: {
-        form_data: JSON.stringify(payload),
-      },
-      success: () => {
-        dispatch(updateSliceName(slice, sliceName));
-        notify.success('This slice name was saved successfully.');
-      },
-      error: () => {
-        // if server-side reject the overwrite action,
-        // revert to old state
-        dispatch(updateSliceName(slice, oldName));
-        notify.error("You don't have the rights to alter this slice");
-      },
-    });
-  };
-}
-
-const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
-export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
-export function toggleFaveStar(isStarred) {
-  return { type: TOGGLE_FAVE_STAR, isStarred };
-}
-
-export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
-export function fetchFaveStar(id) {
-  return function (dispatch) {
-    const url = `${FAVESTAR_BASE_URL}/${id}/count`;
-    return $.get(url)
-      .done((data) => {
-        if (data.count > 0) {
-          dispatch(toggleFaveStar(true));
-        }
-      });
-  };
-}
-
-export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
-export function saveFaveStar(id, isStarred) {
-  return function (dispatch) {
-    const urlSuffix = isStarred ? 'unselect' : 'select';
-    const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
-    $.get(url);
-    dispatch(toggleFaveStar(!isStarred));
-  };
-}
-
-export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
-export function toggleExpandSlice(slice, isExpanded) {
-  return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded };
-}
-
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-export function setEditMode(editMode) {
-  return { type: SET_EDIT_MODE, editMode };
-}
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
new file mode 100644
index 0000000..2262729
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -0,0 +1,166 @@
+/* eslint camelcase: 0 */
+import $ from 'jquery';
+
+import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
+import { chart as initChart } from '../../chart/chartReducer';
+import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
+import { applyDefaultFormData } from '../../explore/stores/store';
+
+export const ADD_FILTER = 'ADD_FILTER';
+export function addFilter(chart, col, vals, merge = true, refresh = true) {
+  return { type: ADD_FILTER, chart, col, vals, merge, refresh };
+}
+
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export function removeFilter(sliceId, col, vals, refresh = true) {
+  return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
+}
+
+export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
+export function updateDashboardTitle(title) {
+  return { type: UPDATE_DASHBOARD_TITLE, title };
+}
+
+export const ADD_SLICE = 'ADD_SLICE';
+export function addSlice(slice) {
+  return { type: ADD_SLICE, slice };
+}
+
+export const REMOVE_SLICE = 'REMOVE_SLICE';
+export function removeSlice(sliceId) {
+  return { type: REMOVE_SLICE, sliceId };
+}
+
+const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
+export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
+export function toggleFaveStar(isStarred) {
+  return { type: TOGGLE_FAVE_STAR, isStarred };
+}
+
+export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
+export function fetchFaveStar(id) {
+  return function (dispatch) {
+    const url = `${FAVESTAR_BASE_URL}/${id}/count`;
+    return $.get(url)
+      .done((data) => {
+        if (data.count > 0) {
+          dispatch(toggleFaveStar(true));
+        }
+      });
+  };
+}
+
+export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
+export function saveFaveStar(id, isStarred) {
+  return function (dispatch) {
+    const urlSuffix = isStarred ? 'unselect' : 'select';
+    const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
+    $.get(url);
+    dispatch(toggleFaveStar(!isStarred));
+  };
+}
+
+export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
+export function toggleExpandSlice(sliceId) {
+  return { type: TOGGLE_EXPAND_SLICE, sliceId };
+}
+
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export function setEditMode(editMode) {
+  return { type: SET_EDIT_MODE, editMode };
+}
+
+export const ON_CHANGE = 'ON_CHANGE';
+export function onChange() {
+  return { type: ON_CHANGE };
+}
+
+export const ON_SAVE = 'ON_SAVE';
+export function onSave() {
+  return { type: ON_SAVE };
+}
+
+export function fetchCharts(chartList = [], force = false, interval = 0) {
+  return (dispatch, getState) => {
+    const timeout = getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
+    if (!interval) {
+      chartList.forEach(chart => (dispatch(refreshChart(chart, force, timeout))));
+      return;
+    }
+
+    const { metadata: meta } = getState().dashboardInfo;
+    const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
+    if (typeof meta.stagger_refresh !== 'boolean') {
+      meta.stagger_refresh = meta.stagger_refresh === undefined ?
+        true : meta.stagger_refresh === 'true';
+    }
+    const delay = meta.stagger_refresh ? refreshTime / (chartList.length - 1) : 0;
+    chartList.forEach((chart, i) => {
+      setTimeout(() => dispatch(refreshChart(chart, force, timeout)), delay * i);
+    });
+  };
+}
+
+let refreshTimer = null;
+export function startPeriodicRender(interval) {
+  const stopPeriodicRender = () => {
+    if (refreshTimer) {
+      clearTimeout(refreshTimer);
+      refreshTimer = null;
+    }
+  };
+
+  return (dispatch, getState) => {
+    stopPeriodicRender();
+
+    const { metadata } = getState().dashboardInfo;
+    const immune = metadata.timed_refresh_immune_slices || [];
+    const refreshAll = () => {
+      const affected =
+        Object.values(getState().charts)
+          .filter(chart => immune.indexOf(chart.id) === -1);
+      return dispatch(fetchCharts(affected, true, interval * 0.2));
+    };
+    const fetchAndRender = () => {
+      refreshAll();
+      if (interval > 0) {
+        refreshTimer = setTimeout(fetchAndRender, interval);
+      }
+    };
+
+    fetchAndRender();
+  };
+}
+
+export const TOGGLE_BUILDER_PANE = 'TOGGLE_BUILDER_PANE';
+export function toggleBuilderPane() {
+  return { type: TOGGLE_BUILDER_PANE };
+}
+
+export function addSliceToDashboard(id) {
+  return (dispatch, getState) => {
+    const { sliceEntities } = getState();
+    const selectedSlice = sliceEntities.slices[id];
+    const form_data = selectedSlice.form_data;
+    const newChart = {
+      ...initChart,
+      id,
+      form_data,
+      formData: applyDefaultFormData(form_data),
+    };
+
+    return Promise
+      .all([
+        dispatch(addChart(newChart, id)),
+        dispatch(fetchDatasourceMetadata(form_data.datasource)),
+      ])
+      .then(() => dispatch(addSlice(selectedSlice)));
+  };
+}
+
+export function removeSliceFromDashboard(chart) {
+  return (dispatch) => {
+    dispatch(removeSlice(chart.id));
+    dispatch(removeChart(chart.id));
+  };
+}
diff --git a/superset/assets/src/dashboard/actions/datasources.js b/superset/assets/src/dashboard/actions/datasources.js
new file mode 100644
index 0000000..a00bb17
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/datasources.js
@@ -0,0 +1,35 @@
+import $ from 'jquery';
+
+export const SET_DATASOURCE = 'SET_DATASOURCE';
+export function setDatasource(datasource, key) {
+  return { type: SET_DATASOURCE, datasource, key };
+}
+
+export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
+export function fetchDatasourceStarted(key) {
+  return { type: FETCH_DATASOURCE_STARTED, key };
+}
+
+export const FETCH_DATASOURCE_FAILED = 'FETCH_DATASOURCE_FAILED';
+export function fetchDatasourceFailed(error, key) {
+  return { type: FETCH_DATASOURCE_FAILED, error, key };
+}
+
+export function fetchDatasourceMetadata(key) {
+  return (dispatch, getState) => {
+    const { datasources } = getState();
+    const datasource = datasources[key];
+
+    if (datasource) {
+      return dispatch(setDatasource(datasource, key));
+    }
+
+    const url = `/superset/fetch_datasource_metadata?datasourceKey=${key}`;
+    return $.ajax({
+      type: 'GET',
+      url,
+      success: data => dispatch(setDatasource(data, key)),
+      error: error => dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
+    });
+  };
+}
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
new file mode 100644
index 0000000..3a1b1dc
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -0,0 +1,93 @@
+/* eslint camelcase: 0 */
+/* global notify */
+import $ from 'jquery';
+
+export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
+export function updateSliceName(key, sliceName) {
+  return { type: UPDATE_SLICE_NAME, key, sliceName };
+}
+
+export function saveSliceName(slice, sliceName) {
+  const oldName = slice.slice_name;
+  return (dispatch) => {
+    const sliceParams = {};
+    sliceParams.slice_id = slice.slice_id;
+    sliceParams.action = 'overwrite';
+    sliceParams.slice_name = sliceName;
+
+    const url = slice.slice_url + '&' +
+      Object.keys(sliceParams)
+      .map(key => (key + '=' + sliceParams[key]))
+      .join('&');
+    const key = slice.slice_id;
+    return $.ajax({
+      url,
+      type: 'POST',
+      success: () => {
+        dispatch(updateSliceName(key, sliceName));
+        notify.success('This slice name was saved successfully.');
+      },
+      error: () => {
+        // if server-side reject the overwrite action,
+        // revert to old state
+        dispatch(updateSliceName(key, oldName));
+        notify.error("You don't have the rights to alter this slice");
+      },
+    });
+  };
+}
+
+export const SET_ALL_SLICES = 'SET_ALL_SLICES';
+export function setAllSlices(slices) {
+  return { type: SET_ALL_SLICES, slices };
+}
+
+export const FETCH_ALL_SLICES_STARTED = 'FETCH_ALL_SLICES_STARTED';
+export function fetchAllSlicesStarted() {
+  return { type: FETCH_ALL_SLICES_STARTED };
+}
+
+export const FETCH_ALL_SLICES_FAILED = 'FETCH_ALL_SLICES_FAILED';
+export function fetchAllSlicesFailed(error) {
+  return { type: FETCH_ALL_SLICES_FAILED, error };
+}
+
+export function fetchAllSlices(userId) {
+  return (dispatch, getState) => {
+    const { sliceEntities }  = getState();
+    if (sliceEntities.lastUpdated === 0) {
+      dispatch(fetchAllSlicesStarted());
+
+      const uri = `/sliceaddview/api/read?_flt_0_created_by=${userId}`;
+      return $.ajax({
+        url: uri,
+        type: 'GET',
+        success: (response) => {
+          const slices = {};
+          response.result.forEach((slice) => {
+            const form_data = JSON.parse(slice.params);
+            slices[slice.id] = {
+              slice_id: slice.id,
+              slice_url: slice.slice_url,
+              slice_name: slice.slice_name,
+              edit_url: slice.edit_url,
+              form_data,
+              datasource: form_data.datasource,
+              datasource_name: slice.datasource_name_text,
+              datasource_link: slice.datasource_link,
+              changed_on: new Date(slice.changed_on).getTime(),
+              description: slice.description,
+              description_markdown: slice.description_markeddown,
+              viz_type: slice.viz_type,
+              modified: slice.modified,
+            };
+          });
+          return dispatch(setAllSlices(slices));
+        },
+        error: error => dispatch(fetchAllSlicesFailed(error)),
+      });
+    }
+
+    return dispatch(setAllSlices(sliceEntities.slices));
+  };
+}
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
new file mode 100644
index 0000000..aaae4df
--- /dev/null
+++ b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { MenuItem } from 'react-bootstrap';
+
+import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+
+export function MenuItemContent({ faIcon, text, tooltip, children }) {
+  return (
+    <span>
+      {faIcon &&
+        <i className={`fa fa-${faIcon}`}>&nbsp;</i>
+      }
+      {text} {''}
+      <InfoTooltipWithTrigger
+        tooltip={tooltip}
+        label={faIcon ? `dash-${faIcon}` : ''}
+        placement="top"
+      />
+      {children}
+    </span>
+  );
+}
+MenuItemContent.propTypes = {
+  faIcon: PropTypes.string,
+  text: PropTypes.string,
+  tooltip: PropTypes.string,
+  children: PropTypes.node,
+};
+
+export function ActionMenuItem(props) {
+  return (
+    <MenuItem
+      onClick={props.onClick}
+      href={props.href}
+      target={props.target}
+    >
+      <MenuItemContent {...props} />
+    </MenuItem>
+  );
+}
+ActionMenuItem.propTypes = {
+  onClick: PropTypes.func,
+  href: PropTypes.string,
+  target: PropTypes.string,
+};
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index baad7bb..8755e8f 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -1,69 +1,34 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
+import $ from 'jquery';
+import { DropdownButton } from 'react-bootstrap';
 
-import CssEditor from './CssEditor';
 import RefreshIntervalModal from './RefreshIntervalModal';
 import SaveModal from './SaveModal';
-import SliceAdder from './SliceAdder';
+import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
 import { t } from '../../locales';
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
-
-const $ = window.$ = require('jquery');
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
+  dashboardInfo: PropTypes.object.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
+  expandedSlices: PropTypes.object.isRequired,
   slices: PropTypes.array,
-  userId: PropTypes.string.isRequired,
-  addSlicesToDashboard: PropTypes.func,
   onSave: PropTypes.func,
   onChange: PropTypes.func,
-  renderSlices: PropTypes.func,
-  serialize: PropTypes.func,
+  forceRefreshAllCharts: PropTypes.func,
   startPeriodicRender: PropTypes.func,
   editMode: PropTypes.bool,
 };
 
-function MenuItemContent({ faIcon, text, tooltip, children }) {
-  return (
-    <span>
-      <i className={`fa fa-${faIcon}`} /> {text} {''}
-      <InfoTooltipWithTrigger
-        tooltip={tooltip}
-        label={`dash-${faIcon}`}
-        placement="top"
-      />
-      {children}
-    </span>
-  );
-}
-MenuItemContent.propTypes = {
-  faIcon: PropTypes.string.isRequired,
-  text: PropTypes.string,
-  tooltip: PropTypes.string,
-  children: PropTypes.node,
-};
-
-function ActionMenuItem(props) {
-  return (
-    <MenuItem onClick={props.onClick}>
-      <MenuItemContent {...props} />
-    </MenuItem>
-  );
-}
-ActionMenuItem.propTypes = {
-  onClick: PropTypes.func,
-};
-
 class Controls extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      css: props.dashboard.css || '',
+      css: '',
       cssTemplates: [],
     };
-    this.refresh = this.refresh.bind(this);
     this.toggleModal = this.toggleModal.bind(this);
     this.updateDom = this.updateDom.bind(this);
   }
@@ -79,10 +44,6 @@ class Controls extends React.PureComponent {
       this.setState({ cssTemplates });
     });
   }
-  refresh() {
-    // Force refresh all slices
-    this.props.renderSlices(true);
-  }
   toggleModal(modal) {
     let currentModal;
     if (modal !== this.state.currentModal) {
@@ -114,12 +75,12 @@ class Controls extends React.PureComponent {
     }
   }
   render() {
-    const { dashboard, userId, filters,
-      addSlicesToDashboard, startPeriodicRender,
-      serialize, onSave, editMode } = this.props;
+    const { dashboardTitle, layout, filters, expandedSlices,
+      startPeriodicRender, forceRefreshAllCharts, onSave,
+      editMode } = this.props;
     const emailBody = t('Checkout this dashboard: %s', window.location.href);
     const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
-      + `${dashboard.dashboard_title}&Body=${emailBody}`;
+      + `${dashboardTitle}&Body=${emailBody}`;
     let saveText = t('Save as');
     if (editMode) {
       saveText = t('Save');
@@ -130,8 +91,7 @@ class Controls extends React.PureComponent {
           <ActionMenuItem
             text={t('Force Refresh')}
             tooltip={t('Force refresh the whole dashboard')}
-            faIcon="refresh"
-            onClick={this.refresh}
+            onClick={forceRefreshAllCharts}
           />
           <RefreshIntervalModal
             onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
@@ -139,32 +99,29 @@ class Controls extends React.PureComponent {
               <MenuItemContent
                 text={t('Set autorefresh')}
                 tooltip={t('Set the auto-refresh interval for this session')}
-                faIcon="clock-o"
               />
             }
           />
-          {dashboard.dash_save_perm &&
-            <SaveModal
-              dashboard={dashboard}
-              filters={filters}
-              serialize={serialize}
-              onSave={onSave}
-              css={this.state.css}
-              triggerNode={
-                <MenuItemContent
-                  text={saveText}
-                  tooltip={t('Save the dashboard')}
-                  faIcon="save"
-                />
-              }
-            />
-          }
+          <SaveModal
+            dashboardId={this.props.dashboardInfo.id}
+            dashboardTitle={dashboardTitle}
+            layout={layout}
+            filters={filters}
+            expandedSlices={expandedSlices}
+            onSave={onSave}
+            css={this.state.css}
+            triggerNode={
+              <MenuItemContent
+                text={saveText}
+                tooltip={t('Save the dashboard')}
+              />
+            }
+          />
           {editMode &&
             <ActionMenuItem
               text={t('Edit properties')}
               tooltip={t("Edit the dashboards's properties")}
-              faIcon="edit"
-              onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
+              onClick={() => { window.location = `/dashboardmodelview/edit/${this.props.dashboardInfo.id}`; }}
             />
           }
           {editMode &&
@@ -172,36 +129,6 @@ class Controls extends React.PureComponent {
               text={t('Email')}
               tooltip={t('Email a link to this dashboard')}
               onClick={() => { window.location = emailLink; }}
-              faIcon="envelope"
-            />
-          }
-          {editMode &&
-            <SliceAdder
-              dashboard={dashboard}
-              addSlicesToDashboard={addSlicesToDashboard}
-              userId={userId}
-              triggerNode={
-                <MenuItemContent
-                  text={t('Add Charts')}
-                  tooltip={t('Add some charts to this dashboard')}
-                  faIcon="plus"
-                />
-              }
-            />
-          }
-          {editMode &&
-            <CssEditor
-              dashboard={dashboard}
-              triggerNode={
-                <MenuItemContent
-                  text={t('Edit CSS')}
-                  tooltip={t('Change the style of the dashboard using CSS code')}
-                  faIcon="css3"
-                />
-              }
-              initialCss={this.state.css}
-              templates={this.state.cssTemplates}
-              onChange={this.changeCss.bind(this)}
             />
           }
         </DropdownButton>
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 2cb08c3..939476c 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -3,81 +3,69 @@ import PropTypes from 'prop-types';
 
 import AlertsWrapper from '../../components/AlertsWrapper';
 import GridLayout from './GridLayout';
-import Header from './Header';
+import {
+  chartPropShape,
+  slicePropShape,
+  dashboardInfoPropShape,
+  dashboardStatePropShape,
+} from '../v2/util/propShapes';
 import { exportChart } from '../../explore/exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
+import { getChartIdsFromLayout } from '../util/dashboardHelper';
 import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
   LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
 import { t } from '../../locales';
 
-import '../../../stylesheets/dashboard.css';
+import '../../../stylesheets/dashboard.less';
+import '../v2/stylesheets/index.less';
 
 const propTypes = {
-  actions: PropTypes.object,
+  actions: PropTypes.object.isRequired,
+  dashboardInfo: dashboardInfoPropShape.isRequired,
+  dashboardState: dashboardStatePropShape.isRequired,
+  charts: PropTypes.objectOf(chartPropShape).isRequired,
+  slices: PropTypes.objectOf(slicePropShape).isRequired,
+  datasources: PropTypes.object.isRequired,
+  layout: PropTypes.object.isRequired,
+  impressionId: PropTypes.string.isRequired,
   initMessages: PropTypes.array,
-  dashboard: PropTypes.object.isRequired,
-  slices: PropTypes.object,
-  datasources: PropTypes.object,
-  filters: PropTypes.object,
-  refresh: PropTypes.bool,
   timeout: PropTypes.number,
   userId: PropTypes.string,
-  isStarred: PropTypes.bool,
-  editMode: PropTypes.bool,
-  impressionId: PropTypes.string,
 };
 
 const defaultProps = {
   initMessages: [],
-  dashboard: {},
-  slices: {},
-  datasources: {},
-  filters: {},
-  refresh: false,
   timeout: 60,
   userId: '',
-  isStarred: false,
-  editMode: false,
 };
 
 class Dashboard extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.refreshTimer = null;
+
     this.firstLoad = true;
     this.loadingLog = new ActionLog({
       impressionId: props.impressionId,
       actionType: LOG_ACTIONS_PAGE_LOAD,
       source: 'dashboard',
-      sourceId: props.dashboard.id,
+      sourceId: props.dashboardInfo.id,
       eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
     });
     Logger.start(this.loadingLog);
 
-    // alert for unsaved changes
-    this.state = { unsavedChanges: false };
-
     this.rerenderCharts = this.rerenderCharts.bind(this);
-    this.updateDashboardTitle = this.updateDashboardTitle.bind(this);
-    this.onSave = this.onSave.bind(this);
-    this.onChange = this.onChange.bind(this);
-    this.serialize = this.serialize.bind(this);
-    this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices());
-    this.startPeriodicRender = this.startPeriodicRender.bind(this);
-    this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
-    this.fetchSlice = this.fetchSlice.bind(this);
+    this.getFilters = this.getFilters.bind(this);
+    this.refreshExcept = this.refreshExcept.bind(this);
     this.getFormDataExtra = this.getFormDataExtra.bind(this);
     this.exploreChart = this.exploreChart.bind(this);
     this.exportCSV = this.exportCSV.bind(this);
-    this.props.actions.fetchFaveStar = this.props.actions.fetchFaveStar.bind(this);
-    this.props.actions.saveFaveStar = this.props.actions.saveFaveStar.bind(this);
-    this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this);
-    this.props.actions.removeSlice = this.props.actions.removeSlice.bind(this);
-    this.props.actions.removeChart = this.props.actions.removeChart.bind(this);
-    this.props.actions.updateDashboardLayout = this.props.actions.updateDashboardLayout.bind(this);
-    this.props.actions.toggleExpandSlice = this.props.actions.toggleExpandSlice.bind(this);
+
+    this.props.actions.saveSliceName = this.props.actions.saveSliceName.bind(this);
+    this.props.actions.removeSliceFromDashboard =
+      this.props.actions.removeSliceFromDashboard.bind(this);
+    this.props.actions.toggleExpandSlice =
+      this.props.actions.toggleExpandSlice.bind(this);
     this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
-    this.props.actions.clearFilter = this.props.actions.clearFilter.bind(this);
     this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
   }
 
@@ -87,22 +75,37 @@ class Dashboard extends React.PureComponent {
 
   componentWillReceiveProps(nextProps) {
     if (this.firstLoad &&
-      Object.values(nextProps.slices)
-        .every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
+      Object.values(nextProps.charts)
+        .every(chart => (['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1))
     ) {
       Logger.end(this.loadingLog);
       this.firstLoad = false;
     }
+
+    const currentChartIds = getChartIdsFromLayout(this.props.layout);
+    const nextChartIds = getChartIdsFromLayout(nextProps.layout);
+    if (currentChartIds.length < nextChartIds.length) {
+      // adding new chart
+      const newChartId = nextChartIds.find(key => (currentChartIds.indexOf(key) === -1));
+      this.props.actions.addSliceToDashboard(newChartId);
+      this.props.actions.onChange();
+    } else if (currentChartIds.length > nextChartIds.length) {
+      // remove chart
+      const removedChartId = currentChartIds.find(key => (nextChartIds.indexOf(key) === -1));
+      this.props.actions.removeSliceFromDashboard(this.props.charts[removedChartId]);
+      this.props.actions.onChange();
+    }
   }
 
   componentDidUpdate(prevProps) {
-    if (this.props.refresh) {
+    const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
+    if (refresh) {
       let changedFilterKey;
-      const prevFiltersKeySet = new Set(Object.keys(prevProps.filters));
-      Object.keys(this.props.filters).some((key) => {
+      const prevFiltersKeySet = new Set(Object.keys(prevProps.dashboardState.filters));
+      Object.keys(filters).some((key) => {
         prevFiltersKeySet.delete(key);
-        if (prevProps.filters[key] === undefined ||
-          !areObjectsEqual(prevProps.filters[key], this.props.filters[key])) {
+        if (prevProps.dashboardState.filters[key] === undefined ||
+          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])) {
           changedFilterKey = key;
           return true;
         }
@@ -113,6 +116,12 @@ class Dashboard extends React.PureComponent {
         this.refreshExcept(changedFilterKey);
       }
     }
+
+    if (hasUnsavedChanges) {
+      this.onBeforeUnload(true);
+    } else {
+      this.onBeforeUnload(false);
+    }
   }
 
   componentWillUnmount() {
@@ -127,29 +136,22 @@ class Dashboard extends React.PureComponent {
     }
   }
 
-  onChange() {
-    this.onBeforeUnload(true);
-    this.setState({ unsavedChanges: true });
-  }
-
-  onSave() {
-    this.onBeforeUnload(false);
-    this.setState({ unsavedChanges: false });
-  }
-
   // return charts in array
-  getAllSlices() {
-    return Object.values(this.props.slices);
+  getAllCharts() {
+    return Object.values(this.props.charts);
   }
 
-  getFormDataExtra(slice) {
-    const formDataExtra = Object.assign({}, slice.formData);
-    formDataExtra.extra_filters = this.effectiveExtraFilters(slice.slice_id);
+  getFormDataExtra(chart) {
+    const extraFilters = this.effectiveExtraFilters(chart.id);
+    const formDataExtra = {
+      ...chart.formData,
+      extra_filters: extraFilters,
+    };
     return formDataExtra;
   }
 
   getFilters(sliceId) {
-    return this.props.filters[sliceId];
+    return this.props.dashboardState.filters[sliceId];
   }
 
   unload() {
@@ -159,8 +161,8 @@ class Dashboard extends React.PureComponent {
   }
 
   effectiveExtraFilters(sliceId) {
-    const metadata = this.props.dashboard.metadata;
-    const filters = this.props.filters;
+    const metadata = this.props.dashboardInfo.metadata;
+    const filters = this.props.dashboardState.filters;
     const f = [];
     const immuneSlices = metadata.filter_immune_slices || [];
     if (sliceId && immuneSlices.includes(sliceId)) {
@@ -195,154 +197,75 @@ class Dashboard extends React.PureComponent {
   }
 
   refreshExcept(filterKey) {
-    const immune = this.props.dashboard.metadata.filter_immune_slices || [];
-    let slices = this.getAllSlices();
+    const immune = this.props.dashboardInfo.metadata.filter_immune_slices || [];
+    let charts = this.getAllCharts();
     if (filterKey) {
-      slices = slices.filter(slice => (
-        String(slice.slice_id) !== filterKey &&
-        immune.indexOf(slice.slice_id) === -1
-      ));
-    }
-    this.fetchSlices(slices);
-  }
-
-  stopPeriodicRender() {
-    if (this.refreshTimer) {
-      clearTimeout(this.refreshTimer);
-      this.refreshTimer = null;
-    }
-  }
-
-  startPeriodicRender(interval) {
-    this.stopPeriodicRender();
-    const immune = this.props.dashboard.metadata.timed_refresh_immune_slices || [];
-    const refreshAll = () => {
-      const affectedSlices = this.getAllSlices()
-        .filter(slice => immune.indexOf(slice.slice_id) === -1);
-      this.fetchSlices(affectedSlices, true, interval * 0.2);
-    };
-    const fetchAndRender = () => {
-      refreshAll();
-      if (interval > 0) {
-        this.refreshTimer = setTimeout(fetchAndRender, interval);
-      }
-    };
-
-    fetchAndRender();
-  }
-
-  updateDashboardTitle(title) {
-    this.props.actions.updateDashboardTitle(title);
-    this.onChange();
-  }
-
-  serialize() {
-    return this.props.dashboard.layout.map(reactPos => ({
-      slice_id: reactPos.i,
-      col: reactPos.x + 1,
-      row: reactPos.y,
-      size_x: reactPos.w,
-      size_y: reactPos.h,
-    }));
-  }
-
-  addSlicesToDashboard(sliceIds) {
-    return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds);
-  }
-
-  fetchSlice(slice, force = false) {
-    return this.props.actions.runQuery(
-      this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey,
-    );
-  }
-
-  // fetch and render an list of slices
-  fetchSlices(slc, force = false, interval = 0) {
-    const slices = slc || this.getAllSlices();
-    if (!interval) {
-      slices.forEach((slice) => { this.fetchSlice(slice, force); });
-      return;
+      charts = charts.filter(
+        chart => (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1),
+      );
     }
-
-    const meta = this.props.dashboard.metadata;
-    const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
-    if (typeof meta.stagger_refresh !== 'boolean') {
-      meta.stagger_refresh = meta.stagger_refresh === undefined ?
-        true : meta.stagger_refresh === 'true';
-    }
-    const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
-    slices.forEach((slice, i) => {
-      setTimeout(() => { this.fetchSlice(slice, force); }, delay * i);
+    charts.forEach((chart) => {
+      const updatedFormData = this.getFormDataExtra(chart);
+      this.props.actions.runQuery(updatedFormData, false, this.props.timeout, chart.id);
     });
   }
 
-  exploreChart(slice) {
-    const formData = this.getFormDataExtra(slice);
+  exploreChart(chartId) {
+    const chart = this.props.charts[chartId];
+    const formData = this.getFormDataExtra(chart);
     exportChart(formData);
   }
 
-  exportCSV(slice) {
-    const formData = this.getFormDataExtra(slice);
+  exportCSV(chartId) {
+    const chart = this.props.charts[chartId];
+    const formData = this.getFormDataExtra(chart);
     exportChart(formData, 'csv');
   }
 
   // re-render chart without fetch
   rerenderCharts() {
-    this.getAllSlices().forEach((slice) => {
+    this.getAllCharts().forEach((chart) => {
       setTimeout(() => {
-        this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+        this.props.actions.renderTriggered(new Date().getTime(), chart.id);
       }, 50);
     });
   }
 
   render() {
+    const {
+      expandedSlices = {}, filters, sliceIds,
+      editMode, showBuilderPane,
+    } = this.props.dashboardState;
+
     return (
       <div id="dashboard-container">
-        <div id="dashboard-header">
+        <div>
           <AlertsWrapper initMessages={this.props.initMessages} />
-          <Header
-            dashboard={this.props.dashboard}
-            unsavedChanges={this.state.unsavedChanges}
-            filters={this.props.filters}
-            userId={this.props.userId}
-            isStarred={this.props.isStarred}
-            updateDashboardTitle={this.updateDashboardTitle}
-            onSave={this.onSave}
-            onChange={this.onChange}
-            serialize={this.serialize}
-            fetchFaveStar={this.props.actions.fetchFaveStar}
-            saveFaveStar={this.props.actions.saveFaveStar}
-            renderSlices={this.fetchAllSlices}
-            startPeriodicRender={this.startPeriodicRender}
-            addSlicesToDashboard={this.addSlicesToDashboard}
-            editMode={this.props.editMode}
-            setEditMode={this.props.actions.setEditMode}
-          />
-        </div>
-        <div id="grid-container" className="slice-grid gridster">
-          <GridLayout
-            dashboard={this.props.dashboard}
-            datasources={this.props.datasources}
-            filters={this.props.filters}
-            charts={this.props.slices}
-            timeout={this.props.timeout}
-            onChange={this.onChange}
-            getFormDataExtra={this.getFormDataExtra}
-            exploreChart={this.exploreChart}
-            exportCSV={this.exportCSV}
-            fetchSlice={this.fetchSlice}
-            saveSlice={this.props.actions.saveSlice}
-            removeSlice={this.props.actions.removeSlice}
-            removeChart={this.props.actions.removeChart}
-            updateDashboardLayout={this.props.actions.updateDashboardLayout}
-            toggleExpandSlice={this.props.actions.toggleExpandSlice}
-            addFilter={this.props.actions.addFilter}
-            getFilters={this.getFilters}
-            clearFilter={this.props.actions.clearFilter}
-            removeFilter={this.props.actions.removeFilter}
-            editMode={this.props.editMode}
-          />
         </div>
+        <GridLayout
+          dashboardInfo={this.props.dashboardInfo}
+          layout={this.props.layout}
+          datasources={this.props.datasources}
+          slices={this.props.slices}
+          sliceIds={sliceIds}
+          expandedSlices={expandedSlices}
+          filters={filters}
+          charts={this.props.charts}
+          timeout={this.props.timeout}
+          onChange={this.onChange}
+          rerenderCharts={this.rerenderCharts}
+          getFormDataExtra={this.getFormDataExtra}
+          exploreChart={this.exploreChart}
+          exportCSV={this.exportCSV}
+          refreshChart={this.props.actions.refreshChart}
+          saveSliceName={this.props.actions.saveSliceName}
+          toggleExpandSlice={this.props.actions.toggleExpandSlice}
+          addFilter={this.props.actions.addFilter}
+          getFilters={this.getFilters}
+          removeFilter={this.props.actions.removeFilter}
+          editMode={editMode}
+          showBuilderPane={showBuilderPane}
+        />
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
index d429461..31fe035 100644
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ b/superset/assets/src/dashboard/components/DashboardContainer.jsx
@@ -1,28 +1,48 @@
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-import * as dashboardActions from '../actions';
-import * as chartActions from '../../chart/chartAction';
-import Dashboard from '../v2/components/Dashboard';
+import {
+  toggleExpandSlice,
+  addFilter,
+  removeFilter,
+  addSliceToDashboard,
+  removeSliceFromDashboard,
+  onChange,
+} from '../actions/dashboardState';
+import { saveSliceName } from '../actions/sliceEntities';
+import { refreshChart, runQuery, renderTriggered } from '../../chart/chartAction';
+import Dashboard from './Dashboard';
 
-function mapStateToProps(/* { charts, dashboard, impressionId } */) {
+function mapStateToProps({ datasources, sliceEntities, charts,
+                           dashboardInfo, dashboardState,
+                           dashboardLayout, impressionId }) {
   return {
-    // initMessages: dashboard.common.flash_messages,
-    // timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    // dashboard: dashboard.dashboard,
-    // slices: charts,
-    // datasources: dashboard.datasources,
-    // filters: dashboard.filters,
-    // refresh: !!dashboard.refresh,
-    // userId: dashboard.userId,
-    // isStarred: !!dashboard.isStarred,
-    // editMode: dashboard.editMode,
-    // impressionId,
+    initMessages: dashboardInfo.common.flash_messages,
+    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    userId: dashboardInfo.userId,
+    dashboardInfo,
+    dashboardState,
+    charts,
+    datasources,
+    slices: sliceEntities.slices,
+    layout: dashboardLayout.present,
+    impressionId,
   };
 }
 
 function mapDispatchToProps(dispatch) {
-  const actions = { ...chartActions, ...dashboardActions };
+  const actions = {
+    refreshChart,
+    runQuery,
+    renderTriggered,
+    saveSliceName,
+    toggleExpandSlice,
+    addFilter,
+    removeFilter,
+    addSliceToDashboard,
+    removeSliceFromDashboard,
+    onChange,
+  };
   return {
     actions: bindActionCreators(actions, dispatch),
   };
diff --git a/superset/assets/src/dashboard/components/GridCell.jsx b/superset/assets/src/dashboard/components/GridCell.jsx
index 91fe839..3273272 100644
--- a/superset/assets/src/dashboard/components/GridCell.jsx
+++ b/superset/assets/src/dashboard/components/GridCell.jsx
@@ -4,8 +4,7 @@ import PropTypes from 'prop-types';
 
 import SliceHeader from './SliceHeader';
 import ChartContainer from '../../chart/ChartContainer';
-
-import '../../../stylesheets/dashboard.css';
+import { chartPropShape, slicePropShape } from '../v2/util/propShapes';
 
 const propTypes = {
   timeout: PropTypes.number,
@@ -16,34 +15,30 @@ const propTypes = {
   isExpanded: PropTypes.bool,
   widgetHeight: PropTypes.number,
   widgetWidth: PropTypes.number,
-  slice: PropTypes.object,
-  chartKey: PropTypes.string,
+  slice: slicePropShape.isRequired,
+  chart: chartPropShape.isRequired,
   formData: PropTypes.object,
   filters: PropTypes.object,
-  forceRefresh: PropTypes.func,
-  removeSlice: PropTypes.func,
+  refreshChart: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   exploreChart: PropTypes.func,
   exportCSV: PropTypes.func,
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
   editMode: PropTypes.bool,
   annotationQuery: PropTypes.object,
 };
 
 const defaultProps = {
-  forceRefresh: () => ({}),
-  removeSlice: () => ({}),
+  refreshChart: () => ({}),
   updateSliceName: () => ({}),
   toggleExpandSlice: () => ({}),
   exploreChart: () => ({}),
   exportCSV: () => ({}),
   addFilter: () => ({}),
   getFilters: () => ({}),
-  clearFilter: () => ({}),
   removeFilter: () => ({}),
   editMode: false,
 };
@@ -53,9 +48,9 @@ class GridCell extends React.PureComponent {
     super(props);
 
     const sliceId = this.props.slice.slice_id;
-    this.addFilter = this.props.addFilter.bind(this, sliceId);
+    this.forceRefresh = this.forceRefresh.bind(this);
+    this.addFilter = this.props.addFilter.bind(this, this.props.chart);
     this.getFilters = this.props.getFilters.bind(this, sliceId);
-    this.clearFilter = this.props.clearFilter.bind(this, sliceId);
     this.removeFilter = this.props.removeFilter.bind(this, sliceId);
   }
 
@@ -68,7 +63,7 @@ class GridCell extends React.PureComponent {
   }
 
   width() {
-    return this.props.widgetWidth - 10;
+    return this.props.widgetWidth - 32;
   }
 
   height(slice) {
@@ -80,7 +75,7 @@ class GridCell extends React.PureComponent {
       descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
     }
 
-    return widgetHeight - headerHeight - descriptionHeight;
+    return widgetHeight - headerHeight - descriptionHeight - 32;
   }
 
   headerHeight(slice) {
@@ -88,13 +83,18 @@ class GridCell extends React.PureComponent {
     return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
   }
 
+  forceRefresh() {
+    return this.props.refreshChart(this.props.chart, true, this.props.timeout);
+  }
+
   render() {
     const {
       isExpanded, isLoading, isCached, cachedDttm,
-      removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
-      chartKey, slice, datasource, formData, timeout, annotationQuery,
-      exploreChart, exportCSV,
+      updateSliceName, toggleExpandSlice,
+      chart, slice, datasource, formData, timeout, annotationQuery,
+      exploreChart, exportCSV, editMode,
     } = this.props;
+
     return (
       <div
         className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
@@ -106,11 +106,10 @@ class GridCell extends React.PureComponent {
             isExpanded={isExpanded}
             isCached={isCached}
             cachedDttm={cachedDttm}
-            removeSlice={removeSlice}
             updateSliceName={updateSliceName}
             toggleExpandSlice={toggleExpandSlice}
-            forceRefresh={forceRefresh}
-            editMode={this.props.editMode}
+            forceRefresh={this.forceRefresh}
+            editMode={editMode}
             annotationQuery={annotationQuery}
             exploreChart={exploreChart}
             exportCSV={exportCSV}
@@ -128,21 +127,23 @@ class GridCell extends React.PureComponent {
           ref={this.getDescriptionId(slice)}
           dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
         />
-        <div className="row chart-container">
+        <div
+          className="chart-container"
+          style={{ width: this.width(), height: this.height(slice) }}
+        >
           <input type="hidden" value="false" />
           <ChartContainer
             containerId={`slice-container-${slice.slice_id}`}
-            chartKey={chartKey}
+            chartId={chart.id}
             datasource={datasource}
             formData={formData}
             headerHeight={this.headerHeight(slice)}
             height={this.height(slice)}
             width={this.width()}
             timeout={timeout}
-            vizType={slice.formData.viz_type}
+            vizType={slice.viz_type}
             addFilter={this.addFilter}
             getFilters={this.getFilters}
-            clearFilter={this.clearFilter}
             removeFilter={this.removeFilter}
           />
         </div>
diff --git a/superset/assets/src/dashboard/components/GridLayout.jsx b/superset/assets/src/dashboard/components/GridLayout.jsx
index ef0ec24..fd561e2 100644
--- a/superset/assets/src/dashboard/components/GridLayout.jsx
+++ b/superset/assets/src/dashboard/components/GridLayout.jsx
@@ -1,51 +1,49 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { Responsive, WidthProvider } from 'react-grid-layout';
+import cx from 'classnames';
 
 import GridCell from './GridCell';
-
-require('react-grid-layout/css/styles.css');
-require('react-resizable/css/styles.css');
-
-const ResponsiveReactGridLayout = WidthProvider(Responsive);
+import { slicePropShape, chartPropShape } from '../v2/util/propShapes';
+import DashboardBuilder from '../v2/containers/DashboardBuilder';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
+  dashboardInfo: PropTypes.shape().isRequired,
+  layout: PropTypes.object.isRequired,
   datasources: PropTypes.object,
-  charts: PropTypes.object.isRequired,
+  charts: PropTypes.objectOf(chartPropShape).isRequired,
+  slices: PropTypes.objectOf(slicePropShape).isRequired,
+  expandedSlices: PropTypes.object.isRequired,
+  sliceIds: PropTypes.object.isRequired,
   filters: PropTypes.object,
   timeout: PropTypes.number,
   onChange: PropTypes.func,
+  rerenderCharts: PropTypes.func,
   getFormDataExtra: PropTypes.func,
   exploreChart: PropTypes.func,
   exportCSV: PropTypes.func,
-  fetchSlice: PropTypes.func,
-  saveSlice: PropTypes.func,
-  removeSlice: PropTypes.func,
-  removeChart: PropTypes.func,
-  updateDashboardLayout: PropTypes.func,
+  refreshChart: PropTypes.func,
+  saveSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
   editMode: PropTypes.bool.isRequired,
+  showBuilderPane: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
+  expandedSlices: {},
+  filters: {},
+  timeout: 60,
   onChange: () => ({}),
   getFormDataExtra: () => ({}),
   exploreChart: () => ({}),
   exportCSV: () => ({}),
-  fetchSlice: () => ({}),
-  saveSlice: () => ({}),
-  removeSlice: () => ({}),
-  removeChart: () => ({}),
-  updateDashboardLayout: () => ({}),
+  refreshChart: () => ({}),
+  saveSliceName: () => ({}),
   toggleExpandSlice: () => ({}),
   addFilter: () => ({}),
   getFilters: () => ({}),
-  clearFilter: () => ({}),
   removeFilter: () => ({}),
 };
 
@@ -53,141 +51,101 @@ class GridLayout extends React.Component {
   constructor(props) {
     super(props);
 
-    this.onResizeStop = this.onResizeStop.bind(this);
-    this.onDragStop = this.onDragStop.bind(this);
-    this.forceRefresh = this.forceRefresh.bind(this);
-    this.removeSlice = this.removeSlice.bind(this);
-    this.updateSliceName = this.props.dashboard.dash_edit_perm ?
+    this.updateSliceName = this.props.dashboardInfo.dash_edit_perm ?
       this.updateSliceName.bind(this) : null;
   }
 
-  onResizeStop(layout) {
-    this.props.updateDashboardLayout(layout);
-    this.props.onChange();
-  }
-
-  onDragStop(layout) {
-    this.props.updateDashboardLayout(layout);
-    this.props.onChange();
+  componentDidUpdate(prevProps) {
+    if (prevProps.editMode !== this.props.editMode ||
+      prevProps.showBuilderPane !== this.props.showBuilderPane) {
+      this.props.rerenderCharts();
+    }
   }
 
-  getWidgetId(slice) {
-    return 'widget_' + slice.slice_id;
+  getWidgetId(sliceId) {
+    return 'widget_' + sliceId;
   }
 
-  getWidgetHeight(slice) {
-    const widgetId = this.getWidgetId(slice);
+  getWidgetHeight(sliceId) {
+    const widgetId = this.getWidgetId(sliceId);
     if (!widgetId || !this.refs[widgetId]) {
       return 400;
     }
-    return this.refs[widgetId].offsetHeight;
+    return this.refs[widgetId].parentNode.clientHeight;
   }
 
-  getWidgetWidth(slice) {
-    const widgetId = this.getWidgetId(slice);
+  getWidgetWidth(sliceId) {
+    const widgetId = this.getWidgetId(sliceId);
     if (!widgetId || !this.refs[widgetId]) {
       return 400;
     }
-    return this.refs[widgetId].offsetWidth;
-  }
-
-  findSliceIndexById(sliceId) {
-    return this.props.dashboard.slices
-      .map(slice => (slice.slice_id)).indexOf(sliceId);
-  }
-
-  forceRefresh(sliceId) {
-    return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true);
-  }
-
-  removeSlice(slice) {
-    if (!slice) {
-      return;
-    }
-
-    // remove slice dashboard and charts
-    this.props.removeSlice(slice);
-    this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
-    this.props.onChange();
+    return this.refs[widgetId].parentNode.clientWidth;
   }
 
   updateSliceName(sliceId, sliceName) {
-    const index = this.findSliceIndexById(sliceId);
-    if (index === -1) {
-      return;
-    }
-
-    const currentSlice = this.props.dashboard.slices[index];
-    if (currentSlice.slice_name === sliceName) {
+    const key = sliceId;
+    const currentSlice = this.props.slices[key];
+    if (!currentSlice || currentSlice.slice_name === sliceName) {
       return;
     }
 
-    this.props.saveSlice(currentSlice, sliceName);
+    this.props.saveSliceName(currentSlice, sliceName);
   }
 
-  isExpanded(slice) {
-    return this.props.dashboard.metadata.expanded_slices &&
-      this.props.dashboard.metadata.expanded_slices[slice.slice_id];
+  isExpanded(sliceId) {
+    return this.props.expandedSlices[sliceId];
   }
 
   render() {
-    const cells = this.props.dashboard.slices.map((slice) => {
-      const chartKey = `slice_${slice.slice_id}`;
-      const currentChart = this.props.charts[chartKey];
-      const queryResponse = currentChart.queryResponse || {};
-      return (
-        <div
-          id={'slice_' + slice.slice_id}
-          key={slice.slice_id}
-          data-slice-id={slice.slice_id}
-          className={`widget ${slice.form_data.viz_type}`}
-          ref={this.getWidgetId(slice)}
-        >
-          <GridCell
-            slice={slice}
-            chartKey={chartKey}
-            datasource={this.props.datasources[slice.form_data.datasource]}
-            filters={this.props.filters}
-            formData={this.props.getFormDataExtra(slice)}
-            timeout={this.props.timeout}
-            widgetHeight={this.getWidgetHeight(slice)}
-            widgetWidth={this.getWidgetWidth(slice)}
-            exploreChart={this.props.exploreChart}
-            exportCSV={this.props.exportCSV}
-            isExpanded={!!this.isExpanded(slice)}
-            isLoading={currentChart.chartStatus === 'loading'}
-            isCached={queryResponse.is_cached}
-            cachedDttm={queryResponse.cached_dttm}
-            toggleExpandSlice={this.props.toggleExpandSlice}
-            forceRefresh={this.forceRefresh}
-            removeSlice={this.removeSlice}
-            updateSliceName={this.updateSliceName}
-            addFilter={this.props.addFilter}
-            getFilters={this.props.getFilters}
-            clearFilter={this.props.clearFilter}
-            removeFilter={this.props.removeFilter}
-            editMode={this.props.editMode}
-            annotationQuery={currentChart.annotationQuery}
-            annotationError={currentChart.annotationError}
-          />
-        </div>);
+    const cells = {};
+    this.props.sliceIds.forEach((sliceId) => {
+      const key = sliceId;
+      const currentChart = this.props.charts[key];
+      const currentSlice = this.props.slices[key];
+      if (currentChart) {
+        const currentDatasource = this.props.datasources[currentChart.form_data.datasource];
+        const queryResponse = currentChart.queryResponse || {};
+        cells[key] = (
+          <div
+            id={key}
+            key={sliceId}
+            className={cx('widget', `${currentSlice.viz_type}`, { 'is-edit': this.props.editMode })}
+            ref={this.getWidgetId(sliceId)}
+          >
+            <GridCell
+              slice={currentSlice}
+              chart={currentChart}
+              datasource={currentDatasource}
+              filters={this.props.filters}
+              formData={this.props.getFormDataExtra(currentChart)}
+              timeout={this.props.timeout}
+              widgetHeight={this.getWidgetHeight(sliceId)}
+              widgetWidth={this.getWidgetWidth(sliceId)}
+              exploreChart={this.props.exploreChart}
+              exportCSV={this.props.exportCSV}
+              isExpanded={!!this.isExpanded(sliceId)}
+              isLoading={currentChart.chartStatus === 'loading'}
+              isCached={queryResponse.is_cached}
+              cachedDttm={queryResponse.cached_dttm}
+              toggleExpandSlice={this.props.toggleExpandSlice}
+              refreshChart={this.props.refreshChart}
+              updateSliceName={this.updateSliceName}
+              addFilter={this.props.addFilter}
+              getFilters={this.props.getFilters}
+              removeFilter={this.props.removeFilter}
+              editMode={this.props.editMode}
+              annotationQuery={currentChart.annotationQuery}
+              annotationError={currentChart.annotationError}
+            />
+          </div>
+        );
+      }
     });
 
     return (
-      <ResponsiveReactGridLayout
-        className="layout"
-        layouts={{ lg: this.props.dashboard.layout }}
-        onResizeStop={this.onResizeStop}
-        onDragStop={this.onDragStop}
-        cols={{ lg: 48, md: 48, sm: 40, xs: 32, xxs: 24 }}
-        rowHeight={10}
-        autoSize
-        margin={[20, 20]}
-        useCSSTransforms
-        draggableHandle=".drag"
-      >
-        {cells}
-      </ResponsiveReactGridLayout>
+      <DashboardBuilder
+        cells={cells}
+      />
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index eabd3f4..f533506 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,47 +1,65 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
 
 import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-import URLShortLinkButton from '../../components/URLShortLinkButton';
 import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import { chartPropShape } from '../v2/util/propShapes';
 import { t } from '../../locales';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
+  dashboardInfo: PropTypes.object.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  charts: PropTypes.objectOf(chartPropShape).isRequired,
+  layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
-  userId: PropTypes.string.isRequired,
-  isStarred: PropTypes.bool,
-  addSlicesToDashboard: PropTypes.func,
-  onSave: PropTypes.func,
-  onChange: PropTypes.func,
+  expandedSlices: PropTypes.object.isRequired,
+  isStarred: PropTypes.bool.isRequired,
+  onSave: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
   fetchFaveStar: PropTypes.func,
-  renderSlices: PropTypes.func,
+  fetchCharts: PropTypes.func.isRequired,
   saveFaveStar: PropTypes.func,
-  serialize: PropTypes.func,
-  startPeriodicRender: PropTypes.func,
-  updateDashboardTitle: PropTypes.func,
+  startPeriodicRender: PropTypes.func.isRequired,
+  updateDashboardTitle: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
   setEditMode: PropTypes.func.isRequired,
-  unsavedChanges: PropTypes.bool.isRequired,
+  showBuilderPane: PropTypes.bool.isRequired,
+  toggleBuilderPane: PropTypes.func.isRequired,
+  hasUnsavedChanges: PropTypes.bool.isRequired,
+
+  // redux
+  onUndo: PropTypes.func.isRequired,
+  onRedo: PropTypes.func.isRequired,
+  canUndo: PropTypes.bool.isRequired,
+  canRedo: PropTypes.bool.isRequired,
 };
 
 class Header extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.handleSaveTitle = this.handleSaveTitle.bind(this);
+    this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
+    this.forceRefresh = this.forceRefresh.bind(this);
+  }
+  forceRefresh() {
+    return this.props.fetchCharts(Object.values(this.props.charts), true);
   }
-  handleSaveTitle(title) {
-    this.props.updateDashboardTitle(title);
+  handleChangeText(nextText) {
+    const { updateDashboardTitle, onChange } = this.props;
+    if (nextText && this.props.dashboardTitle !== nextText) {
+      updateDashboardTitle(nextText);
+      onChange();
+    }
   }
   toggleEditMode() {
     this.props.setEditMode(!this.props.editMode);
   }
   renderUnsaved() {
-    if (!this.props.unsavedChanges) {
+    if (!this.props.hasUnsavedChanges) {
       return null;
     }
     return (
@@ -54,66 +72,86 @@ class Header extends React.PureComponent {
       />
     );
   }
+  renderInsertButton() {
+    if (!this.props.editMode) {
+      return null;
+    }
+    const btnText = this.props.showBuilderPane ? t('Hide builder pane') : t('Insert components');
+    return (
+      <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+        {btnText}
+      </Button>
+    );
+  }
   renderEditButton() {
-    if (!this.props.dashboard.dash_save_perm) {
+    if (!this.props.dashboardInfo.dash_save_perm) {
       return null;
     }
-    const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
+    const btnText = this.props.editMode ? t('Switch to View Mode') : t('Edit Dashboard');
     return (
-      <Button
-        bsStyle="default"
-        className="m-r-5"
-        style={{ width: '150px' }}
-        onClick={this.toggleEditMode}
-      >
+      <Button bsSize="small" onClick={this.toggleEditMode}>
         {btnText}
-      </Button>);
+      </Button>
+    );
   }
   render() {
-    const dashboard = this.props.dashboard;
+    const {
+      dashboardTitle,
+      layout,
+      filters,
+      expandedSlices,
+      onUndo,
+      onRedo,
+      canUndo,
+      canRedo,
+      onChange,
+      onSave,
+      editMode,
+    } = this.props;
+
     return (
-      <div className="title">
-        <div className="pull-left">
-          <h1 className="outer-container pull-left">
-            <EditableTitle
-              title={dashboard.dashboard_title}
-              canEdit={dashboard.dash_save_perm && this.props.editMode}
-              onSaveTitle={this.handleSaveTitle}
-              showTooltip={this.props.editMode}
-            />
-            <span className="favstar m-r-5">
-              <FaveStar
-                itemId={dashboard.id}
-                fetchFaveStar={this.props.fetchFaveStar}
-                saveFaveStar={this.props.saveFaveStar}
-                isStarred={this.props.isStarred}
-              />
-            </span>
-            {this.renderUnsaved()}
-          </h1>
-        </div>
-        <div className="pull-right" style={{ marginTop: '35px' }}>
-          <span className="m-r-5">
-            <URLShortLinkButton
-              emailSubject="Superset Dashboard"
-              emailContent="Check out this dashboard: "
+      <div className="dashboard-header">
+        <div className="dashboard-component-header header-large">
+          <EditableTitle
+            title={dashboardTitle}
+            canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
+            onSaveTitle={this.handleChangeText}
+            showTooltip={editMode}
+          />
+          <span className="favstar m-r-5">
+            <FaveStar
+              itemId={this.props.dashboardInfo.id}
+              fetchFaveStar={this.props.fetchFaveStar}
+              saveFaveStar={this.props.saveFaveStar}
+              isStarred={this.props.isStarred}
             />
           </span>
-          {this.renderEditButton()}
+          {this.renderUnsaved()}
+        </div>
+        <ButtonToolbar>
+          <ButtonGroup>
+            <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
+              Undo
+            </Button>
+            <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
+              Redo
+            </Button>
+            {this.renderInsertButton()}
+            {this.renderEditButton()}
+          </ButtonGroup>
           <Controls
-            dashboard={dashboard}
-            filters={this.props.filters}
-            userId={this.props.userId}
-            addSlicesToDashboard={this.props.addSlicesToDashboard}
-            onSave={this.props.onSave}
-            onChange={this.props.onChange}
-            renderSlices={this.props.renderSlices}
-            serialize={this.props.serialize}
+            dashboardInfo={this.props.dashboardInfo}
+            dashboardTitle={dashboardTitle}
+            layout={layout}
+            filters={filters}
+            expandedSlices={expandedSlices}
+            onSave={onSave}
+            onChange={onChange}
+            forceRefreshAllCharts={this.forceRefresh}
             startPeriodicRender={this.props.startPeriodicRender}
-            editMode={this.props.editMode}
+            editMode={editMode}
           />
-        </div>
-        <div className="clearfix" />
+        </ButtonToolbar>
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
index 93c4272..c2a5637 100644
--- a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
@@ -48,8 +48,11 @@ class RefreshIntervalModal extends React.PureComponent {
               options={options}
               value={this.state.refreshFrequency}
               onChange={(opt) => {
-                this.setState({ refreshFrequency: opt.value });
-                this.props.onChange(opt.value);
+                const value = opt ? opt.value : options[0].value;
+                this.setState({
+                  refreshFrequency: value,
+                });
+                this.props.onChange(value);
               }}
             />
           </div>
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index d693385..2e76bf4 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,31 +1,30 @@
 /* global notify */
 import React from 'react';
 import PropTypes from 'prop-types';
+import $ from 'jquery';
+
 import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
 import { getAjaxErrorMsg } from '../../modules/utils';
 import ModalTrigger from '../../components/ModalTrigger';
 import { t } from '../../locales';
 import Checkbox from '../../components/Checkbox';
 
-const $ = window.$ = require('jquery');
-
 const propTypes = {
-  css: PropTypes.string,
-  dashboard: PropTypes.object.isRequired,
+  dashboardId: PropTypes.number.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  expandedSlices: PropTypes.object.isRequired,
+  layout: PropTypes.object.isRequired,
   triggerNode: PropTypes.node.isRequired,
   filters: PropTypes.object.isRequired,
-  serialize: PropTypes.func,
-  onSave: PropTypes.func,
+  onSave: PropTypes.func.isRequired,
 };
 
 class SaveModal extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      dashboard: props.dashboard,
-      css: props.css,
       saveType: 'overwrite',
-      newDashName: props.dashboard.dashboard_title + ' [copy]',
+      newDashName: props.dashboardTitle + ' [copy]',
       duplicateSlices: false,
     };
     this.modal = null;
@@ -50,7 +49,6 @@ class SaveModal extends React.PureComponent {
   saveDashboardRequest(data, url, saveType) {
     const saveModal = this.modal;
     const onSaveDashboard = this.props.onSave;
-    Object.assign(data, { css: this.props.css });
     $.ajax({
       type: 'POST',
       url,
@@ -74,19 +72,17 @@ class SaveModal extends React.PureComponent {
     });
   }
   saveDashboard(saveType, newDashboardTitle) {
-    const dashboard = this.props.dashboard;
-    const positions = this.props.serialize();
+    const { dashboardTitle, layout: positions, expandedSlices, filters, dashboardId } = this.props;
     const data = {
       positions,
-      css: this.state.css,
-      expanded_slices: dashboard.metadata.expanded_slices || {},
-      dashboard_title: dashboard.dashboard_title,
-      default_filters: JSON.stringify(this.props.filters),
+      expanded_slices: expandedSlices,
+      dashboard_title: dashboardTitle,
+      default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
     let url = null;
     if (saveType === 'overwrite') {
-      url = `/superset/save_dash/${dashboard.id}/`;
+      url = `/superset/save_dash/${dashboardId}/`;
       this.saveDashboardRequest(data, url, saveType);
     } else if (saveType === 'newDashboard') {
       if (!newDashboardTitle) {
@@ -97,7 +93,7 @@ class SaveModal extends React.PureComponent {
         });
       } else {
         data.dashboard_title = newDashboardTitle;
-        url = `/superset/copy_dash/${dashboard.id}/`;
+        url = `/superset/copy_dash/${dashboardId}/`;
         this.saveDashboardRequest(data, url, saveType);
       }
     }
@@ -116,7 +112,7 @@ class SaveModal extends React.PureComponent {
               onChange={this.handleSaveTypeChange}
               checked={this.state.saveType === 'overwrite'}
             >
-              {t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
+              {t('Overwrite Dashboard [%s]', this.props.dashboardTitle)}
             </Radio>
             <hr />
             <Radio
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index e99d00f..6477fc4 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,219 +1,214 @@
 import React from 'react';
-import $ from 'jquery';
 import PropTypes from 'prop-types';
-import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+import cx from 'classnames';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+import { List } from 'react-virtualized';
+import SearchInput, { createFilter } from 'react-search-input';
 
-import ModalTrigger from '../../components/ModalTrigger';
-import { t } from '../../locales';
-
-require('react-bootstrap-table/css/react-bootstrap-table.css');
+import DragDroppable from '../v2/components/dnd/DragDroppable';
+import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../v2/util/componentTypes';
+import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../v2/util/constants';
+import { slicePropShape } from '../v2/util/propShapes';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
-  triggerNode: PropTypes.node.isRequired,
+  fetchAllSlices: PropTypes.func.isRequired,
+  isLoading: PropTypes.bool.isRequired,
+  slices: PropTypes.objectOf(slicePropShape).isRequired,
+  lastUpdated: PropTypes.number.isRequired,
+  errorMessage: PropTypes.string,
   userId: PropTypes.string.isRequired,
-  addSlicesToDashboard: PropTypes.func,
+  selectedSliceIds: PropTypes.object,
+  editMode: PropTypes.bool,
+};
+
+const defaultProps = {
+  selectedSliceIds: new Set(),
+  editMode: false,
 };
 
+const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
+const KEYS_TO_SORT = [
+  { key: 'slice_name', label: 'Name' },
+  { key: 'viz_type', label: 'Visualization' },
+  { key: 'datasource_name', label: 'Datasource' },
+  { key: 'changed_on', label: 'Recent' },
+];
+
 class SliceAdder extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
-      slices: [],
-      slicesLoaded: false,
-      selectionMap: {},
+      filteredSlices: [],
+      searchTerm: '',
+      sortBy: KEYS_TO_SORT.findIndex(item => (item.key === 'changed_on')),
     };
 
-    this.options = {
-      defaultSortOrder: 'desc',
-      defaultSortName: 'modified',
-      sizePerPage: 10,
-    };
+    this.rowRenderer = this.rowRenderer.bind(this);
+    this.searchUpdated = this.searchUpdated.bind(this);
+    this.handleKeyPress = this.handleKeyPress.bind(this);
+    this.handleSelect = this.handleSelect.bind(this);
+  }
 
-    this.addSlices = this.addSlices.bind(this);
-    this.toggleSlice = this.toggleSlice.bind(this);
+  componentDidMount() {
+    this.slicesRequest = this.props.fetchAllSlices(this.props.userId);
+  }
 
-    this.selectRowProp = {
-      mode: 'checkbox',
-      clickToSelect: true,
-      onSelect: this.toggleSlice,
-    };
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.lastUpdated !== this.props.lastUpdated) {
+      this.setState({
+        filteredSlices: Object.values(nextProps.slices)
+          .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
+          .sort(this.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key)),
+      });
+    }
   }
 
   componentWillUnmount() {
-    if (this.slicesRequest) {
+    if (this.slicesRequest && this.slicesRequest.abort) {
       this.slicesRequest.abort();
     }
   }
 
-  onEnterModal() {
-    const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
-    this.slicesRequest = $.ajax({
-      url: uri,
-      type: 'GET',
-      success: (response) => {
-        // Prepare slice data for table
-        const slices = response.result.map(slice => ({
-          id: slice.id,
-          sliceName: slice.slice_name,
-          vizType: slice.viz_type,
-          datasourceLink: slice.datasource_link,
-          modified: slice.modified,
-        }));
-
-        this.setState({
-          slices,
-          selectionMap: {},
-          slicesLoaded: true,
-        });
-      },
-      error: (error) => {
-        this.errored = true;
-        this.setState({
-          errorMsg: t('Sorry, there was an error fetching charts to this dashboard: ') +
-          this.getAjaxErrorMsg(error),
-        });
-      },
-    });
+  getFilteredSortedSlices(searchTerm, sortBy) {
+    return Object.values(this.props.slices)
+      .filter(createFilter(searchTerm, KEYS_TO_FILTERS))
+      .sort(this.sortByComparator(KEYS_TO_SORT[sortBy].key));
   }
 
-  getAjaxErrorMsg(error) {
-    const respJSON = error.responseJSON;
-    return (respJSON && respJSON.message) ? respJSON.message :
-      error.responseText;
+  sortByComparator(attr) {
+    const desc = (attr === 'changed_on') ? -1 : 1;
+
+    return (a, b) => {
+      if (a[attr] < b[attr]) {
+        return -1 * desc;
+      } else if (a[attr] > b[attr]) {
+        return 1 * desc;
+      }
+      return 0;
+    };
   }
 
-  addSlices() {
-    const adder = this;
-    this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
-      // if successful, page will be reloaded.
-      .fail((error) => {
-        adder.errored = true;
-        adder.setState({
-          errorMsg: t('Sorry, there was an error adding charts to this dashboard: ') +
-          this.getAjaxErrorMsg(error),
-        });
-      });
+  handleKeyPress(ev) {
+    if (ev.key === 'Enter') {
+      ev.preventDefault();
+
+      this.searchUpdated(ev.target.value);
+    }
   }
 
-  toggleSlice(slice) {
-    const selectionMap = Object.assign({}, this.state.selectionMap);
-    selectionMap[slice.id] = !selectionMap[slice.id];
-    this.setState({ selectionMap });
+  searchUpdated(searchTerm) {
+    this.setState({
+      searchTerm,
+      filteredSlices: this.getFilteredSortedSlices(searchTerm, this.state.sortBy),
+    });
   }
 
-  modifiedDateComparator(a, b, order) {
-    if (order === 'desc') {
-      if (a.changed_on > b.changed_on) {
-        return -1;
-      } else if (a.changed_on < b.changed_on) {
-        return 1;
-      }
-      return 0;
-    }
+  handleSelect(sortBy) {
+    this.setState({
+      sortBy,
+      filteredSlices: this.getFilteredSortedSlices(this.state.searchTerm, sortBy),
+    });
+  }
 
-    if (a.changed_on < b.changed_on) {
-      return -1;
-    } else if (a.changed_on > b.changed_on) {
-      return 1;
-    }
-    return 0;
+  rowRenderer({ key, index, style }) {
+    const cellData = this.state.filteredSlices[index];
+    const duration = cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : '';
+    const isSelected = this.props.selectedSliceIds.has(cellData.slice_id);
+    const type = CHART_TYPE;
+    const id = NEW_CHART_ID;
+    const meta = {
+      chartId: cellData.slice_id,
+    };
+
+    return (
+      <DragDroppable
+        component={{ type, id, meta }}
+        parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
+        index={0}
+        depth={0}
+        disableDragDrop={isSelected}
+        editMode={this.props.editMode}
+      >
+        {({ dragSourceRef }) => (
+          <div
+            ref={dragSourceRef}
+            className="chart-card-container"
+            key={key}
+            style={style}
+          >
+            <div className={cx('chart-card', { 'is-selected': isSelected })}>
+              <div className="card-title">{cellData.slice_name}</div>
+              <div className="card-body">
+                <div className="item">
+                  <span>Modified </span>
+                  <span>{duration}</span>
+                </div>
+                <div className="item">
+                  <span>Visualization </span>
+                  <span>{cellData.viz_type}</span>
+                </div>
+                <div className="item">
+                  <span>Data source </span>
+                  <span dangerouslySetInnerHTML={{ __html: cellData.datasource_link }} />
+                </div>
+              </div>
+            </div>
+          </div>
+        )}
+      </DragDroppable>
+    );
   }
 
   render() {
-    const hideLoad = this.state.slicesLoaded || this.errored;
-    let enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap);
-    if (enableAddSlice) {
-      enableAddSlice = enableAddSlice.some(function (key) {
-        return this.state.selectionMap[key];
-      }, this);
-    }
-    const modalContent = (
-      <div>
-        <img
-          src="/static/assets/images/loading.gif"
-          className={'loading ' + (hideLoad ? 'hidden' : '')}
-          alt={hideLoad ? '' : 'loading'}
-        />
-        <div className={this.errored ? '' : 'hidden'}>
-          {this.state.errorMsg}
-        </div>
-        <div className={this.state.slicesLoaded ? '' : 'hidden'}>
-          <BootstrapTable
-            ref="table"
-            data={this.state.slices}
-            selectRow={this.selectRowProp}
-            options={this.options}
-            hover
-            search
-            pagination
-            condensed
-            height="auto"
+    return (
+      <div className="slice-adder-container">
+        <div className="controls">
+          <DropdownButton
+            title={KEYS_TO_SORT[this.state.sortBy].label}
+            onSelect={this.handleSelect}
+            id="slice-adder-sortby"
           >
-            <TableHeaderColumn
-              dataField="id"
-              isKey
-              dataSort
-              hidden
+            {KEYS_TO_SORT.map((item, index) => (
+              <MenuItem key={item.key} eventKey={index}>{item.label}</MenuItem>
+            ))}
+          </DropdownButton>
+
+          <SearchInput
+            onChange={this.searchUpdated}
+            onKeyPress={this.handleKeyPress}
+          />
+        </div>
+
+        {this.props.isLoading &&
+          <img
+            src="/static/assets/images/loading.gif"
+            className="loading"
+            alt="loading"
+          />
+        }
+        <div className={this.props.errorMessage ? '' : 'hidden'}>
+          {this.props.errorMessage}
+        </div>
+        <div className={!this.props.isLoading ? '' : 'hidden'}>
+          {this.state.filteredSlices.length > 0 &&
+            <List
+              width={376}
+              height={500}
+              rowCount={this.state.filteredSlices.length}
+              rowHeight={136}
+              rowRenderer={this.rowRenderer}
+              searchTerm={this.state.searchTerm}
+              sortBy={this.state.sortBy}
+              selectedSliceIds={this.props.selectedSliceIds}
             />
-            <TableHeaderColumn
-              dataField="sliceName"
-              dataSort
-            >
-              {t('Name')}
-            </TableHeaderColumn>
-            <TableHeaderColumn
-              dataField="vizType"
-              dataSort
-            >
-              {t('Viz')}
-            </TableHeaderColumn>
-            <TableHeaderColumn
-              dataField="datasourceLink"
-              dataSort
-              // Will cause react-bootstrap-table to interpret the HTML returned
-              dataFormat={datasourceLink => datasourceLink}
-            >
-              {t('Datasource')}
-            </TableHeaderColumn>
-            <TableHeaderColumn
-              dataField="modified"
-              dataSort
-              sortFunc={this.modifiedDateComparator}
-              // Will cause react-bootstrap-table to interpret the HTML returned
-              dataFormat={modified => modified}
-            >
-              {t('Modified')}
-            </TableHeaderColumn>
-          </BootstrapTable>
-          <button
-            type="button"
-            className="btn btn-default"
-            data-dismiss="modal"
-            onClick={this.addSlices}
-            disabled={!enableAddSlice}
-          >
-            {t('Add Charts')}
-          </button>
+          }
         </div>
       </div>
     );
-
-    return (
-      <ModalTrigger
-        triggerNode={this.props.triggerNode}
-        tooltip={t('Add a new chart to the dashboard')}
-        beforeOpen={this.onEnterModal.bind(this)}
-        isMenuItem
-        modalBody={modalContent}
-        bsSize="large"
-        setModalAsTriggerChildren
-        modalTitle={t('Add Charts to Dashboard')}
-      />
-    );
   }
 }
 
 SliceAdder.propTypes = propTypes;
+SliceAdder.defaultProps = defaultProps;
 
 export default SliceAdder;
diff --git a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
new file mode 100644
index 0000000..b4f10d9
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
@@ -0,0 +1,25 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { fetchAllSlices } from '../actions/sliceEntities';
+import SliceAdder from './SliceAdder';
+
+function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
+  return {
+    userId: dashboardInfo.userId,
+    selectedSliceIds: dashboardState.sliceIds,
+    slices: sliceEntities.slices,
+    isLoading: sliceEntities.isLoading,
+    errorMessage: sliceEntities.errorMessage,
+    lastUpdated: sliceEntities.lastUpdated,
+    editMode: dashboardState.editMode,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators({
+    fetchAllSlices
+  }, dispatch);
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 6db9c68..f126949 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -1,11 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import moment from 'moment';
-import { connect } from 'react-redux';
 
 import { t } from '../../locales';
 import EditableTitle from '../../components/EditableTitle';
 import TooltipWrapper from '../../components/TooltipWrapper';
+import SliceHeaderControls from './SliceHeaderControls';
 
 const propTypes = {
   slice: PropTypes.object.isRequired,
@@ -14,7 +13,6 @@ const propTypes = {
   isExpanded: PropTypes.bool,
   isCached: PropTypes.bool,
   cachedDttm: PropTypes.string,
-  removeSlice: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   forceRefresh: PropTypes.func,
@@ -40,11 +38,6 @@ class SliceHeader extends React.PureComponent {
     super(props);
 
     this.onSaveTitle = this.onSaveTitle.bind(this);
-    this.onToggleExpandSlice = this.onToggleExpandSlice.bind(this);
-    this.exportCSV = this.props.exportCSV.bind(this, this.props.slice);
-    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice);
-    this.forceRefresh = this.props.forceRefresh.bind(this, this.props.slice.slice_id);
-    this.removeSlice = this.props.removeSlice.bind(this, this.props.slice);
   }
 
   onSaveTitle(newTitle) {
@@ -53,17 +46,12 @@ class SliceHeader extends React.PureComponent {
     }
   }
 
-  onToggleExpandSlice() {
-    this.props.toggleExpandSlice(this.props.slice, !this.props.isExpanded);
-  }
-
   render() {
-    const slice = this.props.slice;
-    const isCached = this.props.isCached;
-    const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
-    const refreshTooltip = isCached ?
-      t('Served from data cached %s . Click to force refresh.', cachedWhen) :
-      t('Force refresh data');
+    const {
+      slice, isExpanded, isCached, cachedDttm,
+      toggleExpandSlice, forceRefresh,
+      exploreChart, exportCSV,
+    } = this.props;
     const annoationsLoading = t('Annotation layers are still loading.');
     const annoationsError = t('One ore more annotation layers failed loading.');
 
@@ -96,83 +84,18 @@ class SliceHeader extends React.PureComponent {
                 <i className="fa fa-exclamation-circle danger" />
               </TooltipWrapper>
             }
-          </div>
-          <div className="chart-controls">
-            <div id={'controls_' + slice.slice_id} className="pull-right">
-              {this.props.editMode &&
-                <a>
-                  <TooltipWrapper
-                    placement="top"
-                    label="move"
-                    tooltip={t('Move chart')}
-                  >
-                    <i className="fa fa-arrows drag" />
-                  </TooltipWrapper>
-                </a>
-              }
-              <a className={`refresh ${isCached ? 'danger' : ''}`} onClick={this.forceRefresh}>
-                <TooltipWrapper
-                  placement="top"
-                  label="refresh"
-                  tooltip={refreshTooltip}
-                >
-                  <i className="fa fa-repeat" />
-                </TooltipWrapper>
-              </a>
-              {slice.description &&
-              <a onClick={this.onToggleExpandSlice}>
-                <TooltipWrapper
-                  placement="top"
-                  label="description"
-                  tooltip={t('Toggle chart description')}
-                >
-                  <i className="fa fa-info-circle slice_info" />
-                </TooltipWrapper>
-              </a>
-              }
-              {this.props.sliceCanEdit &&
-                <a href={slice.edit_url} target="_blank">
-                  <TooltipWrapper
-                    placement="top"
-                    label="edit"
-                    tooltip={t('Edit chart')}
-                  >
-                    <i className="fa fa-pencil" />
-                  </TooltipWrapper>
-                </a>
-              }
-              <a className="exportCSV" onClick={this.exportCSV}>
-                <TooltipWrapper
-                  placement="top"
-                  label="exportCSV"
-                  tooltip={t('Export CSV')}
-                >
-                  <i className="fa fa-table" />
-                </TooltipWrapper>
-              </a>
-              {this.props.supersetCanExplore &&
-                <a className="exploreChart" onClick={this.exploreChart}>
-                  <TooltipWrapper
-                    placement="top"
-                    label="exploreChart"
-                    tooltip={t('Explore chart')}
-                  >
-                    <i className="fa fa-share" />
-                  </TooltipWrapper>
-                </a>
-              }
-              {this.props.editMode &&
-                <a className="remove-chart" onClick={this.removeSlice}>
-                  <TooltipWrapper
-                    placement="top"
-                    label="close"
-                    tooltip={t('Remove chart from dashboard')}
-                  >
-                    <i className="fa fa-close" />
-                  </TooltipWrapper>
-                </a>
-              }
-            </div>
+            {!this.props.editMode &&
+              <SliceHeaderControls
+                slice={slice}
+                isCached={isCached}
+                isExpanded={isExpanded}
+                cachedDttm={cachedDttm}
+                toggleExpandSlice={toggleExpandSlice}
+                forceRefresh={forceRefresh}
+                exploreChart={exploreChart}
+                exportCSV={exportCSV}
+              />
+            }
           </div>
         </div>
       </div>
@@ -183,12 +106,4 @@ class SliceHeader extends React.PureComponent {
 SliceHeader.propTypes = propTypes;
 SliceHeader.defaultProps = defaultProps;
 
-function mapStateToProps({ dashboard }) {
-  return {
-    supersetCanExplore: dashboard.dashboard.superset_can_explore,
-    sliceCanEdit: dashboard.dashboard.slice_can_edit,
-  };
-}
-
-export { SliceHeader };
-export default connect(mapStateToProps, () => ({}))(SliceHeader);
+export default SliceHeader;
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
new file mode 100644
index 0000000..f61e59b
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import moment from 'moment';
+import { DropdownButton } from 'react-bootstrap';
+
+import { ActionMenuItem } from './ActionMenuItem';
+import { t } from '../../locales';
+
+const propTypes = {
+  slice: PropTypes.object.isRequired,
+  isCached: PropTypes.bool,
+  isExpanded: PropTypes.bool,
+  cachedDttm: PropTypes.string,
+  toggleExpandSlice: PropTypes.func,
+  forceRefresh: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+};
+
+class SliceHeaderControls extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.exportCSV = this.props.exportCSV.bind(this, this.props.slice.slice_id);
+    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice.slice_id);
+    this.toggleExpandSlice = this.props.toggleExpandSlice.bind(this, this.props.slice.slice_id);
+    this.toggleControls = this.toggleControls.bind(this);
+
+    this.state = {
+      showControls: false,
+    };
+  }
+
+  toggleControls() {
+    this.setState({
+      showControls: !this.state.showControls,
+    });
+  }
+
+  render() {
+    const slice = this.props.slice;
+    const isCached = this.props.isCached;
+    const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
+    const refreshTooltip = isCached ?
+      t('Served from data cached %s . Click to force refresh.', cachedWhen) :
+      t('Force refresh data');
+
+    // @TODO account for
+    //  dashboard.dashboard.superset_can_explore
+    //  dashboard.dashboard.slice_can_edit
+    return (
+      <DropdownButton
+        title=""
+        id={`slice_${slice.slice_id}-controls`}
+        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', { 'is-cached': isCached })}
+        pullRight
+        noCaret
+      >
+        <ActionMenuItem
+          text={t('Force refresh data')}
+          tooltip={refreshTooltip}
+          onClick={this.props.forceRefresh}
+        />
+
+        {slice.description &&
+          <ActionMenuItem
+            text={t('Toggle chart description')}
+            tooltip={t('Toggle chart description')}
+            onClick={this.toggleExpandSlice}
+          />
+        }
+
+        <ActionMenuItem
+          text={t('Edit chart')}
+          tooltip={t('Edit the chart\'s properties')}
+          href={slice.edit_url}
+          target="_blank"
+        />
+
+        <ActionMenuItem
+          text={t('Export CSV')}
+          tooltip={t('Export CSV')}
+          onClick={this.exportCSV}
+        />
+
+        <ActionMenuItem
+          text={t('Explore chart')}
+          tooltip={t('Explore chart')}
+          onClick={this.exploreChart}
+        />
+      </DropdownButton>
+    );
+  }
+}
+
+SliceHeaderControls.propTypes = propTypes;
+SliceHeaderControls.defaultProps = defaultProps;
+
+export default SliceHeaderControls;
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index 1aadc58..9c00f9e 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -8,36 +8,18 @@ import { initEnhancer } from '../reduxUtils';
 import { appSetup } from '../common';
 import { initJQueryAjax } from '../modules/utils';
 import DashboardContainer from './components/DashboardContainer';
-// import rootReducer, { getInitialState } from './reducers';
-
-import emptyDashboardLayout from './v2/fixtures/emptyDashboardLayout';
-import rootReducer from './v2/reducers/';
+import getInitialState from './reducers/getInitialState';
+import rootReducer from './reducers/index';
 
 appSetup();
 initJQueryAjax();
 
 const appContainer = document.getElementById('app');
-// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
-// const initState = Object.assign({}, getInitialState(bootstrapData));
-
-const initState = {
-  dashboardLayout: {
-    past: [],
-    present: emptyDashboardLayout,
-    future: [],
-  },
-  editMode: true,
-  messageToasts: [],
-};
+const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
+const initState = getInitialState(bootstrapData);
 
 const store = createStore(
-  rootReducer,
-  initState,
-  compose(
-    applyMiddleware(thunk),
-    initEnhancer(false),
-  ),
-);
+  rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
 
 ReactDOM.render(
   <Provider store={store}>
diff --git a/superset/assets/src/dashboard/reducers.js b/superset/assets/src/dashboard/reducers.js
deleted file mode 100644
index 01e6dc2..0000000
--- a/superset/assets/src/dashboard/reducers.js
+++ /dev/null
@@ -1,214 +0,0 @@
-/* eslint-disable camelcase */
-import { combineReducers } from 'redux';
-import d3 from 'd3';
-import shortid from 'shortid';
-
-import charts, { chart } from '../chart/chartReducer';
-import * as actions from './actions';
-import { getParam } from '../modules/utils';
-import { alterInArr, removeFromArr } from '../reduxUtils';
-import { applyDefaultFormData } from '../explore/store';
-import { getColorFromScheme } from '../modules/colors';
-
-export function getInitialState(bootstrapData) {
-  const { user_id, datasources, common, editMode } = bootstrapData;
-  delete common.locale;
-  delete common.language_pack;
-
-  const dashboard = { ...bootstrapData.dashboard_data };
-  let filters = {};
-  try {
-    // allow request parameter overwrite dashboard metadata
-    filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
-  } catch (e) {
-    //
-  }
-
-  // Priming the color palette with user's label-color mapping provided in
-  // the dashboard's JSON metadata
-  if (dashboard.metadata && dashboard.metadata.label_colors) {
-    const colorMap = dashboard.metadata.label_colors;
-    for (const label in colorMap) {
-      getColorFromScheme(label, null, colorMap[label]);
-    }
-  }
-
-  dashboard.posDict = {};
-  dashboard.layout = [];
-  if (Array.isArray(dashboard.position_json)) {
-    dashboard.position_json.forEach((position) => {
-      dashboard.posDict[position.slice_id] = position;
-    });
-  } else {
-    dashboard.position_json = [];
-  }
-
-  const lastRowId = Math.max(0, Math.max.apply(null,
-    dashboard.position_json.map(pos => (pos.row + pos.size_y))));
-  let newSliceCounter = 0;
-  dashboard.slices.forEach((slice) => {
-    const sliceId = slice.slice_id;
-    let pos = dashboard.posDict[sliceId];
-    if (!pos) {
-      // append new slices to dashboard bottom, 3 slices per row
-      pos = {
-        col: (newSliceCounter % 3) * 16 + 1,
-        row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
-        size_x: 16,
-        size_y: 16,
-      };
-      newSliceCounter++;
-    }
-
-    dashboard.layout.push({
-      i: String(sliceId),
-      x: pos.col - 1,
-      y: pos.row,
-      w: pos.size_x,
-      minW: 2,
-      h: pos.size_y,
-    });
-  });
-
-  // will use charts action/reducers to handle chart render
-  const initCharts = {};
-  dashboard.slices.forEach((slice) => {
-    const chartKey = 'slice_' + slice.slice_id;
-    initCharts[chartKey] = { ...chart,
-      chartKey,
-      slice_id: slice.slice_id,
-      form_data: slice.form_data,
-      formData: applyDefaultFormData(slice.form_data),
-    };
-  });
-
-  // also need to add formData for dashboard.slices
-  dashboard.slices = dashboard.slices.map(slice =>
-    ({ ...slice, formData: applyDefaultFormData(slice.form_data) }),
-  );
-
-  return {
-    charts: initCharts,
-    dashboard: { filters, dashboard, userId: user_id, datasources, common, editMode },
-  };
-}
-
-export const dashboard = function (state = {}, action) {
-  const actionHandlers = {
-    [actions.UPDATE_DASHBOARD_TITLE]() {
-      const newDashboard = { ...state.dashboard, dashboard_title: action.title };
-      return { ...state, dashboard: newDashboard };
-    },
-    [actions.UPDATE_DASHBOARD_LAYOUT]() {
-      const newDashboard = { ...state.dashboard, layout: action.layout };
-      return { ...state, dashboard: newDashboard };
-    },
-    [actions.REMOVE_SLICE]() {
-      const key = String(action.slice.slice_id);
-      const newLayout = state.dashboard.layout.filter(reactPos => (reactPos.i !== key));
-      const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
-      // if this slice is a filter
-      const newFilter = { ...state.filters };
-      let refresh = false;
-      if (state.filters[key]) {
-        delete newFilter[key];
-        refresh = true;
-      }
-      return {
-        ...state,
-        dashboard: { ...newDashboard, layout: newLayout },
-        filters: newFilter,
-        refresh,
-      };
-    },
-    [actions.TOGGLE_FAVE_STAR]() {
-      return { ...state, isStarred: action.isStarred };
-    },
-    [actions.SET_EDIT_MODE]() {
-      return { ...state, editMode: action.editMode };
-    },
-    [actions.TOGGLE_EXPAND_SLICE]() {
-      const updatedExpandedSlices = { ...state.dashboard.metadata.expanded_slices };
-      const sliceId = action.slice.slice_id;
-      if (action.isExpanded) {
-        updatedExpandedSlices[sliceId] = true;
-      } else {
-        delete updatedExpandedSlices[sliceId];
-      }
-      const metadata = { ...state.dashboard.metadata, expanded_slices: updatedExpandedSlices };
-      const newDashboard = { ...state.dashboard, metadata };
-      return { ...state, dashboard: newDashboard };
-    },
-
-    // filters
-    [actions.ADD_FILTER]() {
-      const selectedSlice = state.dashboard.slices
-        .find(slice => (slice.slice_id === action.sliceId));
-      if (!selectedSlice) {
-        return state;
-      }
-
-      let filters = state.filters;
-      const { sliceId, col, vals, merge, refresh } = action;
-      const filterKeys = ['__from', '__to', '__time_col',
-        '__time_grain', '__time_origin', '__granularity'];
-      if (filterKeys.indexOf(col) >= 0 ||
-        selectedSlice.formData.groupby.indexOf(col) !== -1) {
-        let newFilter = {};
-        if (!(sliceId in filters)) {
-          // Straight up set the filters if none existed for the slice
-          newFilter = { [col]: vals };
-        } else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
-          newFilter = { ...filters[sliceId], [col]: vals };
-          // d3.merge pass in array of arrays while some value form filter components
-          // from and to filter box require string to be process and return
-        } else if (filters[sliceId][col] instanceof Array) {
-          newFilter[col] = d3.merge([filters[sliceId][col], vals]);
-        } else {
-          newFilter[col] = d3.merge([[filters[sliceId][col]], vals])[0] || '';
-        }
-        filters = { ...filters, [sliceId]: newFilter };
-      }
-      return { ...state, filters, refresh };
-    },
-    [actions.CLEAR_FILTER]() {
-      const newFilters = { ...state.filters };
-      delete newFilters[action.sliceId];
-      return { ...state, filter: newFilters, refresh: true };
-    },
-    [actions.REMOVE_FILTER]() {
-      const { sliceId, col, vals, refresh } = action;
-      const excluded = new Set(vals);
-      const valFilter = val => !excluded.has(val);
-
-      let filters = state.filters;
-      // Have to be careful not to modify the dashboard state so that
-      // the render actually triggers
-      if (sliceId in state.filters && col in state.filters[sliceId]) {
-        const newFilter = filters[sliceId][col].filter(valFilter);
-        filters = { ...filters, [sliceId]: newFilter };
-      }
-      return { ...state, filters, refresh };
-    },
-
-    // slice reducer
-    [actions.UPDATE_SLICE_NAME]() {
-      const newDashboard = alterInArr(
-        state.dashboard, 'slices',
-        action.slice, { slice_name: action.sliceName },
-        'slice_id');
-      return { ...state, dashboard: newDashboard };
-    },
-  };
-
-  if (action.type in actionHandlers) {
-    return actionHandlers[action.type]();
-  }
-  return state;
-};
-
-export default combineReducers({
-  charts,
-  dashboard,
-  impressionId: () => (shortid.generate()),
-});
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
new file mode 100644
index 0000000..84ee58e
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -0,0 +1,128 @@
+/* eslint-disable camelcase */
+import { merge as mergeArray } from 'd3';
+
+import {
+  ADD_SLICE,
+  ADD_FILTER,
+  ON_CHANGE,
+  ON_SAVE,
+  REMOVE_SLICE,
+  REMOVE_FILTER,
+  SET_EDIT_MODE,
+  TOGGLE_BUILDER_PANE,
+  TOGGLE_EXPAND_SLICE,
+  TOGGLE_FAVE_STAR,
+  UPDATE_DASHBOARD_TITLE,
+} from '../actions/dashboardState';
+
+export default function (state = {}, action) {
+  const actionHandlers = {
+    [UPDATE_DASHBOARD_TITLE]() {
+      return { ...state, title: action.title };
+    },
+    [ADD_SLICE]() {
+      const updatedSliceIds = new Set(state.sliceIds);
+      updatedSliceIds.add(action.slice.slice_id);
+      return {
+        ...state,
+        sliceIds: updatedSliceIds,
+      };
+    },
+    [REMOVE_SLICE]() {
+      const sliceId = action.sliceId;
+      const updatedSliceIds = new Set(state.sliceIds);
+      updatedSliceIds.delete(sliceId);
+
+      const key = sliceId;
+      // if this slice is a filter
+      const newFilter = { ...state.filters };
+      let refresh = false;
+      if (state.filters[key]) {
+        delete newFilter[key];
+        refresh = true;
+      }
+      return {
+        ...state,
+        sliceIds: updatedSliceIds,
+        filters: newFilter,
+        refresh,
+      };
+    },
+    [TOGGLE_FAVE_STAR]() {
+      return { ...state, isStarred: action.isStarred };
+    },
+    [SET_EDIT_MODE]() {
+      return { ...state, editMode: action.editMode };
+    },
+    [TOGGLE_BUILDER_PANE]() {
+      return { ...state, showBuilderPane: !state.showBuilderPane };
+    },
+    [TOGGLE_EXPAND_SLICE]() {
+      const updatedExpandedSlices = { ...state.expandedSlices };
+      const sliceId = action.sliceId;
+      if (updatedExpandedSlices[sliceId]) {
+        delete updatedExpandedSlices[sliceId];
+      } else {
+        updatedExpandedSlices[sliceId] = true;
+      }
+      return { ...state, expandedSlices: updatedExpandedSlices };
+    },
+    [ON_CHANGE]() {
+      return { ...state, hasUnsavedChanges: true };
+    },
+    [ON_SAVE]() {
+      return { ...state, hasUnsavedChanges: false };
+    },
+
+    // filters
+    [ADD_FILTER]() {
+      const hasSelectedFilter = state.sliceIds.has(action.chart.id);
+      if (!hasSelectedFilter) {
+        return state;
+      }
+
+      let filters = state.filters;
+      const { chart, col, vals, merge, refresh } = action;
+      const sliceId = chart.id;
+      const filterKeys = ['__from', '__to', '__time_col',
+        '__time_grain', '__time_origin', '__granularity'];
+      if (filterKeys.indexOf(col) >= 0 ||
+        action.chart.formData.groupby.indexOf(col) !== -1) {
+        let newFilter = {};
+        if (!(sliceId in filters)) {
+          // Straight up set the filters if none existed for the slice
+          newFilter = { [col]: vals };
+        } else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
+          newFilter = { ...filters[sliceId], [col]: vals };
+          // d3.merge pass in array of arrays while some value form filter components
+          // from and to filter box require string to be process and return
+        } else if (filters[sliceId][col] instanceof Array) {
+          newFilter[col] = mergeArray([filters[sliceId][col], vals]);
+        } else {
+          newFilter[col] = mergeArray([[filters[sliceId][col]], vals])[0] || '';
+        }
+        filters = { ...filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+    [REMOVE_FILTER]() {
+      const { sliceId, col, vals, refresh } = action;
+      const excluded = new Set(vals);
+      const valFilter = val => !excluded.has(val);
+
+      let filters = state.filters;
+      // Have to be careful not to modify the dashboard state so that
+      // the render actually triggers
+      if (sliceId in state.filters && col in state.filters[sliceId]) {
+        const newFilter = filters[sliceId][col].filter(valFilter);
+        filters = { ...filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return actionHandlers[action.type]();
+  }
+  return state;
+}
diff --git a/superset/assets/src/dashboard/reducers/datasources.js b/superset/assets/src/dashboard/reducers/datasources.js
new file mode 100644
index 0000000..4df7507
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/datasources.js
@@ -0,0 +1,17 @@
+import * as actions from '../actions/datasources';
+
+export default function datasourceReducer(datasources = {}, action) {
+  const actionHandlers = {
+    [actions.SET_DATASOURCE]() {
+      return action.datasource;
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return {
+      ...datasources,
+      [action.key]: actionHandlers[action.type](datasources[action.key], action),
+    };
+  }
+  return datasources;
+}
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
new file mode 100644
index 0000000..1129210
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -0,0 +1,109 @@
+/* eslint-disable camelcase */
+import shortid from 'shortid';
+
+import { chart } from '../../chart/chartReducer';
+import { initSliceEntities } from './sliceEntities';
+import { getParam } from '../../modules/utils';
+import { applyDefaultFormData } from '../../explore/stores/store';
+import { getColorFromScheme } from '../../modules/colors';
+import layoutConverter from '../util/dashboardLayoutConverter';
+import { DASHBOARD_ROOT_ID } from '../v2/util/constants';
+
+export default function (bootstrapData) {
+  const { user_id, datasources, common } = bootstrapData;
+  delete common.locale;
+  delete common.language_pack;
+
+  const dashboard = { ...bootstrapData.dashboard_data };
+  let filters = {};
+  try {
+    // allow request parameter overwrite dashboard metadata
+    filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+  } catch (e) {
+    //
+  }
+
+  // Priming the color palette with user's label-color mapping provided in
+  // the dashboard's JSON metadata
+  if (dashboard.metadata && dashboard.metadata.label_colors) {
+    const colorMap = dashboard.metadata.label_colors;
+    for (const label in colorMap) {
+      getColorFromScheme(label, null, colorMap[label]);
+    }
+  }
+
+  // dashboard layout
+  const positionJson = dashboard.position_json;
+  let layout;
+  if (!positionJson || !positionJson[DASHBOARD_ROOT_ID]) {
+    layout = layoutConverter(dashboard);
+  } else {
+    layout = positionJson;
+  }
+
+  const dashboardLayout = {
+    past: [],
+    present: layout,
+    future: [],
+  };
+  delete dashboard.position_json;
+  delete dashboard.css;
+
+  const chartQueries = {};
+  const slices = {};
+  const sliceIds = new Set();
+  dashboard.slices.forEach((slice) => {
+    const key = slice.slice_id;
+    chartQueries[key] = { ...chart,
+      id: key,
+      form_data: slice.form_data,
+      formData: applyDefaultFormData(slice.form_data),
+    };
+
+    slices[key] = {
+      slice_id: key,
+      slice_url: slice.slice_url,
+      slice_name: slice.slice_name,
+      form_data: slice.form_data,
+      edit_url: slice.edit_url,
+      viz_type: slice.form_data.viz_type,
+      datasource: slice.form_data.datasource,
+      description: slice.description,
+      description_markeddown: slice.description_markeddown,
+    };
+
+    sliceIds.add(key);
+  });
+
+  return {
+    datasources,
+    sliceEntities: { ...initSliceEntities, slices, isLoading: false },
+    charts: chartQueries,
+    dashboardInfo: {  /* readOnly props */
+      id: dashboard.id,
+      slug: dashboard.slug,
+      metadata: {
+        filter_immune_slice_fields: dashboard.metadata.filter_immune_slice_fields,
+        filter_immune_slices: dashboard.metadata.filter_immune_slices,
+        timed_refresh_immune_slices: dashboard.metadata.timed_refresh_immune_slices,
+      },
+      userId: user_id,
+      dash_edit_perm: dashboard.dash_edit_perm,
+      dash_save_perm: dashboard.dash_save_perm,
+      common,
+    },
+    dashboardState: {
+      title: dashboard.dashboard_title,
+      sliceIds,
+      refresh: false,
+      filters,
+      expandedSlices: dashboard.metadata.expanded_slices || {},
+      editMode: false,
+      showBuilderPane: false,
+      hasUnsavedChanges: false,
+    },
+    dashboardLayout,
+    messageToasts: [],
+    impressionId: shortid.generate(),
+  };
+}
diff --git a/superset/assets/src/dashboard/reducers/index.js b/superset/assets/src/dashboard/reducers/index.js
new file mode 100644
index 0000000..a2397e0
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/index.js
@@ -0,0 +1,22 @@
+import { combineReducers } from 'redux';
+
+import charts from '../../chart/chartReducer';
+import dashboardState from './dashboardState';
+import datasources from './datasources';
+import sliceEntities from './sliceEntities';
+import dashboardLayout from '../v2/reducers/index';
+import messageToasts from '../v2/reducers/messageToasts';
+
+const dashboardInfo = (state = {}) => (state);
+const impressionId = (state = '') => (state);
+
+export default combineReducers({
+  charts,
+  datasources,
+  sliceEntities,
+  dashboardInfo,
+  dashboardState,
+  dashboardLayout,
+  messageToasts,
+  impressionId,
+});
diff --git a/superset/assets/src/dashboard/reducers/sliceEntities.js b/superset/assets/src/dashboard/reducers/sliceEntities.js
new file mode 100644
index 0000000..61a58f6
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/sliceEntities.js
@@ -0,0 +1,62 @@
+import {
+  FETCH_ALL_SLICES_FAILED,
+  FETCH_ALL_SLICES_STARTED,
+  SET_ALL_SLICES,
+  UPDATE_SLICE_NAME,
+} from '../actions/sliceEntities';
+import { t } from '../../locales';
+
+export const initSliceEntities = {
+  slices: {},
+  isLoading: true,
+  errorMessage: null,
+  lastUpdated: 0,
+};
+
+export default function (state = initSliceEntities, action) {
+  const actionHandlers = {
+    [UPDATE_SLICE_NAME]() {
+      const updatedSlice = {
+        ...state.slices[action.key],
+        slice_name: action.sliceName,
+      };
+      const updatedSlices = {
+        ...state.slices,
+        [action.key]: updatedSlice,
+      };
+      return { ...state, slices: updatedSlices };
+    },
+    [FETCH_ALL_SLICES_STARTED]() {
+      return {
+        ...state,
+        isLoading: true,
+      };
+    },
+    [SET_ALL_SLICES]() {
+      return {
+        ...state,
+        isLoading: false,
+        slices: { ...state.slices, ...action.slices }, // append more slices
+        lastUpdated: new Date().getTime(),
+      };
+    },
+    [FETCH_ALL_SLICES_FAILED]() {
+      const respJSON = action.error.responseJSON;
+      const errorMessage =
+        t('Sorry, there was an error adding slices to this dashboard: ') +
+        (respJSON && respJSON.message) ? respJSON.message :
+          error.responseText;
+      return {
+        ...state,
+        isLoading: false,
+        errorMessage,
+        lastUpdated: new Date().getTime(),
+      };
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return actionHandlers[action.type]();
+  }
+  return state;
+}
diff --git a/superset/assets/src/dashboard/util/dashboardHelper.js b/superset/assets/src/dashboard/util/dashboardHelper.js
new file mode 100644
index 0000000..c9a6021
--- /dev/null
+++ b/superset/assets/src/dashboard/util/dashboardHelper.js
@@ -0,0 +1,9 @@
+export function getChartIdsFromLayout(layout) {
+  return Object.values(layout)
+    .reduce((chartIds, value) => {
+      if (value && value.meta && value.meta.chartId) {
+        chartIds.push(value.meta.chartId);
+      }
+      return chartIds;
+    }, []);
+}
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
new file mode 100644
index 0000000..854ca65
--- /dev/null
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -0,0 +1,322 @@
+/* eslint-disable no-param-reassign */
+/* eslint-disable camelcase */
+/* eslint-disable no-loop-func */
+import {
+  ROW_TYPE,
+  COLUMN_TYPE,
+  CHART_TYPE,
+  DASHBOARD_HEADER_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  DASHBOARD_GRID_TYPE,
+} from '../v2/util/componentTypes';
+import {
+  DASHBOARD_GRID_ID,
+  DASHBOARD_HEADER_ID,
+  DASHBOARD_ROOT_ID,
+} from '../v2/util/constants';
+
+const MAX_RECURSIVE_LEVEL = 6;
+const GRID_RATIO = 4;
+const ROW_HEIGHT = 8;
+const generateId = (() => {
+  let componentId = 1;
+  return () => (componentId++);
+})();
+
+/**
+ *
+ * @param positions: single array of slices
+ * @returns boundary object {top: number, bottom: number, left: number, right: number}
+ */
+function getBoundary(positions) {
+  let top = Number.MAX_VALUE;
+  let bottom = 0;
+  let left = Number.MAX_VALUE;
+  let right = 1;
+  positions.forEach((item) => {
+    const { row, col, size_x, size_y } = item;
+    if (row <= top) top = row;
+    if (col <= left) left = col;
+    if (bottom <= row + size_y) bottom = row + size_y;
+    if (right <= col + size_x) right = col + size_x;
+  });
+
+  return {
+    top,
+    bottom,
+    left,
+    right,
+  };
+}
+
+function getRowContainer() {
+  const id = 'DASHBOARD_ROW_TYPE-' + generateId();
+  return {
+    version: 'v2',
+    type: ROW_TYPE,
+    id,
+    children: [],
+    meta: {
+      background: 'BACKGROUND_TRANSPARENT',
+    },
+  };
+}
+
+function getColContainer() {
+  const id = 'DASHBOARD_COLUMN_TYPE-' + generateId();
+  return {
+    version: 'v2',
+    type: COLUMN_TYPE,
+    id,
+    children: [],
+    meta: {
+      background: 'BACKGROUND_TRANSPARENT',
+    },
+  };
+}
+
+function getChartHolder(item) {
+  const { row, col, size_x, size_y, slice_id } = item;
+  const converted = {
+    row: Math.round(row / GRID_RATIO),
+    col: Math.floor((col - 1) / GRID_RATIO) + 1,
+    size_x: Math.max(1, Math.floor(size_x / GRID_RATIO)),
+    size_y: Math.max(1, Math.round(size_y / GRID_RATIO)),
+    slice_id,
+  };
+
+  return {
+    version: 'v2',
+    type: CHART_TYPE,
+    id: 'DASHBOARD_CHART_TYPE-' + generateId(),
+    children: [],
+    meta: {
+      width: converted.size_x,
+      height: Math.round(converted.size_y * 100 / ROW_HEIGHT),
+      chartId: slice_id,
+    },
+  };
+}
+
+function getChildrenMax(items, attr, layout) {
+  return Math.max.apply(null, items.map(child => (layout[child].meta[attr])));
+}
+
+function getChildrenSum(items, attr, layout) {
+  return items.reduce((preValue, child) => (preValue + layout[child].meta[attr]), 0);
+}
+
+function sortByRowId(item1, item2) {
+  return item1.row - item2.row;
+}
+
+function sortByColId(item1, item2) {
+  return item1.col - item2.col;
+}
+
+function hasOverlap(positions, xAxis = true) {
+  return positions.slice()
+    .sort(xAxis ? sortByColId : sortByRowId)
+    .some((item, index, arr) => {
+      if (index === arr.length - 1) {
+        return false;
+      }
+
+      if (xAxis) {
+        return (item.col + item.size_x) > arr[index + 1].col;
+      }
+      return (item.row + item.size_y) > arr[index + 1].row;
+    });
+}
+
+function doConvert(positions, level, parent, root) {
+  if (positions.length === 0) {
+    return;
+  }
+
+  if (positions.length === 1 || level >= MAX_RECURSIVE_LEVEL) {
+    // special treatment for single chart dash, always wrap chart inside a row
+    if (parent.type === 'DASHBOARD_GRID_TYPE') {
+      const rowContainer = getRowContainer();
+      root[rowContainer.id] = rowContainer;
+      parent.children.push(rowContainer.id);
+      parent = rowContainer;
+    }
+
+    const chartHolder = getChartHolder(positions[0]);
+    root[chartHolder.id] = chartHolder;
+    parent.children.push(chartHolder.id);
+    return;
+  }
+
+  let currentItems = positions.slice();
+  const { top, bottom, left, right } = getBoundary(positions);
+  // find row dividers
+  const layers = [];
+  let currentRow = top + 1;
+  while (currentItems.length && currentRow <= bottom) {
+    const upper = [];
+    const lower = [];
+
+    const isRowDivider = currentItems.every((item) => {
+      const { row, size_y } = item;
+      if (row + size_y <= currentRow) {
+        lower.push(item);
+        return true;
+      } else if (row >= currentRow) {
+        upper.push(item);
+        return true;
+      }
+      return false;
+    });
+
+    if (isRowDivider) {
+      currentItems = upper.slice();
+      layers.push(lower);
+    }
+    currentRow++;
+  }
+
+  layers.forEach((layer) => {
+    if (layer.length === 0) {
+      return;
+    }
+
+    if (layer.length === 1) {
+      const chartHolder = getChartHolder(layer[0]);
+      root[chartHolder.id] = chartHolder;
+      parent.children.push(chartHolder.id);
+      return;
+    }
+
+    // create a new row
+    const rowContainer = getRowContainer();
+    root[rowContainer.id] = rowContainer;
+    parent.children.push(rowContainer.id);
+
+    currentItems = layer.slice();
+    if (!hasOverlap(currentItems)) {
+      currentItems.sort(sortByColId).forEach((item) => {
+        const chartHolder = getChartHolder(item);
+        root[chartHolder.id] = chartHolder;
+        rowContainer.children.push(chartHolder.id);
+      });
+    } else {
+      // find col dividers for each layer
+      let currentCol = left + 1;
+      while (currentItems.length && currentCol <= right) {
+        const upper = [];
+        const lower = [];
+
+        const isColDivider = currentItems.every((item) => {
+          const { col, size_x } = item;
+          if (col + size_x <= currentCol) {
+            lower.push(item);
+            return true;
+          } else if (col >= currentCol) {
+            upper.push(item);
+            return true;
+          }
+          return false;
+        });
+
+        if (isColDivider) {
+          if (lower.length === 1) {
+            const chartHolder = getChartHolder(lower[0]);
+            root[chartHolder.id] = chartHolder;
+            rowContainer.children.push(chartHolder.id);
+          } else {
+            // create a new column
+            const colContainer = getColContainer();
+            root[colContainer.id] = colContainer;
+            rowContainer.children.push(colContainer.id);
+
+            if (!hasOverlap(lower, false)) {
+              lower.sort(sortByRowId).forEach((item) => {
+                const chartHolder = getChartHolder(item);
+                root[chartHolder.id] = chartHolder;
+                colContainer.children.push(chartHolder.id);
+              });
+            } else {
+              doConvert(lower, level + 2, colContainer, root);
+            }
+
+            // add col meta
+            colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
+          }
+
+          currentItems = upper.slice();
+        }
+        currentCol++;
+      }
+    }
+
+    rowContainer.meta.width = getChildrenSum(rowContainer.children, 'width', root);
+  });
+}
+
+export default function (dashboard) {
+  const positions = [];
+
+  // position data clean up. some dashboard didn't have position_json
+  let { position_json } = dashboard;
+  const posDict = {};
+  if (Array.isArray(position_json)) {
+    position_json.forEach((position) => {
+      posDict[position.slice_id] = position;
+    });
+  } else {
+    position_json = [];
+  }
+
+  const lastRowId = Math.max(0, Math.max.apply(null,
+    position_json.map(pos => (pos.row + pos.size_y))));
+  let newSliceCounter = 0;
+  dashboard.slices.forEach((slice) => {
+    const sliceId = slice.slice_id;
+    let pos = posDict[sliceId];
+    if (!pos) {
+      // append new slices to dashboard bottom, 3 slices per row
+      pos = {
+        col: (newSliceCounter % 3) * 16 + 1,
+        row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
+        size_x: 16,
+        size_y: 16,
+        slice_id: String(sliceId),
+      };
+      newSliceCounter++;
+    }
+
+    positions.push(pos);
+  });
+
+  const root = {
+    [DASHBOARD_ROOT_ID]: {
+      version: 'v2',
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: [DASHBOARD_GRID_ID],
+    },
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: [],
+    },
+    [DASHBOARD_HEADER_ID]: {
+      type: DASHBOARD_HEADER_TYPE,
+      id: DASHBOARD_HEADER_ID,
+    },
+  };
+  doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
+
+  // remove row's width/height and col's height
+  Object.values(root).forEach((item) => {
+    if (ROW_TYPE === item.type) {
+      const meta = item.meta;
+      delete meta.width;
+    }
+  });
+
+  // console.log(JSON.stringify(root));
+  return root;
+}
diff --git a/superset/assets/src/dashboard/v2/actions/messageToasts.js b/superset/assets/src/dashboard/v2/actions/messageToasts.js
index af10ead..2ebc06c 100644
--- a/superset/assets/src/dashboard/v2/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/v2/actions/messageToasts.js
@@ -6,7 +6,6 @@ function getToastUuid(type) {
 
 export const ADD_TOAST = 'ADD_TOAST';
 export function addToast({ toastType, text }) {
-  debugger;
   return {
     type: ADD_TOAST,
     payload: {
diff --git a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
index efef5a5..f9a37cc 100644
--- a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
@@ -1,37 +1,69 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+import cx from 'classnames';
 
-import NewChart from './gridComponents/new/NewChart';
 import NewColumn from './gridComponents/new/NewColumn';
 import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
 import NewTabs from './gridComponents/new/NewTabs';
+import SliceAdderContainer from '../../../dashboard/components/SliceAdderContainer';
 
-const propTypes = {
-  editMode: PropTypes.bool,
-};
+import '../stylesheets/builder-sidepane.less';
 
 class BuilderComponentPane extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      showSlices: false,
+    };
+
+    this.openSlicesPane = this.showSlices.bind(this, true);
+    this.closeSlicesPane = this.showSlices.bind(this, false);
+  }
+
+  showSlices(show) {
+    this.setState({
+      showSlices: show,
+    });
+  }
+
   render() {
     return (
       <div className="dashboard-builder-sidepane">
         <div className="dashboard-builder-sidepane-header">
           Insert components
+          {this.state.showSlices &&
+            <i className="fa fa-times close trigger" onClick={this.closeSlicesPane} role="none" />
+          }
         </div>
-        <NewChart />
-        <NewHeader />
 
-        <NewDivider />
+        <div className="component-layer">
+          <div
+            className="dragdroppable dragdroppable-row"
+            onClick={this.openSlicesPane}
+            role="none"
+          >
+            <div className="new-component static">
+              <div className="new-component-placeholder fa fa-area-chart" />
+              Chart
+              <i className="fa fa-arrow-right open trigger" />
+            </div>
+          </div>
 
-        <NewTabs />
-        <NewRow />
-        <NewColumn />
+          <NewHeader />
+          <NewDivider />
+
+          <NewTabs />
+          <NewRow />
+          <NewColumn />
+        </div>
+
+        <div className={cx('slices-layer', this.state.showSlices && 'show')}>
+          <SliceAdderContainer />
+        </div>
       </div>
     );
   }
 }
 
-BuilderComponentPane.propTypes = propTypes;
-
 export default BuilderComponentPane;
diff --git a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
index 8e2d985..f3f5867 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
@@ -20,15 +20,18 @@ import {
 } from '../util/constants';
 
 const propTypes = {
+  cells: PropTypes.object.isRequired,
+
   // redux
   dashboardLayout: PropTypes.object.isRequired,
   deleteTopLevelTabs: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
+  showBuilderPane: PropTypes.bool,
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
-  editMode: true,
+  showBuilderPane: false,
 };
 
 class DashboardBuilder extends React.Component {
@@ -105,6 +108,7 @@ class DashboardBuilder extends React.Component {
               index={0}
               renderTabContent={false}
               onChangeTab={this.handleChangeTab}
+              cells={this.props.cells}
             />
           </WithPopoverMenu>}
 
@@ -112,8 +116,11 @@ class DashboardBuilder extends React.Component {
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
+            cells={this.props.cells}
           />
-          {editMode && <BuilderComponentPane />}
+          {this.props.editMode && this.props.showBuilderPane &&
+            <BuilderComponentPane />
+          }
         </div>
         <ToastPresenter />
       </div>
diff --git a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
index 9f4cb93..2aa82af 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
@@ -71,7 +71,7 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
+    const { gridComponent, handleComponentDrop, depth, editMode, cells } = this.props;
     const { isResizing, rowGuideTop } = this.state;
 
     return (
@@ -93,6 +93,7 @@ class DashboardGrid extends React.PureComponent {
                     index={index}
                     availableColumnCount={GRID_COLUMN_COUNT}
                     columnWidth={columnWidth}
+                    cells={cells}
                     onResizeStart={this.handleResizeStart}
                     onResize={this.handleResize}
                     onResizeStop={this.handleResizeStop}
diff --git a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
index ca204e5..d3ec7ac 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
@@ -52,7 +52,7 @@ class DashboardHeader extends React.Component {
       <div className="dashboard-header">
         <div className="dashboard-component-header header-large">
           <EditableTitle
-            title={component.meta.text}
+            title={'Test title'}
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
             canEdit={editMode}
diff --git a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
index 55d7e1d..54ce67e 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
@@ -17,6 +17,7 @@ export const dragConfig = [
       return {
         type: component.type,
         id: component.id,
+        meta: component.meta,
         index,
         parentId: parentComponent.id,
         parentType: parentComponent.type,
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
index f27b604..7cb630d 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
@@ -35,6 +35,7 @@ export default function handleDrop(props, monitor, Component) {
     dragging: {
       id: draggingItem.id,
       type: draggingItem.type,
+      meta: draggingItem.meta,
     },
   };
 
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
index 668d268..2aed4b2 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
@@ -19,6 +19,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  chart: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -35,7 +36,7 @@ const propTypes = {
 const defaultProps = {
 };
 
-class Chart extends React.Component {
+class ChartHolder extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
@@ -112,7 +113,7 @@ class Chart extends React.Component {
               editMode={editMode}
             >
               <div className="dashboard-component dashboard-component-chart">
-                <div className="fa fa-area-chart" />
+                {this.props.chart}
               </div>
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
@@ -124,7 +125,7 @@ class Chart extends React.Component {
   }
 }
 
-Chart.propTypes = propTypes;
-Chart.defaultProps = defaultProps;
+ChartHolder.propTypes = propTypes;
+ChartHolder.defaultProps = defaultProps;
 
-export default Chart;
+export default ChartHolder;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
index fe5a721..490d7bd 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
@@ -25,6 +25,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -92,6 +93,7 @@ class Column extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
+      cells,
     } = this.props;
 
     const columnItems = columnComponent.children || [];
@@ -154,19 +156,20 @@ class Column extends React.PureComponent {
                   </HoverMenu>}
 
                 {columnItems.map((componentId, itemIndex) => (
-                  <DashboardComponent
-                    key={componentId}
-                    id={componentId}
-                    parentId={columnComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex}
-                    availableColumnCount={columnComponent.meta.width}
-                    columnWidth={columnWidth}
-                    onResizeStart={onResizeStart}
-                    onResize={onResize}
-                    onResizeStop={onResizeStop}
-                  />
-                ))}
+                    <DashboardComponent
+                      key={componentId}
+                      id={componentId}
+                      parentId={columnComponent.id}
+                      depth={depth + 1}
+                      index={itemIndex }
+                      availableColumnCount={columnComponent.meta.width}
+                      columnWidth={columnWidth}
+                      cells={cells}
+                      onResizeStart={onResizeStart}
+                      onResize={onResize}
+                      onResizeStop={onResizeStop}
+                    />
+                  ))}
 
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
index 9866bc8..8faaee1 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
@@ -23,6 +23,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -92,6 +93,7 @@ class Row extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
+      cells,
     } = this.props;
 
     const rowItems = rowComponent.children || [];
@@ -142,19 +144,20 @@ class Row extends React.PureComponent {
                 </HoverMenu>}
 
               {rowItems.map((componentId, itemIndex) => (
-                <DashboardComponent
-                  key={componentId}
-                  id={componentId}
-                  parentId={rowComponent.id}
-                  depth={depth + 1}
-                  index={itemIndex}
-                  availableColumnCount={availableColumnCount - occupiedColumnCount}
-                  columnWidth={columnWidth}
-                  onResizeStart={onResizeStart}
-                  onResize={onResize}
-                  onResizeStop={onResizeStop}
-                />
-              ))}
+
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={rowComponent.id}
+                    depth={depth + 1}
+                    index={itemIndex }
+                    availableColumnCount={availableColumnCount - occupiedColumnCount}
+                    columnWidth={columnWidth}
+                    cells={cells}onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                  />
+                ))}
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
             </div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/index.js b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
index 96c9a19..ef6d13f 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
@@ -9,7 +9,7 @@ import {
   TABS_TYPE,
 } from '../../util/componentTypes';
 
-import Chart from './Chart';
+import ChartHolder from './ChartHolder';
 import Column from './Column';
 import Divider from './Divider';
 import Header from './Header';
@@ -17,7 +17,7 @@ import Row from './Row';
 import Tab from './Tab';
 import Tabs from './Tabs';
 
-export { default as Chart } from './Chart';
+export { default as ChartHolder } from './ChartHolder';
 export { default as Column } from './Column';
 export { default as Divider } from './Divider';
 export { default as Header } from './Header';
@@ -26,7 +26,7 @@ export { default as Tab } from './Tab';
 export { default as Tabs } from './Tabs';
 
 export default {
-  [CHART_TYPE]: Chart,
+  [CHART_TYPE]: ChartHolder,
   [COLUMN_TYPE]: Column,
   [DIVIDER_TYPE]: Divider,
   [HEADER_TYPE]: Header,
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
index b8d717e..62fc94a 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
@@ -7,10 +7,12 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
   return {
     dashboardLayout: undoableLayout.present,
-    editMode,
+    cells: ownProps.cells,
+    editMode: dashboard.editMode,
+    showBuilderPane: dashboard.showBuilderPane,
   };
 }
 
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
index add5a6d..01f7805 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 import ComponentLookup from '../components/gridComponents';
 import getTotalChildWidth from '../util/getChildWidth';
 import { componentShape } from '../util/propShapes';
-import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { CHART_TYPE, COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
 import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
 
 import {
@@ -25,14 +25,14 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
   const dashboardLayout = undoableLayout.present;
-  const { id, parentId } = ownProps;
+  const { id, parentId, cells } = ownProps;
   const component = dashboardLayout[id];
   const props = {
     component,
     parentComponent: dashboardLayout[parentId],
-    editMode,
+    editMode: dashboard.editMode,
   };
 
   // rows and columns need more data about their child dimensions
@@ -51,6 +51,11 @@ function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps
         );
       }
     });
+  } else if (props.component.type === CHART_TYPE) {
+    const chartId = props.component.meta && props.component.meta.chartId;
+    if (chartId) {
+      props.chart = cells[chartId];
+    }
   }
 
   return props;
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
index 67b2396..2adc390 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
@@ -7,6 +7,13 @@ import {
   resizeComponent,
 } from '../actions/dashboardLayout';
 
+function mapStateToProps({ dashboardState: dashboard }, ownProps) {
+  return {
+    editMode: dashboard.editMode,
+    cells: ownProps.cells,
+  };
+}
+
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
     handleComponentDrop,
@@ -14,4 +21,4 @@ function mapDispatchToProps(dispatch) {
   }, dispatch);
 }
 
-export default connect(({ editMode }) => ({ editMode }), mapDispatchToProps)(DashboardGrid);
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
index 8855d2c..cc8e944 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
@@ -2,32 +2,55 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-import DashboardHeader from '../components/DashboardHeader';
-import { DASHBOARD_HEADER_ID } from '../util/constants';
-
+import DashboardHeader from '../../components/Header';
+import {
+  setEditMode,
+  toggleBuilderPane,
+  fetchFaveStar,
+  saveFaveStar,
+  fetchCharts,
+  startPeriodicRender,
+  updateDashboardTitle,
+  onChange,
+  onSave,
+} from '../../actions/dashboardState';
 import {
-  updateComponents,
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-import { setEditMode } from '../actions/editMode';
-
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard,
+                           dashboardInfo, charts }) {
   return {
-    component: undoableLayout.present[DASHBOARD_HEADER_ID],
+    dashboardInfo,
     canUndo: undoableLayout.past.length > 0,
     canRedo: undoableLayout.future.length > 0,
-    editMode,
+    layout: undoableLayout.present,
+    filters: dashboard.filters,
+    dashboardTitle: dashboard.title,
+    expandedSlices: dashboard.expandedSlices,
+    charts,
+    userId: dashboardInfo.userId,
+    isStarred: !!dashboard.isStarred,
+    hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
+    editMode: !!dashboard.editMode,
+    showBuilderPane: !!dashboard.showBuilderPane,
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
-    updateComponents,
     handleComponentDrop,
     onUndo: UndoActionCreators.undo,
     onRedo: UndoActionCreators.redo,
     setEditMode,
+    toggleBuilderPane,
+    fetchFaveStar,
+    saveFaveStar,
+    fetchCharts,
+    startPeriodicRender,
+    updateDashboardTitle,
+    onChange,
+    onSave,
   }, dispatch);
 }
 
diff --git a/superset/assets/src/dashboard/v2/reducers/index.js b/superset/assets/src/dashboard/v2/reducers/index.js
index 731734d..061255d 100644
--- a/superset/assets/src/dashboard/v2/reducers/index.js
+++ b/superset/assets/src/dashboard/v2/reducers/index.js
@@ -1,17 +1,8 @@
-import { combineReducers } from 'redux';
 import undoable, { distinctState } from 'redux-undo';
 
 import dashboardLayout from './dashboardLayout';
-import editMode from './editMode';
-import messageToasts from './messageToasts';
 
-const undoableLayout = undoable(dashboardLayout, {
+export default undoable(dashboardLayout, {
   limit: 15,
   filter: distinctState(),
 });
-
-export default combineReducers({
-  dashboardLayout: undoableLayout,
-  editMode,
-  messageToasts,
-});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
new file mode 100644
index 0000000..d9f1069
--- /dev/null
+++ b/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
@@ -0,0 +1,103 @@
+.dashboard-builder-sidepane {
+  .trigger {
+    height: 25px;
+    width: 25px;
+    color: #879399;
+    position: relative;
+
+    &.close {
+      top: 3px;
+    }
+
+    &.open {
+      position: absolute;
+      right: 14px;
+    }
+  }
+
+  .component-layer {
+    .new-component.static {
+      cursor: pointer;
+    }
+  }
+
+  .slices-layer {
+    position: absolute;
+    width: 2px;
+    top: 51px;
+    right: 1px;
+    background: #fff;
+    transition-property: width;
+    transition-duration: 1s;
+    transition-timing-function: ease;
+    overflow: hidden;
+
+    &.show {
+      width: 374px;
+    }
+  }
+
+  .chart-card-container {
+    padding: 16px;
+    cursor: move;
+
+    .chart-card {
+      border: 1px solid #ccc;
+      height: 120px;
+      padding: 16px;
+      pointer-events: unset;
+    }
+
+    .chart-card.is-selected {
+      opacity: 0.45;
+      pointer-events: none;
+    }
+
+    .card-title {
+      margin-bottom: 8px;
+      font-weight: bold;
+    }
+
+    .card-body {
+      display: flex;
+      flex-direction: column;
+
+      .item {
+        height: 18px;
+      }
+
+      label {
+        margin-right: 5px;
+      }
+    }
+  }
+
+  .slice-adder-container {
+    .controls {
+      display: flex;
+      padding: 16px;
+
+      .dropdown.btn-group button,
+      input {
+        font-size: 14px;
+        line-height: 16px;
+        padding: 7px 12px;
+        height: 32px;
+      }
+
+      input {
+        margin-left: 16px;
+        width: 169px;
+        border: 1px solid #b3b3b3;
+
+        &:focus {
+          outline: none;
+        }
+      }
+    }
+
+    .ReactVirtualized__Grid.ReactVirtualized__List:focus {
+      outline: none;
+    }
+  }
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder.less b/superset/assets/src/dashboard/v2/stylesheets/builder.less
index 3651c57..2ff99a4 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/v2/stylesheets/builder.less
@@ -1,5 +1,5 @@
 .dashboard-v2 {
-  margin-top: -20px;
+  //margin-top: -20px;
   position: relative;
   color: @almost-black;
 }
@@ -48,6 +48,7 @@
   flex: 0 0 376px;
   border: 1px solid @gray-light;
   z-index: 1;
+  position: relative;
 }
 
 .dashboard-builder-sidepane-header {
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
index 141c3e9..ce03797 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
@@ -7,10 +7,11 @@
   display: flex;
   align-items: center;
   justify-content: center;
+  position: relative;
 }
 
 .dashboard-component-chart .fa {
-  font-size: 100px;
+  //font-size: 100px;
   opacity: 0.3;
 }
 
diff --git a/superset/assets/src/dashboard/v2/util/constants.js b/superset/assets/src/dashboard/v2/util/constants.js
index 36ef71b..f35614c 100644
--- a/superset/assets/src/dashboard/v2/util/constants.js
+++ b/superset/assets/src/dashboard/v2/util/constants.js
@@ -18,7 +18,7 @@ export const DASHBOARD_ROOT_DEPTH = 0;
 export const GRID_BASE_UNIT = 8;
 export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
 export const GRID_COLUMN_COUNT = 12;
-export const GRID_MIN_COLUMN_COUNT = 3;
+export const GRID_MIN_COLUMN_COUNT = 2;
 export const GRID_MIN_ROW_UNITS = 5;
 export const GRID_MAX_ROW_UNITS = 100;
 export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
diff --git a/superset/assets/src/dashboard/v2/util/newComponentFactory.js b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
index af69eb8..b428ddd 100644
--- a/superset/assets/src/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
@@ -34,7 +34,7 @@ function uuid(type) {
   return `${type}-${Math.random().toString(16)}`;
 }
 
-export default function entityFactory(type) {
+export default function entityFactory(type, meta) {
   return {
     version: 'v0',
     type,
@@ -42,6 +42,7 @@ export default function entityFactory(type) {
     children: [],
     meta: {
       ...typeToDefaultMetaData[type],
+      ...meta,
     },
   };
 }
diff --git a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
index 9e49643..7cccc5f 100644
--- a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
@@ -11,9 +11,10 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
   const { dragging, destination } = dropResult;
 
   const dragType = dragging.type;
+  const dragMeta = dragging.meta;
   const dropEntity = components[destination.id];
   const dropType = dropEntity.type;
-  let newDropChild = newComponentFactory(dragType);
+  let newDropChild = newComponentFactory(dragType, dragMeta);
   const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
 
   const newEntities = {
diff --git a/superset/assets/src/dashboard/v2/util/propShapes.jsx b/superset/assets/src/dashboard/v2/util/propShapes.jsx
index 8acc192..388c726 100644
--- a/superset/assets/src/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/v2/util/propShapes.jsx
@@ -16,7 +16,6 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
     height: PropTypes.number,
 
     // Header
-    text: PropTypes.string,
     headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)),
 
     // Row
@@ -29,3 +28,52 @@ export const toastShape = PropTypes.shape({
   toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
   text: PropTypes.string.isRequired,
 });
+
+export const chartPropShape = PropTypes.shape({
+  id: PropTypes.number.isRequired,
+  chartAlert: PropTypes.string,
+  chartStatus: PropTypes.string,
+  chartUpdateEndTime: PropTypes.number,
+  chartUpdateStartTime: PropTypes.number,
+  latestQueryFormData: PropTypes.object,
+  queryRequest: PropTypes.object,
+  queryResponse: PropTypes.object,
+  triggerQuery: PropTypes.bool,
+  lastRendered: PropTypes.number,
+});
+
+export const slicePropShape = PropTypes.shape({
+  slice_id: PropTypes.number.isRequired,
+  slice_url: PropTypes.string.isRequired,
+  slice_name: PropTypes.string.isRequired,
+  edit_url: PropTypes.string.isRequired,
+  datasource: PropTypes.string,
+  datasource_name: PropTypes.string,
+  datasource_link: PropTypes.string,
+  changedOn: PropTypes.number,
+  modified: PropTypes.string,
+  viz_type: PropTypes.string.isRequired,
+  description: PropTypes.string,
+  description_markeddown: PropTypes.string,
+});
+
+export const dashboardStatePropShape = PropTypes.shape({
+  title: PropTypes.string.isRequired,
+  sliceIds: PropTypes.object.isRequired,
+  refresh: PropTypes.bool.isRequired,
+  filters: PropTypes.object,
+  expandedSlices: PropTypes.object,
+  editMode: PropTypes.bool,
+  showBuilderPane: PropTypes.bool,
+  hasUnsavedChanges: PropTypes.bool,
+});
+
+export const dashboardInfoPropShape = PropTypes.shape({
+  id: PropTypes.number.isRequired,
+  metadata: PropTypes.object,
+  slug: PropTypes.string,
+  dash_edit_perm: PropTypes.bool.isRequired,
+  dash_save_perm: PropTypes.bool.isRequired,
+  common: PropTypes.object,
+  userId: PropTypes.string.isRequired,
+});
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 69871dc..19416b0 100644
--- a/superset/assets/src/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
 import ExploreActionButtons from './ExploreActionButtons';
 import RowCountLabel from './RowCountLabel';
 import EditableTitle from '../../components/EditableTitle';
@@ -28,13 +28,13 @@ const propTypes = {
   table_name: PropTypes.string,
   form_data: PropTypes.object,
   timeout: PropTypes.number,
-  chart: PropTypes.shape(chartPropType),
+  chart: chartPropShape,
 };
 
 class ExploreChartHeader extends React.PureComponent {
   runQuery() {
     this.props.actions.runQuery(this.props.form_data, true,
-      this.props.timeout, this.props.chart.chartKey);
+      this.props.timeout, this.props.chart.id);
   }
 
   updateChartTitleOrSaveSlice(newTitle) {
diff --git a/superset/assets/src/explore/components/ExploreChartPanel.jsx b/superset/assets/src/explore/components/ExploreChartPanel.jsx
index bfb24ff..21c6a64 100644
--- a/superset/assets/src/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/src/explore/components/ExploreChartPanel.jsx
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Panel } from 'react-bootstrap';
 
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
 import ChartContainer from '../../chart/ChartContainer';
 import ExploreChartHeader from './ExploreChartHeader';
 
@@ -27,7 +27,7 @@ const propTypes = {
   standalone: PropTypes.bool,
   timeout: PropTypes.number,
   refreshOverlayVisible: PropTypes.bool,
-  chart: PropTypes.shape(chartPropType),
+  chart: chartPropShape,
   errorMessage: PropTypes.node,
 };
 
@@ -45,7 +45,7 @@ class ExploreChartPanel extends React.PureComponent {
         formData={this.props.form_data}
         height={this.getHeight()}
         slice={this.props.slice}
-        chartKey={this.props.chart.chartKey}
+        chartId={this.props.chart.id}
         setControlValue={this.props.actions.setControlValue}
         timeout={this.props.timeout}
         vizType={this.props.vizType}
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index bc875d4..3e761eb 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -11,7 +11,7 @@ import QueryAndSaveBtns from './QueryAndSaveBtns';
 import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { getFormDataFromControls } from '../store';
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
 import * as chartActions from '../../chart/chartAction';
@@ -22,7 +22,7 @@ const propTypes = {
   actions: PropTypes.object.isRequired,
   datasource_type: PropTypes.string.isRequired,
   isDatasourceMetaLoading: PropTypes.bool.isRequired,
-  chart: PropTypes.shape(chartPropType).isRequired,
+  chart: chartPropShape.isRequired,
   slice: PropTypes.object,
   controls: PropTypes.object.isRequired,
   forcedHeight: PropTypes.string,
@@ -72,7 +72,7 @@ class ExploreViewContainer extends React.Component {
     }
     if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
       this.props.actions.resetControls();
-      this.props.actions.triggerQuery(true, this.props.chart.chartKey);
+      this.props.actions.triggerQuery(true, this.props.chart.id);
     }
     if (
       np.controls.datasource && (
@@ -86,8 +86,8 @@ class ExploreViewContainer extends React.Component {
     const changedControlKeys = this.findChangedControlKeys(this.props.controls, np.controls);
     if (this.hasDisplayControlChanged(changedControlKeys, np.controls)) {
       this.props.actions.updateQueryFormData(
-        getFormDataFromControls(np.controls), this.props.chart.chartKey);
-      this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.chartKey);
+        getFormDataFromControls(np.controls), this.props.chart.id);
+      this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.id);
     }
     if (this.hasQueryControlChanged(changedControlKeys, np.controls)) {
       this.setState({ chartIsStale: true, refreshOverlayVisible: true });
@@ -112,7 +112,7 @@ class ExploreViewContainer extends React.Component {
   onQuery() {
     // remove alerts when query
     this.props.actions.removeControlPanelAlert();
-    this.props.actions.triggerQuery(true, this.props.chart.chartKey);
+    this.props.actions.triggerQuery(true, this.props.chart.id);
 
     this.setState({ chartIsStale: false, refreshOverlayVisible: false });
     this.addHistory({});
@@ -158,7 +158,7 @@ class ExploreViewContainer extends React.Component {
   triggerQueryIfNeeded() {
     if (this.props.chart.triggerQuery && !this.hasErrors()) {
       this.props.actions.runQuery(this.props.form_data, false,
-        this.props.timeout, this.props.chart.chartKey);
+        this.props.timeout, this.props.chart.id);
     }
   }
 
@@ -198,7 +198,7 @@ class ExploreViewContainer extends React.Component {
         formData,
         false,
         this.props.timeout,
-        this.props.chart.chartKey,
+        this.props.chart.id,
       );
     }
   }
diff --git a/superset/assets/src/explore/exploreUtils.js b/superset/assets/src/explore/exploreUtils.js
index 1c1271b..fcab33f 100644
--- a/superset/assets/src/explore/exploreUtils.js
+++ b/superset/assets/src/explore/exploreUtils.js
@@ -3,7 +3,7 @@ import URI from 'urijs';
 
 export function getChartKey(explore) {
   const slice = explore.slice;
-  return slice ? ('slice_' + slice.slice_id) : 'slice';
+  return slice ? (slice.slice_id) : 0;
 }
 
 export function getAnnotationJsonUrl(slice_id, form_data, isNative) {
diff --git a/superset/assets/src/explore/index.jsx b/superset/assets/src/explore/index.jsx
index 5989b5f..07870e4 100644
--- a/superset/assets/src/explore/index.jsx
+++ b/superset/assets/src/explore/index.jsx
@@ -49,7 +49,7 @@ const chartKey = getChartKey(bootstrappedState);
 const initState = {
   charts: {
     [chartKey]: {
-      chartKey,
+      id: chartKey,
       chartAlert: null,
       chartStatus: 'loading',
       chartUpdateEndTime: null,
diff --git a/superset/assets/src/explore/reducers/index.js b/superset/assets/src/explore/reducers/index.js
index 13d0ed1..953b0b5 100644
--- a/superset/assets/src/explore/reducers/index.js
+++ b/superset/assets/src/explore/reducers/index.js
@@ -1,13 +1,14 @@
 import { combineReducers } from 'redux';
-import shortid from 'shortid';
 
 import charts from '../../chart/chartReducer';
 import saveModal from './saveModalReducer';
 import explore from './exploreReducer';
 
+const impressionId = (state = '') => (state);
+
 export default combineReducers({
   charts,
   saveModal,
   explore,
-  impressionId: () => (shortid.generate()),
+  impressionId,
 });
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index 016444a..c4ea9ce 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -165,7 +165,6 @@ export const controllerInterface = {
   addFiler: () => {},
   setFilter: () => {},
   getFilters: () => false,
-  clearFilter: () => {},
   removeFilter: () => {},
   filters: {},
 };
diff --git a/superset/assets/src/visualizations/table.css b/superset/assets/src/visualizations/table.css
index a5b8462..9af0c0e 100644
--- a/superset/assets/src/visualizations/table.css
+++ b/superset/assets/src/visualizations/table.css
@@ -30,11 +30,10 @@ table.table thead th.sorting:after, table.table thead th.sorting_asc:after, tabl
   white-space: pre-wrap;
 }
 
+.widget.table {
+  width: auto;
+  max-width: unset;
+}
 .widget.table thead tr {
   height: 25px;
 }
-
-.dashboard .slice_container.table {
-  padding-left: 10px;
-  padding-right: 10px;
-}
diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.less
similarity index 58%
rename from superset/assets/stylesheets/dashboard.css
rename to superset/assets/stylesheets/dashboard.less
index c1f08a7..b812a42 100644
--- a/superset/assets/stylesheets/dashboard.css
+++ b/superset/assets/stylesheets/dashboard.less
@@ -1,3 +1,5 @@
+@import "./less/cosmo/variables.less";
+
 .dashboard a i {
   cursor: pointer;
 }
@@ -11,28 +13,58 @@
   border-color: #AAA;
   opacity: 0.3;
 }
-div.widget .chart-controls {
-  background-clip: content-box;
+.dashboard .widget {
   position: absolute;
-  z-index: 100;
-  right: 0;
-  top: 5px;
-  padding: 5px 5px;
-  opacity: 0;
-  transition: opacity 0.5s ease-in-out;
-}
-div.widget:hover .chart-controls {
-  opacity: 0.75;
-  transition: opacity 0.5s ease-in-out;
-}
-.slice-grid div.widget {
-  border-radius: 0;
-  border: 0;
+  top: 16px;
+  left: 16px;
   box-shadow: none;
-  background-color: #fff;
+  background-color: transparent;
   overflow: visible;
 }
+.dashboard .chart-header {
+  .dropdown.btn-group {
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+
+  .dropdown-menu.dropdown-menu-right {
+    right: 7px;
+    top: -3px
+  }
+}
+
+.slice-header-controls-trigger {
+  border: 0;
+  padding: 0 0 0 20px;
+  background: none;
+  outline: none;
+  box-shadow: none;
+  color: #263238;
 
+  &.is-cached {
+    color: red;
+  }
+
+  &:hover, &:focus {
+    background: none;
+    cursor: pointer;
+  }
+
+  .controls-container.dropdown-menu {
+    top: 0;
+    left: unset;
+    right: 10px;
+
+    &.is-open {
+      display: block;
+    }
+
+    & li {
+      white-space: nowrap;
+    }
+  }
+}
 .slice-grid .slice_container {
   background-color: #fff;
 }
@@ -73,26 +105,16 @@ div.widget:hover .chart-controls {
   display: none;
 }
 
-.slice-grid div.separator.widget {
- border: 1px solid transparent;
-  box-shadow: none;
-  z-index: 1;
-}
-.slice-grid div.separator.widget:hover {
-  border: 1px solid #EEE;
-}
-.slice-grid div.separator.widget .chart-header {
-  background-color: transparent;
-  color: transparent;
-}
-.slice-grid div.separator.widget h1,h2,h3,h4 {
-  margin-top: 0px;
-}
-
 .slice-cell {
   box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
   transition: box-shadow 1s ease-in;
-  height: 100%;
+
+  .dropdown,
+  .dropdown-menu {
+    .fa {
+      font-size: 14px;
+    }
+  }
 }
 
 .slice-cell-highlight {
@@ -104,30 +126,8 @@ div.widget:hover .chart-controls {
   font-weight: bold;
 }
 
-.dashboard .separator.widget .slice_container {
-  padding: 0;
-  overflow: visible;
-}
-.dashboard .separator.widget .slice_container hr {
-  margin-top: 5px;
-  margin-bottom: 5px;
-}
-.separator .chart-container {
-  position: absolute;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-}
-
-.dashboard .title {
-  margin: 0 20px;
-}
-
-.dashboard .title .favstar {
-  font-size: 20px;
-  position: relative;
-  top: -5px;
+.chart-container {
+  box-sizing: border-box;
 }
 
 .chart-header .header {
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 743daa8..6987544 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -162,7 +162,6 @@ li.widget:hover {
 div.widget .chart-header {
   padding-top: 8px;
   color: #333;
-  border-bottom: 1px solid #aaa;
   margin: 0 10px;
 }
 
@@ -177,10 +176,6 @@ div.widget .chart-header {
 }
 
 
-div.widget .chart-header a {
-  margin-left: 5px;
-}
-
 #is_cached {
   display: none;
 }
@@ -458,6 +453,17 @@ g.annotation-container {
   border-color: @brand-primary;
 }
 
+.fave-unfave-icon {
+  .fa-star-o,
+  .fa-star {
+    &,
+    &:hover,
+    &:active {
+      color: #263238;
+    }
+  }
+}
+
 .metric-edit-popover-label-input {
   border-radius: 4px;
   height: 30px;
diff --git a/superset/models/core.py b/superset/models/core.py
index 4674520..b450be0 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -146,6 +146,11 @@ class Slice(Model, AuditMixinNullable, ImportMixin):
         datasource = self.datasource
         return datasource.link if datasource else None
 
+    def datasource_name_text(self):
+        # pylint: disable=no-member
+        datasource = self.datasource
+        return datasource.name if datasource else None
+
     @property
     def datasource_edit_url(self):
         # pylint: disable=no-member
@@ -338,14 +343,6 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
 
     @property
     def url(self):
-        if self.json_metadata:
-            # add default_filters to the preselect_filters of dashboard
-            json_metadata = json.loads(self.json_metadata)
-            default_filters = json_metadata.get('default_filters')
-            if default_filters:
-                filters = parse.quote(default_filters.encode('utf8'))
-                return '/superset/dashboard/{}/?preselect_filters={}'.format(
-                    self.slug or self.id, filters)
         return '/superset/dashboard/{}/'.format(self.slug or self.id)
 
     @property
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index 25633da..1a158d9 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -3,6 +3,7 @@
 {% block body %}
 <div
   id="app"
+  class="dashboard container-fluid"
   data-bootstrap="{{ bootstrap_data }}"
 >
 </div>
diff --git a/superset/views/core.py b/superset/views/core.py
index 3a0f28f..bd7715d 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -518,9 +518,10 @@ appbuilder.add_view_no_menu(SliceAsync)
 
 class SliceAddView(SliceModelView):  # noqa
     list_columns = [
-        'id', 'slice_name', 'slice_link', 'viz_type',
-        'datasource_link', 'owners', 'modified', 'changed_on']
-    show_columns = list(set(SliceModelView.edit_columns + list_columns))
+        'id', 'slice_name', 'slice_url', 'edit_url', 'viz_type', 'params',
+        'description', 'description_markeddown',
+        'datasource_name_text', 'datasource_link',
+        'owners', 'modified', 'changed_on']
 
 
 appbuilder.add_view_no_menu(SliceAddView)
@@ -1593,9 +1594,17 @@ class Superset(BaseSupersetView):
     @staticmethod
     def _set_dash_metadata(dashboard, data):
         positions = data['positions']
-        slice_ids = [int(d['slice_id']) for d in positions]
-        dashboard.slices = [o for o in dashboard.slices if o.id in slice_ids]
-        positions = sorted(data['positions'], key=lambda x: int(x['slice_id']))
+        # find slices in the position data
+        slice_ids = []
+        for value in positions.values():
+            if value.get('meta') and value.get('meta').get('chartId'):
+                slice_ids.append(int(value.get('meta').get('chartId')))
+        session = db.session()
+        Slice = models.Slice  # noqa
+        current_slices = session.query(Slice).filter(
+            Slice.id.in_(slice_ids)).all()
+
+        dashboard.slices = current_slices
         dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
         md = dashboard.params_dict
         dashboard.css = data['css']
@@ -1608,7 +1617,11 @@ class Superset(BaseSupersetView):
         if 'filter_immune_slice_fields' not in md:
             md['filter_immune_slice_fields'] = {}
         md['expanded_slices'] = data['expanded_slices']
-        md['default_filters'] = data.get('default_filters', '')
+        default_filters_data = json.loads(data.get('default_filters', ''))
+        for key in default_filters_data.keys():
+            if int(key) not in slice_ids:
+                del default_filters_data[key]
+        md['default_filters'] = json.dumps(default_filters_data)
         dashboard.json_metadata = json.dumps(md, indent=4)
 
     @api


[incubator-superset] 03/26: [dashboard-builder] add top-level tabs + undo-redo (#4626)

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

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

commit abc3ec076af5fcd61e5544d743288f8b326e851f
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Fri Mar 23 10:53:48 2018 -0700

    [dashboard-builder] add top-level tabs + undo-redo (#4626)
    
    * [top-level-tabs] initial working version of top-level tabs
    
    * [top-level-tabs] simplify redux and disable ability to displace top-level tabs with other tabs
    
    * [top-level-tabs] improve tab drag and drop css
    
    * [undo-redo] add redux undo redo
    
    * [dnd] clean up dropResult shape, add new component source id + type, use css for drop indicator instead of styles and fix tab indicators.
    
    * [top-level-tabs] add 'Collapse tab content' to delete tabs button
    
    * [dnd] add depth validation to drag and drop logic
    
    * [dashboard-builder] add resize action, enforce minimum width of columns, column children inherit column size when necessary, meta.rowStyle => meta.background, add background to columns
    
    * [dashboard-builder] make sure getChildWidth returns a number
---
 .../javascripts/dashboard/v2/actions/index.js      | 113 +++++++++++++--
 .../dashboard/v2/components/Dashboard.jsx          |  21 +--
 .../dashboard/v2/components/DashboardBuilder.jsx   |  97 ++++++++++++-
 .../dashboard/v2/components/DashboardGrid.jsx      |  64 +++-----
 .../dashboard/v2/components/DashboardHeader.jsx    |  71 +++++++--
 .../dashboard/v2/components/IconButton.jsx         |  12 +-
 .../dashboard/v2/components/dnd/DragDroppable.jsx  |  41 ++++--
 .../v2/components/dnd/dragDroppableConfig.js       |  11 +-
 .../dashboard/v2/components/dnd/handleDrop.js      |  20 ++-
 .../dashboard/v2/components/dnd/handleHover.js     |  18 +--
 .../v2/components/gridComponents/Chart.jsx         |   5 +-
 .../v2/components/gridComponents/Column.jsx        | 132 ++++++++++++-----
 .../v2/components/gridComponents/Divider.jsx       |   3 +
 .../v2/components/gridComponents/Header.jsx        |  23 +--
 .../dashboard/v2/components/gridComponents/Row.jsx |  27 ++--
 .../v2/components/gridComponents/Spacer.jsx        |   9 +-
 .../dashboard/v2/components/gridComponents/Tab.jsx |  31 ++--
 .../v2/components/gridComponents/Tabs.jsx          |  61 +++++---
 .../gridComponents/new/DraggableNewComponent.jsx   |   5 +-
 ...yleDropdown.jsx => BackgroundStyleDropdown.jsx} |  12 +-
 .../v2/components/menu/WithPopoverMenu.jsx         |  10 +-
 .../v2/components/resizable/ResizableContainer.jsx |  41 ++++--
 .../{DashboardGrid.jsx => DashboardBuilder.jsx}    |  12 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  33 +++--
 .../dashboard/v2/containers/DashboardGrid.jsx      |  12 +-
 .../dashboard/v2/containers/DashboardHeader.jsx    |  31 ++++
 .../dashboard/v2/fixtures/emptyDashboardLayout.js  |  36 +++++
 .../dashboard/v2/fixtures/testLayout.js            | 161 ---------------------
 .../javascripts/dashboard/v2/reducers/dashboard.js | 146 +++++++++++++++++--
 .../javascripts/dashboard/v2/reducers/index.js     |   9 +-
 .../dashboard/v2/stylesheets/builder.less          |  64 ++++++++
 .../dashboard/v2/stylesheets/buttons.less          |   8 +-
 .../v2/stylesheets/components/DashboardBuilder.jsx | 127 ++++++++++++++++
 .../v2/stylesheets/components/column.less          |  10 +-
 .../v2/stylesheets/components/new-component.less   |   1 +
 .../dashboard/v2/stylesheets/components/row.less   |   6 +-
 .../dashboard/v2/stylesheets/components/tabs.less  |  39 +++--
 .../javascripts/dashboard/v2/stylesheets/dnd.less  |  54 ++++---
 .../javascripts/dashboard/v2/stylesheets/grid.less |  43 +++++-
 .../dashboard/v2/stylesheets/hover-menu.less       |  14 +-
 .../dashboard/v2/stylesheets/index.less            |   1 +
 .../dashboard/v2/stylesheets/popover-menu.less     |  24 ++-
 .../dashboard/v2/stylesheets/resizable.less        |  12 +-
 .../dashboard/v2/util/backgroundStyleOptions.js    |   7 +
 .../dashboard/v2/util/componentTypes.js            |  10 +-
 .../javascripts/dashboard/v2/util/constants.js     |  11 +-
 .../dashboard/v2/util/countChildRowsAndColumns.js  |  14 --
 .../javascripts/dashboard/v2/util/dnd-reorder.js   |  18 +--
 .../javascripts/dashboard/v2/util/findParentId.js  |  15 ++
 .../javascripts/dashboard/v2/util/getChildWidth.js |  16 ++
 .../dashboard/v2/util/getDropPosition.js           |  16 +-
 .../javascripts/dashboard/v2/util/isValidChild.js  |  96 +++++++-----
 .../dashboard/v2/util/newComponentFactory.js       |  12 +-
 .../dashboard/v2/util/newEntitiesFromDrop.js       |  20 +--
 .../javascripts/dashboard/v2/util/propShapes.jsx   |   4 +-
 .../dashboard/v2/util/resizableConfig.js           |   7 +-
 .../dashboard/v2/util/rowStyleOptions.js           |   7 -
 .../dashboard/v2/util/shouldWrapChildInRow.js      |   4 +-
 superset/assets/package.json                       |   1 +
 superset/assets/src/components/EditableTitle.jsx   |   6 +-
 superset/assets/src/dashboard/index.jsx            |   8 +-
 superset/assets/stylesheets/dashboard-v2.css       |  42 ------
 superset/assets/stylesheets/superset.less          |   2 +-
 superset/templates/appbuilder/navbar.html          |  15 --
 64 files changed, 1305 insertions(+), 696 deletions(-)

diff --git a/superset/assets/javascripts/dashboard/v2/actions/index.js b/superset/assets/javascripts/dashboard/v2/actions/index.js
index 005a77e..a6c7b77 100644
--- a/superset/assets/javascripts/dashboard/v2/actions/index.js
+++ b/superset/assets/javascripts/dashboard/v2/actions/index.js
@@ -1,3 +1,12 @@
+import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import findParentId from '../util/findParentId';
+import {
+  CHART_TYPE,
+  MARKDOWN_TYPE,
+  TABS_TYPE,
+} from '../util/componentTypes';
+
+// Component CRUD -------------------------------------------------------------
 export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
 export function updateComponents(nextComponents) {
   return {
@@ -29,6 +38,67 @@ export function createComponent(dropResult) {
   };
 }
 
+// Tabs -----------------------------------------------------------------------
+export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
+export function createTopLevelTabs(dropResult) {
+  return {
+    type: CREATE_TOP_LEVEL_TABS,
+    payload: {
+      dropResult,
+    },
+  };
+}
+
+export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
+export function deleteTopLevelTabs() {
+  return {
+    type: DELETE_TOP_LEVEL_TABS,
+    payload: {},
+  };
+}
+
+// Resize ---------------------------------------------------------------------
+export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
+export function resizeComponent({ id, width, height }) {
+  return (dispatch, getState) => {
+    const { dashboard: undoableDashboard } = getState();
+    const { present: dashboard } = undoableDashboard;
+    const component = dashboard[id];
+
+    if (
+      component &&
+      (component.meta.width !== width || component.meta.height !== height)
+    ) {
+      // update the size of this component + any resizable children
+      const updatedComponents = {
+        [id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            width: width || component.meta.width,
+            height: height || component.meta.height,
+          },
+        },
+      };
+
+      component.children.forEach((childId) => {
+        const child = dashboard[childId];
+        if ([CHART_TYPE, MARKDOWN_TYPE].includes(child.type)) {
+          updatedComponents[childId] = {
+            ...child,
+            meta: {
+              ...child.meta,
+              width: width || component.meta.width,
+              height: height || component.meta.height,
+            },
+          };
+        }
+      });
+
+      dispatch(updateComponents(updatedComponents));
+    }
+  };
+}
 
 // Drag and drop --------------------------------------------------------------
 export const MOVE_COMPONENT = 'MOVE_COMPONENT';
@@ -43,27 +113,38 @@ export function moveComponent(dropResult) {
 
 export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
 export function handleComponentDrop(dropResult) {
-  return (dispatch) => {
-    if (
-      dropResult.destination
-      && dropResult.source
+  return (dispatch, getState) => {
+    const { source, destination } = dropResult;
+    const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
+    const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
+
+    if (droppedOnRoot) {
+      dispatch(createTopLevelTabs(dropResult));
+    } else if (destination && isNewComponent) {
+      dispatch(createComponent(dropResult));
+    } else if (
+      destination
+      && source
       && !( // ensure it has moved
-        dropResult.destination.droppableId === dropResult.source.droppableId
-        && dropResult.destination.index === dropResult.source.index
+        destination.id === source.id
+        && destination.index === source.index
       )
     ) {
-      return dispatch(moveComponent(dropResult));
+      dispatch(moveComponent(dropResult));
+    }
 
-      // new components don't have a source
-    } else if (dropResult.destination && !dropResult.source) {
-      return dispatch(createComponent(dropResult));
+    // if we moved a tab and the parent tabs no longer has children, delete it.
+    if (!isNewComponent) {
+      const { dashboard: undoableDashboard } = getState();
+      const { present: dashboard } = undoableDashboard;
+      const sourceComponent = dashboard[source.id];
+
+      if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
+        const parentId = findParentId({ childId: source.id, components: dashboard });
+        dispatch(deleteComponent(source.id, parentId));
+      }
     }
+
     return null;
   };
 }
-
-// Resize ---------------------------------------------------------------------
-
-// export function dashboardComponentResizeStart() {}
-// export function dashboardComponentResize() {}
-// export function dashboardComponentResizeStop() {}
diff --git a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
index a2ed1a0..ffd1280 100644
--- a/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/Dashboard.jsx
@@ -1,11 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import DashboardBuilder from './DashboardBuilder';
-import StaticDashboard from './StaticDashboard';
-import DashboardHeader from './DashboardHeader';
+import DashboardBuilder from '../containers/DashboardBuilder';
 
-import '../../../../stylesheets/dashboard-v2.css';
 import '../stylesheets/index.less';
 
 const propTypes = {
@@ -22,20 +19,8 @@ const defaultProps = {
 
 class Dashboard extends React.Component {
   render() {
-    const { editMode, actions } = this.props;
-    const { setEditMode, updateDashboardTitle } = actions;
-    return (
-      <div className="dashboard-v2">
-        <DashboardHeader
-          editMode={true}
-          setEditMode={setEditMode}
-          updateDashboardTitle={updateDashboardTitle}
-        />
-
-        {true ?
-          <DashboardBuilder /> : <StaticDashboard />}
-      </div>
-    );
+    // @TODO delete this component?
+    return <DashboardBuilder />;
   }
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index 1878db6..f371718 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -2,13 +2,28 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import HTML5Backend from 'react-dnd-html5-backend';
 import { DragDropContext } from 'react-dnd';
-import cx from 'classnames';
 
 import BuilderComponentPane from './BuilderComponentPane';
+import DashboardHeader from '../containers/DashboardHeader';
 import DashboardGrid from '../containers/DashboardGrid';
+import IconButton from './IconButton';
+import DragDroppable from './dnd/DragDroppable';
+import DashboardComponent from '../containers/DashboardComponent';
+import WithPopoverMenu from './menu/WithPopoverMenu';
+
+import {
+  DASHBOARD_GRID_ID,
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_ROOT_DEPTH,
+} from '../util/constants';
 
 const propTypes = {
   editMode: PropTypes.bool,
+
+  // redux
+  dashboard: PropTypes.object.isRequired,
+  deleteTopLevelTabs: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -16,17 +31,87 @@ const defaultProps = {
 };
 
 class DashboardBuilder extends React.Component {
+  static shouldFocusTabs(event, container) {
+    // don't focus the tabs when we click on a tab
+    return event.target.tagName === 'UL' || (
+      /icon-button/.test(event.target.className) && container.contains(event.target)
+    );
+  }
+
   constructor(props) {
     super(props);
-    // this component might control the state of the side pane etc. in the future
-    this.state = {};
+    this.state = {
+      tabIndex: 0, // top-level tabs
+    };
+    this.handleChangeTab = this.handleChangeTab.bind(this);
+  }
+
+  handleChangeTab({ tabIndex }) {
+    this.setState(() => ({ tabIndex }));
   }
 
   render() {
+    const { tabIndex } = this.state;
+    const { handleComponentDrop, dashboard, deleteTopLevelTabs } = this.props;
+    const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
+    const rootChildId = dashboardRoot.children[0];
+    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
+
+    const gridComponentId = topLevelTabs
+      ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
+      : DASHBOARD_GRID_ID;
+
+    const gridComponent = dashboard[gridComponentId];
+
     return (
-      <div className={cx('dashboard-builder')}>
-        <DashboardGrid />
-        <BuilderComponentPane />
+      <div className="dashboard-v2">
+        {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
+          <DashboardHeader />
+        ) : (
+          <DragDroppable
+            component={dashboardRoot}
+            parentComponent={null}
+            depth={DASHBOARD_ROOT_DEPTH}
+            index={0}
+            orientation="column"
+            onDrop={topLevelTabs ? null : handleComponentDrop}
+          >
+            {({ dropIndicatorProps }) => (
+              <div>
+                <DashboardHeader />
+                {dropIndicatorProps && <div {...dropIndicatorProps} />}
+              </div>
+            )}
+          </DragDroppable>)}
+
+        {topLevelTabs &&
+          <WithPopoverMenu
+            shouldFocus={DashboardBuilder.shouldFocusTabs}
+            menuItems={[
+              <IconButton
+                className="fa fa-level-down"
+                label="Collapse tab content"
+                onClick={deleteTopLevelTabs}
+              />,
+            ]}
+          >
+            <DashboardComponent
+              id={topLevelTabs.id}
+              parentId={DASHBOARD_ROOT_ID}
+              depth={DASHBOARD_ROOT_DEPTH + 1}
+              index={0}
+              renderTabContent={false}
+              onChangeTab={this.handleChangeTab}
+            />
+          </WithPopoverMenu>}
+
+        <div className="dashboard-builder">
+          <DashboardGrid
+            gridComponent={gridComponent}
+            depth={DASHBOARD_ROOT_DEPTH + 1}
+          />
+          <BuilderComponentPane />
+        </div>
       </div>
     );
   }
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
index c92161a..cfe99c7 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -1,21 +1,21 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ParentSize from '@vx/responsive/build/components/ParentSize';
-import cx from 'classnames';
 
-import DragDroppable from './dnd/DragDroppable';
+import { componentShape } from '../util/propShapes';
 import DashboardComponent from '../containers/DashboardComponent';
+import DragDroppable from './dnd/DragDroppable';
 
 import {
-  DASHBOARD_ROOT_ID,
   GRID_GUTTER_SIZE,
   GRID_COLUMN_COUNT,
 } from '../util/constants';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
-  updateComponents: PropTypes.func.isRequired,
+  depth: PropTypes.number.isRequired,
+  gridComponent: componentShape.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
+  resizeComponent: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -60,24 +60,9 @@ class DashboardGrid extends React.PureComponent {
     }
   }
 
-  handleResizeStop({ id, widthMultiple, heightMultiple }) {
-    const { dashboard: components, updateComponents } = this.props;
-    const component = components[id];
-    if (
-      component &&
-      (component.meta.width !== widthMultiple || component.meta.height !== heightMultiple)
-    ) {
-      updateComponents({
-        [id]: {
-          ...component,
-          meta: {
-            ...component.meta,
-            width: widthMultiple || component.meta.width,
-            height: heightMultiple || component.meta.height,
-          },
-        },
-      });
-    }
+  handleResizeStop({ id, widthMultiple: width, heightMultiple: height }) {
+    this.props.resizeComponent({ id, width, height });
+
     this.setState(() => ({
       isResizing: false,
       rowGuideTop: null,
@@ -85,18 +70,11 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { dashboard: components, handleComponentDrop } = this.props;
+    const { gridComponent, handleComponentDrop, depth } = this.props;
     const { isResizing, rowGuideTop } = this.state;
-    const rootComponent = components[DASHBOARD_ROOT_ID];
 
     return (
-      <div
-        ref={(ref) => { this.grid = ref; }}
-        className={cx(
-          'grid-container',
-          isResizing && 'grid-container--resizing',
-        )}
-      >
+      <div className="grid-container" ref={(ref) => { this.grid = ref; }}>
         <ParentSize>
           {({ width }) => {
             // account for (COLUMN_COUNT - 1) gutters
@@ -104,13 +82,13 @@ class DashboardGrid extends React.PureComponent {
             const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
 
             return width < 50 ? null : (
-              <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
-                {(rootComponent.children || []).map((id, index) => (
+              <div className="grid-content">
+                {gridComponent.children.map((id, index) => (
                   <DashboardComponent
                     key={id}
                     id={id}
-                    parentId={rootComponent.id}
-                    depth={0}
+                    parentId={gridComponent.id}
+                    depth={depth + 1}
                     index={index}
                     availableColumnCount={GRID_COLUMN_COUNT}
                     columnWidth={columnWidth}
@@ -120,19 +98,19 @@ class DashboardGrid extends React.PureComponent {
                   />
                 ))}
 
-                {rootComponent.children.length === 0 &&
+                {/* render an empty drop target */}
+                {gridComponent.children.length === 0 &&
                   <DragDroppable
-                    component={rootComponent}
+                    component={gridComponent}
+                    depth={depth}
                     parentComponent={null}
                     index={0}
                     orientation="column"
                     onDrop={handleComponentDrop}
+                    className="empty-grid-droptarget"
                   >
-                    {({ dropIndicatorProps }) => (
-                      <div style={{ width: '100%', height: '100%' }}>
-                        {dropIndicatorProps && <div {...dropIndicatorProps} />}
-                      </div>
-                    )}
+                    {({ dropIndicatorProps }) => dropIndicatorProps &&
+                      <div {...dropIndicatorProps} />}
                   </DragDroppable>}
 
                 {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
index 8ffe677..e0d14c4 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardHeader.jsx
@@ -1,44 +1,83 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
+import { ButtonGroup, ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
 
 import Button from '../../../components/Button';
+import { componentShape } from '../util/propShapes';
 import EditableTitle from '../../../components/EditableTitle';
 
 const propTypes = {
-  updateDashboardTitle: PropTypes.func,
-  editMode: PropTypes.bool.isRequired,
-  setEditMode: PropTypes.func.isRequired,
+  // editMode: PropTypes.bool.isRequired,
+  // setEditMode: PropTypes.func.isRequired,
+  component: componentShape.isRequired,
+
+  // redux
+  updateComponents: PropTypes.func.isRequired,
+  onUndo: PropTypes.func.isRequired,
+  onRedo: PropTypes.func.isRequired,
+  canUndo: PropTypes.bool.isRequired,
+  canRedo: PropTypes.bool.isRequired,
 };
 
-class Header extends React.Component {
+class DashboardHeader extends React.Component {
   constructor(props) {
     super(props);
-    this.handleSaveTitle = this.handleSaveTitle.bind(this);
+    this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
   }
 
-  handleSaveTitle(title) {
-    this.props.updateDashboardTitle(title);
+  toggleEditMode() {
+    console.log('@TODO toggleEditMode');
+    // this.props.setEditMode(!this.props.editMode);
   }
 
-  toggleEditMode() {
-    this.props.setEditMode(!this.props.editMode);
+  handleChangeText(nextText) {
+    const { updateComponents, component } = this.props;
+    if (nextText && component.meta.text !== nextText) {
+      updateComponents({
+        [component.id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            text: nextText,
+          },
+        },
+      });
+    }
   }
 
   render() {
-    const { editMode } = this.props;
+    const { component, onUndo, onRedo, canUndo, canRedo } = this.props;
+    const editMode = true;
+
     return (
       <div className="dashboard-header">
         <h1>
           <EditableTitle
-            title={'Example header'}
-            canEdit={false}
-            onSaveTitle={() => {}}
+            title={component.meta.text}
+            onSaveTitle={this.handleChangeText}
             showTooltip={false}
+            canEdit={editMode}
           />
         </h1>
         <ButtonToolbar>
+          <ButtonGroup>
+            <Button
+              bsSize="small"
+              onClick={onUndo}
+              disabled={!canUndo}
+            >
+              Undo
+            </Button>
+            <Button
+              bsSize="small"
+              onClick={onRedo}
+              disabled={!canRedo}
+            >
+              Redo
+            </Button>
+          </ButtonGroup>
+
           <DropdownButton title="Actions" bsSize="small" id="btn-dashboard-actions">
             <MenuItem>Action 1</MenuItem>
             <MenuItem>Action 2</MenuItem>
@@ -57,6 +96,6 @@ class Header extends React.Component {
   }
 }
 
-Header.propTypes = propTypes;
+DashboardHeader.propTypes = propTypes;
 
-export default Header;
+export default DashboardHeader;
diff --git a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
index 98044c9..18fd3b1 100644
--- a/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/IconButton.jsx
@@ -1,14 +1,15 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import cx from 'classnames';
 
 const propTypes = {
   onClick: PropTypes.func.isRequired,
   className: PropTypes.string,
+  label: PropTypes.string,
 };
 
 const defaultProps = {
   className: null,
+  label: null,
 };
 
 export default class IconButton extends React.PureComponent {
@@ -24,14 +25,17 @@ export default class IconButton extends React.PureComponent {
   }
 
   render() {
-    const { className } = this.props;
+    const { className, label } = this.props;
     return (
       <div
-        className={cx('icon-button', className)}
+        className="icon-button"
         onClick={this.handleClick}
         tabIndex="0"
         role="button"
-      />
+      >
+        <span className={className} />
+        {label && <span className="icon-button-label">{label}</span>}
+      </div>
     );
   }
 }
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
index 320872b..89664e5 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
@@ -3,17 +3,21 @@ import PropTypes from 'prop-types';
 import { DragSource, DropTarget } from 'react-dnd';
 import cx from 'classnames';
 
-import { dragConfig, dropConfig } from './dragDroppableConfig';
 import { componentShape } from '../../util/propShapes';
-
+import { dragConfig, dropConfig } from './dragDroppableConfig';
+import { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
 
 const propTypes = {
   children: PropTypes.func,
+  className: PropTypes.string,
   component: componentShape.isRequired,
   parentComponent: componentShape,
+  depth: PropTypes.number.isRequired,
   disableDragDrop: PropTypes.bool,
   orientation: PropTypes.oneOf(['row', 'column']),
   index: PropTypes.number.isRequired,
+  style: PropTypes.object,
+  onDrop: PropTypes.func,
 
   // from react-dnd
   isDragging: PropTypes.bool.isRequired,
@@ -22,12 +26,11 @@ const propTypes = {
   droppableRef: PropTypes.func.isRequired,
   dragSourceRef: PropTypes.func.isRequired,
   dragPreviewRef: PropTypes.func.isRequired,
-
-  // from redux
-  onDrop: PropTypes.func,
 };
 
 const defaultProps = {
+  className: null,
+  style: null,
   parentComponent: null,
   disableDragDrop: false,
   children() {},
@@ -41,6 +44,7 @@ class DragDroppable extends React.Component {
     this.state = {
       dropIndicator: null, // this gets set/modified by the react-dnd HOCs
     };
+    this.setRef = this.setRef.bind(this);
   }
 
   componentDidMount() {
@@ -51,38 +55,47 @@ class DragDroppable extends React.Component {
     this.mounted = false;
   }
 
+  setRef(ref) {
+    this.ref = ref;
+    this.props.dragPreviewRef(ref);
+    this.props.droppableRef(ref);
+  }
+
   render() {
     const {
       children,
+      className,
       orientation,
-      droppableRef,
       dragSourceRef,
-      dragPreviewRef,
       isDragging,
       isDraggingOver,
+      style,
     } = this.props;
 
     const { dropIndicator } = this.state;
 
     return (
       <div
-        ref={(ref) => {
-          this.ref = ref;
-          dragPreviewRef(ref);
-          droppableRef(ref);
-        }}
+        style={style}
+        ref={this.setRef}
         className={cx(
           'dragdroppable',
           orientation === 'row' && 'dragdroppable-row',
           orientation === 'column' && 'dragdroppable-column',
           isDragging && 'dragdroppable--dragging',
+          className,
         )}
       >
         {children({
           dragSourceRef,
           dropIndicatorProps: isDraggingOver && dropIndicator && {
-            className: 'drop-indicator',
-            style: dropIndicator,
+            className: cx(
+              'drop-indicator',
+              dropIndicator === DROP_TOP && 'drop-indicator--top',
+              dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
+              dropIndicator === DROP_LEFT && 'drop-indicator--left',
+              dropIndicator === DROP_RIGHT && 'drop-indicator--right',
+            ),
           },
         })}
       </div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
index e6d5533..55d7e1d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/dragDroppableConfig.js
@@ -10,13 +10,16 @@ export const dragConfig = [
     canDrag(props) {
       return !props.disableDragDrop;
     },
+
+    // this defines the dragging item object returned by monitor.getItem()
     beginDrag(props /* , monitor, component */) {
-      const { component, index, parentComponent } = props;
+      const { component, index, parentComponent = {} } = props;
       return {
-        draggableId: component.id,
-        index,
-        parentId: parentComponent && parentComponent.id,
         type: component.type,
+        id: component.id,
+        index,
+        parentId: parentComponent.id,
+        parentType: parentComponent.type,
       };
     },
   },
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
index cf790da..2207ca6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleDrop.js
@@ -2,7 +2,7 @@ import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '.
 
 export default function handleDrop(props, monitor, Component) {
   // this may happen due to throttling
-  if (!Component.mounted) return undefined;
+  if (!Component.mounted || !Component.props.onDrop) return undefined;
 
   Component.setState(() => ({ dropIndicator: null }));
   const dropPosition = getDropPosition(monitor, Component);
@@ -27,17 +27,22 @@ export default function handleDrop(props, monitor, Component) {
     ? 'sibling' : 'child';
 
   const dropResult = {
-    source: draggingItem.parentId ? {
-      droppableId: draggingItem.parentId,
+    source: {
+      id: draggingItem.parentId,
+      type: draggingItem.parentType,
       index: draggingItem.index,
-    } : null,
-    draggableId: draggingItem.draggableId,
+    },
+    dragging: {
+      id: draggingItem.id,
+      type: draggingItem.type,
+    },
   };
 
   // simplest case, append as child
   if (dropAsChildOrSibling === 'child') {
     dropResult.destination = {
-      droppableId: component.id,
+      id: component.id,
+      type: component.type,
       index: component.children.length,
     };
   } else {
@@ -52,7 +57,8 @@ export default function handleDrop(props, monitor, Component) {
     }
 
     dropResult.destination = {
-      droppableId: parentComponent.id,
+      id: parentComponent.id,
+      type: parentComponent.type,
       index: nextIndex,
     };
   }
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
index 1eadef4..a303e13 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/handleHover.js
@@ -1,5 +1,5 @@
 import throttle from 'lodash.throttle';
-import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+import getDropPosition from '../../util/getDropPosition';
 
 const HOVER_THROTTLE_MS = 200;
 
@@ -14,22 +14,8 @@ function handleHover(props, monitor, Component) {
     return;
   }
 
-  // @TODO
-  // drop-indicator
-  // drop-indicator--top/right/bottom/left
   Component.setState(() => ({
-    dropIndicator: {
-      top: dropPosition === DROP_BOTTOM ? '100%' : 0,
-      left: dropPosition === DROP_RIGHT ? '100%' : 0,
-      height: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? '100%' : 3,
-      width: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? '100%' : 3,
-      minHeight: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? 16 : null,
-      minWidth: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? 16 : null,
-      margin: 'auto',
-      backgroundColor: '#44C0FF',
-      position: 'absolute',
-      zIndex: 10,
-    },
+    dropIndicator: dropPosition,
   }));
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
index 9daa8cf..7ca506d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
@@ -8,7 +8,7 @@ import HoverMenu from '../menu/HoverMenu';
 import ResizableContainer from '../resizable/ResizableContainer';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
-
+import { ROW_TYPE } from '../../util/componentTypes';
 import {
   GRID_MIN_COLUMN_COUNT,
   GRID_MIN_ROW_UNITS,
@@ -79,13 +79,14 @@ class Chart extends React.Component {
         parentComponent={parentComponent}
         orientation={depth % 2 === 1 ? 'column' : 'row'}
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
         disableDragDrop={isFocused}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <ResizableContainer
             id={component.id}
-            adjustableWidth={depth <= 1}
+            adjustableWidth={parentComponent.type === ROW_TYPE}
             adjustableHeight
             widthStep={columnWidth}
             widthMultiple={component.meta.width}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
index 8409bc1..d51870d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
@@ -7,10 +7,18 @@ import DeleteComponentButton from '../DeleteComponentButton';
 import DragDroppable from '../dnd/DragDroppable';
 import DragHandle from '../dnd/DragHandle';
 import HoverMenu from '../menu/HoverMenu';
+import IconButton from '../IconButton';
 import ResizableContainer from '../resizable/ResizableContainer';
+import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
 import { componentShape } from '../../util/propShapes';
 
-import { GRID_GUTTER_SIZE, GRID_MIN_COLUMN_COUNT } from '../../util/constants';
+import {
+  BACKGROUND_TRANSPARENT,
+  GRID_GUTTER_SIZE,
+} from '../../util/constants';
 
 const GUTTER = 'GUTTER';
 
@@ -21,11 +29,11 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
-  // occupiedRowCount: PropTypes.number,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
   columnWidth: PropTypes.number.isRequired,
+  minColumnWidth: PropTypes.number.isRequired,
   onResizeStart: PropTypes.func.isRequired,
   onResize: PropTypes.func.isRequired,
   onResizeStop: PropTypes.func.isRequired,
@@ -33,15 +41,20 @@ const propTypes = {
   // dnd
   deleteComponent: PropTypes.func.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
-  // occupiedRowCount: null,
 };
 
 class Column extends React.PureComponent {
   constructor(props) {
     super(props);
+    this.state = {
+      isFocused: false,
+    };
+    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
   }
 
@@ -50,6 +63,25 @@ class Column extends React.PureComponent {
     deleteComponent(id, parentId);
   }
 
+  handleChangeFocus(nextFocus) {
+    this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+  }
+
+  handleUpdateMeta(metaKey, nextValue) {
+    const { updateComponents, component } = this.props;
+    if (nextValue && component.meta[metaKey] !== nextValue) {
+      updateComponents({
+        [component.id]: {
+          ...component,
+          meta: {
+            ...component.meta,
+            [metaKey]: nextValue,
+          },
+        },
+      });
+    }
+  }
+
   render() {
     const {
       component: columnComponent,
@@ -57,7 +89,7 @@ class Column extends React.PureComponent {
       index,
       availableColumnCount,
       columnWidth,
-      // occupiedRowCount,
+      minColumnWidth,
       depth,
       onResizeStart,
       onResize,
@@ -74,12 +106,19 @@ class Column extends React.PureComponent {
       }
     });
 
+    const backgroundStyle = backgroundStyleOptions.find(
+      opt => opt.value === (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
+    );
+
+    console.log('occupied/avail cols', columnComponent.meta.width, '/', availableColumnCount, 'min width', minColumnWidth)
+
     return (
       <DragDroppable
         component={columnComponent}
         parentComponent={parentComponent}
         orientation="column"
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
@@ -89,47 +128,64 @@ class Column extends React.PureComponent {
             adjustableHeight={false}
             widthStep={columnWidth}
             widthMultiple={columnComponent.meta.width}
-            // heightMultiple={occupiedRowCount}
-            minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+            minWidthMultiple={minColumnWidth}
             maxWidthMultiple={availableColumnCount + (columnComponent.meta.width || 0)}
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
           >
-            <div
-              className={cx(
-                'grid-column',
-                columnItems.length === 0 && 'grid-column--empty',
-              )}
+            <WithPopoverMenu
+              isFocused={this.state.isFocused}
+              onChangeFocus={this.handleChangeFocus}
+              disableClick
+              menuItems={[
+                <BackgroundStyleDropdown
+                  id={`${columnComponent.id}-background`}
+                  value={columnComponent.meta.background}
+                  onChange={this.handleChangeBackground}
+                />,
+              ]}
             >
-              <HoverMenu innerRef={dragSourceRef} position="top">
-                <DragHandle position="top" />
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-              </HoverMenu>
-
-              {columnItems.map((componentId, itemIndex) => {
-                if (componentId === GUTTER) {
-                  return <div key={`gutter-${itemIndex}`} style={{ height: GRID_GUTTER_SIZE }} />;
-                }
-
-                return (
-                  <DashboardComponent
-                    key={componentId}
-                    id={componentId}
-                    parentId={columnComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex / 2} // account for gutters!
-                    availableColumnCount={availableColumnCount}
-                    columnWidth={columnWidth}
-                    onResizeStart={onResizeStart}
-                    onResize={onResize}
-                    onResizeStop={onResizeStop}
+              <div
+                className={cx(
+                  'grid-column',
+                  columnItems.length === 0 && 'grid-column--empty',
+                  backgroundStyle.className,
+                )}
+              >
+                <HoverMenu innerRef={dragSourceRef} position="top">
+                  <DragHandle position="top" />
+                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                  <IconButton
+                    onClick={this.handleChangeFocus}
+                    className="fa fa-cog"
                   />
-                );
-              })}
-
-              {dropIndicatorProps && <div {...dropIndicatorProps} />}
-            </div>
+                </HoverMenu>
+
+                {columnItems.map((componentId, itemIndex) => {
+                  if (componentId === GUTTER) {
+                    return <div key={`gutter-${itemIndex}`} style={{ height: GRID_GUTTER_SIZE }} />;
+                  }
+
+                  return (
+                    <DashboardComponent
+                      key={componentId}
+                      id={componentId}
+                      parentId={columnComponent.id}
+                      depth={depth + 1}
+                      index={itemIndex / 2} // account for gutters!
+                      availableColumnCount={columnComponent.meta.width}
+                      columnWidth={columnWidth}
+                      onResizeStart={onResizeStart}
+                      onResize={onResize}
+                      onResizeStop={onResizeStop}
+                    />
+                  );
+                })}
+
+                {dropIndicatorProps && <div {...dropIndicatorProps} />}
+              </div>
+            </WithPopoverMenu>
           </ResizableContainer>
         )}
       </DragDroppable>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
index 29437e1..ff29c3f 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Divider.jsx
@@ -10,6 +10,7 @@ const propTypes = {
   id: PropTypes.string.isRequired,
   parentId: PropTypes.string.isRequired,
   component: componentShape.isRequired,
+  depth: PropTypes.number.isRequired,
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   handleComponentDrop: PropTypes.func.isRequired,
@@ -30,6 +31,7 @@ class Divider extends React.PureComponent {
   render() {
     const {
       component,
+      depth,
       parentComponent,
       index,
       handleComponentDrop,
@@ -41,6 +43,7 @@ class Divider extends React.PureComponent {
         parentComponent={parentComponent}
         orientation="row"
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
index 967b483..d8744d6 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Header.jsx
@@ -7,18 +7,19 @@ import DragHandle from '../dnd/DragHandle';
 import EditableTitle from '../../../../components/EditableTitle';
 import HoverMenu from '../menu/HoverMenu';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
-import RowStyleDropdown from '../menu/RowStyleDropdown';
+import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
 import DeleteComponentButton from '../DeleteComponentButton';
 import PopoverDropdown from '../menu/PopoverDropdown';
 import headerStyleOptions from '../../util/headerStyleOptions';
-import rowStyleOptions from '../../util/rowStyleOptions';
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
 import { componentShape } from '../../util/propShapes';
-import { SMALL_HEADER, ROW_TRANSPARENT } from '../../util/constants';
+import { SMALL_HEADER, BACKGROUND_TRANSPARENT } from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
   parentId: PropTypes.string.isRequired,
   component: componentShape.isRequired,
+  depth: PropTypes.number.isRequired,
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
 
@@ -41,7 +42,7 @@ class Header extends React.PureComponent {
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
     this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize');
-    this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
     this.handleChangeText = this.handleUpdateMeta.bind(this, 'text');
   }
 
@@ -74,6 +75,7 @@ class Header extends React.PureComponent {
 
     const {
       component,
+      depth,
       parentComponent,
       index,
       handleComponentDrop,
@@ -83,8 +85,8 @@ class Header extends React.PureComponent {
       opt => opt.value === (component.meta.headerSize || SMALL_HEADER),
     );
 
-    const rowStyle = rowStyleOptions.find(
-      opt => opt.value === (component.meta.rowStyle || ROW_TRANSPARENT),
+    const rowStyle = backgroundStyleOptions.find(
+      opt => opt.value === (component.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -93,6 +95,7 @@ class Header extends React.PureComponent {
         parentComponent={parentComponent}
         orientation="row"
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
         disableDragDrop={isFocused}
       >
@@ -112,10 +115,10 @@ class Header extends React.PureComponent {
                   onChange={this.handleChangeSize}
                   renderTitle={option => `${option.label} header`}
                 />,
-                <RowStyleDropdown
-                  id={`${component.id}-row-style`}
-                  value={component.meta.rowStyle}
-                  onChange={this.handleChangeRowStyle}
+                <BackgroundStyleDropdown
+                  id={`${component.id}-background`}
+                  value={component.meta.background}
+                  onChange={this.handleChangeBackground}
                 />,
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
               ]}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
index 3386f8c..a60524f 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -8,12 +8,12 @@ import DashboardComponent from '../../containers/DashboardComponent';
 import DeleteComponentButton from '../DeleteComponentButton';
 import HoverMenu from '../menu/HoverMenu';
 import IconButton from '../IconButton';
-import RowStyleDropdown from '../menu/RowStyleDropdown';
+import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 
 import { componentShape } from '../../util/propShapes';
-import rowStyleOptions from '../../util/rowStyleOptions';
-import { GRID_GUTTER_SIZE, ROW_TRANSPARENT } from '../../util/constants';
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
+import { GRID_GUTTER_SIZE, BACKGROUND_TRANSPARENT } from '../../util/constants';
 
 const GUTTER = 'GUTTER';
 
@@ -29,7 +29,6 @@ const propTypes = {
   availableColumnCount: PropTypes.number.isRequired,
   columnWidth: PropTypes.number.isRequired,
   occupiedColumnCount: PropTypes.number.isRequired,
-  occupiedRowCount: PropTypes.number.isRequired,
   onResizeStart: PropTypes.func.isRequired,
   onResize: PropTypes.func.isRequired,
   onResizeStop: PropTypes.func.isRequired,
@@ -52,7 +51,7 @@ class Row extends React.PureComponent {
     };
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
     this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
-    this.handleChangeRowStyle = this.handleUpdateMeta.bind(this, 'rowStyle');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
   }
 
@@ -88,7 +87,6 @@ class Row extends React.PureComponent {
       availableColumnCount,
       columnWidth,
       occupiedColumnCount,
-      occupiedRowCount,
       depth,
       onResizeStart,
       onResize,
@@ -106,8 +104,8 @@ class Row extends React.PureComponent {
       }
     });
 
-    const rowStyle = rowStyleOptions.find(
-      opt => opt.value === (rowComponent.meta.rowStyle || ROW_TRANSPARENT),
+    const backgroundStyle = backgroundStyleOptions.find(
+      opt => opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -116,6 +114,7 @@ class Row extends React.PureComponent {
         parentComponent={parentComponent}
         orientation="row"
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
@@ -124,19 +123,18 @@ class Row extends React.PureComponent {
             onChangeFocus={this.handleChangeFocus}
             disableClick
             menuItems={[
-              <RowStyleDropdown
-                id={`${rowComponent.id}-row-style`}
-                value={rowComponent.meta.rowStyle}
-                onChange={this.handleChangeRowStyle}
+              <BackgroundStyleDropdown
+                id={`${rowComponent.id}-background`}
+                value={rowComponent.meta.background}
+                onChange={this.handleChangeBackground}
               />,
             ]}
           >
-
             <div
               className={cx(
                 'grid-row',
                 rowItems.length === 0 && 'grid-row--empty',
-                rowStyle.className,
+                backgroundStyle.className,
               )}
             >
               <HoverMenu innerRef={dragSourceRef} position="left">
@@ -161,7 +159,6 @@ class Row extends React.PureComponent {
                     depth={depth + 1}
                     index={itemIndex / 2} // account for gutters!
                     availableColumnCount={availableColumnCount - occupiedColumnCount}
-                    occupiedRowCount={occupiedRowCount}
                     columnWidth={columnWidth}
                     onResizeStart={onResizeStart}
                     onResize={onResize}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
index faac589..7a287d8 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Spacer.jsx
@@ -18,7 +18,6 @@ const propTypes = {
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
   columnWidth: PropTypes.number.isRequired,
-  occupiedRowCount: PropTypes.number,
   onResizeStart: PropTypes.func.isRequired,
   onResize: PropTypes.func.isRequired,
   onResizeStop: PropTypes.func.isRequired,
@@ -29,7 +28,6 @@ const propTypes = {
 };
 
 const defaultProps = {
-  occupiedRowCount: null,
 };
 
 class Spacer extends React.PureComponent {
@@ -51,7 +49,6 @@ class Spacer extends React.PureComponent {
       depth,
       availableColumnCount,
       columnWidth,
-      occupiedRowCount,
       onResizeStart,
       onResize,
       onResizeStop,
@@ -63,12 +60,15 @@ class Spacer extends React.PureComponent {
     const adjustableWidth = orientation === 'column';
     const adjustableHeight = orientation === 'row';
 
+    console.log('spacer', availableColumnCount)
+
     return (
       <DragDroppable
         component={component}
         parentComponent={parentComponent}
         orientation={orientation}
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
@@ -77,9 +77,8 @@ class Spacer extends React.PureComponent {
             adjustableWidth={adjustableWidth}
             adjustableHeight={adjustableHeight}
             widthStep={columnWidth}
-            widthMultiple={component.meta.width}
+            widthMultiple={component.meta.width || 1}
             heightMultiple={adjustableHeight ? component.meta.height || 1 : undefined}
-            staticHeightMultiple={!adjustableHeight ? occupiedRowCount || 5 : undefined}
             minWidthMultiple={1}
             minHeightMultiple={1}
             maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
index 74cd9ae..9548a4b 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tab.jsx
@@ -20,13 +20,14 @@ const propTypes = {
   depth: PropTypes.number.isRequired,
   renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
   onDropOnTab: PropTypes.func,
+  onDeleteTab: PropTypes.func,
 
   // grid related
-  availableColumnCount: PropTypes.number.isRequired,
-  columnWidth: PropTypes.number.isRequired,
-  onResizeStart: PropTypes.func.isRequired,
-  onResize: PropTypes.func.isRequired,
-  onResizeStop: PropTypes.func.isRequired,
+  availableColumnCount: PropTypes.number,
+  columnWidth: PropTypes.number,
+  onResizeStart: PropTypes.func,
+  onResize: PropTypes.func,
+  onResizeStop: PropTypes.func,
 
   // redux
   handleComponentDrop: PropTypes.func.isRequired,
@@ -35,7 +36,13 @@ const propTypes = {
 };
 
 const defaultProps = {
-  onDropOnTab: null,
+  availableColumnCount: 0,
+  columnWidth: 0,
+  onDropOnTab() {},
+  onDeleteTab() {},
+  onResizeStart() {},
+  onResize() {},
+  onResizeStop() {},
 };
 
 export default class Tab extends React.PureComponent {
@@ -70,14 +77,14 @@ export default class Tab extends React.PureComponent {
   }
 
   handleDeleteComponent() {
-    const { deleteComponent, id, parentId } = this.props;
+    const { onDeleteTab, index, deleteComponent, id, parentId } = this.props;
     deleteComponent(id, parentId);
+    onDeleteTab(index);
   }
 
   handleDrop(dropResult) {
-    const { handleComponentDrop, onDropOnTab } = this.props;
-    handleComponentDrop(dropResult);
-    if (onDropOnTab) onDropOnTab(dropResult);
+    this.props.handleComponentDrop(dropResult);
+    this.props.onDropOnTab(dropResult);
   }
 
   renderTabContent() {
@@ -98,7 +105,7 @@ export default class Tab extends React.PureComponent {
             key={componentId}
             id={componentId}
             parentId={tabComponent.id}
-            depth={depth}
+            depth={depth} // see isValidChild.js for why tabs don't increment child depth
             index={componentIndex}
             onDrop={this.handleDrop}
             availableColumnCount={availableColumnCount}
@@ -118,6 +125,7 @@ export default class Tab extends React.PureComponent {
       component,
       parentComponent,
       index,
+      depth,
     } = this.props;
 
     return (
@@ -126,6 +134,7 @@ export default class Tab extends React.PureComponent {
         parentComponent={parentComponent}
         orientation="column"
         index={index}
+        depth={depth}
         onDrop={this.handleDrop}
         disableDragDrop={isFocused}
       >
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
index 1e2e64c..cc5f637 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Tabs.jsx
@@ -8,8 +8,9 @@ import DashboardComponent from '../../containers/DashboardComponent';
 import DeleteComponentButton from '../DeleteComponentButton';
 import HoverMenu from '../menu/HoverMenu';
 import { componentShape } from '../../util/propShapes';
-import { NEW_TAB_ID } from '../../util/constants';
+import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
 import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
+import { TAB_TYPE } from '../../util/componentTypes';
 
 const NEW_TAB_INDEX = -1;
 const MAX_TAB_COUNT = 5;
@@ -21,13 +22,14 @@ const propTypes = {
   parentComponent: componentShape.isRequired,
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
+  renderTabContent: PropTypes.bool,
 
   // grid related
-  availableColumnCount: PropTypes.number.isRequired,
-  columnWidth: PropTypes.number.isRequired,
-  onResizeStart: PropTypes.func.isRequired,
-  onResize: PropTypes.func.isRequired,
-  onResizeStop: PropTypes.func.isRequired,
+  availableColumnCount: PropTypes.number,
+  columnWidth: PropTypes.number,
+  onResizeStart: PropTypes.func,
+  onResize: PropTypes.func,
+  onResizeStop: PropTypes.func,
 
   // dnd
   createComponent: PropTypes.func.isRequired,
@@ -40,6 +42,12 @@ const propTypes = {
 const defaultProps = {
   onChangeTab: null,
   children: null,
+  renderTabContent: true,
+  availableColumnCount: 0,
+  columnWidth: 0,
+  onResizeStart() {},
+  onResize() {},
+  onResizeStop() {},
 };
 
 class Tabs extends React.PureComponent {
@@ -48,8 +56,9 @@ class Tabs extends React.PureComponent {
     this.state = {
       tabIndex: 0,
     };
-    this.handleClicKTab = this.handleClicKTab.bind(this);
+    this.handleClickTab = this.handleClickTab.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+    this.handleDeleteTab = this.handleDeleteTab.bind(this);
     this.handleDropOnTab = this.handleDropOnTab.bind(this);
   }
 
@@ -60,7 +69,7 @@ class Tabs extends React.PureComponent {
     }
   }
 
-  handleClicKTab(tabIndex) {
+  handleClickTab(tabIndex) {
     const { onChangeTab, component, createComponent } = this.props;
 
     if (tabIndex !== NEW_TAB_INDEX && tabIndex !== this.state.tabIndex) {
@@ -71,10 +80,14 @@ class Tabs extends React.PureComponent {
     } else if (tabIndex === NEW_TAB_INDEX) {
       createComponent({
         destination: {
-          droppableId: component.id,
+          id: component.id,
+          type: component.type,
           index: component.children.length,
         },
-        draggableId: NEW_TAB_ID,
+        dragging: {
+          id: NEW_TAB_ID,
+          type: TAB_TYPE,
+        },
       });
     }
   }
@@ -84,19 +97,23 @@ class Tabs extends React.PureComponent {
     deleteComponent(id, parentId);
   }
 
+  handleDeleteTab(tabIndex) {
+    this.handleClickTab(Math.max(0, tabIndex - 1));
+  }
+
   handleDropOnTab(dropResult) {
     const { component } = this.props;
 
     // Ensure dropped tab is visible
     const { destination } = dropResult;
     if (destination) {
-      const dropTabIndex = destination.droppableId === component.id
+      const dropTabIndex = destination.id === component.id
         ? destination.index // dropped ON tabs
-        : component.children.indexOf(destination.droppableId); // dropped IN tab
+        : component.children.indexOf(destination.id); // dropped IN tab
 
       if (dropTabIndex > -1) {
         setTimeout(() => {
-          this.handleClicKTab(dropTabIndex);
+          this.handleClickTab(dropTabIndex);
         }, 30);
       }
     }
@@ -114,6 +131,7 @@ class Tabs extends React.PureComponent {
       onResize,
       onResizeStop,
       handleComponentDrop,
+      renderTabContent,
     } = this.props;
 
     const { tabIndex: selectedTabIndex } = this.state;
@@ -125,6 +143,7 @@ class Tabs extends React.PureComponent {
         parentComponent={parentComponent}
         orientation="row"
         index={index}
+        depth={depth}
         onDrop={handleComponentDrop}
       >
         {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
@@ -137,7 +156,7 @@ class Tabs extends React.PureComponent {
             <BootstrapTabs
               id={tabsComponent.id}
               activeKey={selectedTabIndex}
-              onSelect={this.handleClicKTab}
+              onSelect={this.handleClickTab}
               animation={false}
             >
               {tabIds.map((tabId, tabIndex) => (
@@ -156,10 +175,8 @@ class Tabs extends React.PureComponent {
                       renderType={RENDER_TAB}
                       availableColumnCount={availableColumnCount}
                       columnWidth={columnWidth}
-                      onResizeStart={onResizeStart}
-                      onResize={onResize}
-                      onResizeStop={onResizeStop}
                       onDropOnTab={this.handleDropOnTab}
+                      onDeleteTab={this.handleDeleteTab}
                     />
                   }
                 >
@@ -168,11 +185,11 @@ class Tabs extends React.PureComponent {
                     render potentially-expensive charts (this also enables lazy loading
                     their content)
                   */}
-                  {tabIndex === selectedTabIndex &&
+                  {tabIndex === selectedTabIndex && renderTabContent &&
                     <DashboardComponent
                       id={tabId}
                       parentId={tabsComponent.id}
-                      depth={depth}
+                      depth={depth} // see isValidChild.js for why tabs don't increment child depth
                       index={tabIndex}
                       renderType={RENDER_TAB_CONTENT}
                       availableColumnCount={availableColumnCount}
@@ -188,14 +205,14 @@ class Tabs extends React.PureComponent {
               {tabIds.length < MAX_TAB_COUNT &&
                 <BootstrapTab
                   eventKey={NEW_TAB_INDEX}
-                  title={<div className="fa fa-plus-square" />}
+                  title={<div className="fa fa-plus" />}
                 />}
 
             </BootstrapTabs>
 
+            {/* don't indicate that a drop on root is allowed when tabs already exist */}
             {tabsDropIndicatorProps
-              && tabsDropIndicatorProps.style
-              && tabsDropIndicatorProps.style.width === '100%'
+              && parentComponent.id !== DASHBOARD_ROOT_ID
               && <div {...tabsDropIndicatorProps} />}
 
           </div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
index c4d8d62..778f58e 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
 import cx from 'classnames';
 
 import DragDroppable from '../../dnd/DragDroppable';
+import { NEW_COMPONENTS_SOURCE_ID } from '../../../util/constants';
+import { NEW_COMPONENT_SOURCE_TYPE } from '../../../util/componentTypes';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -21,8 +23,9 @@ export default class DraggableNewComponent extends React.PureComponent {
     return (
       <DragDroppable
         component={{ type, id }}
-        parentComponent={null}
+        parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
         index={0}
+        depth={0}
       >
         {({ dragSourceRef }) => (
           <div ref={dragSourceRef} className="new-component">
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
similarity index 65%
rename from superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx
rename to superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
index d3c7eff..41cf1df 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/RowStyleDropdown.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 
-import rowStyleOptions from '../../util/rowStyleOptions';
+import backgroundStyleOptions from '../../util/backgroundStyleOptions';
 import PopoverDropdown from './PopoverDropdown';
 
 const propTypes = {
@@ -13,7 +13,7 @@ const propTypes = {
 
 function renderButton(option) {
   return (
-    <div className={cx('row-style-option', option.className)}>
+    <div className={cx('background-style-option', option.className)}>
       {`${option.label} background`}
     </div>
   );
@@ -21,19 +21,19 @@ function renderButton(option) {
 
 function renderOption(option) {
   return (
-    <div className={cx('row-style-option', option.className)}>
+    <div className={cx('background-style-option', option.className)}>
       {option.label}
     </div>
   );
 }
 
-export default class RowStyleDropdown extends React.PureComponent {
+export default class BackgroundStyleDropdown extends React.PureComponent {
   render() {
     const { id, value, onChange } = this.props;
     return (
       <PopoverDropdown
         id={id}
-        options={rowStyleOptions}
+        options={backgroundStyleOptions}
         value={value}
         onChange={onChange}
         renderButton={renderButton}
@@ -43,4 +43,4 @@ export default class RowStyleDropdown extends React.PureComponent {
   }
 }
 
-RowStyleDropdown.propTypes = propTypes;
+BackgroundStyleDropdown.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
index 7fb24cd..2054090 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
@@ -8,6 +8,7 @@ const propTypes = {
   menuItems: PropTypes.arrayOf(PropTypes.node),
   onChangeFocus: PropTypes.func,
   isFocused: PropTypes.bool,
+  shouldFocus: PropTypes.func,
 };
 
 const defaultProps = {
@@ -17,6 +18,7 @@ const defaultProps = {
   onPressDelete() {},
   menuItems: [],
   isFocused: false,
+  shouldFocus: (event, container) => container.contains(event.target),
 };
 
 class WithPopoverMenu extends React.PureComponent {
@@ -47,8 +49,10 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   handleClick(event) {
-    const { onChangeFocus } = this.props;
-    if (!this.state.isFocused) {
+    const { onChangeFocus, shouldFocus: shouldFocusThunk } = this.props;
+    const shouldFocus = shouldFocusThunk(event, this.container);
+
+    if (shouldFocus && !this.state.isFocused) {
       // if not focused, set focus and add a window event listener to capture outside clicks
       // this enables us to not set a click listener for ever item on a dashboard
       document.addEventListener('click', this.handleClick, true);
@@ -57,7 +61,7 @@ class WithPopoverMenu extends React.PureComponent {
       if (onChangeFocus) {
         onChangeFocus(true);
       }
-    } else if (!this.container.contains(event.target)) {
+    } else if (!shouldFocus && this.state.isFocused) {
       document.removeEventListener('click', this.handleClick, true);
       document.removeEventListener('drag', this.handleClick, true);
       this.setState(() => ({ isFocused: false }));
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
index 5e43678..fbb7d1d 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -50,9 +50,9 @@ const defaultProps = {
   onResizeStart: null,
 };
 
-// because columns are not actually multiples of a single variable (width = n*cols + (n-1)*gutters)
-// we snap to the base unit and then snap to actual column multiples on stop
-const snapToGrid = [GRID_BASE_UNIT, GRID_BASE_UNIT];
+// because columns are not multiples of a single variable (width = n*cols + (n-1) * gutters)
+// we snap to the base unit and then snap to _actual_ column multiples on stop
+const SNAP_TO_GRID = [GRID_BASE_UNIT, GRID_BASE_UNIT];
 
 class ResizableContainer extends React.PureComponent {
   constructor(props) {
@@ -120,9 +120,12 @@ class ResizableContainer extends React.PureComponent {
       adjustableHeight,
       widthStep,
       heightStep,
-      staticHeightMultiple,
       widthMultiple,
       heightMultiple,
+      staticHeight,
+      staticHeightMultiple,
+      staticWidth,
+      staticWidthMultiple,
       minWidthMultiple,
       maxWidthMultiple,
       minHeightMultiple,
@@ -132,42 +135,48 @@ class ResizableContainer extends React.PureComponent {
 
     const size = {
       width: adjustableWidth
-        ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth : undefined,
+        ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth
+        : (staticWidthMultiple && staticWidthMultiple * widthStep)
+          || staticWidth
+          || undefined,
       height: adjustableHeight
         ? heightStep * heightMultiple
-        : (staticHeightMultiple && staticHeightMultiple * heightStep) || undefined,
+        : (staticHeightMultiple && staticHeightMultiple * heightStep)
+          || staticHeight
+          || undefined,
     };
 
-    let enableConfig = resizableConfig.widthAndHeight;
-    if (!adjustableHeight) enableConfig = resizableConfig.widthOnly;
-    else if (!adjustableWidth) enableConfig = resizableConfig.heightOnly;
+    let enableConfig = resizableConfig.notAdjustable;
+    if (adjustableWidth && adjustableHeight) enableConfig = resizableConfig.widthAndHeight;
+    else if (adjustableWidth) enableConfig = resizableConfig.widthOnly;
+    else if (adjustableHeight) enableConfig = resizableConfig.heightOnly;
 
     const { isResizing } = this.state;
 
     return (
       <Resizable
         enable={enableConfig}
-        grid={snapToGrid}
+        grid={SNAP_TO_GRID}
         minWidth={adjustableWidth
           ? (minWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
-          : size.width}
+          : undefined}
         minHeight={adjustableHeight
           ? (minHeightMultiple * heightStep)
-          : size.height}
+          : undefined}
         maxWidth={adjustableWidth
           ? (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
-          : size.width}
+          : undefined}
         maxHeight={adjustableHeight
           ? (maxHeightMultiple * heightStep)
-          : size.height}
+          : undefined}
         size={size}
         onResizeStart={this.handleResizeStart}
         onResize={this.handleResize}
         onResizeStop={this.handleResizeStop}
         handleComponent={ResizableHandle}
         className={cx(
-          'grid-resizable-container',
-          isResizing && 'grid-resizable-container--resizing',
+          'resizable-container',
+          isResizing && 'resizable-container--resizing',
         )}
       >
         {children}
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
similarity index 59%
copy from superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
copy to superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
index 741151b..6bd8658 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
@@ -1,23 +1,23 @@
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
-import DashboardGrid from '../components/DashboardGrid';
+import DashboardBuilder from '../components/DashboardBuilder';
 
 import {
-  updateComponents,
+  deleteTopLevelTabs,
   handleComponentDrop,
 } from '../actions';
 
-function mapStateToProps({ dashboard = {} }) {
+function mapStateToProps({ dashboard: undoableDashboard }) {
   return {
-    dashboard,
+    dashboard: undoableDashboard.present,
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
-    updateComponents,
+    deleteTopLevelTabs,
     handleComponentDrop,
   }, dispatch);
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardBuilder);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
index 1340781..f7e86cc 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
@@ -4,9 +4,10 @@ import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
 import ComponentLookup from '../components/gridComponents';
-import countChildRowsAndColumns from '../util/countChildRowsAndColumns';
+import getTotalChildWidth from '../util/getChildWidth';
 import { componentShape } from '../util/propShapes';
-import { ROW_TYPE } from '../util/componentTypes';
+import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
 
 import {
   createComponent,
@@ -24,23 +25,31 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboard = {} }, ownProps) {
+function mapStateToProps({ dashboard: undoableDashboard }, ownProps) {
+  const components = undoableDashboard.present;
   const { id, parentId } = ownProps;
+  const component = components[id];
   const props = {
-    component: dashboard[id],
-    parentComponent: dashboard[parentId],
+    component,
+    parentComponent: components[parentId],
   };
 
-  // row is a special component that needs extra dims about its children
+  // 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) {
-    const { rowCount, columnCount } = countChildRowsAndColumns({
-      component: props.component,
-      components: dashboard,
-    });
+    props.occupiedColumnCount = getTotalChildWidth({ id, components });
+  } else if (props.component.type === COLUMN_TYPE) {
+    props.minColumnWidth = GRID_MIN_COLUMN_COUNT;
 
-    props.occupiedRowCount = rowCount;
-    props.occupiedColumnCount = columnCount;
+    component.children.forEach((childId) => {
+      // rows don't have widths, so find the width of its children
+      if (components[childId].type === ROW_TYPE) {
+        props.minColumnWidth = Math.max(
+          props.minColumnWidth,
+          getTotalChildWidth({ id: childId, components }),
+        );
+      }
+    });
   }
 
   return props;
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
index 741151b..eb01616 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
@@ -3,21 +3,15 @@ import { connect } from 'react-redux';
 import DashboardGrid from '../components/DashboardGrid';
 
 import {
-  updateComponents,
   handleComponentDrop,
+  resizeComponent,
 } from '../actions';
 
-function mapStateToProps({ dashboard = {} }) {
-  return {
-    dashboard,
-  };
-}
-
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
-    updateComponents,
     handleComponentDrop,
+    resizeComponent,
   }, dispatch);
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
+export default connect(null, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
new file mode 100644
index 0000000..52e7e7a
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardHeader.jsx
@@ -0,0 +1,31 @@
+import { ActionCreators as UndoActionCreators } from 'redux-undo'
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import DashboardHeader from '../components/DashboardHeader';
+import { DASHBOARD_HEADER_ID } from '../util/constants';
+
+import {
+  updateComponents,
+  handleComponentDrop,
+} from '../actions';
+
+function mapStateToProps({ dashboard: undoableDashboard }) {
+  return {
+    component: undoableDashboard.present[DASHBOARD_HEADER_ID],
+    canUndo: undoableDashboard.past.length > 0,
+    canRedo: undoableDashboard.future.length > 0,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators({
+    updateComponents,
+    handleComponentDrop,
+    onUndo: UndoActionCreators.undo,
+    onRedo: UndoActionCreators.redo,
+  }, dispatch);
+}
+
+
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader);
diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js
new file mode 100644
index 0000000..7816cc2
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/fixtures/emptyDashboardLayout.js
@@ -0,0 +1,36 @@
+import {
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_HEADER_TYPE,
+  DASHBOARD_ROOT_TYPE,
+} from '../util/componentTypes';
+
+import {
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_HEADER_ID,
+  DASHBOARD_GRID_ID,
+} from '../util/constants';
+
+export default {
+  [DASHBOARD_ROOT_ID]: {
+    type: DASHBOARD_ROOT_TYPE,
+    id: DASHBOARD_ROOT_ID,
+    children: [
+      DASHBOARD_GRID_ID,
+    ],
+  },
+
+  [DASHBOARD_GRID_ID]: {
+    type: DASHBOARD_GRID_TYPE,
+    id: DASHBOARD_GRID_ID,
+    children: [],
+    meta: {},
+  },
+
+  [DASHBOARD_HEADER_ID]: {
+    type: DASHBOARD_HEADER_TYPE,
+    id: DASHBOARD_HEADER_ID,
+    meta: {
+      text: 'New dashboard',
+    },
+  },
+};
diff --git a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js b/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js
deleted file mode 100644
index c3ce897..0000000
--- a/superset/assets/javascripts/dashboard/v2/fixtures/testLayout.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import {
-  COLUMN_TYPE,
-  HEADER_TYPE,
-  ROW_TYPE,
-  SPACER_TYPE,
-  TAB_TYPE,
-  TABS_TYPE,
-  CHART_TYPE,
-  DIVIDER_TYPE,
-  GRID_ROOT_TYPE,
-} from '../util/componentTypes';
-
-import { DASHBOARD_ROOT_ID } from '../util/constants';
-
-export default {
-  [DASHBOARD_ROOT_ID]: {
-    type: GRID_ROOT_TYPE,
-    id: DASHBOARD_ROOT_ID,
-    children: [
-      // 'header0',
-      // 'row0',
-      // 'divider0',
-      // 'row1',
-      // 'tabs0',
-      // 'divider1',
-    ],
-  },
-  // row0: {
-  //   id: 'row0',
-  //   type: INVISIBLE_ROW_TYPE,
-  //   children: [
-  //     // 'charta',
-  //     // 'chartb',
-  //     // 'chartc',
-  //   ],
-  // },
-  // row1: {
-  //   id: 'row1',
-  //   type: ROW_TYPE,
-  //   children: [
-  //     'header1',
-  //   ],
-  // },
-  // row2: {
-  //   id: 'row2',
-  //   type: ROW_TYPE,
-  //   children: [
-  //     'chartd',
-  //     'spacer0',
-  //     'charte',
-  //   ],
-  // },
-  // tabs0: {
-  //   id: 'tabs0',
-  //   type: TABS_TYPE,
-  //   children: [
-  //     'tab0',
-  //     'tab1',
-  //     'tab3',
-  //   ],
-  //   meta: {
-  //   },
-  // },
-  // tab0: {
-  //   id: 'tab0',
-  //   type: TAB_TYPE,
-  //   children: [
-  //     // 'row2',
-  //   ],
-  //   meta: {
-  //     text: 'Tab A',
-  //   },
-  // },
-  // tab1: {
-  //   id: 'tab1',
-  //   type: TAB_TYPE,
-  //   children: [
-  //   ],
-  //   meta: {
-  //     text: 'Tab B',
-  //   },
-  // },
-  // tab3: {
-  //   id: 'tab3',
-  //   type: TAB_TYPE,
-  //   children: [
-  //   ],
-  //   meta: {
-  //     text: 'Tab C',
-  //   },
-  // },
-  // header0: {
-  //   id: 'header0',
-  //   type: HEADER_TYPE,
-  //   meta: {
-  //     text: 'Header 1',
-  //   },
-  // },
-  // header1: {
-  //   id: 'header1',
-  //   type: HEADER_TYPE,
-  //   meta: {
-  //     text: 'Header 2',
-  //   },
-  // },
-  // divider0: {
-  //   id: 'divider0',
-  //   type: DIVIDER_TYPE,
-  // },
-  // divider1: {
-  //   id: 'divider1',
-  //   type: DIVIDER_TYPE,
-  // },
-  // charta: {
-  //   id: 'charta',
-  //   type: CHART_TYPE,
-  //   meta: {
-  //     width: 3,
-  //     height: 10,
-  //   },
-  // },
-  // chartb: {
-  //   id: 'chartb',
-  //   type: CHART_TYPE,
-  //   meta: {
-  //     width: 3,
-  //     height: 10,
-  //   },
-  // },
-  // chartc: {
-  //   id: 'chartc',
-  //   type: CHART_TYPE,
-  //   meta: {
-  //     width: 3,
-  //     height: 10,
-  //   },
-  // },
-  // chartd: {
-  //   id: 'chartd',
-  //   type: CHART_TYPE,
-  //   meta: {
-  //     width: 3,
-  //     height: 10,
-  //   },
-  // },
-  // charte: {
-  //   id: 'charte',
-  //   type: CHART_TYPE,
-  //   meta: {
-  //     width: 3,
-  //     height: 10,
-  //   },
-  // },
-  // spacer0: {
-  //   id: 'spacer0',
-  //   type: SPACER_TYPE,
-  //   meta: {
-  //     width: 1,
-  //   },
-  // },
-};
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
index 19fa9d7..9b03861 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboard.js
@@ -1,14 +1,25 @@
+import { DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
 import shouldWrapChildInRow from '../util/shouldWrapChildInRow';
-import { ROW_TYPE } from '../util/componentTypes';
+import {
+  CHART_TYPE,
+  COLUMN_TYPE,
+  MARKDOWN_TYPE,
+  ROW_TYPE,
+  TAB_TYPE,
+  TABS_TYPE,
+
+} from '../util/componentTypes';
 
 import {
   UPDATE_COMPONENTS,
   DELETE_COMPONENT,
   CREATE_COMPONENT,
   MOVE_COMPONENT,
+  CREATE_TOP_LEVEL_TABS,
+  DELETE_TOP_LEVEL_TABS,
 } from '../actions';
 
 const actionHandlers = {
@@ -28,12 +39,11 @@ const actionHandlers = {
     const nextComponents = { ...state };
 
     // recursively find children to remove
-    let deleteCount = 0;
     function recursivelyDeleteChildren(componentId, componentParentId) {
       // delete child and it's children
       const component = nextComponents[componentId];
       delete nextComponents[componentId];
-      deleteCount += 1;
+
       const { children = [] } = component;
       children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); });
 
@@ -52,14 +62,30 @@ const actionHandlers = {
     }
 
     recursivelyDeleteChildren(id, parentId);
-    console.log('deleted', deleteCount, 'total components', nextComponents);
 
     return nextComponents;
   },
 
   [CREATE_COMPONENT](state, action) {
     const { payload: { dropResult } } = action;
+    const { destination, dragging } = dropResult;
     const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+
+    // inherit the width of a column parent
+    if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+      const newEntitiesArray = Object.values(newEntities);
+      const component = newEntitiesArray.find(entity => entity.type === dragging.type);
+      const parentColumn = newEntities[destination.id];
+
+      newEntities[component.id] = {
+        ...component,
+        meta: {
+          ...component.meta,
+          width: parentColumn.meta.width,
+        },
+      };
+    }
+
     return {
       ...state,
       ...newEntities,
@@ -68,9 +94,9 @@ const actionHandlers = {
 
   [MOVE_COMPONENT](state, action) {
     const { payload: { dropResult } } = action;
-    const { source, destination, draggableId } = dropResult;
+    const { source, destination, dragging } = dropResult;
 
-    if (!source || !destination || !draggableId) return state;
+    if (!source || !destination || !dragging) return state;
 
     const nextEntities = reorderItem({
       entitiesMap: state,
@@ -78,16 +104,14 @@ const actionHandlers = {
       destination,
     });
 
-    // wrap the dragged component in a row depening on destination type
-    const destinationType = (state[destination.droppableId] || {}).type;
-    const draggableType = (state[draggableId] || {}).type;
+    // wrap the dragged component in a row depending on destination type
     const wrapInRow = shouldWrapChildInRow({
-      parentType: destinationType,
-      childType: draggableType,
+      parentType: destination.type,
+      childType: dragging.type,
     });
 
     if (wrapInRow) {
-      const destinationEntity = nextEntities[destination.droppableId];
+      const destinationEntity = nextEntities[destination.id];
       const destinationChildren = destinationEntity.children;
       const newRow = newComponentFactory(ROW_TYPE);
       newRow.children = [destinationChildren[destination.index]];
@@ -95,11 +119,109 @@ const actionHandlers = {
       nextEntities[newRow.id] = newRow;
     }
 
+    // inherit the width of a column parent
+    if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+      const component = nextEntities[dragging.id];
+      const parentColumn = nextEntities[destination.id];
+      nextEntities[dragging.id] = {
+        ...component,
+        meta: {
+          ...component.meta,
+          width: parentColumn.meta.width,
+        },
+      };
+    }
+
     return {
       ...state,
       ...nextEntities,
     };
   },
+
+  [CREATE_TOP_LEVEL_TABS](state, action) {
+    const { payload: { dropResult } } = action;
+    const { source, dragging } = dropResult;
+
+    // move children of current root to be children of the dragging tab
+    const rootComponent = state[DASHBOARD_ROOT_ID];
+    const topLevelId = rootComponent.children[0];
+    const topLevelComponent = state[topLevelId];
+
+    if (source.id !== NEW_COMPONENTS_SOURCE_ID) {
+      // component already exists
+      const draggingTabs = state[dragging.id];
+      const draggingTabId = draggingTabs.children[0];
+      const draggingTab = state[draggingTabId];
+
+      // move all children except the one that is dragging
+      const childrenToMove = [...topLevelComponent.children].filter(id => id !== dragging.id);
+
+      return {
+        ...state,
+        [DASHBOARD_ROOT_ID]: {
+          ...rootComponent,
+          children: [dragging.id],
+        },
+        [topLevelId]: {
+          ...topLevelComponent,
+          children: [],
+        },
+        [draggingTabId]: {
+          ...draggingTab,
+          children: [
+            ...draggingTab.children,
+            ...childrenToMove,
+          ],
+        },
+      };
+    }
+
+    // create new component
+    const newEntities = newEntitiesFromDrop({ dropResult, components: state });
+    const newEntitiesArray = Object.values(newEntities);
+    const tabComponent = newEntitiesArray.find(component => component.type === TAB_TYPE);
+    const tabsComponent = newEntitiesArray.find(component => component.type === TABS_TYPE);
+
+    tabComponent.children = [...topLevelComponent.children];
+    newEntities[topLevelId] = { ...topLevelComponent, children: [] };
+    newEntities[DASHBOARD_ROOT_ID] = { ...rootComponent, children: [tabsComponent.id] };
+
+    return {
+      ...state,
+      ...newEntities,
+    };
+  },
+
+  [DELETE_TOP_LEVEL_TABS](state) {
+    const rootComponent = state[DASHBOARD_ROOT_ID];
+    const topLevelId = rootComponent.children[0];
+    const topLevelTabs = state[topLevelId];
+
+    if (topLevelTabs.type !== TABS_TYPE) return state;
+
+    let childrenToMove = [];
+    const nextEntities = { ...state };
+
+    topLevelTabs.children.forEach((tabId) => {
+      const tabComponent = state[tabId];
+      childrenToMove = [...childrenToMove, ...tabComponent.children];
+      delete nextEntities[tabId];
+    });
+
+    delete nextEntities[topLevelId];
+
+    nextEntities[DASHBOARD_ROOT_ID] = {
+      ...rootComponent,
+      children: [DASHBOARD_GRID_ID],
+    };
+
+    nextEntities[DASHBOARD_GRID_ID] = {
+      ...(state[DASHBOARD_GRID_ID]),
+      children: childrenToMove,
+    };
+
+    return nextEntities;
+  },
 };
 
 export default function dashboardReducer(state = {}, action) {
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index 103fda0..9c0575e 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -1,6 +1,13 @@
 import { combineReducers } from 'redux';
+import undoable, { distinctState } from 'redux-undo';
+
 import dashboard from './dashboard';
 
+const undoableDashboard = undoable(dashboard, {
+  limit: 10,
+  filter: distinctState(),
+});
+
 export default combineReducers({
-  dashboard,
+  dashboard: undoableDashboard,
 });
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
new file mode 100644
index 0000000..5f1a5b0
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/builder.less
@@ -0,0 +1,64 @@
+.dashboard-v2 {
+  margin-top: -20px;
+  position: relative;
+  color: @almost-black;
+}
+
+.dashboard-header {
+  background: white;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 24px;
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1); /* @TODO color */
+}
+
+.dashboard-builder {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  height: auto;
+}
+
+/* only top-level tabs have popover, give it more padding to match header + tabs */
+.dashboard-v2 > .with-popover-menu > .popover-menu {
+  left: 24px;
+}
+
+/* drop shadow for top-level tabs only */
+.dashboard-v2 .dashboard-component-tabs {
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
+  padding-left: 8px; /* note this is added to tab-level padding, to match header */
+}
+
+.dashboard-builder .grid-container .dashboard-component-tabs {
+  box-shadow: none;
+  padding-left: 0;
+}
+
+.dashboard-builder > div:first-child {
+  width: 100%;
+  flex-grow: 1;
+  position: relative;
+}
+
+.dashboard-builder-sidepane {
+  background: white;
+  flex: 0 0 376px;
+  border: 1px solid @gray-light;
+  z-index: 1;
+}
+
+.dashboard-builder-sidepane-header {
+  font-size: 15px;
+  font-weight: 700;
+  border-bottom: 1px solid @gray-light;
+  padding: 14px;
+}
+
+/* @TODO remove upon new theme */
+.btn.btn-primary {
+  background: @almost-black !important;
+  color: white !important;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less b/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
index a8dd661..41ca478 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/buttons.less
@@ -1,6 +1,6 @@
 .icon-button {
   color: @gray;
-  font-size: 1em;
+  font-size: 1.2em;
   display: flex;
   flex-direction: row;
   align-items: center;
@@ -15,3 +15,9 @@
   outline: none;
   text-decoration: none;
 }
+
+.icon-button-label {
+  color: @gray-dark;
+  padding-left: 8px;
+  font-size: 0.9em;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx
new file mode 100644
index 0000000..e011ad4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/DashboardBuilder.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import HTML5Backend from 'react-dnd-html5-backend';
+import { DragDropContext } from 'react-dnd';
+
+import BuilderComponentPane from './BuilderComponentPane';
+import DashboardHeader from '../containers/DashboardHeader';
+import DashboardGrid from './DashboardGrid';
+import IconButton from './IconButton';
+import DragDroppable from './dnd/DragDroppable';
+import DashboardComponent from '../containers/DashboardComponent';
+import WithPopoverMenu from './menu/WithPopoverMenu';
+
+import {
+  DASHBOARD_GRID_ID,
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_ROOT_DEPTH,
+} from '../util/constants';
+
+const propTypes = {
+  editMode: PropTypes.bool,
+
+  // redux
+  dashboard: PropTypes.object.isRequired,
+  deleteTopLevelTabs: PropTypes.func.isRequired,
+  updateComponents: PropTypes.func.isRequired,
+  handleComponentDrop: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  editMode: true,
+};
+
+class DashboardBuilder extends React.Component {
+  static shouldFocusTabs(event, container) {
+    // don't focus the tabs when we click on a tab
+    return event.target.tagName === 'UL' || (
+      /icon-button/.test(event.target.className) && container.contains(event.target)
+    );
+  }
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      tabIndex: 0, // top-level tabs
+    };
+    this.handleChangeTab = this.handleChangeTab.bind(this);
+  }
+
+  handleChangeTab({ tabIndex }) {
+    this.setState(() => ({ tabIndex }));
+  }
+
+  render() {
+    const { tabIndex } = this.state;
+    const { handleComponentDrop, updateComponents, dashboard, deleteTopLevelTabs } = this.props;
+    const dashboardRoot = dashboard[DASHBOARD_ROOT_ID];
+    const rootChildId = dashboardRoot.children[0];
+    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboard[rootChildId];
+
+    const gridComponentId = topLevelTabs
+      ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
+      : DASHBOARD_GRID_ID;
+
+    const gridComponent = dashboard[gridComponentId];
+
+    return (
+      <div className="dashboard-v2">
+        {topLevelTabs ? ( // you cannot drop on/displace tabs if they already exist
+          <DashboardHeader />
+        ) : (
+          <DragDroppable
+            component={dashboardRoot}
+            parentComponent={null}
+            depth={DASHBOARD_ROOT_DEPTH}
+            index={0}
+            orientation="column"
+            onDrop={topLevelTabs ? null : handleComponentDrop}
+          >
+            {({ dropIndicatorProps }) => (
+              <div>
+                <DashboardHeader />
+                {dropIndicatorProps && <div {...dropIndicatorProps} />}
+              </div>
+            )}
+          </DragDroppable>)}
+
+        {topLevelTabs &&
+          <WithPopoverMenu
+            shouldFocus={DashboardBuilder.shouldFocusTabs}
+            menuItems={[
+              <IconButton
+                className="fa fa-level-down"
+                label="Collapse tab content"
+                onClick={deleteTopLevelTabs}
+              />,
+            ]}
+          >
+            <DashboardComponent
+              id={topLevelTabs.id}
+              parentId={DASHBOARD_ROOT_ID}
+              depth={DASHBOARD_ROOT_DEPTH + 1}
+              index={0}
+              renderTabContent={false}
+              onChangeTab={this.handleChangeTab}
+            />
+          </WithPopoverMenu>}
+
+        <div className="dashboard-builder">
+          <DashboardGrid
+            gridComponent={gridComponent}
+            dashboard={dashboard}
+            handleComponentDrop={handleComponentDrop}
+            updateComponents={updateComponents}
+            depth={DASHBOARD_ROOT_DEPTH + 1}
+          />
+          <BuilderComponentPane />
+        </div>
+      </div>
+    );
+  }
+}
+
+DashboardBuilder.propTypes = propTypes;
+DashboardBuilder.defaultProps = defaultProps;
+
+export default DragDropContext(HTML5Backend)(DashboardBuilder);
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
index b96b14b..31ae21d 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
@@ -7,7 +7,15 @@
   top: -20px;
 }
 
-.grid-column--empty:after {
+.grid-column.background--transparent {
+  background-color: transparent;
+}
+
+.grid-column.background--white {
+  background-color: white;
+}
+
+.grid-column--empty:before {
   content: "Empty column";
   position: absolute;
   top: 0;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
index 31e84cb..e36fee2 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/new-component.less
@@ -5,6 +5,7 @@
   align-items: center;
   padding: 16px;
   background: white;
+  cursor: move;
 }
 
 .new-component-placeholder {
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
index 8859926..2036815 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
@@ -8,11 +8,11 @@
   background-color: transparent;
 }
 
-.grid-row--transparent {
+.grid-row.background--transparent {
   background-color: transparent;
 }
 
-.grid-row--white {
+.grid-row.background--white {
   background-color: white;
 }
 
@@ -25,7 +25,7 @@
   height: 80px;
 }
 
-.grid-row--empty:after {
+.grid-row--empty:before {
   position: absolute;
   top: 0;
   left: 0;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
index 23e0469..f67c151 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/tabs.less
@@ -2,6 +2,7 @@
   width: 100%;
   background-color: white;
 }
+
 .dashboard-component-tabs .dashboard-component-tabs-content {
   min-height: 48px;
   margin-top: 1px;
@@ -13,13 +14,15 @@
 
 /* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
 .dashboard-component-tabs .nav-tabs > li {
-  padding: 0 16px;
+  margin: 0 16px;
 }
 
 .dashboard-component-tabs .nav-tabs > li > a {
-  color: #263238;
+  color: @almost-black;
   border: none;
   padding: 12px 0 14px 0;
+  font-size: 15px;
+  margin-right: 0;
 }
 
 .dashboard-component-tabs .nav-tabs > li.active > a {
@@ -38,7 +41,7 @@
 .dashboard-component-tabs .nav-tabs > li > a:hover {
   border: none;
   background: inherit;
-  color: #000000;
+  color: @almost-black;
 }
 
 .dashboard-component-tabs .nav-tabs > li > a:focus {
@@ -51,15 +54,27 @@
 }
 
 .dashboard-component-tabs .nav-tabs > li .drop-indicator {
-  height: 40px !important;
-  top: -10px !important;
-  opacity: 0.5;
+  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 .fa-plus-square {
-  background: linear-gradient(135deg, #E32464, #2C2261);
-  -webkit-background-clip: text;
-  -webkit-text-fill-color: transparent;
-  display: initial;
-  font-size: 16px;
+.dashboard-component-tabs li .fa-plus {
+  color: @gray-dark;
+  font-size: 14px;
+  margin-top: 3px;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
index fb010e0..45a9784 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
@@ -3,32 +3,54 @@
 }
 
 .dragdroppable--dragging {
-  opacity: 0.25;
+  opacity: 0.15;
 }
 
 .dragdroppable-row {
   width: 100%;
 }
 
-.grid-container .dragdroppable-row:after,
-.grid-container .dragdroppable-column:after {
-  border: 1px dashed transparent;
-  content: "";
+/* drop indicators */
+.drop-indicator {
+  margin: auto;
+  background-color: @indicator-color;
   position: absolute;
+  z-index: 10;
+}
+
+.drop-indicator--top {
+  top: 0;
+  left: 0;
+  height: 4px;
   width: 100%;
-  height: 100%;
-  top: 1px;
+  min-width: 16px;
+}
+
+.drop-indicator--bottom {
+  top: 100%;
   left: 0;
-  z-index: 1;
-  pointer-events: none;
+  height: 4px;
+  width: 100%;
+  min-width: 16px;
 }
 
-  .grid-container .dragdroppable-row:hover:after,
-  .grid-container .dragdroppable-column:hover:after {
-    border: 1px dashed #aaa;
-  }
+.drop-indicator--right {
+  top: 0;
+  left: 100%;
+  height: 100%;
+  width: 4px;
+  min-height: 16px;
+}
+
+.drop-indicator--left {
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 4px;
+  min-height: 16px;
+}
 
-/* Drag handle */
+/* drag handles */
 .drag-handle {
   overflow: hidden;
   width: 16px;
@@ -39,10 +61,6 @@
   width: 8px;
 }
 
-.drag-handle--top {
-  /*margin: 10px auto;*/
-}
-
 .drag-handle-dot {
   float: left;
   height: 2px;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
index c26ee0a..7c55dee 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
@@ -1,9 +1,17 @@
 .grid-container {
-   flex-grow: 1;
-   min-width: 66%;
-   margin: 24px 32px;
-   height: 100%;
-   position: relative;
+  position: relative;
+  margin: 24px;
+}
+
+.grid-content {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.empty-grid-droptarget {
+  width: 100%;
+  height: 100%;
 }
 
 /* Editing guides */
@@ -19,7 +27,28 @@
 .grid-row-guide {
   position: absolute;
   left: 0;
-  height: 1;
-  background-color: var(--indicator-color);
+  bottom: 2;
+  height: 2;
+  background-color: @indicator-color;
   pointer-events: none;
+  z-index: 10;
+}
+
+
+.grid-container .grid-row:after,
+.grid-container .grid-column:after {
+  border: 1px dashed transparent;
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 1px;
+  left: 0;
+  z-index: 1;
+  pointer-events: none;
+}
+
+.grid-container .grid-row:hover:after,
+.grid-container .grid-column:hover:after {
+  border: 1px solid @gray-light;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
index bc2935c..77edb06 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/hover-menu.less
@@ -5,10 +5,10 @@
 }
 
 .hover-menu--left {
-  width: 20px;
+  width: 24px;
   height: 100%;
   top: 0;
-  left: -20px;
+  left: -24px;
   display: flex;
   flex-direction: column;
   justify-content: center;
@@ -16,7 +16,7 @@
 }
 
 .hover-menu--left > :nth-child(n):not(:only-child):not(:last-child) {
-  margin-bottom: 8px;
+  margin-bottom: 12px;
 }
 
 .dragdroppable-row .dragdroppable-row .hover-menu--left {
@@ -25,7 +25,7 @@
 
 .hover-menu--top {
   width: 100%;
-  height: 20px;
+  height: 24px;
   top: 0;
   left: 0;
   display: flex;
@@ -35,10 +35,10 @@
 }
 
 .hover-menu--top > :nth-child(n):not(:only-child):not(:last-child) {
-  margin-right: 8px;
+  margin-right: 12px;
 }
 
-.dragdroppable:hover .hover-menu,
-.dragdroppable .hover-menu:hover {
+div:hover > .hover-menu,
+.hover-menu:hover {
   opacity: 1;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
index 125c894..d2a41a8 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
@@ -1,5 +1,6 @@
 @import './variables.less';
 
+@import './builder.less';
 @import './buttons.less';
 @import './dnd.less';
 @import './grid.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
index f68cf13..a36ab1c 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/popover-menu.less
@@ -37,6 +37,18 @@
   z-index: 10;
 }
 
+/* the focus menu doesn't account for parent padding */
+.dashboard-component-tabs li .with-popover-menu--focused:after {
+  top: -12px;
+  left: -2px;
+  width: ~"calc(100% + 4px)"; /* escape for .less */
+  height: ~"calc(100% + 28px)";
+}
+
+.dashboard-component-tabs li .popover-menu {
+  top: -56px;
+}
+
 .popover-menu .menu-item {
   display: flex;
   flex-direction: row;
@@ -87,12 +99,12 @@
   color: @almost-black;
 }
 
-/* row style menu */
-.row-style-option {
+/* background style menu */
+.background-style-option {
   display: inline-block;
 }
 
-.row-style-option:before {
+.background-style-option:before {
   content: "";
   width: 1em;
   height: 1em;
@@ -101,16 +113,16 @@
   vertical-align: middle;
 }
 
-.row-style-option.grid-row--white {
+.background-style-option.background--white {
   padding-left: 0;
   background: transparent;
 }
 
-.row-style-option.grid-row--white:before {
+.background-style-option.background--white:before {
   background: white;
   border: 1px solid @gray-light;
 }
 
-.row-style-option.grid-row--transparent:before {
+.background-style-option.background--transparent:before {
   background: @gray-light;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
index 0ccd2f8..3ce5cfd 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
@@ -1,10 +1,10 @@
-.grid-resizable-container {
+.resizable-container {
   background-color: transparent;
   position: relative;
 }
 
 /* after ensures border visibility on top of any children */
-.grid-resizable-container--resizing:after {
+.resizable-container--resizing:after {
   content: "";
   position: absolute;
   top: 0;
@@ -18,8 +18,8 @@
   opacity: 0;
 }
 
-  .grid-resizable-container:hover .resize-handle,
-  .grid-resizable-container--resizing .resize-handle {
+  .resizable-container:hover .resize-handle,
+  .resizable-container--resizing .resize-handle {
     opacity: 1;
   }
 
@@ -59,14 +59,14 @@
   border-bottom: 1px solid @gray;
 }
 
-.grid-resizable-container--resizing > span .resize-handle {
+.resizable-container--resizing > span .resize-handle {
   border-color: @indicator-color;
 }
 
 /* re-resizable sets an empty div to 100% width and height, which doesn't
   play well with many 100% height containers we need
  */
-.grid-resizable-container ~ div {
+.resizable-container ~ div {
   width: auto !important;
   height: auto !important;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js
new file mode 100644
index 0000000..cda678f
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/backgroundStyleOptions.js
@@ -0,0 +1,7 @@
+import { t } from '../../../locales';
+import { BACKGROUND_TRANSPARENT, BACKGROUND_WHITE } from './constants';
+
+export default [
+  { value: BACKGROUND_TRANSPARENT, label: t('Transparent'), className: 'background--transparent' },
+  { value: BACKGROUND_WHITE, label: t('White'), className: 'background--white' },
+];
diff --git a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
index fd5d294..c667138 100644
--- a/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
+++ b/superset/assets/javascripts/dashboard/v2/util/componentTypes.js
@@ -1,9 +1,12 @@
 export const CHART_TYPE = 'DASHBOARD_CHART_TYPE';
 export const COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE';
+export const DASHBOARD_GRID_TYPE = 'DASHBOARD_GRID_TYPE';
+export const DASHBOARD_HEADER_TYPE = 'DASHBOARD_DASHBOARD_HEADER_TYPE';
+export const DASHBOARD_ROOT_TYPE = 'DASHBOARD_ROOT_TYPE';
 export const DIVIDER_TYPE = 'DASHBOARD_DIVIDER_TYPE';
-export const GRID_ROOT_TYPE = 'DASHBOARD_GRID_ROOT_TYPE';
 export const HEADER_TYPE = 'DASHBOARD_HEADER_TYPE';
 export const MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE';
+export const NEW_COMPONENT_SOURCE_TYPE = 'NEW_COMPONENT_SOURCE_TYPE';
 export const ROW_TYPE = 'DASHBOARD_ROW_TYPE';
 export const SPACER_TYPE = 'DASHBOARD_SPACER_TYPE';
 export const TABS_TYPE = 'DASHBOARD_TABS_TYPE';
@@ -12,10 +15,13 @@ export const TAB_TYPE = 'DASHBOARD_TAB_TYPE';
 export default {
   CHART_TYPE,
   COLUMN_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_HEADER_TYPE,
+  DASHBOARD_ROOT_TYPE,
   DIVIDER_TYPE,
-  GRID_ROOT_TYPE,
   HEADER_TYPE,
   MARKDOWN_TYPE,
+  NEW_COMPONENT_SOURCE_TYPE,
   ROW_TYPE,
   SPACER_TYPE,
   TABS_TYPE,
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js
index 44a0f0e..e892456 100644
--- a/superset/assets/javascripts/dashboard/v2/util/constants.js
+++ b/superset/assets/javascripts/dashboard/v2/util/constants.js
@@ -1,5 +1,9 @@
 // Ids
+export const DASHBOARD_GRID_ID = 'DASHBOARD_GRID_ID';
+export const DASHBOARD_HEADER_ID = 'DASHBOARD_HEADER_ID';
 export const DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID';
+
+export const NEW_COMPONENTS_SOURCE_ID = 'NEW_COMPONENTS_SOURCE_ID';
 export const NEW_CHART_ID = 'NEW_CHART_ID';
 export const NEW_COLUMN_ID = 'NEW_COLUMN_ID';
 export const NEW_DIVIDER_ID = 'NEW_DIVIDER_ID';
@@ -11,6 +15,7 @@ export const NEW_TAB_ID = 'NEW_TAB_ID';
 export const NEW_TABS_ID = 'NEW_TABS_ID';
 
 // grid constants
+export const DASHBOARD_ROOT_DEPTH = 0;
 export const GRID_BASE_UNIT = 8;
 export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
 export const GRID_ROW_HEIGHT_UNIT = 2 * GRID_BASE_UNIT;
@@ -25,6 +30,6 @@ export const SMALL_HEADER = 'SMALL_HEADER';
 export const MEDIUM_HEADER = 'MEDIUM_HEADER';
 export const LARGE_HEADER = 'LARGE_HEADER';
 
-// Row types
-export const ROW_WHITE = 'ROW_WHITE';
-export const ROW_TRANSPARENT = 'ROW_TRANSPARENT';
+// Style types
+export const BACKGROUND_WHITE = 'BACKGROUND_WHITE';
+export const BACKGROUND_TRANSPARENT = 'BACKGROUND_TRANSPARENT';
diff --git a/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js b/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js
deleted file mode 100644
index dbc63cd..0000000
--- a/superset/assets/javascripts/dashboard/v2/util/countChildRowsAndColumns.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default function countChildRowsAndColumns({ component, components }) {
-  let columnCount = 0;
-  let rowCount = 0;
-
-  (component.children || []).forEach((childId) => {
-    const childComponent = components[childId];
-    columnCount += (childComponent.meta || {}).width || 0;
-    if ((childComponent.meta || {}).height) {
-      rowCount = Math.max(rowCount, childComponent.meta.height);
-    }
-  });
-
-  return { columnCount, rowCount };
-}
diff --git a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
index 5ebca8c..9a0dedf 100644
--- a/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
+++ b/superset/assets/javascripts/dashboard/v2/util/dnd-reorder.js
@@ -11,12 +11,12 @@ export default function reorderItem({
   source,
   destination,
 }) {
-  const current = [...entitiesMap[source.droppableId].children];
-  const next = [...entitiesMap[destination.droppableId].children];
+  const current = [...entitiesMap[source.id].children];
+  const next = [...entitiesMap[destination.id].children];
   const target = current[source.index];
 
   // moving to same list
-  if (source.droppableId === destination.droppableId) {
+  if (source.id === destination.id) {
     const reordered = reorder(
       current,
       source.index,
@@ -25,8 +25,8 @@ export default function reorderItem({
 
     const result = {
       ...entitiesMap,
-      [source.droppableId]: {
-        ...entitiesMap[source.droppableId],
+      [source.id]: {
+        ...entitiesMap[source.id],
         children: reordered,
       },
     };
@@ -40,12 +40,12 @@ export default function reorderItem({
 
   const result = {
     ...entitiesMap,
-    [source.droppableId]: {
-      ...entitiesMap[source.droppableId],
+    [source.id]: {
+      ...entitiesMap[source.id],
       children: current,
     },
-    [destination.droppableId]: {
-      ...entitiesMap[destination.droppableId],
+    [destination.id]: {
+      ...entitiesMap[destination.id],
       children: next,
     },
   };
diff --git a/superset/assets/javascripts/dashboard/v2/util/findParentId.js b/superset/assets/javascripts/dashboard/v2/util/findParentId.js
new file mode 100644
index 0000000..0ca15a6
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/findParentId.js
@@ -0,0 +1,15 @@
+export default function findParentId({ childId, components = {} }) {
+  let parentId = null;
+
+  const ids = Object.keys(components);
+  for (let i = 0; i < ids.length - 1; i += 1) {
+    const id = ids[i];
+    const component = components[id] || {};
+    if (id !== childId && component.children && component.children.includes(childId)) {
+      parentId = id;
+      break;
+    }
+  }
+
+  return parentId;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js b/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
new file mode 100644
index 0000000..516624d
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/getChildWidth.js
@@ -0,0 +1,16 @@
+export default function getTotalChildWidth({ id, components, recurse = false }) {
+  const component = components[id];
+  if (!component) return 0;
+
+  let width = 0;
+
+  (component.children || []).forEach((childId) => {
+    const child = components[childId];
+    width += child.meta.width || 0;
+    if (recurse) {
+      width += getTotalChildWidth({ id: childId, components, recurse }) || 0;
+    }
+  });
+
+  return width;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
index e1dfbd3..6a3bd0e 100644
--- a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
+++ b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
@@ -1,14 +1,16 @@
 import isValidChild from './isValidChild';
+import { TAB_TYPE, TABS_TYPE } from './componentTypes';
 
 export const DROP_TOP = 'DROP_TOP';
 export const DROP_RIGHT = 'DROP_RIGHT';
 export const DROP_BOTTOM = 'DROP_BOTTOM';
 export const DROP_LEFT = 'DROP_LEFT';
 
-const SIBLING_DROP_THRESHOLD = 10;
+const SIBLING_DROP_THRESHOLD = 15;
 
 export default function getDropPosition(monitor, Component) {
   const {
+    depth: componentDepth,
     parentComponent,
     component,
     orientation,
@@ -18,17 +20,23 @@ export default function getDropPosition(monitor, Component) {
   const draggingItem = monitor.getItem();
 
   // if dropped self on self, do nothing
-  if (!draggingItem || draggingItem.draggableId === component.id || !isDraggingOverShallow) {
+  if (!draggingItem || draggingItem.id === component.id || !isDraggingOverShallow) {
     return null;
   }
 
   const validChild = isValidChild({
     parentType: component.type,
+    parentDepth: componentDepth,
     childType: draggingItem.type,
   });
 
+  const parentType = parentComponent && parentComponent.type;
+  const parentDepth = // see isValidChild.js for why tabs don't increment child depth
+    componentDepth + (parentType === TAB_TYPE || parentType === TABS_TYPE ? 0 : -1);
+
   const validSibling = isValidChild({
-    parentType: parentComponent && parentComponent.type,
+    parentType,
+    parentDepth,
     childType: draggingItem.type,
   });
 
@@ -36,7 +44,7 @@ export default function getDropPosition(monitor, Component) {
     return null;
   }
 
-  const hasChildren = component.children.length > 0;
+  const hasChildren = (component.children || []).length > 0;
   const childDropOrientation = orientation === 'row' ? 'vertical' : 'horizontal';
   const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical';
 
diff --git a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
index c8921ec..9c6ae8e 100644
--- a/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
+++ b/superset/assets/javascripts/dashboard/v2/util/isValidChild.js
@@ -1,9 +1,26 @@
+/* eslint max-len: 0 */
+/**
+  * When determining if a component is a valid child of another component we must consider both
+  *   - parent + child component types
+  *   - component depth, or depth of nesting of container components
+  *
+  * We consider types because some components aren't containers (e.g. a heading) and we consider
+  * depth to prevent infinite nesting of container components.
+  *
+  * The following example container nestings should be valid, which means that some containers
+  * don't increase the (depth) of their children, namely tabs and tab:
+  *   (a) root (0) > grid (1) >                         row (2) > column (3) > row (4) > non-container (5)
+  *   (b) root (0) > grid (1) >    tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+  *   (c) root (0) > top-tab (1) >                      row (2) > column (3) > row (4) > non-container (5)
+  *   (d) root (0) > top-tab (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+  */
 import {
   CHART_TYPE,
   COLUMN_TYPE,
+  DASHBOARD_GRID_TYPE,
+  DASHBOARD_ROOT_TYPE,
   DIVIDER_TYPE,
   HEADER_TYPE,
-  GRID_ROOT_TYPE,
   MARKDOWN_TYPE,
   ROW_TYPE,
   SPACER_TYPE,
@@ -11,59 +28,70 @@ import {
   TAB_TYPE,
 } from './componentTypes';
 
-const typeToValidChildType = {
-  // while some components are wrapped in Rows, most types are valid root children
-  [GRID_ROOT_TYPE]: {
-    [CHART_TYPE]: true,
-    [COLUMN_TYPE]: true,
-    [DIVIDER_TYPE]: true,
-    [HEADER_TYPE]: true,
-    [ROW_TYPE]: true,
-    [SPACER_TYPE]: true,
-    [TABS_TYPE]: true,
+import { DASHBOARD_ROOT_DEPTH as rootDepth } from './constants';
+
+const depthOne = rootDepth + 1;
+const depthTwo = rootDepth + 2;
+const depthThree = rootDepth + 3;
+const depthFour = rootDepth + 4;
+
+// when moving components around the depth of child is irrelevant, note these are parent depths
+const parentMaxDepthLookup = {
+  [DASHBOARD_ROOT_TYPE]: {
+    [TABS_TYPE]: rootDepth,
+    [DASHBOARD_GRID_TYPE]: rootDepth,
+  },
+
+  [DASHBOARD_GRID_TYPE]: {
+    [CHART_TYPE]: depthOne,
+    [COLUMN_TYPE]: depthOne,
+    [DIVIDER_TYPE]: depthOne,
+    [HEADER_TYPE]: depthOne,
+    [ROW_TYPE]: depthOne,
+    [SPACER_TYPE]: depthOne,
+    [TABS_TYPE]: depthOne,
   },
 
   [ROW_TYPE]: {
-    [CHART_TYPE]: true,
-    [MARKDOWN_TYPE]: true,
-    [COLUMN_TYPE]: true,
-    [SPACER_TYPE]: true,
+    [CHART_TYPE]: depthFour,
+    [MARKDOWN_TYPE]: depthFour,
+    [COLUMN_TYPE]: depthTwo,
+    [SPACER_TYPE]: depthFour,
   },
 
   [TABS_TYPE]: {
-    [TAB_TYPE]: true,
+    [TAB_TYPE]: depthTwo,
   },
 
   [TAB_TYPE]: {
-    [CHART_TYPE]: true,
-    [COLUMN_TYPE]: true,
-    [DIVIDER_TYPE]: true,
-    [HEADER_TYPE]: true,
-    [ROW_TYPE]: true,
-    [SPACER_TYPE]: true,
+    [CHART_TYPE]: depthTwo,
+    [COLUMN_TYPE]: depthTwo,
+    [DIVIDER_TYPE]: depthTwo,
+    [HEADER_TYPE]: depthTwo,
+    [ROW_TYPE]: depthTwo,
+    [SPACER_TYPE]: depthTwo,
+    [TABS_TYPE]: depthTwo,
   },
 
   [COLUMN_TYPE]: {
-    [CHART_TYPE]: true,
-    [MARKDOWN_TYPE]: true,
-    [HEADER_TYPE]: true,
-    [SPACER_TYPE]: true,
+    [CHART_TYPE]: depthThree,
+    [HEADER_TYPE]: depthThree,
+    [MARKDOWN_TYPE]: depthThree,
+    [ROW_TYPE]: depthThree,
+    [SPACER_TYPE]: depthThree,
   },
 
   // these have no valid children
   [CHART_TYPE]: {},
-  [MARKDOWN_TYPE]: {},
   [DIVIDER_TYPE]: {},
   [HEADER_TYPE]: {},
+  [MARKDOWN_TYPE]: {},
   [SPACER_TYPE]: {},
 };
 
-export default function isValidChild({ parentType, childType }) {
-  if (!parentType || !childType) return false;
-
-  const isValid = Boolean(
-    typeToValidChildType[parentType][childType],
-  );
+export default function isValidChild({ parentType, childType, parentDepth }) {
+  if (!parentType || !childType || typeof parentDepth !== 'number') return false;
+  const maxParentDepth = (parentMaxDepthLookup[parentType] || {})[childType];
 
-  return isValid;
+  return typeof maxParentDepth === 'number' && parentDepth <= maxParentDepth;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
index c1ed03e..9bc01a7 100644
--- a/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/javascripts/dashboard/v2/util/newComponentFactory.js
@@ -12,16 +12,20 @@ import {
 
 import {
   MEDIUM_HEADER,
-  ROW_TRANSPARENT,
+  BACKGROUND_TRANSPARENT,
 } from './constants';
 
 const typeToDefaultMetaData = {
   [CHART_TYPE]: { width: 3, height: 15 },
-  [COLUMN_TYPE]: { width: 3 },
+  [COLUMN_TYPE]: { width: 3, background: BACKGROUND_TRANSPARENT },
   [DIVIDER_TYPE]: null,
-  [HEADER_TYPE]: { text: 'New header', headerSize: MEDIUM_HEADER, rowStyle: ROW_TRANSPARENT },
+  [HEADER_TYPE]: {
+    text: 'New header',
+    headerSize: MEDIUM_HEADER,
+    background: BACKGROUND_TRANSPARENT,
+  },
   [MARKDOWN_TYPE]: { width: 3, height: 15 },
-  [ROW_TYPE]: { rowStyle: ROW_TRANSPARENT },
+  [ROW_TYPE]: { background: BACKGROUND_TRANSPARENT },
   [SPACER_TYPE]: {},
   [TABS_TYPE]: null,
   [TAB_TYPE]: { text: 'New Tab' },
diff --git a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
index a0d92fa..9e49643 100644
--- a/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/javascripts/dashboard/v2/util/newEntitiesFromDrop.js
@@ -1,4 +1,3 @@
-import newComponentIdToType from './newComponentIdToType';
 import shouldWrapChildInRow from './shouldWrapChildInRow';
 import newComponentFactory from './newComponentFactory';
 
@@ -9,21 +8,10 @@ import {
 } from './componentTypes';
 
 export default function newEntitiesFromDrop({ dropResult, components }) {
-  const { draggableId, destination } = dropResult;
-
-  const dragType = newComponentIdToType[draggableId];
-  const dropEntity = components[destination.droppableId];
-
-  if (!dropEntity) {
-    console.warn('Drop target entity', destination.droppableId, 'not found');
-    return null;
-  }
-
-  if (!dragType) {
-    console.warn('Drag type not found for id', draggableId);
-    return null;
-  }
+  const { dragging, destination } = dropResult;
 
+  const dragType = dragging.type;
+  const dropEntity = components[destination.id];
   const dropType = dropEntity.type;
   let newDropChild = newComponentFactory(dragType);
   const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
@@ -46,7 +34,7 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
   const nextDropChildren = [...dropEntity.children];
   nextDropChildren.splice(destination.index, 0, newDropChild.id);
 
-  newEntities[destination.droppableId] = {
+  newEntities[destination.id] = {
     ...dropEntity,
     children: nextDropChildren,
   };
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
index be84965..d701cc2 100644
--- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
@@ -1,6 +1,6 @@
 import PropTypes from 'prop-types';
 import componentTypes from './componentTypes';
-import rowStyleOptions from './rowStyleOptions';
+import backgroundStyleOptions from './backgroundStyleOptions';
 import headerStyleOptions from './headerStyleOptions';
 
 export const componentShape = PropTypes.shape({ // eslint-disable-line
@@ -19,6 +19,6 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
     headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)),
 
     // Row
-    rowStyle: PropTypes.oneOf(rowStyleOptions.map(opt => opt.value)),
+    background: PropTypes.oneOf(backgroundStyleOptions.map(opt => opt.value)),
   }),
 });
diff --git a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
index 40e9af6..f94914e 100644
--- a/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
+++ b/superset/assets/javascripts/dashboard/v2/util/resizableConfig.js
@@ -1,5 +1,4 @@
 // config for a ResizableContainer
-
 const adjustableWidthAndHeight = {
   top: false,
   right: false,
@@ -23,8 +22,14 @@ const adjustableHeight = {
   bottomRight: false,
 };
 
+const notAdjustable = {
+  ...adjustableWidthAndHeight,
+  bottomRight: false,
+};
+
 export default {
   widthAndHeight: adjustableWidthAndHeight,
   widthOnly: adjustableWidth,
   heightOnly: adjustableHeight,
+  notAdjustable,
 };
diff --git a/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js b/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js
deleted file mode 100644
index ad42492..0000000
--- a/superset/assets/javascripts/dashboard/v2/util/rowStyleOptions.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { t } from '../../../locales';
-import { ROW_TRANSPARENT, ROW_WHITE } from './constants';
-
-export default [
-  { value: ROW_TRANSPARENT, label: t('Transparent'), className: 'grid-row--transparent' },
-  { value: ROW_WHITE, label: t('White'), className: 'grid-row--white' },
-];
diff --git a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
index 487e247..e7e648c 100644
--- a/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
+++ b/superset/assets/javascripts/dashboard/v2/util/shouldWrapChildInRow.js
@@ -1,5 +1,5 @@
 import {
-  GRID_ROOT_TYPE,
+  DASHBOARD_GRID_TYPE,
   CHART_TYPE,
   COLUMN_TYPE,
   MARKDOWN_TYPE,
@@ -7,7 +7,7 @@ import {
 } from './componentTypes';
 
 const typeToWrapChildLookup = {
-  [GRID_ROOT_TYPE]: {
+  [DASHBOARD_GRID_TYPE]: {
     [CHART_TYPE]: true,
     [COLUMN_TYPE]: true,
     [MARKDOWN_TYPE]: true,
diff --git a/superset/assets/package.json b/superset/assets/package.json
index b3379f3..75f9504 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -113,6 +113,7 @@
     "redux": "^3.5.2",
     "redux-localstorage": "^0.4.1",
     "redux-thunk": "^2.1.0",
+    "redux-undo": "^0.6.1",
     "shortid": "^2.2.6",
     "sprintf-js": "^1.1.1",
     "srcdoc-polyfill": "^1.0.0",
diff --git a/superset/assets/src/components/EditableTitle.jsx b/superset/assets/src/components/EditableTitle.jsx
index 1497676..a7e3f17 100644
--- a/superset/assets/src/components/EditableTitle.jsx
+++ b/superset/assets/src/components/EditableTitle.jsx
@@ -28,7 +28,7 @@ class EditableTitle extends React.PureComponent {
     this.handleClick = this.handleClick.bind(this);
     this.handleBlur = this.handleBlur.bind(this);
     this.handleChange = this.handleChange.bind(this);
-    this.handleKeyDown = this.handleKeyDown.bind(this);
+    this.handleKeyUp = this.handleKeyUp.bind(this);
     this.handleKeyPress = this.handleKeyPress.bind(this);
   }
 
@@ -79,7 +79,7 @@ class EditableTitle extends React.PureComponent {
     }
   }
 
-  handleKeyDown(ev) {
+  handleKeyUp(ev) {
     // this entire method exists to support using EditableTitle as the title of a
     // react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
     //
@@ -121,7 +121,7 @@ class EditableTitle extends React.PureComponent {
         required
         type={this.state.isEditing ? 'text' : 'button'}
         value={this.state.title}
-        onKeyDown={this.handleKeyDown}
+        onKeyUp={this.handleKeyUp}
         onChange={this.handleChange}
         onBlur={this.handleBlur}
         onClick={this.handleClick}
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index c9236bd..bb21a43 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -10,7 +10,7 @@ import { initJQueryAjax } from '../modules/utils';
 import DashboardContainer from './components/DashboardContainer';
 // import rootReducer, { getInitialState } from './reducers';
 
-import testLayout from './v2/fixtures/testLayout';
+import emptyDashboardLayout from './v2/fixtures/emptyDashboardLayout';
 import rootReducer from './v2/reducers/';
 
 appSetup();
@@ -20,7 +20,11 @@ const appContainer = document.getElementById('app');
 // const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
 // const initState = Object.assign({}, getInitialState(bootstrapData));
 const initState = {
-  dashboard: testLayout,
+  dashboard: {
+    past: [],
+    present: emptyDashboardLayout,
+    future: [],
+  },
 };
 
 const store = createStore(
diff --git a/superset/assets/stylesheets/dashboard-v2.css b/superset/assets/stylesheets/dashboard-v2.css
deleted file mode 100644
index 534a17e..0000000
--- a/superset/assets/stylesheets/dashboard-v2.css
+++ /dev/null
@@ -1,42 +0,0 @@
-.dashboard-v2 {
-  margin-top: -20px;
-  position: relative;
-  color: #263238;
-}
-
-.dashboard-header {
-  background: white;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 24px;
-  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
-  margin-bottom: 2px;
-}
-
-.dashboard-builder {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: nowrap;
-  height: auto;
-}
-
-.dashboard-builder-sidepane {
-  background: white;
-  flex: 0 0 376px;
-  box-shadow: 0 0 0 1px #ccc; /* @TODO color */
-}
-
-.dashboard-builder-sidepane-header {
-  font-size: 16;
-  font-weight: 700;
-  border-bottom: 1px solid #ccc;
-  padding: 16px;
-}
-
-/* @TODO remove upon new theme */
-.btn.btn-primary {
-  background: #263238 !important;
-  color: white !important;
-}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 2c405cd..e9f508b 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -232,7 +232,7 @@ table.table-no-hover tr:hover {
   background: transparent;
   border: none;
   box-shadow: none;
-  padding-left: 0;
+  padding: 0;
 }
 
 .editable-title input[type="button"] {
diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html
index acb292c..77248f0 100644
--- a/superset/templates/appbuilder/navbar.html
+++ b/superset/templates/appbuilder/navbar.html
@@ -29,21 +29,6 @@
       </ul>
       <ul class="nav navbar-nav navbar-right">
         {% include 'appbuilder/navbar_right.html' %}
-        <li>
-          <a href="/static/assets/version_info.json" title="Version info">
-            <i class="fa fa-code-fork"></i> &nbsp;
-          </a>
-        </li>
-        <li>
-          <a href="https://github.com/apache/incubator-superset" title="Superset's Github" target="_blank">
-            <i class="fa fa-github"></i> &nbsp;
-          </a>
-        </li>
-        <li>
-          <a href="https://superset.incubator.apache.org" title="Documentation" target="_blank">
-            <i class="fa fa-book"></i> &nbsp;
-          </a>
-        </li>
       </ul>
     </div>
   </div>