You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2019/06/08 00:59:59 UTC

[incubator-superset] branch release--0.33 updated (fbf4080 -> cc7ef2e)

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

michellet pushed a change to branch release--0.33
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git.


    from fbf4080  0.33.0rc1.dev2
     new 4a970cf  [dashboard] allow user re-order top-level tabs (#7390)
     new 9fdbfcd  [SQL Lab] Old query showing success state but not showing results (#7628)
     new 642d6c1  [SQL Lab] Prevent failed query error from disappearing (#7624)
     new 2b6e8f5  Allow trailing spaces in simple filter values (#7617)
     new 33d9032  [dashboard] click tab anchor link (#7640)
     new 5c607c1  [SQL Lab] Show warning when user used up localStorage (#7572)
     new 123d5b5  [dashboard] pass dashboard filters to share chart url in dropdown (#7642)
     new cc7ef2e  0.33.0rc1.dev3

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


Summary of changes:
 superset/assets/package-lock.json                  |  2 +-
 superset/assets/package.json                       |  2 +-
 .../dashboard/actions/dashboardLayout_spec.js      | 67 +++++++++++++++++++++-
 .../spec/javascripts/sqllab/SouthPane_spec.jsx     | 14 ++++-
 .../spec/javascripts/sqllab/SqlEditor_spec.jsx     |  1 -
 .../sqllab/utils/emptyQueryResults_spec.js}        | 37 ++++++------
 superset/assets/src/SqlLab/App.jsx                 | 18 +++++-
 superset/assets/src/SqlLab/components/App.jsx      | 39 ++++++++++++-
 .../assets/src/SqlLab/components/SouthPane.jsx     |  9 ++-
 .../assets/src/SqlLab/components/SqlEditor.jsx     |  1 +
 superset/assets/src/SqlLab/constants.js            | 15 +++++
 .../assets/src/SqlLab/reducers/getInitialState.js  |  1 +
 superset/assets/src/SqlLab/reducers/index.js       |  2 +
 .../reducers/{common.js => localStorageUsage.js}   |  2 +-
 superset/assets/src/SqlLab/reducers/sqlLab.js      |  5 +-
 .../utils/emptyQueryResults.js}                    | 23 ++++++--
 superset/assets/src/chart/Chart.jsx                |  9 ++-
 superset/assets/src/chart/ChartRenderer.jsx        |  8 +--
 superset/assets/src/components/AnchorLink.jsx      |  4 +-
 .../src/dashboard/actions/dashboardLayout.js       | 29 +++++++++-
 .../src/dashboard/components/DashboardBuilder.jsx  |  7 ++-
 .../dashboard/components/gridComponents/Chart.jsx  |  2 +-
 .../dashboard/components/gridComponents/Tab.jsx    |  5 --
 superset/assets/src/dashboard/containers/Chart.jsx |  2 +-
 .../src/dashboard/reducers/getInitialState.js      |  4 ++
 superset/connectors/base/models.py                 |  2 +-
 tests/druid_func_tests.py                          |  9 ++-
 27 files changed, 258 insertions(+), 61 deletions(-)
 copy superset/assets/{cypress/integration/explore/visualizations/filter_box.js => spec/javascripts/sqllab/utils/emptyQueryResults_spec.js} (50%)
 copy superset/assets/src/SqlLab/reducers/{common.js => localStorageUsage.js} (93%)
 copy superset/assets/src/{components/BootstrapSliderWrapper.css => SqlLab/utils/emptyQueryResults.js} (61%)


[incubator-superset] 03/08: [SQL Lab] Prevent failed query error from disappearing (#7624)

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

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

commit 642d6c1364aab16dfd6bfb85b4691156b26c7184
Author: Erik Ritter <er...@airbnb.com>
AuthorDate: Fri May 31 15:27:08 2019 -0700

    [SQL Lab] Prevent failed query error from disappearing (#7624)
    
    
    (cherry picked from commit 5895d8c8e1f99903d0422c67189f4526cedf80c4)
---
 superset/assets/src/SqlLab/reducers/sqlLab.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/superset/assets/src/SqlLab/reducers/sqlLab.js b/superset/assets/src/SqlLab/reducers/sqlLab.js
index 93ff325..6b263bd 100644
--- a/superset/assets/src/SqlLab/reducers/sqlLab.js
+++ b/superset/assets/src/SqlLab/reducers/sqlLab.js
@@ -257,7 +257,10 @@ export default function sqlLabReducer(state = {}, action) {
       let queriesLastUpdate = state.queriesLastUpdate;
       for (const id in action.alteredQueries) {
         const changedQuery = action.alteredQueries[id];
-        if (!state.queries.hasOwnProperty(id) || state.queries[id].state !== 'stopped') {
+        if (
+          !state.queries.hasOwnProperty(id)
+          || (state.queries[id].state !== 'stopped' && state.queries[id].state !== 'failed')
+        ) {
           if (changedQuery.changedOn > queriesLastUpdate) {
             queriesLastUpdate = changedQuery.changedOn;
           }


[incubator-superset] 04/08: Allow trailing spaces in simple filter values (#7617)

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

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

commit 2b6e8f57278d7aafb6d21bb774dbbb38b875d278
Author: Erik Ritter <er...@airbnb.com>
AuthorDate: Fri May 31 17:06:41 2019 -0700

    Allow trailing spaces in simple filter values (#7617)
    
    
    (cherry picked from commit 722043c67276e1bb72bf297eaa3cf37d3ce83ee0)
---
 superset/connectors/base/models.py | 2 +-
 tests/druid_func_tests.py          | 9 ++++++++-
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index eed92a6..ac4016d 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -220,7 +220,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
         def handle_single_value(v):
             # backward compatibility with previous <select> components
             if isinstance(v, str):
-                v = v.strip('\t\n \'"')
+                v = v.strip('\t\n\'"')
                 if target_column_is_numeric:
                     # For backwards compatibility and edge cases
                     # where a column data type might have changed
diff --git a/tests/druid_func_tests.py b/tests/druid_func_tests.py
index cf0c5e9..b6fe46c 100644
--- a/tests/druid_func_tests.py
+++ b/tests/druid_func_tests.py
@@ -234,12 +234,19 @@ class DruidFuncTestCase(unittest.TestCase):
         self.assertIsNone(res)
 
     def test_get_filters_extracts_values_in_quotes(self):
-        filtr = {'col': 'A', 'op': 'in', 'val': ['  "a" ']}
+        filtr = {'col': 'A', 'op': 'in', 'val': ['"a"']}
         col = DruidColumn(column_name='A')
         column_dict = {'A': col}
         res = DruidDatasource.get_filters([filtr], [], column_dict)
         self.assertEqual('a', res.filter['filter']['value'])
 
+    def test_get_filters_keeps_trailing_spaces(self):
+        filtr = {'col': 'A', 'op': 'in', 'val': ['a ']}
+        col = DruidColumn(column_name='A')
+        column_dict = {'A': col}
+        res = DruidDatasource.get_filters([filtr], [], column_dict)
+        self.assertEqual('a ', res.filter['filter']['value'])
+
     def test_get_filters_converts_strings_to_num(self):
         filtr = {'col': 'A', 'op': 'in', 'val': ['6']}
         col = DruidColumn(column_name='A')


[incubator-superset] 02/08: [SQL Lab] Old query showing success state but not showing results (#7628)

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

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

commit 9fdbfcdd52e16b533042f0f1921d8cf7a3c304cc
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Fri May 31 15:26:41 2019 -0700

    [SQL Lab] Old query showing success state but not showing results (#7628)
    
    
    (cherry picked from commit 5701629d952c0e51a74f48871b3988f586b289b6)
---
 superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx | 14 +++++++++++++-
 superset/assets/src/SqlLab/components/SouthPane.jsx        |  4 +++-
 superset/assets/src/SqlLab/components/SqlEditor.jsx        |  1 +
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx b/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx
index da6da52..7baba2b 100644
--- a/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx
@@ -25,6 +25,7 @@ import { shallow } from 'enzyme';
 import { STATUS_OPTIONS } from '../../../src/SqlLab/constants';
 import { initialState } from './fixtures';
 import SouthPaneContainer, { SouthPane } from '../../../src/SqlLab/components/SouthPane';
+import ResultSet from '../../../src/SqlLab/components/ResultSet';
 
 describe('SouthPane', () => {
   const middlewares = [thunk];
@@ -32,7 +33,13 @@ describe('SouthPane', () => {
   const store = mockStore(initialState);
 
   const mockedProps = {
-    editorQueries: [],
+    editorQueries: [
+      { cached: false, changedOn: 1559238552333, db: 'main', dbId: 1, id: 'LCly_kkIN' },
+      { cached: false, changedOn: 1559238500401, db: 'main', dbId: 1, id: 'lXJa7F9_r' },
+      { cached: false, changedOn: 1559238506925, db: 'main', dbId: 1, id: '2g2_iRFMl' },
+      { cached: false, changedOn: 1559238516395, db: 'main', dbId: 1, id: 'erWdqEWPm' },
+    ],
+    latestQueryId: 'LCly_kkIN',
     dataPreviewQueries: [],
     actions: {},
     activeSouthPaneTab: '',
@@ -57,4 +64,9 @@ describe('SouthPane', () => {
     wrapper.setProps({ offline: true });
     expect(wrapper.find('.m-r-3').render().text()).toBe(STATUS_OPTIONS.offline);
   });
+  it('should pass latest query down to ResultSet component', () => {
+    wrapper = getWrapper();
+    expect(wrapper.find(ResultSet)).toHaveLength(1);
+    expect(wrapper.find(ResultSet).props().query.id).toEqual(mockedProps.latestQueryId);
+  });
 });
diff --git a/superset/assets/src/SqlLab/components/SouthPane.jsx b/superset/assets/src/SqlLab/components/SouthPane.jsx
index c3dd57b..68321f3 100644
--- a/superset/assets/src/SqlLab/components/SouthPane.jsx
+++ b/superset/assets/src/SqlLab/components/SouthPane.jsx
@@ -37,6 +37,7 @@ const TAB_HEIGHT = 44;
 */
 const propTypes = {
   editorQueries: PropTypes.array.isRequired,
+  latestQueryId: PropTypes.string.isRequired,
   dataPreviewQueries: PropTypes.array.isRequired,
   actions: PropTypes.object.isRequired,
   activeSouthPaneTab: PropTypes.string,
@@ -82,7 +83,8 @@ export class SouthPane extends React.PureComponent {
     let latestQuery;
     const props = this.props;
     if (props.editorQueries.length > 0) {
-      latestQuery = props.editorQueries[props.editorQueries.length - 1];
+      // get the latest query
+      latestQuery = props.editorQueries.find(q => q.id === this.props.latestQueryId);
     }
     let results;
     if (latestQuery) {
diff --git a/superset/assets/src/SqlLab/components/SqlEditor.jsx b/superset/assets/src/SqlLab/components/SqlEditor.jsx
index 960a4af..0005ec4 100644
--- a/superset/assets/src/SqlLab/components/SqlEditor.jsx
+++ b/superset/assets/src/SqlLab/components/SqlEditor.jsx
@@ -247,6 +247,7 @@ class SqlEditor extends React.PureComponent {
         </div>
         <SouthPane
           editorQueries={this.props.editorQueries}
+          latestQueryId={this.props.latestQuery ? this.props.latestQuery.id : 0}
           dataPreviewQueries={this.props.dataPreviewQueries}
           actions={this.props.actions}
           height={this.state.southPaneHeight || southPaneHeight}


[incubator-superset] 08/08: 0.33.0rc1.dev3

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

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

commit cc7ef2ee6c145f67f0b7dc4fbf8208e088f94caf
Author: Michelle Thomas <mi...@gmail.com>
AuthorDate: Fri Jun 7 17:20:56 2019 -0700

    0.33.0rc1.dev3
---
 superset/assets/package-lock.json | 2 +-
 superset/assets/package.json      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json
index 8d3a12d..b95f192 100644
--- a/superset/assets/package-lock.json
+++ b/superset/assets/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "superset",
-  "version": "0.33.0rc1.dev2",
+  "version": "0.33.0rc1.dev3",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/superset/assets/package.json b/superset/assets/package.json
index b39740a..5c42520 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -1,6 +1,6 @@
 {
   "name": "superset",
-  "version": "0.33.0rc1.dev2",
+  "version": "0.33.0rc1.dev3",
   "description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
   "license": "Apache-2.0",
   "directories": {


[incubator-superset] 06/08: [SQL Lab] Show warning when user used up localStorage (#7572)

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

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

commit 5c607c16ad8e66499420de35805c67bff7afee3a
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Fri Jun 7 14:27:57 2019 -0700

    [SQL Lab] Show warning when user used up localStorage (#7572)
    
    (cherry picked from commit 39d67cbc5901995e7f45e806d163131db18887df)
---
 .../spec/javascripts/sqllab/SouthPane_spec.jsx     |  8 ++---
 .../spec/javascripts/sqllab/SqlEditor_spec.jsx     |  1 -
 .../sqllab/utils/emptyQueryResults_spec.js         | 42 ++++++++++++++++++++++
 superset/assets/src/SqlLab/App.jsx                 | 18 +++++++++-
 superset/assets/src/SqlLab/components/App.jsx      | 39 +++++++++++++++++++-
 .../assets/src/SqlLab/components/SouthPane.jsx     |  5 +--
 superset/assets/src/SqlLab/constants.js            | 15 ++++++++
 .../assets/src/SqlLab/reducers/getInitialState.js  |  1 +
 superset/assets/src/SqlLab/reducers/index.js       |  2 ++
 .../reducers/{index.js => localStorageUsage.js}    | 14 ++------
 .../{constants.js => utils/emptyQueryResults.js}   | 42 +++++++++-------------
 11 files changed, 142 insertions(+), 45 deletions(-)

diff --git a/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx b/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx
index 7baba2b..b67b2e6 100644
--- a/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx
@@ -34,10 +34,10 @@ describe('SouthPane', () => {
 
   const mockedProps = {
     editorQueries: [
-      { cached: false, changedOn: 1559238552333, db: 'main', dbId: 1, id: 'LCly_kkIN' },
-      { cached: false, changedOn: 1559238500401, db: 'main', dbId: 1, id: 'lXJa7F9_r' },
-      { cached: false, changedOn: 1559238506925, db: 'main', dbId: 1, id: '2g2_iRFMl' },
-      { cached: false, changedOn: 1559238516395, db: 'main', dbId: 1, id: 'erWdqEWPm' },
+      { cached: false, changedOn: Date.now(), db: 'main', dbId: 1, id: 'LCly_kkIN', startDttm: Date.now() },
+      { cached: false, changedOn: 1559238500401, db: 'main', dbId: 1, id: 'lXJa7F9_r', startDttm: 1559238500401 },
+      { cached: false, changedOn: 1559238506925, db: 'main', dbId: 1, id: '2g2_iRFMl', startDttm: 1559238506925 },
+      { cached: false, changedOn: 1559238516395, db: 'main', dbId: 1, id: 'erWdqEWPm', startDttm: 1559238516395 },
     ],
     latestQueryId: 'LCly_kkIN',
     dataPreviewQueries: [],
diff --git a/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx b/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx
index 046b2e6..c2500f5 100644
--- a/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx
@@ -31,7 +31,6 @@ describe('SqlEditor', () => {
     queryEditor: initialState.sqlLab.queryEditors[0],
     latestQuery: queries[0],
     tables: [table],
-    queries,
     getHeight: () => ('100px'),
     editorQueries: [],
     dataPreviewQueries: [],
diff --git a/superset/assets/spec/javascripts/sqllab/utils/emptyQueryResults_spec.js b/superset/assets/spec/javascripts/sqllab/utils/emptyQueryResults_spec.js
new file mode 100644
index 0000000..f202430
--- /dev/null
+++ b/superset/assets/spec/javascripts/sqllab/utils/emptyQueryResults_spec.js
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import emptyQueryResults from '../../../../src/SqlLab/utils/emptyQueryResults';
+import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../../../src/SqlLab/constants';
+import { queries } from '../fixtures';
+
+describe('emptyQueryResults', () => {
+  const queriesObj = {};
+  beforeEach(() => {
+    queries.forEach((q) => {
+      queriesObj[q.id] = q;
+    });
+  });
+
+  it('should empty query.results if query.startDttm is > LOCALSTORAGE_MAX_QUERY_AGE_MS', () => {
+    // make sure sample data contains old query
+    const oldQuery = queries[0];
+    const { id, startDttm } = oldQuery;
+    expect(Date.now() - startDttm).toBeGreaterThan(LOCALSTORAGE_MAX_QUERY_AGE_MS);
+    expect(Object.keys(oldQuery.results)).toContain('data');
+
+    const emptiedQuery = emptyQueryResults(queriesObj);
+    expect(emptiedQuery[id].startDttm).toBe(startDttm);
+    expect(emptiedQuery[id].results).toEqual({});
+  });
+});
diff --git a/superset/assets/src/SqlLab/App.jsx b/superset/assets/src/SqlLab/App.jsx
index 359cd73..3883591 100644
--- a/superset/assets/src/SqlLab/App.jsx
+++ b/superset/assets/src/SqlLab/App.jsx
@@ -27,6 +27,8 @@ import getInitialState from './reducers/getInitialState';
 import rootReducer from './reducers/index';
 import { initEnhancer } from '../reduxUtils';
 import App from './components/App';
+import emptyQueryResults from './utils/emptyQueryResults';
+import { BYTES_PER_CHAR, KB_STORAGE } from './constants';
 import setupApp from '../setup/setupApp';
 
 import './main.less';
@@ -50,8 +52,22 @@ const sqlLabPersistStateConfig = {
         // it caused configurations passed from server-side got override.
         // see PR 6257 for details
         delete state[path].common; // eslint-disable-line no-param-reassign
-        subset[path] = state[path];
+
+        if (path === 'sqlLab') {
+          subset[path] = {
+            ...state[path],
+            queries: emptyQueryResults(state[path].queries),
+          };
+        }
       });
+
+      const data = JSON.stringify(subset);
+      // 2 digit precision
+      const currentSize = Math.round(data.length * BYTES_PER_CHAR / KB_STORAGE * 100) / 100;
+      if (state.localStorageUsageInKilobytes !== currentSize) {
+        state.localStorageUsageInKilobytes = currentSize; // eslint-disable-line no-param-reassign
+      }
+
       return subset;
     },
   },
diff --git a/superset/assets/src/SqlLab/components/App.jsx b/superset/assets/src/SqlLab/components/App.jsx
index e4e516a..c4f2960 100644
--- a/superset/assets/src/SqlLab/components/App.jsx
+++ b/superset/assets/src/SqlLab/components/App.jsx
@@ -21,11 +21,18 @@ import PropTypes from 'prop-types';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 import $ from 'jquery';
+import { t } from '@superset-ui/translation';
+import throttle from 'lodash/throttle';
 
 import TabbedSqlEditors from './TabbedSqlEditors';
 import QueryAutoRefresh from './QueryAutoRefresh';
 import QuerySearch from './QuerySearch';
 import ToastPresenter from '../../messageToasts/containers/ToastPresenter';
+import {
+  LOCALSTORAGE_MAX_USAGE_KB,
+  LOCALSTORAGE_WARNING_THRESHOLD,
+  LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS,
+} from '../constants';
 import * as Actions from '../actions/sqlLab';
 
 class App extends React.PureComponent {
@@ -35,6 +42,12 @@ class App extends React.PureComponent {
       hash: window.location.hash,
       contentHeight: '0px',
     };
+
+    this.showLocalStorageUsageWarning = throttle(
+      this.showLocalStorageUsageWarning,
+      LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS,
+      { trailing: false },
+    );
   }
   componentDidMount() {
     /* eslint-disable react/no-did-mount-set-state */
@@ -42,6 +55,13 @@ class App extends React.PureComponent {
     window.addEventListener('hashchange', this.onHashChanged.bind(this));
     window.addEventListener('resize', this.handleResize.bind(this));
   }
+  componentDidUpdate() {
+    if (this.props.localStorageUsageInKilobytes >=
+      LOCALSTORAGE_WARNING_THRESHOLD * LOCALSTORAGE_MAX_USAGE_KB
+    ) {
+      this.showLocalStorageUsageWarning(this.props.localStorageUsageInKilobytes);
+    }
+  }
   componentWillUnmount() {
     window.removeEventListener('hashchange', this.onHashChanged.bind(this));
     window.removeEventListener('resize', this.handleResize.bind(this));
@@ -65,6 +85,15 @@ class App extends React.PureComponent {
     const alertHeight = alertEl.length > 0 ? alertEl.outerHeight() : 0;
     return `${window.innerHeight - headerHeight - tabsHeight - warningHeight - alertHeight}px`;
   }
+  showLocalStorageUsageWarning(currentUsage) {
+    this.props.actions.addDangerToast(
+      t('SQL Lab uses your browser\'s local storage to store queries and results.' +
+        `\n Currently, you are using ${currentUsage.toFixed(2)} KB out of ${LOCALSTORAGE_MAX_USAGE_KB} KB. storage space.` +
+        '\n To keep SQL Lab from crashing, please delete some query tabs.' +
+        '\n You can re-access these queries by using the Save feature before you delete the tab. ' +
+        'Note that you will need to close other SQL Lab windows before you do this.'),
+    );
+  }
   handleResize() {
     this.setState({ contentHeight: this.getHeight() });
   }
@@ -91,8 +120,16 @@ class App extends React.PureComponent {
 
 App.propTypes = {
   actions: PropTypes.object,
+  localStorageUsageInKilobytes: PropTypes.number.isRequired,
 };
 
+function mapStateToProps(state) {
+  const { localStorageUsageInKilobytes } = state;
+  return {
+    localStorageUsageInKilobytes,
+  };
+}
+
 function mapDispatchToProps(dispatch) {
   return {
     actions: bindActionCreators(Actions, dispatch),
@@ -101,6 +138,6 @@ function mapDispatchToProps(dispatch) {
 
 export { App };
 export default connect(
-  null,
+  mapStateToProps,
   mapDispatchToProps,
 )(App);
diff --git a/superset/assets/src/SqlLab/components/SouthPane.jsx b/superset/assets/src/SqlLab/components/SouthPane.jsx
index 68321f3..6da48ab 100644
--- a/superset/assets/src/SqlLab/components/SouthPane.jsx
+++ b/superset/assets/src/SqlLab/components/SouthPane.jsx
@@ -27,7 +27,7 @@ import { t } from '@superset-ui/translation';
 import * as Actions from '../actions/sqlLab';
 import QueryHistory from './QueryHistory';
 import ResultSet from './ResultSet';
-import { STATUS_OPTIONS, STATE_BSSTYLE_MAP } from '../constants';
+import { STATUS_OPTIONS, STATE_BSSTYLE_MAP, LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../constants';
 
 const TAB_HEIGHT = 44;
 
@@ -87,7 +87,8 @@ export class SouthPane extends React.PureComponent {
       latestQuery = props.editorQueries.find(q => q.id === this.props.latestQueryId);
     }
     let results;
-    if (latestQuery) {
+    if (latestQuery &&
+      (Date.now() - latestQuery.startDttm) <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
       results = (
         <ResultSet
           showControls
diff --git a/superset/assets/src/SqlLab/constants.js b/superset/assets/src/SqlLab/constants.js
index 3bf8ce0..c030ce2 100644
--- a/superset/assets/src/SqlLab/constants.js
+++ b/superset/assets/src/SqlLab/constants.js
@@ -43,3 +43,18 @@ export const TIME_OPTIONS = [
   '90 days ago',
   '1 year ago',
 ];
+
+// SqlEditor layout constants
+export const SQL_EDITOR_GUTTER_HEIGHT = 5;
+export const SQL_EDITOR_GUTTER_MARGIN = 3;
+export const SQL_TOOLBAR_HEIGHT = 51;
+
+// kilobyte storage
+export const KB_STORAGE = 1024;
+export const BYTES_PER_CHAR = 2;
+
+// browser's localStorage max usage constants
+export const LOCALSTORAGE_MAX_QUERY_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
+export const LOCALSTORAGE_MAX_USAGE_KB = 5 * 1024; // 5M
+export const LOCALSTORAGE_WARNING_THRESHOLD = 0.9;
+export const LOCALSTORAGE_WARNING_MESSAGE_THROTTLE_MS = 8000; // danger type toast duration
diff --git a/superset/assets/src/SqlLab/reducers/getInitialState.js b/superset/assets/src/SqlLab/reducers/getInitialState.js
index adb3db4..dbeeb18 100644
--- a/superset/assets/src/SqlLab/reducers/getInitialState.js
+++ b/superset/assets/src/SqlLab/reducers/getInitialState.js
@@ -47,6 +47,7 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
     messageToasts: getToastsFromPyFlashMessages(
       (restBootstrapData.common || {}).flash_messages || [],
     ),
+    localStorageUsageInKilobytes: 0,
     common: {
       flash_messages: restBootstrapData.common.flash_messages,
       conf: restBootstrapData.common.conf,
diff --git a/superset/assets/src/SqlLab/reducers/index.js b/superset/assets/src/SqlLab/reducers/index.js
index 9bea4f1..904b306 100644
--- a/superset/assets/src/SqlLab/reducers/index.js
+++ b/superset/assets/src/SqlLab/reducers/index.js
@@ -19,11 +19,13 @@
 import { combineReducers } from 'redux';
 
 import sqlLab from './sqlLab';
+import localStorageUsageInKilobytes from './localStorageUsage';
 import messageToasts from '../../messageToasts/reducers/index';
 import common from './common';
 
 export default combineReducers({
   sqlLab,
+  localStorageUsageInKilobytes,
   messageToasts,
   common,
 });
diff --git a/superset/assets/src/SqlLab/reducers/index.js b/superset/assets/src/SqlLab/reducers/localStorageUsage.js
similarity index 76%
copy from superset/assets/src/SqlLab/reducers/index.js
copy to superset/assets/src/SqlLab/reducers/localStorageUsage.js
index 9bea4f1..eafbb07 100644
--- a/superset/assets/src/SqlLab/reducers/index.js
+++ b/superset/assets/src/SqlLab/reducers/localStorageUsage.js
@@ -16,14 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { combineReducers } from 'redux';
-
-import sqlLab from './sqlLab';
-import messageToasts from '../../messageToasts/reducers/index';
-import common from './common';
-
-export default combineReducers({
-  sqlLab,
-  messageToasts,
-  common,
-});
+export default function localStorageUsageReducer(state = 0) {
+  return state;
+}
diff --git a/superset/assets/src/SqlLab/constants.js b/superset/assets/src/SqlLab/utils/emptyQueryResults.js
similarity index 61%
copy from superset/assets/src/SqlLab/constants.js
copy to superset/assets/src/SqlLab/utils/emptyQueryResults.js
index 3bf8ce0..2798168 100644
--- a/superset/assets/src/SqlLab/constants.js
+++ b/superset/assets/src/SqlLab/utils/emptyQueryResults.js
@@ -16,30 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export const STATE_BSSTYLE_MAP = {
-  offline: 'danger',
-  failed: 'danger',
-  pending: 'info',
-  fetching: 'info',
-  running: 'warning',
-  stopped: 'danger',
-  success: 'success',
-};
+import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../constants';
 
-export const STATUS_OPTIONS = {
-  success: 'success',
-  failed: 'failed',
-  running: 'running',
-  offline: 'offline',
-  pending: 'pending',
-};
+export default function emptyQueryResults(queries) {
+  return Object.keys(queries)
+    .reduce((accu, key) => {
+      const { startDttm, results } = queries[key];
+      const query = {
+        ...queries[key],
+        results: Date.now() - startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS ?
+          {} : results,
+      };
 
-export const TIME_OPTIONS = [
-  'now',
-  '1 hour ago',
-  '1 day ago',
-  '7 days ago',
-  '28 days ago',
-  '90 days ago',
-  '1 year ago',
-];
+      const updatedQueries = {
+        ...accu,
+        [key]: query,
+      };
+      return updatedQueries;
+    }, {});
+}


[incubator-superset] 05/08: [dashboard] click tab anchor link (#7640)

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

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

commit 33d9032b9dd0e3ea725e73b0769ad0d9f4e0e754
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Mon Jun 3 16:57:21 2019 -0700

    [dashboard] click tab anchor link (#7640)
    
    
    (cherry picked from commit f99ae1ad247298c8b8df3721768f69b13152aef0)
---
 superset/assets/src/components/AnchorLink.jsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/superset/assets/src/components/AnchorLink.jsx b/superset/assets/src/components/AnchorLink.jsx
index b33ee60..dac5e25 100644
--- a/superset/assets/src/components/AnchorLink.jsx
+++ b/superset/assets/src/components/AnchorLink.jsx
@@ -66,8 +66,7 @@ class AnchorLink extends React.PureComponent {
   }
 
   handleClickAnchorLink(ev) {
-    ev.preventDefault();
-    history.pushState(null, null, `#${this.props.anchorLinkId}`);
+    ev.stopPropagation();
   }
 
   render() {
@@ -76,6 +75,7 @@ class AnchorLink extends React.PureComponent {
       <span
         className="anchor-link-container"
         id={anchorLinkId}
+        onClick={this.handleClickAnchorLink}
       >
         {showShortLinkButton &&
         <URLShortLinkButton


[incubator-superset] 07/08: [dashboard] pass dashboard filters to share chart url in dropdown (#7642)

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

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

commit 123d5b53669380c1943d898b8973c989561e84b4
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Fri Jun 7 15:22:56 2019 -0700

    [dashboard] pass dashboard filters to share chart url in dropdown (#7642)
    
    
    (cherry picked from commit f3091c525e90526da4bb4e4310c9cf8646830247)
---
 superset/assets/src/chart/Chart.jsx                              | 9 +++++++--
 superset/assets/src/chart/ChartRenderer.jsx                      | 8 ++++----
 .../assets/src/dashboard/components/gridComponents/Chart.jsx     | 2 +-
 superset/assets/src/dashboard/containers/Chart.jsx               | 2 +-
 superset/assets/src/dashboard/reducers/getInitialState.js        | 4 ++++
 5 files changed, 17 insertions(+), 8 deletions(-)

diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index bc19d63..8239370 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -31,7 +31,12 @@ const propTypes = {
   actions: PropTypes.object,
   chartId: PropTypes.number.isRequired,
   datasource: PropTypes.object.isRequired,
-  filters: PropTypes.object,
+  // original selected values for FilterBox viz
+  // so that FilterBox can pre-populate selected values
+  // only affect UI control
+  initialValues: PropTypes.object,
+  // formData contains chart's own filter parameter
+  // and merged with extra filter that current dashboard applying
   formData: PropTypes.object.isRequired,
   height: PropTypes.number,
   width: PropTypes.number,
@@ -56,7 +61,7 @@ const BLANK = {};
 
 const defaultProps = {
   addFilter: () => BLANK,
-  filters: BLANK,
+  initialValues: BLANK,
   setControlValue() {},
   triggerRender: false,
 };
diff --git a/superset/assets/src/chart/ChartRenderer.jsx b/superset/assets/src/chart/ChartRenderer.jsx
index e0a01f1..ccf1b75 100644
--- a/superset/assets/src/chart/ChartRenderer.jsx
+++ b/superset/assets/src/chart/ChartRenderer.jsx
@@ -30,7 +30,7 @@ const propTypes = {
   actions: PropTypes.object,
   chartId: PropTypes.number.isRequired,
   datasource: PropTypes.object.isRequired,
-  filters: PropTypes.object,
+  initialValues: PropTypes.object,
   formData: PropTypes.object.isRequired,
   height: PropTypes.number,
   width: PropTypes.number,
@@ -51,7 +51,7 @@ const BLANK = {};
 
 const defaultProps = {
   addFilter: () => BLANK,
-  filters: BLANK,
+  initialValues: BLANK,
   setControlValue() {},
   triggerRender: false,
 };
@@ -104,7 +104,7 @@ class ChartRenderer extends React.Component {
       height,
       annotationData,
       datasource,
-      filters,
+      initialValues,
       formData,
       queryResponse,
       setControlValue,
@@ -115,7 +115,7 @@ class ChartRenderer extends React.Component {
       height,
       annotationData,
       datasource,
-      filters,
+      filters: initialValues,
       formData,
       onAddFilter: this.handleAddFilter,
       onError: this.handleRenderFailure,
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
index ff11208..4ede139 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -273,7 +273,7 @@ class Chart extends React.Component {
             chartId={id}
             chartStatus={chart.chartStatus}
             datasource={datasource}
-            filters={filters}
+            initialValues={filters[id]}
             formData={formData}
             queryResponse={chart.queryResponse}
             timeout={timeout}
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 5b27b13..71c38e5 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -53,7 +53,7 @@ function mapStateToProps(
       {},
     slice: sliceEntities.slices[id],
     timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    filters: filters[id] || EMPTY_FILTERS,
+    filters: filters || EMPTY_FILTERS,
     // note: this method caches filters if possible to prevent render cascades
     formData: getFormDataWithExtraFilters({
       chart,
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index 44a491c..a9eb68a 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -196,6 +196,10 @@ export default function(bootstrapData) {
     dashboardState: {
       sliceIds: Array.from(sliceIds),
       refresh: false,
+      // All the filter_box's state in this dashboard
+      // When dashboard is first loaded into browser,
+      // its value is from preselect_filters that dashboard owner saved in dashboard's meta data
+      // When user start interacting with dashboard, it will be user picked values from all filter_box
       filters,
       directPathToChild,
       expandedSlices: dashboard.metadata.expanded_slices || {},


[incubator-superset] 01/08: [dashboard] allow user re-order top-level tabs (#7390)

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

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

commit 4a970cfb0cd310d3beb3fadeebfd9c8cb915600e
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue Apr 30 00:40:35 2019 -0700

    [dashboard] allow user re-order top-level tabs (#7390)
    
    
    (cherry picked from commit 9e703f399b78af1a198ba5d69353385b77dd701e)
---
 .../dashboard/actions/dashboardLayout_spec.js      | 67 +++++++++++++++++++++-
 .../src/dashboard/actions/dashboardLayout.js       | 29 +++++++++-
 .../src/dashboard/components/DashboardBuilder.jsx  |  7 ++-
 .../dashboard/components/gridComponents/Tab.jsx    |  5 --
 4 files changed, 95 insertions(+), 13 deletions(-)

diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index d57207d..0dfca9c 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -39,7 +39,10 @@ import {
 } from '../../../../src/dashboard/actions/dashboardLayout';
 
 import { setUnsavedChanges } from '../../../../src/dashboard/actions/dashboardState';
-import { addInfoToast } from '../../../../src/messageToasts/actions';
+import {
+  addWarningToast,
+  ADD_TOAST,
+} from '../../../../src/messageToasts/actions';
 
 import {
   DASHBOARD_GRID_TYPE,
@@ -334,7 +337,9 @@ describe('dashboardLayout actions', () => {
 
       const thunk = handleComponentDrop(dropResult);
       thunk(dispatch, getState);
-      expect(dispatch.getCall(0).args[0].type).toEqual(addInfoToast('').type);
+      expect(dispatch.getCall(0).args[0].type).toEqual(
+        addWarningToast('').type,
+      );
 
       expect(dispatch.callCount).toBe(1);
     });
@@ -402,6 +407,64 @@ describe('dashboardLayout actions', () => {
 
       expect(dispatch.callCount).toBe(2);
     });
+
+    it('should dispatch a toast if drop top-level tab into nested tab', () => {
+      const { getState, dispatch } = setup({
+        dashboardLayout: {
+          present: {
+            [DASHBOARD_ROOT_ID]: {
+              children: ['TABS-ROOT_TABS'],
+              id: DASHBOARD_ROOT_ID,
+              type: 'ROOT',
+            },
+            'TABS-ROOT_TABS': {
+              children: ['TAB-iMppmTOQy', 'TAB-rt1y8cQ6K9', 'TAB-X_pnCIwPN'],
+              id: 'TABS-ROOT_TABS',
+              meta: {},
+              parents: ['ROOT_ID'],
+              type: TABS_TYPE,
+            },
+            'TABS-ROW_TABS': {
+              children: [
+                'TAB-dKIDBT03bQ',
+                'TAB-PtxY5bbTe',
+                'TAB-Wc2P-yGMz',
+                'TAB-U-xe_si7i',
+              ],
+              id: 'TABS-ROW_TABS',
+              meta: {},
+              parents: ['ROOT_ID', 'TABS-ROOT_TABS', 'TAB-X_pnCIwPN'],
+              type: TABS_TYPE,
+            },
+          },
+        },
+      });
+      const dropResult = {
+        source: {
+          id: 'TABS-ROOT_TABS',
+          index: 1,
+          type: TABS_TYPE,
+        },
+        destination: {
+          id: 'TABS-ROW_TABS',
+          index: 1,
+          type: TABS_TYPE,
+        },
+        dragging: {
+          id: 'TAB-rt1y8cQ6K9',
+          meta: { text: 'New Tab' },
+          type: 'TAB',
+        },
+      };
+
+      const thunk1 = handleComponentDrop(dropResult);
+      thunk1(dispatch, getState);
+
+      const thunk2 = dispatch.getCall(0).args[0];
+      thunk2(dispatch, getState);
+
+      expect(dispatch.getCall(1).args[0].type).toEqual(ADD_TOAST);
+    });
   });
 
   describe('undoLayoutAction', () => {
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index 23cb90e..1b5b0c9 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -17,8 +17,9 @@
  * under the License.
  */
 import { ActionCreators as UndoActionCreators } from 'redux-undo';
+import { t } from '@superset-ui/translation';
 
-import { addInfoToast } from '../../messageToasts/actions';
+import { addWarningToast } from '../../messageToasts/actions';
 import { setUnsavedChanges } from './dashboardState';
 import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
 import {
@@ -153,8 +154,10 @@ export function handleComponentDrop(dropResult) {
 
     if (overflowsParent) {
       return dispatch(
-        addInfoToast(
-          `There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
+        addWarningToast(
+          t(
+            `There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
+          ),
         ),
       );
     }
@@ -162,12 +165,30 @@ export function handleComponentDrop(dropResult) {
     const { source, destination } = dropResult;
     const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
     const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
+    const dashboardRoot = getState().dashboardLayout.present[DASHBOARD_ROOT_ID];
+    const rootChildId =
+      dashboardRoot && dashboardRoot.children ? dashboardRoot.children[0] : '';
 
     if (droppedOnRoot) {
       dispatch(createTopLevelTabs(dropResult));
     } else if (destination && isNewComponent) {
       dispatch(createComponent(dropResult));
     } else if (
+      // Add additional allow-to-drop logic for tag/tags source.
+      // We only allow
+      // - top-level tab => top-level tab: rearrange top-level tab order
+      // - nested tab => top-level tab: allow row tab become top-level tab
+      // Dashboard does not allow top-level tab become nested tab, to avoid
+      // nested tab inside nested tab.
+      source.type === TABS_TYPE &&
+      destination.type === TABS_TYPE &&
+      source.id === rootChildId &&
+      destination.id !== rootChildId
+    ) {
+      return dispatch(
+        addWarningToast(t(`Can not move top level tab into nested tabs`)),
+      );
+    } else if (
       destination &&
       source &&
       !// ensure it has moved
@@ -176,6 +197,8 @@ export function handleComponentDrop(dropResult) {
       dispatch(moveComponent(dropResult));
     }
 
+    // call getState() again down here in case redux state is stale after
+    // previous dispatch(es)
     const { dashboardLayout: undoableLayout } = getState();
 
     // if we moved a child from a Tab or Row parent and it was the only child, delete the parent.
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 345807d..f50410a 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -79,10 +79,11 @@ class DashboardBuilder extends React.Component {
     const { dashboardLayout, directPathToChild } = props;
     const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
-    const topLevelTabs =
-      rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
     const tabIndex = findTabIndexByComponentId({
-      currentComponent: topLevelTabs || dashboardLayout[DASHBOARD_ROOT_ID],
+      currentComponent:
+        rootChildId === DASHBOARD_GRID_ID
+          ? dashboardLayout[DASHBOARD_ROOT_ID]
+          : dashboardLayout[rootChildId],
       directPathToChild,
     });
 
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 49a0f18..d441c8e 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -26,7 +26,6 @@ import AnchorLink from '../../../components/AnchorLink';
 import DeleteComponentModal from '../DeleteComponentModal';
 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';
@@ -219,10 +218,6 @@ export default class Tab extends React.PureComponent {
         index={index}
         depth={depth}
         onDrop={this.handleDrop}
-        // 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}
         editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (