You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2021/01/13 17:10:11 UTC

[superset] branch 1.0 updated (90915db -> 4f35234)

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

villebro pushed a change to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git.


    from 90915db  fix(native-filters): incorrect queriesData state (#12409)
     new 35c15b8  refactor: from superset.utils.core break down date_parser (#12408)
     new 507302d  Fixes control panel fields styling (#12236) (#12326)
     new 288f6bb  feat: Resizable dataset and controls panels on Explore view (#12411)
     new 34da995  fix(dashboard): artefacts shown while drag and dropping deck.gl charts (#12418)
     new 3a553c9  bump superset-ui packages for rolling window change (#12426)
     new ea54e0a  chore: bump superset-ui deckgl plugin (#12466)
     new b3e7ef2  fix: do not show vertical scrollbar for charts in dashboard (#12478)
     new b213c1c  fix(dashboard): use datasource id from slice metadata (#12483)
     new 4f35234  fix(timepicker): make pyparsing thread safe (#12489)

The 9 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:
 .../cypress/integration/explore/advanced.test.ts   |  17 +-
 superset-frontend/package-lock.json                | 221 ++++++----
 superset-frontend/package.json                     |  13 +-
 .../explore/components/SelectControl_spec.jsx      |  12 +-
 superset-frontend/src/components/Label/index.tsx   |   2 +-
 .../src/components/Select/SupersetStyledSelect.tsx |   1 +
 superset-frontend/src/components/Select/styles.tsx |  42 +-
 .../src/dashboard/actions/sliceEntities.js         |  22 +-
 .../dashboard/stylesheets/components/chart.less    |   6 -
 .../src/dashboard/stylesheets/dnd.less             |   7 +
 .../src/explore/components/DatasourcePanel.tsx     |  35 +-
 .../src/explore/components/ExploreChartPanel.jsx   | 104 +++--
 .../explore/components/ExploreViewContainer.jsx    |  21 +-
 .../src/explore/components/OptionControls.tsx      |   4 +-
 .../explore/components/controls/SelectControl.jsx  |  20 +-
 .../explore/components/controls/TextControl.tsx    |   2 +-
 .../src/visualizations/FilterBox/FilterBox.jsx     |  15 +-
 .../src/visualizations/FilterBox/transformProps.js |   4 +
 .../stylesheets/less/cosmo/bootswatch.less         |   9 +
 .../stylesheets/less/cosmo/variables.less          |   4 +-
 superset-frontend/stylesheets/less/variables.less  |   2 +-
 superset/charts/commands/importers/v1/__init__.py  |  26 +-
 superset/common/query_object.py                    |   8 +-
 superset/connectors/druid/models.py                |   9 +-
 ...1c4c6_migrate_num_period_compare_and_period_.py |   2 +-
 superset/tasks/cache.py                            |   2 +-
 superset/utils/core.py                             | 445 +------------------
 superset/utils/date_parser.py                      | 472 +++++++++++++++++++++
 superset/views/api.py                              |   2 +-
 superset/viz.py                                    |   9 +-
 superset/viz_sip38.py                              |   9 +-
 tests/charts/commands_tests.py                     |   7 +-
 tests/utils/date_parser_tests.py                   | 263 ++++++++++++
 tests/utils_tests.py                               | 239 -----------
 34 files changed, 1108 insertions(+), 948 deletions(-)
 create mode 100644 superset/utils/date_parser.py
 create mode 100644 tests/utils/date_parser_tests.py


[superset] 05/09: bump superset-ui packages for rolling window change (#12426)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 3a553c9ef22da57d47695f210842beb5d411ff8a
Author: Phillip Kelley-Dotson <pk...@yahoo.com>
AuthorDate: Tue Jan 12 09:50:01 2021 -0800

    bump superset-ui packages for rolling window change (#12426)
---
 superset-frontend/package-lock.json | 30 +++++++++++++++---------------
 superset-frontend/package.json      | 10 +++++-----
 2 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 8461aba..923be2e 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -18843,9 +18843,9 @@
       }
     },
     "@superset-ui/legacy-plugin-chart-partition": {
-      "version": "0.16.4",
-      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.16.4.tgz",
-      "integrity": "sha512-eE9peFQr9ZODaXqgL3EwsIydTB+OszszaoFRTZgYfibUAMghcOre53dbehvtQV/yX8jbJhZ0Sv1Zp1FDpagLew==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.16.6.tgz",
+      "integrity": "sha512-N/JrdGFSWA0tKeUEc8znDkqFBeKWdP6F9dXpCWF1uwsGwxYeSOV+lgrJlCTYYOOFnIGe8s9FijoqScrnWJ0EyA==",
       "requires": {
         "@superset-ui/chart-controls": "0.16.4",
         "@superset-ui/core": "0.16.4",
@@ -18855,9 +18855,9 @@
       }
     },
     "@superset-ui/legacy-plugin-chart-pivot-table": {
-      "version": "0.16.4",
-      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.16.4.tgz",
-      "integrity": "sha512-4NHRkIVNU4sTsgO4SlD0im/16McHZPDf0ppDK3UVbvAKS6W3R4Xh6jSYAaFBB55KPZwQbmT+Qx2cnls17gxwqQ==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.16.6.tgz",
+      "integrity": "sha512-5golzZMM4E1MuAMdPYJeg+6vrDTo1WFbMEBWIvsC4mI0ckzdSF8WW5XFvJ819fQHERICcdpgujYHgiSRm3mXjQ==",
       "requires": {
         "@superset-ui/chart-controls": "0.16.4",
         "@superset-ui/core": "0.16.4",
@@ -18867,9 +18867,9 @@
       }
     },
     "@superset-ui/legacy-plugin-chart-rose": {
-      "version": "0.16.4",
-      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.16.4.tgz",
-      "integrity": "sha512-6G/pB8OE/9bVY92MtumeM2VGgB/orR/BjIh1TmvVhtu1Z5BiwbIgk8d5HRTbOJuX8+E6EXI3vaWmDi6HBYFrcA==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.16.6.tgz",
+      "integrity": "sha512-ngXsW3pM5pcydbMin1P7lHoaZC7k12aMXirUqfv99fkpGIDd8OTdewgE7rPN262MIoPIolny6oGKtWGX+qu28Q==",
       "requires": {
         "@superset-ui/chart-controls": "0.16.4",
         "@superset-ui/core": "0.16.4",
@@ -18952,9 +18952,9 @@
       }
     },
     "@superset-ui/legacy-preset-chart-big-number": {
-      "version": "0.16.4",
-      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.16.4.tgz",
-      "integrity": "sha512-IEWfOPV/w8HTNlc+CNDASsz1yFMXRduOO4fU+3uT0bs+yiJ6WcQXxuCFcH273U1Fn5rJ5pKeLt0ymPGhJJegQg==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.16.6.tgz",
+      "integrity": "sha512-D8gK2vYSEguCT5gK83r/rNN5zCAjDqjfNV54927bORvyOAot8u5UrRRsNb4UD8hmXoC0rGlQ+fFqYnUOJ9tayw==",
       "requires": {
         "@data-ui/xy-chart": "^0.0.84",
         "@superset-ui/chart-controls": "0.16.4",
@@ -18991,9 +18991,9 @@
       }
     },
     "@superset-ui/legacy-preset-chart-nvd3": {
-      "version": "0.16.5",
-      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.16.5.tgz",
-      "integrity": "sha512-MKdC3/n9ZTEU/AmUorKrLW8gHnoyPeSaG4hKxXvX29koq6EJB0Y08xXbTKZjN/S9fsQEZVBGlEdT74PNx8ePTw==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.16.6.tgz",
+      "integrity": "sha512-lDHa0YKDt0kIcbXi6N1qgmW9ZarMpwYsPj/TeEChFd+yuCjhZ4syuRoQF73tmuimR9deeVK7KCGbnjROhvFCbA==",
       "requires": {
         "@data-ui/xy-chart": "^0.0.84",
         "@superset-ui/chart-controls": "0.16.4",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index fbc0224..e91c2bb 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -78,17 +78,17 @@
     "@superset-ui/legacy-plugin-chart-map-box": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.16.4",
-    "@superset-ui/legacy-plugin-chart-partition": "^0.16.4",
-    "@superset-ui/legacy-plugin-chart-pivot-table": "^0.16.4",
-    "@superset-ui/legacy-plugin-chart-rose": "^0.16.4",
+    "@superset-ui/legacy-plugin-chart-partition": "^0.16.6",
+    "@superset-ui/legacy-plugin-chart-pivot-table": "^0.16.6",
+    "@superset-ui/legacy-plugin-chart-rose": "^0.16.6",
     "@superset-ui/legacy-plugin-chart-sankey": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-sunburst": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-treemap": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-world-map": "^0.16.4",
-    "@superset-ui/legacy-preset-chart-big-number": "^0.16.4",
+    "@superset-ui/legacy-preset-chart-big-number": "^0.16.6",
     "@superset-ui/legacy-preset-chart-deckgl": "^0.4.0",
-    "@superset-ui/legacy-preset-chart-nvd3": "^0.16.5",
+    "@superset-ui/legacy-preset-chart-nvd3": "^0.16.6",
     "@superset-ui/plugin-chart-echarts": "^0.16.4",
     "@superset-ui/plugin-chart-table": "^0.16.4",
     "@superset-ui/plugin-chart-word-cloud": "^0.16.4",


[superset] 07/09: fix: do not show vertical scrollbar for charts in dashboard (#12478)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b3e7ef2da64fa4c67e1cc274c778f052c46988c8
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Tue Jan 12 17:16:25 2021 -0800

    fix: do not show vertical scrollbar for charts in dashboard (#12478)
    
    * fix: do not show vertical scrollbar for charts in dashboard
    
    * Proper fix for #11419
    
    Co-authored-by: Jesse Yang <je...@airbnb.com>
---
 .../src/dashboard/stylesheets/components/chart.less       |  6 ------
 .../src/visualizations/FilterBox/FilterBox.jsx            | 15 ++++-----------
 .../src/visualizations/FilterBox/transformProps.js        |  4 ++++
 3 files changed, 8 insertions(+), 17 deletions(-)

diff --git a/superset-frontend/src/dashboard/stylesheets/components/chart.less b/superset-frontend/src/dashboard/stylesheets/components/chart.less
index 9ebb11f..422a7db 100644
--- a/superset-frontend/src/dashboard/stylesheets/components/chart.less
+++ b/superset-frontend/src/dashboard/stylesheets/components/chart.less
@@ -114,12 +114,6 @@
   }
 }
 
-.chart-slice {
-  height: calc(100% - 32px);
-  overflow: auto;
-  height: 100%;
-}
-
 .dot {
   @dot-diameter: 4px;
 
diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
index 4880a38..9fea2c3 100644
--- a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
+++ b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
@@ -22,7 +22,7 @@ import { debounce } from 'lodash';
 import { max as d3Max } from 'd3-array';
 import { AsyncCreatableSelect, CreatableSelect } from 'src/components/Select';
 import Button from 'src/components/Button';
-import { t, styled, SupersetClient } from '@superset-ui/core';
+import { t, SupersetClient } from '@superset-ui/core';
 
 import { BOOL_FALSE_DISPLAY, BOOL_TRUE_DISPLAY } from 'src/constants';
 import FormLabel from 'src/components/FormLabel';
@@ -95,13 +95,6 @@ const defaultProps = {
   instantFiltering: false,
 };
 
-const Styles = styled.div`
-  height: 100%;
-  min-height: 100%;
-  max-height: 100%;
-  overflow: visible;
-`;
-
 class FilterBox extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -427,9 +420,9 @@ class FilterBox extends React.PureComponent {
   }
 
   render() {
-    const { instantFiltering } = this.props;
+    const { instantFiltering, width, height } = this.props;
     return (
-      <Styles>
+      <div style={{ width, height, overflow: 'auto' }}>
         {this.renderDateFilter()}
         {this.renderDatasourceFilters()}
         {this.renderFilters()}
@@ -443,7 +436,7 @@ class FilterBox extends React.PureComponent {
             {t('Apply')}
           </Button>
         )}
-      </Styles>
+      </div>
     );
   }
 }
diff --git a/superset-frontend/src/visualizations/FilterBox/transformProps.js b/superset-frontend/src/visualizations/FilterBox/transformProps.js
index 4652c6d..dda5558 100644
--- a/superset-frontend/src/visualizations/FilterBox/transformProps.js
+++ b/superset-frontend/src/visualizations/FilterBox/transformProps.js
@@ -27,6 +27,8 @@ export default function transformProps(chartProps) {
     queriesData,
     rawDatasource,
     rawFormData,
+    width,
+    height,
   } = chartProps;
   const {
     onAddFilter = NOOP,
@@ -53,6 +55,8 @@ export default function transformProps(chartProps) {
 
   return {
     chartId: sliceId,
+    width,
+    height,
     datasource: rawDatasource,
     filtersChoices: queriesData[0].data,
     filtersFields,


[superset] 08/09: fix(dashboard): use datasource id from slice metadata (#12483)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b213c1cb1f35c1661ad29d7915c5975adbd23de7
Author: Jesse Yang <je...@airbnb.com>
AuthorDate: Wed Jan 13 08:49:51 2021 -0800

    fix(dashboard): use datasource id from slice metadata (#12483)
---
 .../src/dashboard/actions/sliceEntities.js         | 22 +++++++++---------
 superset/charts/commands/importers/v1/__init__.py  | 26 +++++++++++++---------
 tests/charts/commands_tests.py                     |  7 ++++--
 3 files changed, 31 insertions(+), 24 deletions(-)

diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.js b/superset-frontend/src/dashboard/actions/sliceEntities.js
index 69472d9..2a344cb 100644
--- a/superset-frontend/src/dashboard/actions/sliceEntities.js
+++ b/superset-frontend/src/dashboard/actions/sliceEntities.js
@@ -74,17 +74,15 @@ export function fetchAllSlices(userId) {
           const slices = {};
           json.result.forEach(slice => {
             let form_data = JSON.parse(slice.params);
-            let { datasource } = form_data;
-            if (!datasource) {
-              datasource = getDatasourceParameter(
-                slice.datasource_id,
-                slice.datasource_type,
-              );
-              form_data = {
-                ...form_data,
-                datasource,
-              };
-            }
+            form_data = {
+              ...form_data,
+              // force using datasource stored in relational table prop
+              datasource:
+                getDatasourceParameter(
+                  slice.datasource_id,
+                  slice.datasource_type,
+                ) || form_data.datasource,
+            };
             slices[slice.id] = {
               slice_id: slice.id,
               slice_url: slice.url,
@@ -93,6 +91,8 @@ export function fetchAllSlices(userId) {
               form_data,
               datasource_name: slice.datasource_name_text,
               datasource_url: slice.datasource_url,
+              datasource_id: slice.datasource_id,
+              datasource_type: slice.datasource_type,
               changed_on: new Date(slice.changed_on_utc).getTime(),
               description: slice.description,
               description_markdown: slice.description_markeddown,
diff --git a/superset/charts/commands/importers/v1/__init__.py b/superset/charts/commands/importers/v1/__init__.py
index 4b3f443..0e2b5b3 100644
--- a/superset/charts/commands/importers/v1/__init__.py
+++ b/superset/charts/commands/importers/v1/__init__.py
@@ -25,6 +25,7 @@ from superset.charts.commands.importers.v1.utils import import_chart
 from superset.charts.dao import ChartDAO
 from superset.charts.schemas import ImportV1ChartSchema
 from superset.commands.importers.v1 import ImportModelsCommand
+from superset.connectors.sqla.models import SqlaTable
 from superset.databases.commands.importers.v1.utils import import_database
 from superset.databases.schemas import ImportV1DatabaseSchema
 from superset.datasets.commands.importers.v1.utils import import_dataset
@@ -69,7 +70,7 @@ class ImportChartsCommand(ImportModelsCommand):
                 database_ids[str(database.uuid)] = database.id
 
         # import datasets with the correct parent ref
-        dataset_info: Dict[str, Dict[str, Any]] = {}
+        datasets: Dict[str, SqlaTable] = {}
         for file_name, config in configs.items():
             if (
                 file_name.startswith("datasets/")
@@ -77,18 +78,21 @@ class ImportChartsCommand(ImportModelsCommand):
             ):
                 config["database_id"] = database_ids[config["database_uuid"]]
                 dataset = import_dataset(session, config, overwrite=False)
-                dataset_info[str(dataset.uuid)] = {
-                    "datasource_id": dataset.id,
-                    "datasource_type": "view" if dataset.is_sqllab_view else "table",
-                    "datasource_name": dataset.table_name,
-                }
+                datasets[str(dataset.uuid)] = dataset
 
         # import charts with the correct parent ref
         for file_name, config in configs.items():
-            if (
-                file_name.startswith("charts/")
-                and config["dataset_uuid"] in dataset_info
-            ):
+            if file_name.startswith("charts/") and config["dataset_uuid"] in datasets:
                 # update datasource id, type, and name
-                config.update(dataset_info[config["dataset_uuid"]])
+                dataset = datasets[config["dataset_uuid"]]
+                config.update(
+                    {
+                        "datasource_id": dataset.id,
+                        "datasource_type": "view"
+                        if dataset.is_sqllab_view
+                        else "table",
+                        "datasource_name": dataset.table_name,
+                    }
+                )
+                config["params"].update({"datasource": dataset.uid})
                 import_chart(session, config, overwrite=overwrite)
diff --git a/tests/charts/commands_tests.py b/tests/charts/commands_tests.py
index d1e862e..73c01ea 100644
--- a/tests/charts/commands_tests.py
+++ b/tests/charts/commands_tests.py
@@ -140,10 +140,13 @@ class TestImportChartsCommand(SupersetTestCase):
         command = ImportChartsCommand(contents)
         command.run()
 
-        chart = db.session.query(Slice).filter_by(uuid=chart_config["uuid"]).one()
+        chart: Slice = db.session.query(Slice).filter_by(
+            uuid=chart_config["uuid"]
+        ).one()
+        dataset = chart.datasource
         assert json.loads(chart.params) == {
             "color_picker": {"a": 1, "b": 135, "g": 122, "r": 0},
-            "datasource": "12__table",
+            "datasource": dataset.uid,
             "js_columns": ["color"],
             "js_data_mutator": "data => data.map(d => ({\\n    ...d,\\n    color: colors.hexToRGB(d.extraProps.color)\\n}));",
             "js_onclick_href": "",


[superset] 09/09: fix(timepicker): make pyparsing thread safe (#12489)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 4f35234f3e31e7879b3e83faac385c30e3ba93a7
Author: Yongjie Zhao <yo...@gmail.com>
AuthorDate: Wed Jan 13 23:25:58 2021 +0800

    fix(timepicker): make pyparsing thread safe (#12489)
    
    * fix: make pyparsing thread safe
    
    * remove parenthesis for decorator
---
 superset/utils/date_parser.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py
index aee2c83..bd979f3 100644
--- a/superset/utils/date_parser.py
+++ b/superset/utils/date_parser.py
@@ -32,6 +32,7 @@ from pyparsing import (
     Group,
     Optional as ppOptional,
     ParseException,
+    ParserElement,
     ParseResults,
     pyparsing_common,
     quotedString,
@@ -40,6 +41,8 @@ from pyparsing import (
 
 from .core import memoized
 
+ParserElement.enablePackrat()
+
 logger = logging.getLogger(__name__)
 
 
@@ -375,7 +378,7 @@ class EvalHolidayFunc:  # pylint: disable=too-few-public-methods
         raise ValueError(_("Unable to find such a holiday: [{}]").format(holiday))
 
 
-@memoized()
+@memoized
 def datetime_parser() -> ParseResults:  # pylint: disable=too-many-locals
     (  # pylint: disable=invalid-name
         DATETIME,


[superset] 06/09: chore: bump superset-ui deckgl plugin (#12466)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit ea54e0a7cff489df8bfacf6a1b2af049d676b714
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Wed Jan 13 00:10:47 2021 -0800

    chore: bump superset-ui deckgl plugin (#12466)
---
 superset-frontend/package-lock.json | 170 ++++++++++++++++++++----------------
 superset-frontend/package.json      |   2 +-
 2 files changed, 96 insertions(+), 76 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 923be2e..7200319 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -8023,11 +8023,11 @@
       },
       "dependencies": {
         "math.gl": {
-          "version": "3.3.2",
-          "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.3.2.tgz",
-          "integrity": "sha512-33CWhbeiQxl+lzpnKdKijH9mPdZRQUP9TlBILNh6xhnJdMD9D+Kle6Xhfb8dMllE1UUQcPZlQ8/Sd1qSVtNsKQ==",
+          "version": "3.4.1",
+          "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.4.1.tgz",
+          "integrity": "sha512-D33ZXryVFcHu1YJ+fgcNp2MkyK+mEfHesHMdQUZBz2hFqIsAwXovM1sJ+0rTcs8IyTFmuRJ2ayHf1igEJEOM2g==",
           "requires": {
-            "@math.gl/core": "3.3.2"
+            "@math.gl/core": "3.4.1"
           }
         }
       }
@@ -8042,11 +8042,11 @@
       },
       "dependencies": {
         "math.gl": {
-          "version": "3.3.2",
-          "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.3.2.tgz",
-          "integrity": "sha512-33CWhbeiQxl+lzpnKdKijH9mPdZRQUP9TlBILNh6xhnJdMD9D+Kle6Xhfb8dMllE1UUQcPZlQ8/Sd1qSVtNsKQ==",
+          "version": "3.4.1",
+          "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.4.1.tgz",
+          "integrity": "sha512-D33ZXryVFcHu1YJ+fgcNp2MkyK+mEfHesHMdQUZBz2hFqIsAwXovM1sJ+0rTcs8IyTFmuRJ2ayHf1igEJEOM2g==",
           "requires": {
-            "@math.gl/core": "3.3.2"
+            "@math.gl/core": "3.4.1"
           }
         }
       }
@@ -8144,21 +8144,41 @@
       "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="
     },
     "@math.gl/core": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-3.3.2.tgz",
-      "integrity": "sha512-W0QoVrdjiLs52ivtozrHbCitqWGNsWi4TwdfBckhOaeB5cE/R/pyOXfvLmdwGj2b1TLSg8SwgZLlmkWG776GKw==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-3.4.1.tgz",
+      "integrity": "sha512-miAZL/WPU0B5hKrcg1K2nPU2GnOK6X84bwLoD0eTt2n7qT46ffh51Xu21V9kQp/cisE3l1ypukqSV/VHeaNxhQ==",
       "requires": {
-        "@babel/runtime": "^7.0.0",
+        "@babel/runtime": "^7.12.0",
         "gl-matrix": "^3.0.0"
+      },
+      "dependencies": {
+        "@babel/runtime": {
+          "version": "7.12.5",
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
+          "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
+          "requires": {
+            "regenerator-runtime": "^0.13.4"
+          }
+        }
       }
     },
     "@math.gl/web-mercator": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-3.3.2.tgz",
-      "integrity": "sha512-84NNnwP97Xwm1ynZzat1BuEU2uLyIw0gzG5uvGMrAzezny2P38+E2cUK8t9eptKa8gjk2MaC7hhcO0saFCZTHw==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-3.4.1.tgz",
+      "integrity": "sha512-5LAVmo5U25GY5YIxbI3D0J7r97B9AM5pAcWxnF9YhJx44DSVAYfMdiSISOfS+ivKuBFX44mFZvV9j75QY5aDkQ==",
       "requires": {
-        "@babel/runtime": "^7.0.0",
+        "@babel/runtime": "^7.12.0",
         "gl-matrix": "^3.0.0"
+      },
+      "dependencies": {
+        "@babel/runtime": {
+          "version": "7.12.5",
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
+          "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
+          "requires": {
+            "regenerator-runtime": "^0.13.4"
+          }
+        }
       }
     },
     "@mdx-js/loader": {
@@ -18966,9 +18986,9 @@
       }
     },
     "@superset-ui/legacy-preset-chart-deckgl": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.4.0.tgz",
-      "integrity": "sha512-21TR59Ehfo7IqZ5HQUAkUfdo4r797T0RmFLDXMQtktBxfPL4Yul+kFyO+JVBgFVdYAahVNvE9l/yJtSjt1Qr0A==",
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.4.1.tgz",
+      "integrity": "sha512-3THN+WM8HUU1NlV3VNXRVS1j2jH33CmVAdyPNB35RqtwkY+udgFGOxm0lXumcUElht/3ROGMPvcwo1SXijVuLA==",
       "requires": {
         "@math.gl/web-mercator": "^3.2.2",
         "@types/d3-array": "^2.0.0",
@@ -25298,28 +25318,28 @@
           "dependencies": {
             "abbrev": {
               "version": "1.1.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
               "dev": true,
               "optional": true
             },
             "ansi-regex": {
               "version": "2.1.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
               "dev": true,
               "optional": true
             },
             "aproba": {
               "version": "1.2.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
               "dev": true,
               "optional": true
             },
             "are-we-there-yet": {
               "version": "1.1.5",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
               "dev": true,
               "optional": true,
@@ -25330,14 +25350,14 @@
             },
             "balanced-match": {
               "version": "1.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
               "dev": true,
               "optional": true
             },
             "brace-expansion": {
               "version": "1.1.11",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
               "dev": true,
               "optional": true,
@@ -25348,35 +25368,35 @@
             },
             "code-point-at": {
               "version": "1.1.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
               "dev": true,
               "optional": true
             },
             "concat-map": {
               "version": "0.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
               "dev": true,
               "optional": true
             },
             "console-control-strings": {
               "version": "1.1.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
               "dev": true,
               "optional": true
             },
             "core-util-is": {
               "version": "1.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
               "dev": true,
               "optional": true
             },
             "debug": {
               "version": "4.1.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
               "dev": true,
               "optional": true,
@@ -25386,35 +25406,35 @@
             },
             "deep-extend": {
               "version": "0.6.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
               "dev": true,
               "optional": true
             },
             "delegates": {
               "version": "1.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
               "dev": true,
               "optional": true
             },
             "detect-libc": {
               "version": "1.0.3",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
               "dev": true,
               "optional": true
             },
             "fs.realpath": {
               "version": "1.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
               "dev": true,
               "optional": true
             },
             "gauge": {
               "version": "2.7.4",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
               "dev": true,
               "optional": true,
@@ -25431,7 +25451,7 @@
             },
             "glob": {
               "version": "7.1.3",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
               "dev": true,
               "optional": true,
@@ -25446,14 +25466,14 @@
             },
             "has-unicode": {
               "version": "2.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
               "dev": true,
               "optional": true
             },
             "iconv-lite": {
               "version": "0.4.24",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
               "dev": true,
               "optional": true,
@@ -25463,7 +25483,7 @@
             },
             "ignore-walk": {
               "version": "3.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
               "dev": true,
               "optional": true,
@@ -25473,7 +25493,7 @@
             },
             "inflight": {
               "version": "1.0.6",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
               "dev": true,
               "optional": true,
@@ -25484,14 +25504,14 @@
             },
             "inherits": {
               "version": "2.0.3",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
               "dev": true,
               "optional": true
             },
             "is-fullwidth-code-point": {
               "version": "1.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
               "dev": true,
               "optional": true,
@@ -25501,14 +25521,14 @@
             },
             "isarray": {
               "version": "1.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
               "dev": true,
               "optional": true
             },
             "minimatch": {
               "version": "3.0.4",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
               "dev": true,
               "optional": true,
@@ -25525,14 +25545,14 @@
             },
             "ms": {
               "version": "2.1.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
               "dev": true,
               "optional": true
             },
             "needle": {
               "version": "2.3.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
               "dev": true,
               "optional": true,
@@ -25544,7 +25564,7 @@
             },
             "node-pre-gyp": {
               "version": "0.12.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
               "dev": true,
               "optional": true,
@@ -25563,7 +25583,7 @@
             },
             "nopt": {
               "version": "4.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
               "dev": true,
               "optional": true,
@@ -25574,14 +25594,14 @@
             },
             "npm-bundled": {
               "version": "1.0.6",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
               "dev": true,
               "optional": true
             },
             "npm-packlist": {
               "version": "1.4.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
               "dev": true,
               "optional": true,
@@ -25592,7 +25612,7 @@
             },
             "npmlog": {
               "version": "4.1.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
               "dev": true,
               "optional": true,
@@ -25605,21 +25625,21 @@
             },
             "number-is-nan": {
               "version": "1.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
               "dev": true,
               "optional": true
             },
             "object-assign": {
               "version": "4.1.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
               "dev": true,
               "optional": true
             },
             "once": {
               "version": "1.4.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
               "dev": true,
               "optional": true,
@@ -25629,21 +25649,21 @@
             },
             "os-homedir": {
               "version": "1.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
               "dev": true,
               "optional": true
             },
             "os-tmpdir": {
               "version": "1.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
               "dev": true,
               "optional": true
             },
             "osenv": {
               "version": "0.1.5",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
               "dev": true,
               "optional": true,
@@ -25654,21 +25674,21 @@
             },
             "path-is-absolute": {
               "version": "1.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
               "dev": true,
               "optional": true
             },
             "process-nextick-args": {
               "version": "2.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
               "dev": true,
               "optional": true
             },
             "rc": {
               "version": "1.2.8",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
               "dev": true,
               "optional": true,
@@ -25681,7 +25701,7 @@
             },
             "readable-stream": {
               "version": "2.3.6",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
               "dev": true,
               "optional": true,
@@ -25697,7 +25717,7 @@
             },
             "rimraf": {
               "version": "2.6.3",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
               "dev": true,
               "optional": true,
@@ -25707,49 +25727,49 @@
             },
             "safe-buffer": {
               "version": "5.1.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
               "dev": true,
               "optional": true
             },
             "safer-buffer": {
               "version": "2.1.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
               "dev": true,
               "optional": true
             },
             "sax": {
               "version": "1.2.4",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
               "dev": true,
               "optional": true
             },
             "semver": {
               "version": "5.7.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
               "dev": true,
               "optional": true
             },
             "set-blocking": {
               "version": "2.0.0",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
               "dev": true,
               "optional": true
             },
             "signal-exit": {
               "version": "3.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
               "dev": true,
               "optional": true
             },
             "string-width": {
               "version": "1.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
               "dev": true,
               "optional": true,
@@ -25761,7 +25781,7 @@
             },
             "string_decoder": {
               "version": "1.1.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
               "dev": true,
               "optional": true,
@@ -25771,7 +25791,7 @@
             },
             "strip-ansi": {
               "version": "3.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
               "dev": true,
               "optional": true,
@@ -25781,21 +25801,21 @@
             },
             "strip-json-comments": {
               "version": "2.0.1",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
               "dev": true,
               "optional": true
             },
             "util-deprecate": {
               "version": "1.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
               "dev": true,
               "optional": true
             },
             "wide-align": {
               "version": "1.1.3",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
               "dev": true,
               "optional": true,
@@ -25805,7 +25825,7 @@
             },
             "wrappy": {
               "version": "1.0.2",
-              "resolved": false,
+              "resolved": "",
               "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
               "dev": true,
               "optional": true
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index e91c2bb..f409c6f 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -87,7 +87,7 @@
     "@superset-ui/legacy-plugin-chart-treemap": "^0.16.4",
     "@superset-ui/legacy-plugin-chart-world-map": "^0.16.4",
     "@superset-ui/legacy-preset-chart-big-number": "^0.16.6",
-    "@superset-ui/legacy-preset-chart-deckgl": "^0.4.0",
+    "@superset-ui/legacy-preset-chart-deckgl": "^0.4.1",
     "@superset-ui/legacy-preset-chart-nvd3": "^0.16.6",
     "@superset-ui/plugin-chart-echarts": "^0.16.4",
     "@superset-ui/plugin-chart-table": "^0.16.4",


[superset] 01/09: refactor: from superset.utils.core break down date_parser (#12408)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 35c15b8b3a3180a205982f7dc7e5871148f8784e
Author: Yongjie Zhao <yo...@gmail.com>
AuthorDate: Tue Jan 12 06:16:42 2021 +0800

    refactor: from superset.utils.core break down date_parser (#12408)
---
 superset/common/query_object.py                    |   8 +-
 superset/connectors/druid/models.py                |   9 +-
 ...1c4c6_migrate_num_period_compare_and_period_.py |   2 +-
 superset/tasks/cache.py                            |   2 +-
 superset/utils/core.py                             | 445 +------------------
 superset/utils/date_parser.py                      | 469 +++++++++++++++++++++
 superset/views/api.py                              |   2 +-
 superset/viz.py                                    |   9 +-
 superset/viz_sip38.py                              |   9 +-
 tests/utils/date_parser_tests.py                   | 263 ++++++++++++
 tests/utils_tests.py                               | 239 -----------
 11 files changed, 753 insertions(+), 704 deletions(-)

diff --git a/superset/common/query_object.py b/superset/common/query_object.py
index 7aa7ef7..43f7fee 100644
--- a/superset/common/query_object.py
+++ b/superset/common/query_object.py
@@ -28,12 +28,8 @@ from superset import app, is_feature_enabled
 from superset.exceptions import QueryObjectValidationError
 from superset.typing import Metric
 from superset.utils import pandas_postprocessing
-from superset.utils.core import (
-    DTTM_ALIAS,
-    get_since_until,
-    json_int_dttm_ser,
-    parse_human_timedelta,
-)
+from superset.utils.core import DTTM_ALIAS, json_int_dttm_ser
+from superset.utils.date_parser import get_since_until, parse_human_timedelta
 from superset.views.utils import get_time_range_endpoints
 
 config = app.config
diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py
index fe15b24..2a7e0f9 100644
--- a/superset/connectors/druid/models.py
+++ b/superset/connectors/druid/models.py
@@ -57,6 +57,7 @@ from superset.models.core import Database
 from superset.models.helpers import AuditMixinNullable, ImportExportMixin, QueryResult
 from superset.typing import FilterValues, Granularity, Metric, QueryObjectDict
 from superset.utils import core as utils
+from superset.utils.date_parser import parse_human_datetime, parse_human_timedelta
 
 try:
     import requests
@@ -777,7 +778,7 @@ class DruidDatasource(Model, BaseDatasource):
             granularity["timeZone"] = timezone
 
         if origin:
-            dttm = utils.parse_human_datetime(origin)
+            dttm = parse_human_datetime(origin)
             assert dttm
             granularity["origin"] = dttm.isoformat()
 
@@ -795,7 +796,7 @@ class DruidDatasource(Model, BaseDatasource):
         else:
             granularity["type"] = "duration"
             granularity["duration"] = (
-                utils.parse_human_timedelta(period_name).total_seconds()  # type: ignore
+                parse_human_timedelta(period_name).total_seconds()  # type: ignore
                 * 1000
             )
         return granularity
@@ -938,7 +939,7 @@ class DruidDatasource(Model, BaseDatasource):
         )
         # TODO: Use Lexicographic TopNMetricSpec once supported by PyDruid
         if self.fetch_values_from:
-            from_dttm = utils.parse_human_datetime(self.fetch_values_from)
+            from_dttm = parse_human_datetime(self.fetch_values_from)
             assert from_dttm
         else:
             from_dttm = datetime(1970, 1, 1)
@@ -1426,7 +1427,7 @@ class DruidDatasource(Model, BaseDatasource):
         time_offset = DruidDatasource.time_offset(query_obj["granularity"])
 
         def increment_timestamp(ts: str) -> datetime:
-            dt = utils.parse_human_datetime(ts).replace(tzinfo=DRUID_TZ)
+            dt = parse_human_datetime(ts).replace(tzinfo=DRUID_TZ)
             return dt + timedelta(milliseconds=time_offset)
 
         if DTTM_ALIAS in df.columns and time_offset:
diff --git a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py
index ca4de4e..1d0d81f 100644
--- a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py
+++ b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py
@@ -33,7 +33,7 @@ from sqlalchemy import Column, Integer, String, Text
 from sqlalchemy.ext.declarative import declarative_base
 
 from superset import db
-from superset.utils.core import parse_human_timedelta
+from superset.utils.date_parser import parse_human_timedelta
 
 revision = "3dda56f1c4c6"
 down_revision = "bddc498dd179"
diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py
index 953ed31..e32467d 100644
--- a/superset/tasks/cache.py
+++ b/superset/tasks/cache.py
@@ -31,7 +31,7 @@ from superset.models.core import Log
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
 from superset.models.tags import Tag, TaggedObject
-from superset.utils.core import parse_human_datetime
+from superset.utils.date_parser import parse_human_datetime
 from superset.views.utils import build_extra_filters
 
 logger = get_task_logger(__name__)
diff --git a/superset/utils/core.py b/superset/utils/core.py
index c500c19..7219317 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -15,7 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 """Utility functions used across Superset"""
-import calendar
 import decimal
 import errno
 import functools
@@ -39,7 +38,6 @@ from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from email.utils import formatdate
 from enum import Enum
-from time import struct_time
 from timeit import default_timer
 from types import TracebackType
 from typing import (
@@ -65,29 +63,14 @@ import bleach
 import markdown as md
 import numpy as np
 import pandas as pd
-import parsedatetime
 import sqlalchemy as sa
 from cryptography import x509
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.backends.openssl.x509 import _Certificate
-from dateutil.parser import parse
-from dateutil.relativedelta import relativedelta
 from flask import current_app, flash, g, Markup, render_template
 from flask_appbuilder import SQLA
 from flask_appbuilder.security.sqla.models import Role, User
-from flask_babel import gettext as __, lazy_gettext as _
-from holidays import CountryHoliday
-from pyparsing import (
-    CaselessKeyword,
-    Forward,
-    Group,
-    Optional as ppOptional,
-    ParseException,
-    ParseResults,
-    pyparsing_common,
-    quotedString,
-    Suppress,
-)
+from flask_babel import gettext as __
 from sqlalchemy import event, exc, select, Text
 from sqlalchemy.dialects.mysql import MEDIUMTEXT
 from sqlalchemy.engine import Connection, Engine
@@ -443,58 +426,6 @@ def list_minus(l: List[Any], minus: List[Any]) -> List[Any]:
     return [o for o in l if o not in minus]
 
 
-def parse_human_datetime(human_readable: str) -> datetime:
-    """
-    Returns ``datetime.datetime`` from human readable strings
-
-    >>> from datetime import date, timedelta
-    >>> from dateutil.relativedelta import relativedelta
-    >>> parse_human_datetime('2015-04-03')
-    datetime.datetime(2015, 4, 3, 0, 0)
-    >>> parse_human_datetime('2/3/1969')
-    datetime.datetime(1969, 2, 3, 0, 0)
-    >>> parse_human_datetime('now') <= datetime.now()
-    True
-    >>> parse_human_datetime('yesterday') <= datetime.now()
-    True
-    >>> date.today() - timedelta(1) == parse_human_datetime('yesterday').date()
-    True
-    >>> year_ago_1 = parse_human_datetime('one year ago').date()
-    >>> year_ago_2 = (datetime.now() - relativedelta(years=1)).date()
-    >>> year_ago_1 == year_ago_2
-    True
-    >>> year_after_1 = parse_human_datetime('2 years after').date()
-    >>> year_after_2 = (datetime.now() + relativedelta(years=2)).date()
-    >>> year_after_1 == year_after_2
-    True
-    """
-    try:
-        dttm = parse(human_readable)
-    except Exception:  # pylint: disable=broad-except
-        try:
-            cal = parsedatetime.Calendar()
-            parsed_dttm, parsed_flags = cal.parseDT(human_readable)
-            # when time is not extracted, we 'reset to midnight'
-            if parsed_flags & 2 == 0:
-                parsed_dttm = parsed_dttm.replace(hour=0, minute=0, second=0)
-            dttm = dttm_from_timetuple(parsed_dttm.utctimetuple())
-        except Exception as ex:
-            logger.exception(ex)
-            raise ValueError("Couldn't parse date string [{}]".format(human_readable))
-    return dttm
-
-
-def dttm_from_timetuple(date_: struct_time) -> datetime:
-    return datetime(
-        date_.tm_year,
-        date_.tm_mon,
-        date_.tm_mday,
-        date_.tm_hour,
-        date_.tm_min,
-        date_.tm_sec,
-    )
-
-
 def md5_hex(data: str) -> str:
     return hashlib.md5(data.encode()).hexdigest()
 
@@ -516,39 +447,6 @@ class DashboardEncoder(json.JSONEncoder):
             return json.JSONEncoder(sort_keys=True).default(o)
 
 
-def parse_human_timedelta(
-    human_readable: Optional[str], source_time: Optional[datetime] = None,
-) -> timedelta:
-    """
-    Returns ``datetime.timedelta`` from natural language time deltas
-
-    >>> parse_human_timedelta('1 day') == timedelta(days=1)
-    True
-    """
-    cal = parsedatetime.Calendar()
-    source_dttm = dttm_from_timetuple(
-        source_time.timetuple() if source_time else datetime.now().timetuple()
-    )
-    modified_dttm = dttm_from_timetuple(cal.parse(human_readable or "", source_dttm)[0])
-    return modified_dttm - source_dttm
-
-
-def parse_past_timedelta(
-    delta_str: str, source_time: Optional[datetime] = None
-) -> timedelta:
-    """
-    Takes a delta like '1 year' and finds the timedelta for that period in
-    the past, then represents that past timedelta in positive terms.
-
-    parse_human_timedelta('1 year') find the timedelta 1 year in the future.
-    parse_past_timedelta('1 year') returns -datetime.timedelta(-365)
-    or datetime.timedelta(365).
-    """
-    return -parse_human_timedelta(
-        delta_str if delta_str.startswith("-") else f"-{delta_str}", source_time,
-    )
-
-
 class JSONEncodedDict(TypeDecorator):  # pylint: disable=abstract-method
     """Represents an immutable structure as a json-encoded string."""
 
@@ -1254,347 +1152,6 @@ def ensure_path_exists(path: str) -> None:
             raise
 
 
-class EvalText:  # pylint: disable=too-few-public-methods
-    def __init__(self, tokens: ParseResults) -> None:
-        self.value = tokens[0]
-
-    def eval(self) -> str:
-        # strip quotes
-        return self.value[1:-1]
-
-
-class EvalDateTimeFunc:  # pylint: disable=too-few-public-methods
-    def __init__(self, tokens: ParseResults) -> None:
-        self.value = tokens[1]
-
-    def eval(self) -> datetime:
-        return parse_human_datetime(self.value.eval())
-
-
-class EvalDateAddFunc:  # pylint: disable=too-few-public-methods
-    def __init__(self, tokens: ParseResults) -> None:
-        self.value = tokens[1]
-
-    def eval(self) -> datetime:
-        dttm_expression, delta, unit = self.value
-        dttm = dttm_expression.eval()
-        if unit.lower() == "quarter":
-            delta = delta * 3
-            unit = "month"
-        return dttm + parse_human_timedelta(f"{delta} {unit}s", dttm)
-
-
-class EvalDateTruncFunc:  # pylint: disable=too-few-public-methods
-    def __init__(self, tokens: ParseResults) -> None:
-        self.value = tokens[1]
-
-    def eval(self) -> datetime:
-        dttm_expression, unit = self.value
-        dttm = dttm_expression.eval()
-        if unit == "year":
-            dttm = dttm.replace(
-                month=1, day=1, hour=0, minute=0, second=0, microsecond=0
-            )
-        elif unit == "month":
-            dttm = dttm.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
-        elif unit == "week":
-            dttm = dttm - relativedelta(days=dttm.weekday())
-            dttm = dttm.replace(hour=0, minute=0, second=0, microsecond=0)
-        elif unit == "day":
-            dttm = dttm.replace(hour=0, minute=0, second=0, microsecond=0)
-        elif unit == "hour":
-            dttm = dttm.replace(minute=0, second=0, microsecond=0)
-        elif unit == "minute":
-            dttm = dttm.replace(second=0, microsecond=0)
-        else:
-            dttm = dttm.replace(microsecond=0)
-        return dttm
-
-
-class EvalLastDayFunc:  # pylint: disable=too-few-public-methods
-    def __init__(self, tokens: ParseResults) -> None:
-        self.value = tokens[1]
-
-    def eval(self) -> datetime:
-        dttm_expression, unit = self.value
-        dttm = dttm_expression.eval()
-        if unit == "year":
-            return dttm.replace(
-                month=12, day=31, hour=0, minute=0, second=0, microsecond=0
-            )
-        if unit == "month":
-            return dttm.replace(
-                day=calendar.monthrange(dttm.year, dttm.month)[1],
-                hour=0,
-                minute=0,
-                second=0,
-                microsecond=0,
-            )
-        # unit == "week":
-        mon = dttm - relativedelta(days=dttm.weekday())
-        mon = mon.replace(hour=0, minute=0, second=0, microsecond=0)
-        return mon + relativedelta(days=6)
-
-
-class EvalHolidayFunc:  # pylint: disable=too-few-public-methods
-    def __init__(self, tokens: ParseResults) -> None:
-        self.value = tokens[1]
-
-    def eval(self) -> datetime:
-        holiday = self.value[0].eval()
-        dttm, country = [None, None]
-        if len(self.value) >= 2:
-            dttm = self.value[1].eval()
-        if len(self.value) == 3:
-            country = self.value[2]
-        holiday_year = dttm.year if dttm else parse_human_datetime("today").year
-        country = country.eval() if country else "US"
-
-        holiday_lookup = CountryHoliday(country, years=[holiday_year], observed=False)
-        searched_result = holiday_lookup.get_named(holiday)
-        if len(searched_result) == 1:
-            return dttm_from_timetuple(searched_result[0].timetuple())
-        raise ValueError(_("Unable to find such a holiday: [{}]").format(holiday))
-
-
-@memoized()
-def datetime_parser() -> ParseResults:  # pylint: disable=too-many-locals
-    (  # pylint: disable=invalid-name
-        DATETIME,
-        DATEADD,
-        DATETRUNC,
-        LASTDAY,
-        HOLIDAY,
-        YEAR,
-        QUARTER,
-        MONTH,
-        WEEK,
-        DAY,
-        HOUR,
-        MINUTE,
-        SECOND,
-    ) = map(
-        CaselessKeyword,
-        "datetime dateadd datetrunc lastday holiday "
-        "year quarter month week day hour minute second".split(),
-    )
-    lparen, rparen, comma = map(Suppress, "(),")
-    int_operand = pyparsing_common.signed_integer().setName("int_operand")
-    text_operand = quotedString.setName("text_operand").setParseAction(EvalText)
-
-    # allow expression to be used recursively
-    datetime_func = Forward().setName("datetime")
-    dateadd_func = Forward().setName("dateadd")
-    datetrunc_func = Forward().setName("datetrunc")
-    lastday_func = Forward().setName("lastday")
-    holiday_func = Forward().setName("holiday")
-    date_expr = (
-        datetime_func | dateadd_func | datetrunc_func | lastday_func | holiday_func
-    )
-
-    datetime_func <<= (DATETIME + lparen + text_operand + rparen).setParseAction(
-        EvalDateTimeFunc
-    )
-    dateadd_func <<= (
-        DATEADD
-        + lparen
-        + Group(
-            date_expr
-            + comma
-            + int_operand
-            + comma
-            + (YEAR | QUARTER | MONTH | WEEK | DAY | HOUR | MINUTE | SECOND)
-            + ppOptional(comma)
-        )
-        + rparen
-    ).setParseAction(EvalDateAddFunc)
-    datetrunc_func <<= (
-        DATETRUNC
-        + lparen
-        + Group(
-            date_expr
-            + comma
-            + (YEAR | MONTH | WEEK | DAY | HOUR | MINUTE | SECOND)
-            + ppOptional(comma)
-        )
-        + rparen
-    ).setParseAction(EvalDateTruncFunc)
-    lastday_func <<= (
-        LASTDAY
-        + lparen
-        + Group(date_expr + comma + (YEAR | MONTH | WEEK) + ppOptional(comma))
-        + rparen
-    ).setParseAction(EvalLastDayFunc)
-    holiday_func <<= (
-        HOLIDAY
-        + lparen
-        + Group(
-            text_operand
-            + ppOptional(comma)
-            + ppOptional(date_expr)
-            + ppOptional(comma)
-            + ppOptional(text_operand)
-            + ppOptional(comma)
-        )
-        + rparen
-    ).setParseAction(EvalHolidayFunc)
-
-    return date_expr
-
-
-def datetime_eval(datetime_expression: Optional[str] = None) -> Optional[datetime]:
-    if datetime_expression:
-        try:
-            return datetime_parser().parseString(datetime_expression)[0].eval()
-        except ParseException as error:
-            raise ValueError(error)
-    return None
-
-
-# pylint: disable=too-many-arguments, too-many-locals, too-many-branches
-def get_since_until(
-    time_range: Optional[str] = None,
-    since: Optional[str] = None,
-    until: Optional[str] = None,
-    time_shift: Optional[str] = None,
-    relative_start: Optional[str] = None,
-    relative_end: Optional[str] = None,
-) -> Tuple[Optional[datetime], Optional[datetime]]:
-    """Return `since` and `until` date time tuple from string representations of
-    time_range, since, until and time_shift.
-
-    This functiom supports both reading the keys separately (from `since` and
-    `until`), as well as the new `time_range` key. Valid formats are:
-
-        - ISO 8601
-        - X days/years/hours/day/year/weeks
-        - X days/years/hours/day/year/weeks ago
-        - X days/years/hours/day/year/weeks from now
-        - freeform
-
-    Additionally, for `time_range` (these specify both `since` and `until`):
-
-        - Last day
-        - Last week
-        - Last month
-        - Last quarter
-        - Last year
-        - No filter
-        - Last X seconds/minutes/hours/days/weeks/months/years
-        - Next X seconds/minutes/hours/days/weeks/months/years
-
-    """
-    separator = " : "
-    _relative_start = relative_start if relative_start else "today"
-    _relative_end = relative_end if relative_end else "today"
-
-    if time_range == "No filter":
-        return None, None
-
-    if time_range and time_range.startswith("Last") and separator not in time_range:
-        time_range = time_range + separator + _relative_end
-
-    if time_range and time_range.startswith("Next") and separator not in time_range:
-        time_range = _relative_start + separator + time_range
-
-    if (
-        time_range
-        and time_range.startswith("previous calendar week")
-        and separator not in time_range
-    ):
-        time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, WEEK), WEEK) : DATETRUNC(DATETIME('today'), WEEK)"  # pylint: disable=line-too-long
-    if (
-        time_range
-        and time_range.startswith("previous calendar month")
-        and separator not in time_range
-    ):
-        time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, MONTH), MONTH) : DATETRUNC(DATETIME('today'), MONTH)"  # pylint: disable=line-too-long
-    if (
-        time_range
-        and time_range.startswith("previous calendar year")
-        and separator not in time_range
-    ):
-        time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, YEAR), YEAR) : DATETRUNC(DATETIME('today'), YEAR)"  # pylint: disable=line-too-long
-
-    if time_range and separator in time_range:
-        time_range_lookup = [
-            (
-                r"^last\s+(day|week|month|quarter|year)$",
-                lambda unit: f"DATEADD(DATETIME('{_relative_start}'), -1, {unit})",
-            ),
-            (
-                r"^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s$",
-                lambda delta, unit: f"DATEADD(DATETIME('{_relative_start}'), -{int(delta)}, {unit})",  # pylint: disable=line-too-long
-            ),
-            (
-                r"^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s$",
-                lambda delta, unit: f"DATEADD(DATETIME('{_relative_end}'), {int(delta)}, {unit})",  # pylint: disable=line-too-long
-            ),
-            (
-                r"^(DATETIME.*|DATEADD.*|DATETRUNC.*|LASTDAY.*|HOLIDAY.*)$",
-                lambda text: text,
-            ),
-        ]
-
-        since_and_until_partition = [_.strip() for _ in time_range.split(separator, 1)]
-        since_and_until: List[Optional[str]] = []
-        for part in since_and_until_partition:
-            if not part:
-                # if since or until is "", set as None
-                since_and_until.append(None)
-                continue
-
-            # Is it possible to match to time_range_lookup
-            matched = False
-            for pattern, fn in time_range_lookup:
-                result = re.search(pattern, part, re.IGNORECASE)
-                if result:
-                    matched = True
-                    # converted matched time_range to "formal time expressions"
-                    since_and_until.append(fn(*result.groups()))  # type: ignore
-            if not matched:
-                # default matched case
-                since_and_until.append(f"DATETIME('{part}')")
-
-        _since, _until = map(datetime_eval, since_and_until)
-    else:
-        since = since or ""
-        if since:
-            since = add_ago_to_since(since)
-        _since = parse_human_datetime(since) if since else None
-        _until = (
-            parse_human_datetime(until)
-            if until
-            else parse_human_datetime(_relative_end)
-        )
-
-    if time_shift:
-        time_delta = parse_past_timedelta(time_shift)
-        _since = _since if _since is None else (_since - time_delta)
-        _until = _until if _until is None else (_until - time_delta)
-
-    if _since and _until and _since > _until:
-        raise ValueError(_("From date cannot be larger than to date"))
-
-    return _since, _until
-
-
-def add_ago_to_since(since: str) -> str:
-    """
-    Backwards compatibility hack. Without this slices with since: 7 days will
-    be treated as 7 days in the future.
-
-    :param str since:
-    :returns: Since with ago added if necessary
-    :rtype: str
-    """
-    since_words = since.split(" ")
-    grains = ["days", "years", "hours", "day", "year", "weeks"]
-    if len(since_words) == 2 and since_words[1] in grains:
-        since += " ago"
-    return since
-
-
 def convert_legacy_filters_into_adhoc(  # pylint: disable=invalid-name
     form_data: FormData,
 ) -> None:
diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py
new file mode 100644
index 0000000..aee2c83
--- /dev/null
+++ b/superset/utils/date_parser.py
@@ -0,0 +1,469 @@
+# 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 calendar
+import logging
+import re
+from datetime import datetime, timedelta
+from time import struct_time
+from typing import List, Optional, Tuple
+
+import parsedatetime
+from dateutil.parser import parse
+from dateutil.relativedelta import relativedelta
+from flask_babel import lazy_gettext as _
+from holidays import CountryHoliday
+from pyparsing import (
+    CaselessKeyword,
+    Forward,
+    Group,
+    Optional as ppOptional,
+    ParseException,
+    ParseResults,
+    pyparsing_common,
+    quotedString,
+    Suppress,
+)
+
+from .core import memoized
+
+logger = logging.getLogger(__name__)
+
+
+def parse_human_datetime(human_readable: str) -> datetime:
+    """
+    Returns ``datetime.datetime`` from human readable strings
+
+    >>> from datetime import date, timedelta
+    >>> from dateutil.relativedelta import relativedelta
+    >>> parse_human_datetime('2015-04-03')
+    datetime.datetime(2015, 4, 3, 0, 0)
+    >>> parse_human_datetime('2/3/1969')
+    datetime.datetime(1969, 2, 3, 0, 0)
+    >>> parse_human_datetime('now') <= datetime.now()
+    True
+    >>> parse_human_datetime('yesterday') <= datetime.now()
+    True
+    >>> date.today() - timedelta(1) == parse_human_datetime('yesterday').date()
+    True
+    >>> year_ago_1 = parse_human_datetime('one year ago').date()
+    >>> year_ago_2 = (datetime.now() - relativedelta(years=1)).date()
+    >>> year_ago_1 == year_ago_2
+    True
+    >>> year_after_1 = parse_human_datetime('2 years after').date()
+    >>> year_after_2 = (datetime.now() + relativedelta(years=2)).date()
+    >>> year_after_1 == year_after_2
+    True
+    """
+    try:
+        dttm = parse(human_readable)
+    except Exception:  # pylint: disable=broad-except
+        try:
+            cal = parsedatetime.Calendar()
+            parsed_dttm, parsed_flags = cal.parseDT(human_readable)
+            # when time is not extracted, we 'reset to midnight'
+            if parsed_flags & 2 == 0:
+                parsed_dttm = parsed_dttm.replace(hour=0, minute=0, second=0)
+            dttm = dttm_from_timetuple(parsed_dttm.utctimetuple())
+        except Exception as ex:
+            logger.exception(ex)
+            raise ValueError("Couldn't parse date string [{}]".format(human_readable))
+    return dttm
+
+
+def dttm_from_timetuple(date_: struct_time) -> datetime:
+    return datetime(
+        date_.tm_year,
+        date_.tm_mon,
+        date_.tm_mday,
+        date_.tm_hour,
+        date_.tm_min,
+        date_.tm_sec,
+    )
+
+
+def parse_human_timedelta(
+    human_readable: Optional[str], source_time: Optional[datetime] = None,
+) -> timedelta:
+    """
+    Returns ``datetime.timedelta`` from natural language time deltas
+
+    >>> parse_human_timedelta('1 day') == timedelta(days=1)
+    True
+    """
+    cal = parsedatetime.Calendar()
+    source_dttm = dttm_from_timetuple(
+        source_time.timetuple() if source_time else datetime.now().timetuple()
+    )
+    modified_dttm = dttm_from_timetuple(cal.parse(human_readable or "", source_dttm)[0])
+    return modified_dttm - source_dttm
+
+
+def parse_past_timedelta(
+    delta_str: str, source_time: Optional[datetime] = None
+) -> timedelta:
+    """
+    Takes a delta like '1 year' and finds the timedelta for that period in
+    the past, then represents that past timedelta in positive terms.
+
+    parse_human_timedelta('1 year') find the timedelta 1 year in the future.
+    parse_past_timedelta('1 year') returns -datetime.timedelta(-365)
+    or datetime.timedelta(365).
+    """
+    return -parse_human_timedelta(
+        delta_str if delta_str.startswith("-") else f"-{delta_str}", source_time,
+    )
+
+
+# pylint: disable=too-many-arguments, too-many-locals, too-many-branches
+def get_since_until(
+    time_range: Optional[str] = None,
+    since: Optional[str] = None,
+    until: Optional[str] = None,
+    time_shift: Optional[str] = None,
+    relative_start: Optional[str] = None,
+    relative_end: Optional[str] = None,
+) -> Tuple[Optional[datetime], Optional[datetime]]:
+    """Return `since` and `until` date time tuple from string representations of
+    time_range, since, until and time_shift.
+
+    This functiom supports both reading the keys separately (from `since` and
+    `until`), as well as the new `time_range` key. Valid formats are:
+
+        - ISO 8601
+        - X days/years/hours/day/year/weeks
+        - X days/years/hours/day/year/weeks ago
+        - X days/years/hours/day/year/weeks from now
+        - freeform
+
+    Additionally, for `time_range` (these specify both `since` and `until`):
+
+        - Last day
+        - Last week
+        - Last month
+        - Last quarter
+        - Last year
+        - No filter
+        - Last X seconds/minutes/hours/days/weeks/months/years
+        - Next X seconds/minutes/hours/days/weeks/months/years
+
+    """
+    separator = " : "
+    _relative_start = relative_start if relative_start else "today"
+    _relative_end = relative_end if relative_end else "today"
+
+    if time_range == "No filter":
+        return None, None
+
+    if time_range and time_range.startswith("Last") and separator not in time_range:
+        time_range = time_range + separator + _relative_end
+
+    if time_range and time_range.startswith("Next") and separator not in time_range:
+        time_range = _relative_start + separator + time_range
+
+    if (
+        time_range
+        and time_range.startswith("previous calendar week")
+        and separator not in time_range
+    ):
+        time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, WEEK), WEEK) : DATETRUNC(DATETIME('today'), WEEK)"  # pylint: disable=line-too-long
+    if (
+        time_range
+        and time_range.startswith("previous calendar month")
+        and separator not in time_range
+    ):
+        time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, MONTH), MONTH) : DATETRUNC(DATETIME('today'), MONTH)"  # pylint: disable=line-too-long
+    if (
+        time_range
+        and time_range.startswith("previous calendar year")
+        and separator not in time_range
+    ):
+        time_range = "DATETRUNC(DATEADD(DATETIME('today'), -1, YEAR), YEAR) : DATETRUNC(DATETIME('today'), YEAR)"  # pylint: disable=line-too-long
+
+    if time_range and separator in time_range:
+        time_range_lookup = [
+            (
+                r"^last\s+(day|week|month|quarter|year)$",
+                lambda unit: f"DATEADD(DATETIME('{_relative_start}'), -1, {unit})",
+            ),
+            (
+                r"^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s$",
+                lambda delta, unit: f"DATEADD(DATETIME('{_relative_start}'), -{int(delta)}, {unit})",  # pylint: disable=line-too-long
+            ),
+            (
+                r"^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s$",
+                lambda delta, unit: f"DATEADD(DATETIME('{_relative_end}'), {int(delta)}, {unit})",  # pylint: disable=line-too-long
+            ),
+            (
+                r"^(DATETIME.*|DATEADD.*|DATETRUNC.*|LASTDAY.*|HOLIDAY.*)$",
+                lambda text: text,
+            ),
+        ]
+
+        since_and_until_partition = [_.strip() for _ in time_range.split(separator, 1)]
+        since_and_until: List[Optional[str]] = []
+        for part in since_and_until_partition:
+            if not part:
+                # if since or until is "", set as None
+                since_and_until.append(None)
+                continue
+
+            # Is it possible to match to time_range_lookup
+            matched = False
+            for pattern, fn in time_range_lookup:
+                result = re.search(pattern, part, re.IGNORECASE)
+                if result:
+                    matched = True
+                    # converted matched time_range to "formal time expressions"
+                    since_and_until.append(fn(*result.groups()))  # type: ignore
+            if not matched:
+                # default matched case
+                since_and_until.append(f"DATETIME('{part}')")
+
+        _since, _until = map(datetime_eval, since_and_until)
+    else:
+        since = since or ""
+        if since:
+            since = add_ago_to_since(since)
+        _since = parse_human_datetime(since) if since else None
+        _until = (
+            parse_human_datetime(until)
+            if until
+            else parse_human_datetime(_relative_end)
+        )
+
+    if time_shift:
+        time_delta = parse_past_timedelta(time_shift)
+        _since = _since if _since is None else (_since - time_delta)
+        _until = _until if _until is None else (_until - time_delta)
+
+    if _since and _until and _since > _until:
+        raise ValueError(_("From date cannot be larger than to date"))
+
+    return _since, _until
+
+
+def add_ago_to_since(since: str) -> str:
+    """
+    Backwards compatibility hack. Without this slices with since: 7 days will
+    be treated as 7 days in the future.
+
+    :param str since:
+    :returns: Since with ago added if necessary
+    :rtype: str
+    """
+    since_words = since.split(" ")
+    grains = ["days", "years", "hours", "day", "year", "weeks"]
+    if len(since_words) == 2 and since_words[1] in grains:
+        since += " ago"
+    return since
+
+
+class EvalText:  # pylint: disable=too-few-public-methods
+    def __init__(self, tokens: ParseResults) -> None:
+        self.value = tokens[0]
+
+    def eval(self) -> str:
+        # strip quotes
+        return self.value[1:-1]
+
+
+class EvalDateTimeFunc:  # pylint: disable=too-few-public-methods
+    def __init__(self, tokens: ParseResults) -> None:
+        self.value = tokens[1]
+
+    def eval(self) -> datetime:
+        return parse_human_datetime(self.value.eval())
+
+
+class EvalDateAddFunc:  # pylint: disable=too-few-public-methods
+    def __init__(self, tokens: ParseResults) -> None:
+        self.value = tokens[1]
+
+    def eval(self) -> datetime:
+        dttm_expression, delta, unit = self.value
+        dttm = dttm_expression.eval()
+        if unit.lower() == "quarter":
+            delta = delta * 3
+            unit = "month"
+        return dttm + parse_human_timedelta(f"{delta} {unit}s", dttm)
+
+
+class EvalDateTruncFunc:  # pylint: disable=too-few-public-methods
+    def __init__(self, tokens: ParseResults) -> None:
+        self.value = tokens[1]
+
+    def eval(self) -> datetime:
+        dttm_expression, unit = self.value
+        dttm = dttm_expression.eval()
+        if unit == "year":
+            dttm = dttm.replace(
+                month=1, day=1, hour=0, minute=0, second=0, microsecond=0
+            )
+        elif unit == "month":
+            dttm = dttm.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+        elif unit == "week":
+            dttm = dttm - relativedelta(days=dttm.weekday())
+            dttm = dttm.replace(hour=0, minute=0, second=0, microsecond=0)
+        elif unit == "day":
+            dttm = dttm.replace(hour=0, minute=0, second=0, microsecond=0)
+        elif unit == "hour":
+            dttm = dttm.replace(minute=0, second=0, microsecond=0)
+        elif unit == "minute":
+            dttm = dttm.replace(second=0, microsecond=0)
+        else:
+            dttm = dttm.replace(microsecond=0)
+        return dttm
+
+
+class EvalLastDayFunc:  # pylint: disable=too-few-public-methods
+    def __init__(self, tokens: ParseResults) -> None:
+        self.value = tokens[1]
+
+    def eval(self) -> datetime:
+        dttm_expression, unit = self.value
+        dttm = dttm_expression.eval()
+        if unit == "year":
+            return dttm.replace(
+                month=12, day=31, hour=0, minute=0, second=0, microsecond=0
+            )
+        if unit == "month":
+            return dttm.replace(
+                day=calendar.monthrange(dttm.year, dttm.month)[1],
+                hour=0,
+                minute=0,
+                second=0,
+                microsecond=0,
+            )
+        # unit == "week":
+        mon = dttm - relativedelta(days=dttm.weekday())
+        mon = mon.replace(hour=0, minute=0, second=0, microsecond=0)
+        return mon + relativedelta(days=6)
+
+
+class EvalHolidayFunc:  # pylint: disable=too-few-public-methods
+    def __init__(self, tokens: ParseResults) -> None:
+        self.value = tokens[1]
+
+    def eval(self) -> datetime:
+        holiday = self.value[0].eval()
+        dttm, country = [None, None]
+        if len(self.value) >= 2:
+            dttm = self.value[1].eval()
+        if len(self.value) == 3:
+            country = self.value[2]
+        holiday_year = dttm.year if dttm else parse_human_datetime("today").year
+        country = country.eval() if country else "US"
+
+        holiday_lookup = CountryHoliday(country, years=[holiday_year], observed=False)
+        searched_result = holiday_lookup.get_named(holiday)
+        if len(searched_result) == 1:
+            return dttm_from_timetuple(searched_result[0].timetuple())
+        raise ValueError(_("Unable to find such a holiday: [{}]").format(holiday))
+
+
+@memoized()
+def datetime_parser() -> ParseResults:  # pylint: disable=too-many-locals
+    (  # pylint: disable=invalid-name
+        DATETIME,
+        DATEADD,
+        DATETRUNC,
+        LASTDAY,
+        HOLIDAY,
+        YEAR,
+        QUARTER,
+        MONTH,
+        WEEK,
+        DAY,
+        HOUR,
+        MINUTE,
+        SECOND,
+    ) = map(
+        CaselessKeyword,
+        "datetime dateadd datetrunc lastday holiday "
+        "year quarter month week day hour minute second".split(),
+    )
+    lparen, rparen, comma = map(Suppress, "(),")
+    int_operand = pyparsing_common.signed_integer().setName("int_operand")
+    text_operand = quotedString.setName("text_operand").setParseAction(EvalText)
+
+    # allow expression to be used recursively
+    datetime_func = Forward().setName("datetime")
+    dateadd_func = Forward().setName("dateadd")
+    datetrunc_func = Forward().setName("datetrunc")
+    lastday_func = Forward().setName("lastday")
+    holiday_func = Forward().setName("holiday")
+    date_expr = (
+        datetime_func | dateadd_func | datetrunc_func | lastday_func | holiday_func
+    )
+
+    datetime_func <<= (DATETIME + lparen + text_operand + rparen).setParseAction(
+        EvalDateTimeFunc
+    )
+    dateadd_func <<= (
+        DATEADD
+        + lparen
+        + Group(
+            date_expr
+            + comma
+            + int_operand
+            + comma
+            + (YEAR | QUARTER | MONTH | WEEK | DAY | HOUR | MINUTE | SECOND)
+            + ppOptional(comma)
+        )
+        + rparen
+    ).setParseAction(EvalDateAddFunc)
+    datetrunc_func <<= (
+        DATETRUNC
+        + lparen
+        + Group(
+            date_expr
+            + comma
+            + (YEAR | MONTH | WEEK | DAY | HOUR | MINUTE | SECOND)
+            + ppOptional(comma)
+        )
+        + rparen
+    ).setParseAction(EvalDateTruncFunc)
+    lastday_func <<= (
+        LASTDAY
+        + lparen
+        + Group(date_expr + comma + (YEAR | MONTH | WEEK) + ppOptional(comma))
+        + rparen
+    ).setParseAction(EvalLastDayFunc)
+    holiday_func <<= (
+        HOLIDAY
+        + lparen
+        + Group(
+            text_operand
+            + ppOptional(comma)
+            + ppOptional(date_expr)
+            + ppOptional(comma)
+            + ppOptional(text_operand)
+            + ppOptional(comma)
+        )
+        + rparen
+    ).setParseAction(EvalHolidayFunc)
+
+    return date_expr
+
+
+def datetime_eval(datetime_expression: Optional[str] = None) -> Optional[datetime]:
+    if datetime_expression:
+        try:
+            return datetime_parser().parseString(datetime_expression)[0].eval()
+        except ParseException as error:
+            raise ValueError(error)
+    return None
diff --git a/superset/views/api.py b/superset/views/api.py
index de7d656..7df1c5e 100644
--- a/superset/views/api.py
+++ b/superset/views/api.py
@@ -29,7 +29,7 @@ from superset.legacy import update_time_range
 from superset.models.slice import Slice
 from superset.typing import FlaskResponse
 from superset.utils import core as utils
-from superset.utils.core import get_since_until
+from superset.utils.date_parser import get_since_until
 from superset.views.base import api, BaseSupersetView, handle_api_exception
 
 get_time_range_schema = {"type": "string"}
diff --git a/superset/viz.py b/superset/viz.py
index 6baef6a..21fbdf8 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -75,6 +75,7 @@ from superset.utils.core import (
     QueryMode,
     to_adhoc,
 )
+from superset.utils.date_parser import get_since_until, parse_past_timedelta
 from superset.utils.dates import datetime_to_epoch
 from superset.utils.hashing import md5_sha_from_str
 
@@ -356,7 +357,7 @@ class BaseViz:
         order_desc = form_data.get("order_desc", True)
 
         try:
-            since, until = utils.get_since_until(
+            since, until = get_since_until(
                 relative_start=relative_start,
                 relative_end=relative_end,
                 time_range=form_data.get("time_range"),
@@ -367,7 +368,7 @@ class BaseViz:
             raise QueryObjectValidationError(str(ex))
 
         time_shift = form_data.get("time_shift", "")
-        self.time_shift = utils.parse_past_timedelta(time_shift)
+        self.time_shift = parse_past_timedelta(time_shift)
         from_dttm = None if since is None else (since - self.time_shift)
         to_dttm = None if until is None else (until - self.time_shift)
         if from_dttm and to_dttm and from_dttm > to_dttm:
@@ -1004,7 +1005,7 @@ class CalHeatmapViz(BaseViz):
             data[metric] = values
 
         try:
-            start, end = utils.get_since_until(
+            start, end = get_since_until(
                 relative_start=relative_start,
                 relative_end=relative_end,
                 time_range=form_data.get("time_range"),
@@ -1318,7 +1319,7 @@ class NVD3TimeSeriesViz(NVD3Viz):
         for option in time_compare:
             query_object = self.query_obj()
             try:
-                delta = utils.parse_past_timedelta(option)
+                delta = parse_past_timedelta(option)
             except ValueError as ex:
                 raise QueryObjectValidationError(str(ex))
             query_object["inner_from_dttm"] = query_object["from_dttm"]
diff --git a/superset/viz_sip38.py b/superset/viz_sip38.py
index 5a16639..9ec1752 100644
--- a/superset/viz_sip38.py
+++ b/superset/viz_sip38.py
@@ -63,6 +63,7 @@ from superset.utils.core import (
     merge_extra_filters,
     to_adhoc,
 )
+from superset.utils.date_parser import get_since_until, parse_past_timedelta
 
 import dataclasses  # isort:skip
 
@@ -359,7 +360,7 @@ class BaseViz:
         # default order direction
         order_desc = form_data.get("order_desc", True)
 
-        since, until = utils.get_since_until(
+        since, until = get_since_until(
             relative_start=relative_start,
             relative_end=relative_end,
             time_range=form_data.get("time_range"),
@@ -367,7 +368,7 @@ class BaseViz:
             until=form_data.get("until"),
         )
         time_shift = form_data.get("time_shift", "")
-        self.time_shift = utils.parse_past_timedelta(time_shift)
+        self.time_shift = parse_past_timedelta(time_shift)
         from_dttm = None if since is None else (since - self.time_shift)
         to_dttm = None if until is None else (until - self.time_shift)
         if from_dttm and to_dttm and from_dttm > to_dttm:
@@ -883,7 +884,7 @@ class CalHeatmapViz(BaseViz):
                 values[str(v / 10 ** 9)] = obj.get(metric)
             data[metric] = values
 
-        start, end = utils.get_since_until(
+        start, end = get_since_until(
             relative_start=relative_start,
             relative_end=relative_end,
             time_range=form_data.get("time_range"),
@@ -1265,7 +1266,7 @@ class NVD3TimeSeriesViz(NVD3Viz):
 
         for option in time_compare:
             query_object = self.query_obj()
-            delta = utils.parse_past_timedelta(option)
+            delta = parse_past_timedelta(option)
             query_object["inner_from_dttm"] = query_object["from_dttm"]
             query_object["inner_to_dttm"] = query_object["to_dttm"]
 
diff --git a/tests/utils/date_parser_tests.py b/tests/utils/date_parser_tests.py
new file mode 100644
index 0000000..fc592d3
--- /dev/null
+++ b/tests/utils/date_parser_tests.py
@@ -0,0 +1,263 @@
+# 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.
+from datetime import datetime, timedelta
+from unittest.mock import patch
+
+from superset.utils.date_parser import (
+    datetime_eval,
+    get_since_until,
+    parse_human_timedelta,
+    parse_past_timedelta,
+)
+from tests.base_tests import SupersetTestCase
+
+
+def mock_parse_human_datetime(s):
+    if s == "now":
+        return datetime(2016, 11, 7, 9, 30, 10)
+    elif s == "today":
+        return datetime(2016, 11, 7)
+    elif s == "yesterday":
+        return datetime(2016, 11, 6)
+    elif s == "tomorrow":
+        return datetime(2016, 11, 8)
+    elif s == "Last year":
+        return datetime(2015, 11, 7)
+    elif s == "Last week":
+        return datetime(2015, 10, 31)
+    elif s == "Last 5 months":
+        return datetime(2016, 6, 7)
+    elif s == "Next 5 months":
+        return datetime(2017, 4, 7)
+    elif s in ["5 days", "5 days ago"]:
+        return datetime(2016, 11, 2)
+    elif s == "2018-01-01T00:00:00":
+        return datetime(2018, 1, 1)
+    elif s == "2018-12-31T23:59:59":
+        return datetime(2018, 12, 31, 23, 59, 59)
+
+
+class TestDateParser(SupersetTestCase):
+    @patch("superset.utils.date_parser.parse_human_datetime", mock_parse_human_datetime)
+    def test_get_since_until(self):
+        result = get_since_until()
+        expected = None, datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until(" : now")
+        expected = None, datetime(2016, 11, 7, 9, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("yesterday : tomorrow")
+        expected = datetime(2016, 11, 6), datetime(2016, 11, 8)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("2018-01-01T00:00:00 : 2018-12-31T23:59:59")
+        expected = datetime(2018, 1, 1), datetime(2018, 12, 31, 23, 59, 59)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Last year")
+        expected = datetime(2015, 11, 7), datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Last quarter")
+        expected = datetime(2016, 8, 7), datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Last 5 months")
+        expected = datetime(2016, 6, 7), datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Next 5 months")
+        expected = datetime(2016, 11, 7), datetime(2017, 4, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until(since="5 days")
+        expected = datetime(2016, 11, 2), datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until(since="5 days ago", until="tomorrow")
+        expected = datetime(2016, 11, 2), datetime(2016, 11, 8)
+        self.assertEqual(result, expected)
+
+        result = get_since_until(time_range="yesterday : tomorrow", time_shift="1 day")
+        expected = datetime(2016, 11, 5), datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until(time_range="5 days : now")
+        expected = datetime(2016, 11, 2), datetime(2016, 11, 7, 9, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Last week", relative_end="now")
+        expected = datetime(2016, 10, 31), datetime(2016, 11, 7, 9, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Last week", relative_start="now")
+        expected = datetime(2016, 10, 31, 9, 30, 10), datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("Last week", relative_start="now", relative_end="now")
+        expected = datetime(2016, 10, 31, 9, 30, 10), datetime(2016, 11, 7, 9, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("previous calendar week")
+        expected = datetime(2016, 10, 31, 0, 0, 0), datetime(2016, 11, 7, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("previous calendar month")
+        expected = datetime(2016, 10, 1, 0, 0, 0), datetime(2016, 11, 1, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = get_since_until("previous calendar year")
+        expected = datetime(2015, 1, 1, 0, 0, 0), datetime(2016, 1, 1, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        with self.assertRaises(ValueError):
+            get_since_until(time_range="tomorrow : yesterday")
+
+    @patch("superset.utils.date_parser.parse_human_datetime", mock_parse_human_datetime)
+    def test_datetime_eval(self):
+        result = datetime_eval("datetime('now')")
+        expected = datetime(2016, 11, 7, 9, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetime('today'  )")
+        expected = datetime(2016, 11, 7)
+        self.assertEqual(result, expected)
+
+        # Parse compact arguments spelling
+        result = datetime_eval("dateadd(datetime('today'),1,year,)")
+        expected = datetime(2017, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('today'), -2, year)")
+        expected = datetime(2014, 11, 7)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('today'), 2, quarter)")
+        expected = datetime(2017, 5, 7)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('today'), 3, month)")
+        expected = datetime(2017, 2, 7)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('today'), -3, week)")
+        expected = datetime(2016, 10, 17)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('today'), 3, day)")
+        expected = datetime(2016, 11, 10)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('now'), 3, hour)")
+        expected = datetime(2016, 11, 7, 12, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('now'), 40, minute)")
+        expected = datetime(2016, 11, 7, 10, 10, 10)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("dateadd(datetime('now'), -11, second)")
+        expected = datetime(2016, 11, 7, 9, 29, 59)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), year)")
+        expected = datetime(2016, 1, 1, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), month)")
+        expected = datetime(2016, 11, 1, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), day)")
+        expected = datetime(2016, 11, 7, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), week)")
+        expected = datetime(2016, 11, 7, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), hour)")
+        expected = datetime(2016, 11, 7, 9, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), minute)")
+        expected = datetime(2016, 11, 7, 9, 30, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("datetrunc(datetime('now'), second)")
+        expected = datetime(2016, 11, 7, 9, 30, 10)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("lastday(datetime('now'), year)")
+        expected = datetime(2016, 12, 31, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("lastday(datetime('today'), month)")
+        expected = datetime(2016, 11, 30, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("holiday('Christmas')")
+        expected = datetime(2016, 12, 25, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval("holiday('Labor day', datetime('2018-01-01T00:00:00'))")
+        expected = datetime(2018, 9, 3, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval(
+            "holiday('Boxing day', datetime('2018-01-01T00:00:00'), 'UK')"
+        )
+        expected = datetime(2018, 12, 26, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+        result = datetime_eval(
+            "lastday(dateadd(datetime('2018-01-01T00:00:00'), 1, month), month)"
+        )
+        expected = datetime(2018, 2, 28, 0, 0, 0)
+        self.assertEqual(result, expected)
+
+    @patch("superset.utils.date_parser.datetime")
+    def test_parse_human_timedelta(self, mock_datetime):
+        mock_datetime.now.return_value = datetime(2019, 4, 1)
+        mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
+        self.assertEqual(parse_human_timedelta("now"), timedelta(0))
+        self.assertEqual(parse_human_timedelta("1 year"), timedelta(366))
+        self.assertEqual(parse_human_timedelta("-1 year"), timedelta(-365))
+        self.assertEqual(parse_human_timedelta(None), timedelta(0))
+        self.assertEqual(
+            parse_human_timedelta("1 month", datetime(2019, 4, 1)), timedelta(30),
+        )
+        self.assertEqual(
+            parse_human_timedelta("1 month", datetime(2019, 5, 1)), timedelta(31),
+        )
+        self.assertEqual(
+            parse_human_timedelta("1 month", datetime(2019, 2, 1)), timedelta(28),
+        )
+        self.assertEqual(
+            parse_human_timedelta("-1 month", datetime(2019, 2, 1)), timedelta(-31),
+        )
+
+    @patch("superset.utils.date_parser.datetime")
+    def test_parse_past_timedelta(self, mock_datetime):
+        mock_datetime.now.return_value = datetime(2019, 4, 1)
+        mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
+        self.assertEqual(parse_past_timedelta("1 year"), timedelta(365))
+        self.assertEqual(parse_past_timedelta("-1 year"), timedelta(365))
+        self.assertEqual(parse_past_timedelta("52 weeks"), timedelta(364))
+        self.assertEqual(parse_past_timedelta("1 month"), timedelta(31))
diff --git a/tests/utils_tests.py b/tests/utils_tests.py
index 20c7897..b47f7d1 100644
--- a/tests/utils_tests.py
+++ b/tests/utils_tests.py
@@ -46,7 +46,6 @@ from superset.utils.core import (
     get_iterable,
     get_email_address_list,
     get_or_create_db,
-    get_since_until,
     get_stacktrace,
     json_int_dttm_ser,
     json_iso_dttm_ser,
@@ -55,15 +54,12 @@ from superset.utils.core import (
     merge_extra_filters,
     merge_request_params,
     parse_ssl_cert,
-    parse_human_timedelta,
     parse_js_uri_path_item,
-    parse_past_timedelta,
     split,
     TimeRangeEndpoint,
     validate_json,
     zlib_compress,
     zlib_decompress,
-    datetime_eval,
 )
 from superset.utils import schema
 from superset.views.utils import (
@@ -76,31 +72,6 @@ from tests.base_tests import SupersetTestCase
 from .fixtures.certificates import ssl_certificate
 
 
-def mock_parse_human_datetime(s):
-    if s == "now":
-        return datetime(2016, 11, 7, 9, 30, 10)
-    elif s == "today":
-        return datetime(2016, 11, 7)
-    elif s == "yesterday":
-        return datetime(2016, 11, 6)
-    elif s == "tomorrow":
-        return datetime(2016, 11, 8)
-    elif s == "Last year":
-        return datetime(2015, 11, 7)
-    elif s == "Last week":
-        return datetime(2015, 10, 31)
-    elif s == "Last 5 months":
-        return datetime(2016, 6, 7)
-    elif s == "Next 5 months":
-        return datetime(2017, 4, 7)
-    elif s in ["5 days", "5 days ago"]:
-        return datetime(2016, 11, 2)
-    elif s == "2018-01-01T00:00:00":
-        return datetime(2018, 1, 1)
-    elif s == "2018-12-31T23:59:59":
-        return datetime(2018, 12, 31, 23, 59, 59)
-
-
 def mock_to_adhoc(filt, expressionType="SIMPLE", clause="where"):
     result = {"clause": clause.upper(), "expressionType": expressionType}
 
@@ -147,36 +118,6 @@ class TestUtils(SupersetTestCase):
         assert isinstance(base_json_conv(uuid.uuid4()), str) is True
         assert isinstance(base_json_conv(timedelta(0)), str) is True
 
-    @patch("superset.utils.core.datetime")
-    def test_parse_human_timedelta(self, mock_datetime):
-        mock_datetime.now.return_value = datetime(2019, 4, 1)
-        mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
-        self.assertEqual(parse_human_timedelta("now"), timedelta(0))
-        self.assertEqual(parse_human_timedelta("1 year"), timedelta(366))
-        self.assertEqual(parse_human_timedelta("-1 year"), timedelta(-365))
-        self.assertEqual(parse_human_timedelta(None), timedelta(0))
-        self.assertEqual(
-            parse_human_timedelta("1 month", datetime(2019, 4, 1)), timedelta(30),
-        )
-        self.assertEqual(
-            parse_human_timedelta("1 month", datetime(2019, 5, 1)), timedelta(31),
-        )
-        self.assertEqual(
-            parse_human_timedelta("1 month", datetime(2019, 2, 1)), timedelta(28),
-        )
-        self.assertEqual(
-            parse_human_timedelta("-1 month", datetime(2019, 2, 1)), timedelta(-31),
-        )
-
-    @patch("superset.utils.core.datetime")
-    def test_parse_past_timedelta(self, mock_datetime):
-        mock_datetime.now.return_value = datetime(2019, 4, 1)
-        mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
-        self.assertEqual(parse_past_timedelta("1 year"), timedelta(365))
-        self.assertEqual(parse_past_timedelta("-1 year"), timedelta(365))
-        self.assertEqual(parse_past_timedelta("52 weeks"), timedelta(364))
-        self.assertEqual(parse_past_timedelta("1 month"), timedelta(31))
-
     def test_zlib_compression(self):
         json_str = '{"test": 1}'
         blob = zlib_compress(json_str)
@@ -699,186 +640,6 @@ class TestUtils(SupersetTestCase):
         self.assertEqual(instance.watcher, 4)
         self.assertEqual(result1, result8)
 
-    @patch("superset.utils.core.parse_human_datetime", mock_parse_human_datetime)
-    def test_get_since_until(self):
-        result = get_since_until()
-        expected = None, datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until(" : now")
-        expected = None, datetime(2016, 11, 7, 9, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("yesterday : tomorrow")
-        expected = datetime(2016, 11, 6), datetime(2016, 11, 8)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("2018-01-01T00:00:00 : 2018-12-31T23:59:59")
-        expected = datetime(2018, 1, 1), datetime(2018, 12, 31, 23, 59, 59)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Last year")
-        expected = datetime(2015, 11, 7), datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Last quarter")
-        expected = datetime(2016, 8, 7), datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Last 5 months")
-        expected = datetime(2016, 6, 7), datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Next 5 months")
-        expected = datetime(2016, 11, 7), datetime(2017, 4, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until(since="5 days")
-        expected = datetime(2016, 11, 2), datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until(since="5 days ago", until="tomorrow")
-        expected = datetime(2016, 11, 2), datetime(2016, 11, 8)
-        self.assertEqual(result, expected)
-
-        result = get_since_until(time_range="yesterday : tomorrow", time_shift="1 day")
-        expected = datetime(2016, 11, 5), datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until(time_range="5 days : now")
-        expected = datetime(2016, 11, 2), datetime(2016, 11, 7, 9, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Last week", relative_end="now")
-        expected = datetime(2016, 10, 31), datetime(2016, 11, 7, 9, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Last week", relative_start="now")
-        expected = datetime(2016, 10, 31, 9, 30, 10), datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("Last week", relative_start="now", relative_end="now")
-        expected = datetime(2016, 10, 31, 9, 30, 10), datetime(2016, 11, 7, 9, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("previous calendar week")
-        expected = datetime(2016, 10, 31, 0, 0, 0), datetime(2016, 11, 7, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("previous calendar month")
-        expected = datetime(2016, 10, 1, 0, 0, 0), datetime(2016, 11, 1, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = get_since_until("previous calendar year")
-        expected = datetime(2015, 1, 1, 0, 0, 0), datetime(2016, 1, 1, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        with self.assertRaises(ValueError):
-            get_since_until(time_range="tomorrow : yesterday")
-
-    @patch("superset.utils.core.parse_human_datetime", mock_parse_human_datetime)
-    def test_datetime_eval(self):
-        result = datetime_eval("datetime('now')")
-        expected = datetime(2016, 11, 7, 9, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetime('today'  )")
-        expected = datetime(2016, 11, 7)
-        self.assertEqual(result, expected)
-
-        # Parse compact arguments spelling
-        result = datetime_eval("dateadd(datetime('today'),1,year,)")
-        expected = datetime(2017, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('today'), -2, year)")
-        expected = datetime(2014, 11, 7)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('today'), 2, quarter)")
-        expected = datetime(2017, 5, 7)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('today'), 3, month)")
-        expected = datetime(2017, 2, 7)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('today'), -3, week)")
-        expected = datetime(2016, 10, 17)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('today'), 3, day)")
-        expected = datetime(2016, 11, 10)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('now'), 3, hour)")
-        expected = datetime(2016, 11, 7, 12, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('now'), 40, minute)")
-        expected = datetime(2016, 11, 7, 10, 10, 10)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("dateadd(datetime('now'), -11, second)")
-        expected = datetime(2016, 11, 7, 9, 29, 59)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), year)")
-        expected = datetime(2016, 1, 1, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), month)")
-        expected = datetime(2016, 11, 1, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), day)")
-        expected = datetime(2016, 11, 7, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), week)")
-        expected = datetime(2016, 11, 7, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), hour)")
-        expected = datetime(2016, 11, 7, 9, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), minute)")
-        expected = datetime(2016, 11, 7, 9, 30, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("datetrunc(datetime('now'), second)")
-        expected = datetime(2016, 11, 7, 9, 30, 10)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("lastday(datetime('now'), year)")
-        expected = datetime(2016, 12, 31, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("lastday(datetime('today'), month)")
-        expected = datetime(2016, 11, 30, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("holiday('Christmas')")
-        expected = datetime(2016, 12, 25, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval("holiday('Labor day', datetime('2018-01-01T00:00:00'))")
-        expected = datetime(2018, 9, 3, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval(
-            "holiday('Boxing day', datetime('2018-01-01T00:00:00'), 'UK')"
-        )
-        expected = datetime(2018, 12, 26, 0, 0, 0)
-        self.assertEqual(result, expected)
-
-        result = datetime_eval(
-            "lastday(dateadd(datetime('2018-01-01T00:00:00'), 1, month), month)"
-        )
-        expected = datetime(2018, 2, 28, 0, 0, 0)
-        self.assertEqual(result, expected)
-
     @patch("superset.utils.core.to_adhoc", mock_to_adhoc)
     def test_convert_legacy_filters_into_adhoc_where(self):
         form_data = {"where": "a = 1"}


[superset] 02/09: Fixes control panel fields styling (#12236) (#12326)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 507302d63900ba4d788087f8e24430106206d1c8
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Mon Jan 11 21:47:10 2021 -0300

    Fixes control panel fields styling (#12236) (#12326)
---
 .../cypress/integration/explore/advanced.test.ts   | 17 +++++----
 .../explore/components/SelectControl_spec.jsx      | 12 +++----
 superset-frontend/src/components/Label/index.tsx   |  2 +-
 .../src/components/Select/SupersetStyledSelect.tsx |  1 +
 superset-frontend/src/components/Select/styles.tsx | 42 ++++++++++++++--------
 .../src/explore/components/DatasourcePanel.tsx     |  9 ++---
 .../explore/components/ExploreViewContainer.jsx    |  4 +--
 .../src/explore/components/OptionControls.tsx      |  4 +--
 .../explore/components/controls/SelectControl.jsx  | 20 ++++++-----
 .../explore/components/controls/TextControl.tsx    |  2 +-
 .../stylesheets/less/cosmo/bootswatch.less         |  9 +++++
 .../stylesheets/less/cosmo/variables.less          |  4 +--
 superset-frontend/stylesheets/less/variables.less  |  2 +-
 13 files changed, 76 insertions(+), 52 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
index c0ffbad..c27f054 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
@@ -35,13 +35,9 @@ describe('Advanced analytics', () => {
       .find('input[type=text]')
       .type('28 days{enter}');
 
-    cy.get('[data-test=time_compare]').find('.Select__control').click();
     cy.get('[data-test=time_compare]')
       .find('input[type=text]')
-      .type('364 days{enter}');
-    cy.get('[data-test=time_compare]')
-      .find('.Select__multi-value__label')
-      .contains('364 days');
+      .type('1 year{enter}');
 
     cy.get('button[data-test="run-query-button"]').click();
     cy.wait('@postJson');
@@ -51,10 +47,13 @@ describe('Advanced analytics', () => {
       chartSelector: 'svg',
     });
 
-    cy.get('[data-test=time_compare]').within(() => {
-      cy.get('.Select__multi-value__label').contains('364 days');
-      cy.get('.Select__multi-value__label').contains('28 days');
-    });
+    cy.get('.panel-title').contains('Advanced Analytics').click();
+    cy.get('[data-test=time_compare]')
+      .find('.Select__multi-value__label')
+      .contains('28 days');
+    cy.get('[data-test=time_compare]')
+      .find('.Select__multi-value__label')
+      .contains('1 year');
   });
 });
 
diff --git a/superset-frontend/spec/javascripts/explore/components/SelectControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/SelectControl_spec.jsx
index bfb16aa..35a0fcc 100644
--- a/superset-frontend/spec/javascripts/explore/components/SelectControl_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/SelectControl_spec.jsx
@@ -112,7 +112,7 @@ describe('SelectControl', () => {
               placeholder="add something"
             />,
           );
-          expect(withMulti.html()).not.toContain('placeholder=');
+          expect(withMulti.html()).not.toContain('option(s');
         });
       });
       describe('withSingleChoice', () => {
@@ -125,7 +125,7 @@ describe('SelectControl', () => {
               placeholder="add something"
             />,
           );
-          expect(singleChoice.html()).not.toContain('placeholder=');
+          expect(singleChoice.html()).not.toContain('option(s');
         });
       });
       describe('default placeholder', () => {
@@ -133,7 +133,7 @@ describe('SelectControl', () => {
           const defaultPlaceholder = mount(
             <SelectControl {...defaultProps} choices={[]} multi />,
           );
-          expect(defaultPlaceholder.html()).not.toContain('placeholder=');
+          expect(defaultPlaceholder.html()).not.toContain('option(s');
         });
       });
       describe('all choices selected', () => {
@@ -145,12 +145,12 @@ describe('SelectControl', () => {
               value={['today', '1 year ago']}
             />,
           );
-          expect(allChoicesSelected.html()).toContain('placeholder=""');
+          expect(allChoicesSelected.html()).not.toContain('option(s');
         });
       });
     });
     describe('when select is multi', () => {
-      it('renders the placeholder when a selection has been made', () => {
+      it('does not render the placeholder when a selection has been made', () => {
         wrapper = mount(
           <SelectControl
             {...defaultProps}
@@ -159,7 +159,7 @@ describe('SelectControl', () => {
             placeholder="add something"
           />,
         );
-        expect(wrapper.html()).toContain('add something');
+        expect(wrapper.html()).not.toContain('add something');
       });
       it('shows numbers of options as a placeholder by default', () => {
         wrapper = mount(<SelectControl {...defaultProps} multi />);
diff --git a/superset-frontend/src/components/Label/index.tsx b/superset-frontend/src/components/Label/index.tsx
index bed21fd..18bd1d2 100644
--- a/superset-frontend/src/components/Label/index.tsx
+++ b/superset-frontend/src/components/Label/index.tsx
@@ -93,7 +93,7 @@ const SupersetLabel = styled(BootstrapLabel)`
     background-color: ${({ theme }) => theme.colors.grayscale.light3};
     color: ${({ theme }) => theme.colors.grayscale.dark1};
     border-color: ${({ theme, onClick }) =>
-      onClick ? theme.colors.grayscale.light1 : 'transparent'};
+      onClick ? theme.colors.grayscale.light2 : 'transparent'};
     &:hover {
       background-color: ${({ theme, onClick }) =>
         onClick ? theme.colors.primary.light2 : theme.colors.grayscale.light3};
diff --git a/superset-frontend/src/components/Select/SupersetStyledSelect.tsx b/superset-frontend/src/components/Select/SupersetStyledSelect.tsx
index ec62c6d..fdbf5e2 100644
--- a/superset-frontend/src/components/Select/SupersetStyledSelect.tsx
+++ b/superset-frontend/src/components/Select/SupersetStyledSelect.tsx
@@ -76,6 +76,7 @@ export type SupersetStyledSelectProps<
   // additional props for easier usage or backward compatibility
   labelKey?: string;
   valueKey?: string;
+  assistiveText?: string;
   multi?: boolean;
   clearable?: boolean;
   sortable?: boolean;
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
index 623d6ce..9358021 100644
--- a/superset-frontend/src/components/Select/styles.tsx
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -98,7 +98,7 @@ export const defaultTheme: (
   spacing: {
     baseUnit: 3,
     menuGutter: 0,
-    controlHeight: 28,
+    controlHeight: 34,
     lineHeight: 19,
     fontSize: 14,
     minWidth: '7.5em', // just enough to display 'No options'
@@ -160,9 +160,9 @@ export const DEFAULT_STYLES: PartialStylesConfig = {
     { isFocused, menuIsOpen, theme: { borderRadius, colors } },
   ) => {
     const isPseudoFocused = isFocused && !menuIsOpen;
-    let borderColor = '#ccc';
+    let borderColor = colors.grayBorder;
     if (isPseudoFocused) {
-      borderColor = '#000';
+      borderColor = colors.grayBorderDark;
     } else if (menuIsOpen) {
       borderColor = `${colors.grayBorderDark} ${colors.grayBorder} ${colors.grayBorderLight}`;
     }
@@ -181,6 +181,7 @@ export const DEFAULT_STYLES: PartialStylesConfig = {
           box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
         }
         flex-wrap: nowrap;
+        padding-left: 1px;
       `,
     ];
   },
@@ -312,9 +313,31 @@ const {
   DropdownIndicator,
   Option,
   Input,
+  SelectContainer,
 } = defaultComponents as Required<DeepNonNullable<SelectComponentsType>>;
 
 export const DEFAULT_COMPONENTS: SelectComponentsType = {
+  SelectContainer: ({ children, ...props }) => {
+    const {
+      selectProps: { assistiveText },
+    } = props;
+    return (
+      <div>
+        <SelectContainer {...props}>{children}</SelectContainer>
+        {assistiveText && (
+          <span
+            css={(theme: SupersetTheme) => ({
+              marginLeft: 3,
+              fontSize: theme.typography.sizes.s,
+              color: theme.colors.grayscale.light1,
+            })}
+          >
+            {assistiveText}
+          </span>
+        )}
+      </div>
+    );
+  },
   Option: ({ children, innerProps, data, ...props }) => (
     <ClassNames>
       {({ css }) => (
@@ -344,22 +367,13 @@ export const DEFAULT_COMPONENTS: SelectComponentsType = {
     </DropdownIndicator>
   ),
   Input: (props: InputProps) => {
-    const {
-      selectProps: { isMulti, value, placeholder },
-      getStyles,
-    } = props;
-    const isMultiWithValue = isMulti && Array.isArray(value) && !!value.length;
+    const { getStyles } = props;
     return (
       <Input
         {...props}
-        placeholder={isMultiWithValue ? placeholder : undefined}
         css={getStyles('input', props)}
         autoComplete="chrome-off"
-        inputStyle={
-          isMultiWithValue
-            ? { ...INPUT_TAG_BASE_STYLES, width: '100%' }
-            : INPUT_TAG_BASE_STYLES
-        }
+        inputStyle={INPUT_TAG_BASE_STYLES}
       />
     );
   },
diff --git a/superset-frontend/src/explore/components/DatasourcePanel.tsx b/superset-frontend/src/explore/components/DatasourcePanel.tsx
index 36eb078..b185ea1 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel.tsx
@@ -90,9 +90,6 @@ const DatasourceContainer = styled.div`
     padding-left: ${({ theme }) => theme.gridUnit * 2}px;
     padding-bottom: 0px;
   }
-  .form-control.input-sm {
-    margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
-  }
   .ant-collapse-item {
     background-color: ${({ theme }) => theme.colors.grayscale.light4};
     .anticon.anticon-right.ant-collapse-arrow > svg {
@@ -130,8 +127,8 @@ const DatasourceContainer = styled.div`
     font-size: ${({ theme }) => theme.typography.sizes.s}px;
     color: ${({ theme }) => theme.colors.grayscale.light1};
   }
-  .form-control.input-sm {
-    width: calc(100% - ${({ theme }) => theme.gridUnit * 2}px);
+  .form-control.input-md {
+    width: calc(100% - ${({ theme }) => theme.gridUnit * 4}px);
     margin: ${({ theme }) => theme.gridUnit * 2}px auto;
   }
   .type-label {
@@ -186,7 +183,7 @@ const DataSourcePanel = ({
       <input
         type="text"
         onChange={search}
-        className="form-control input-sm"
+        className="form-control input-md"
         placeholder={t('Search Metrics & Columns')}
       />
       <div className="field-selections">
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer.jsx b/superset-frontend/src/explore/components/ExploreViewContainer.jsx
index d3df42a..6599890 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer.jsx
@@ -81,10 +81,10 @@ const Styles = styled.div`
   border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
   .explore-column {
     display: flex;
-    flex: 0 0 ${({ theme }) => theme.gridUnit * 80}px;
+    flex: 0 0 ${({ theme }) => theme.gridUnit * 95}px;
     flex-direction: column;
     padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
-    max-width: ${({ theme }) => theme.gridUnit * 80}px;
+    max-width: ${({ theme }) => theme.gridUnit * 95}px;
     max-height: 100%;
   }
   .data-source-selection {
diff --git a/superset-frontend/src/explore/components/OptionControls.tsx b/superset-frontend/src/explore/components/OptionControls.tsx
index 8e3121d..8d157c8 100644
--- a/superset-frontend/src/explore/components/OptionControls.tsx
+++ b/superset-frontend/src/explore/components/OptionControls.tsx
@@ -87,7 +87,7 @@ export const HeaderContainer = styled.div`
 export const LabelsContainer = styled.div`
   padding: ${({ theme }) => theme.gridUnit}px;
   border: solid 1px ${({ theme }) => theme.colors.grayscale.light2};
-  border-radius: 3px;
+  border-radius: ${({ theme }) => theme.gridUnit}px;
 `;
 
 export const AddControlLabel = styled.div`
@@ -99,7 +99,7 @@ export const AddControlLabel = styled.div`
   font-size: ${({ theme }) => theme.typography.sizes.s}px;
   color: ${({ theme }) => theme.colors.grayscale.light1};
   border: dashed 1px ${({ theme }) => theme.colors.grayscale.light2};
-  border-radius: 3px;
+  border-radius: ${({ theme }) => theme.gridUnit}px;
   cursor: pointer;
 
   :hover {
diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.jsx
index ccd0574..62e0826 100644
--- a/superset-frontend/src/explore/components/controls/SelectControl.jsx
+++ b/superset-frontend/src/explore/components/controls/SelectControl.jsx
@@ -203,13 +203,6 @@ export default class SelectControl extends React.PureComponent {
     return remainingOptions;
   }
 
-  createPlaceholder() {
-    const optionsRemaining = this.optionsRemaining();
-    const placeholder =
-      this.props.placeholder || t('%s option(s)', optionsRemaining);
-    return optionsRemaining ? placeholder : '';
-  }
-
   createMetaSelectAllOption() {
     const option = { label: 'Select All', meta: true };
     option[this.props.valueKey] = 'Select All';
@@ -235,9 +228,19 @@ export default class SelectControl extends React.PureComponent {
       valueKey,
       valueRenderer,
     } = this.props;
-    const placeholder = this.createPlaceholder();
+
+    const optionsRemaining = this.optionsRemaining();
+    const optionRemaingText = optionsRemaining
+      ? t('%s option(s)', optionsRemaining)
+      : '';
+    const placeholder = this.props.placeholder || optionRemaingText;
     const isMulti = this.props.isMulti || this.props.multi;
 
+    let assistiveText;
+    if (isMulti && optionsRemaining && Array.isArray(value) && !!value.length) {
+      assistiveText = optionRemaingText;
+    }
+
     const selectProps = {
       autoFocus,
       clearable,
@@ -257,6 +260,7 @@ export default class SelectControl extends React.PureComponent {
       optionRenderer,
       options: this.state.options,
       placeholder,
+      assistiveText,
       promptTextCreator,
       selectRef: this.getSelectRef,
       value,
diff --git a/superset-frontend/src/explore/components/controls/TextControl.tsx b/superset-frontend/src/explore/components/controls/TextControl.tsx
index 038d169..ffc1f56 100644
--- a/superset-frontend/src/explore/components/controls/TextControl.tsx
+++ b/superset-frontend/src/explore/components/controls/TextControl.tsx
@@ -107,7 +107,7 @@ export default class TextControl extends React.Component<
     return (
       <div>
         <ControlHeader {...this.props} />
-        <FormGroup controlId={this.state.controlId} bsSize="small">
+        <FormGroup controlId={this.state.controlId} bsSize="medium">
           <FormControl
             type="text"
             data-test="inline-name"
diff --git a/superset-frontend/stylesheets/less/cosmo/bootswatch.less b/superset-frontend/stylesheets/less/cosmo/bootswatch.less
index 576336c..b9902f6 100644
--- a/superset-frontend/stylesheets/less/cosmo/bootswatch.less
+++ b/superset-frontend/stylesheets/less/cosmo/bootswatch.less
@@ -148,6 +148,10 @@ table,
 
 // Forms ======================================================================
 
+.form-control {
+  box-shadow: none;
+}
+
 .has-warning {
   .help-block,
   .control-label,
@@ -395,6 +399,11 @@ label {
   font-size: 24px;
 }
 
+.list-group-item {
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
+
 a.list-group-item {
   &-success {
     &.active {
diff --git a/superset-frontend/stylesheets/less/cosmo/variables.less b/superset-frontend/stylesheets/less/cosmo/variables.less
index 8f13ac3..6b9e3b0 100644
--- a/superset-frontend/stylesheets/less/cosmo/variables.less
+++ b/superset-frontend/stylesheets/less/cosmo/variables.less
@@ -91,7 +91,7 @@
 //
 // ## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 
-@padding-base-vertical: 10px;
+@padding-base-vertical: 6.5px;
 @padding-base-horizontal: 18px;
 
 @padding-large-vertical: 18px;
@@ -152,7 +152,7 @@
 
 @btn-default-color: @bs-gray;
 @btn-default-bg: @lightest;
-@btn-default-border: @bs-gray-light;
+@btn-default-border: @gray-light;
 
 @btn-success-color: @btn-primary-color;
 @btn-success-bg: @brand-success;
diff --git a/superset-frontend/stylesheets/less/variables.less b/superset-frontend/stylesheets/less/variables.less
index 52974c3..34bfdac 100644
--- a/superset-frontend/stylesheets/less/variables.less
+++ b/superset-frontend/stylesheets/less/variables.less
@@ -47,7 +47,7 @@
 
 @almost-black: #263238;
 @gray-dark: #484848;
-@gray-light: #cfd8dc;
+@gray-light: #e0e0e0;
 @gray: #879399;
 @gray-bg: #f7f7f7;
 @gray-heading: #a3a3a3;


[superset] 04/09: fix(dashboard): artefacts shown while drag and dropping deck.gl charts (#12418)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 34da995dd9ae56f35eeb45b97228b6598036680b
Author: Kasia Kucharczyk <25...@users.noreply.github.com>
AuthorDate: Tue Jan 12 21:06:48 2021 +0100

    fix(dashboard): artefacts shown while drag and dropping deck.gl charts (#12418)
    
    * [12181] Fix artifacts while drag and dropping deck.gl charts.
    
    * Run prettier
---
 superset-frontend/src/dashboard/stylesheets/dnd.less | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/superset-frontend/src/dashboard/stylesheets/dnd.less b/superset-frontend/src/dashboard/stylesheets/dnd.less
index 19df3ad..4efdb72 100644
--- a/superset-frontend/src/dashboard/stylesheets/dnd.less
+++ b/superset-frontend/src/dashboard/stylesheets/dnd.less
@@ -20,6 +20,13 @@
   position: relative;
 }
 
+// Fixes ISSUE-12181 - before in chart's contract-trigger breaks drag and drop mode
+.dashboard--editing {
+  .contract-trigger:before {
+    display: none;
+  }
+}
+
 .dragdroppable--dragging {
   opacity: 0.2;
 }


[superset] 03/09: feat: Resizable dataset and controls panels on Explore view (#12411)

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

villebro pushed a commit to branch 1.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 288f6bb88ce3c78640982aa7e7889d4e4981059d
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Tue Jan 12 19:39:56 2021 +0100

    feat: Resizable dataset and controls panels on Explore view (#12411)
    
    * Implement resizable panels on explore view
    
    * Optimize chart rendering while resizing
    
    * Make dataset column narrower
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
---
 superset-frontend/package-lock.json                |  21 +++++
 superset-frontend/package.json                     |   1 +
 .../src/explore/components/DatasourcePanel.tsx     |  26 +++++-
 .../src/explore/components/ExploreChartPanel.jsx   | 104 +++++++++++----------
 .../explore/components/ExploreViewContainer.jsx    |  21 +++--
 5 files changed, 116 insertions(+), 57 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index e5e3916..8461aba 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -39186,6 +39186,11 @@
       "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
       "dev": true
     },
+    "lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+    },
     "lodash.topath": {
       "version": "4.5.2",
       "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
@@ -44123,6 +44128,11 @@
         "performance-now": "^2.1.0"
       }
     },
+    "raf-schd": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz",
+      "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ=="
+    },
     "railroad-diagrams": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
@@ -46500,6 +46510,17 @@
         }
       }
     },
+    "react-resize-detector": {
+      "version": "6.0.1-rc.1",
+      "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.0.1-rc.1.tgz",
+      "integrity": "sha512-r+UZtJottPZaW/2CKAyb4Vgpi6KROsXBH890UChK7mB8DSFf8nEvwqpvE9akfd8wQOGi0cXekcGLHzYp9FiscA==",
+      "requires": {
+        "lodash.debounce": "^4.0.8",
+        "lodash.throttle": "^4.1.1",
+        "raf-schd": "^4.0.2",
+        "resize-observer-polyfill": "^1.5.1"
+      }
+    },
     "react-router": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 574aaa3..fbc0224 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -149,6 +149,7 @@
     "react-loadable": "^5.5.0",
     "react-markdown": "^4.3.1",
     "react-redux": "^7.2.0",
+    "react-resize-detector": "^6.0.1-rc.1",
     "react-router-dom": "^5.1.2",
     "react-search-input": "^0.11.3",
     "react-select": "^3.1.0",
diff --git a/superset-frontend/src/explore/components/DatasourcePanel.tsx b/superset-frontend/src/explore/components/DatasourcePanel.tsx
index b185ea1..95b9d3f 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel.tsx
@@ -141,6 +141,24 @@ const DatasourceContainer = styled.div`
   }
 `;
 
+const LabelContainer = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+
+  & > span {
+    white-space: nowrap;
+  }
+
+  .option-label {
+    display: inline;
+  }
+
+  .metric-option > .option-label {
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+`;
+
 const DataSourcePanel = ({
   datasource,
   controls: { datasource: datasourceControl },
@@ -200,9 +218,9 @@ const DataSourcePanel = ({
               {t(`Showing %s of %s`, metricSlice.length, metrics.length)}
             </div>
             {metricSlice.map(m => (
-              <div key={m.metric_name} className="column">
+              <LabelContainer key={m.metric_name} className="column">
                 <MetricOption metric={m} showType />
-              </div>
+              </LabelContainer>
             ))}
           </Collapse.Panel>
           <Collapse.Panel
@@ -213,9 +231,9 @@ const DataSourcePanel = ({
               {t(`Showing %s of %s`, columnSlice.length, columns.length)}
             </div>
             {columnSlice.map(col => (
-              <div key={col.column_name} className="column">
+              <LabelContainer key={col.column_name} className="column">
                 <ColumnOption column={col} showType />
-              </div>
+              </LabelContainer>
             ))}
           </Collapse.Panel>
         </Collapse>
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx b/superset-frontend/src/explore/components/ExploreChartPanel.jsx
index a1e57a8..bb9bd60 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx
@@ -16,12 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useState, useEffect, useRef, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
 import PropTypes from 'prop-types';
 import Split from 'react-split';
-import { ParentSize } from '@vx/responsive';
 import { styled, useTheme } from '@superset-ui/core';
-import debounce from 'lodash/debounce';
+import { useResizeDetector } from 'react-resize-detector';
 import { chartPropShape } from 'src/dashboard/util/propShapes';
 import ChartContainer from 'src/chart/ChartContainer';
 import ConnectedExploreChartHeader from './ExploreChartHeader';
@@ -55,6 +54,7 @@ const propTypes = {
 const GUTTER_SIZE_FACTOR = 1.25;
 
 const CHART_PANEL_PADDING = 30;
+const HEADER_PADDING = 15;
 
 const INITIAL_SIZES = [90, 10];
 const MIN_SIZES = [300, 50];
@@ -104,20 +104,32 @@ const ExploreChartPanel = props => {
   const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
   const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
 
-  const panelHeadingRef = useRef(null);
+  const { height: hHeight, ref: headerRef } = useResizeDetector({
+    refreshMode: 'debounce',
+    refreshRate: 300,
+  });
+  const { width: chartWidth, ref: chartRef } = useResizeDetector({
+    refreshMode: 'debounce',
+    refreshRate: 300,
+  });
   const [splitSizes, setSplitSizes] = useState(INITIAL_SIZES);
 
   const calcSectionHeight = useCallback(
     percent => {
-      const headerHeight = props.standalone
-        ? 0
-        : panelHeadingRef?.current?.offsetHeight ?? 50;
+      let headerHeight;
+      if (props.standalone) {
+        headerHeight = 0;
+      } else if (hHeight) {
+        headerHeight = hHeight + HEADER_PADDING;
+      } else {
+        headerHeight = 50;
+      }
       const containerHeight = parseInt(props.height, 10) - headerHeight;
       return (
         (containerHeight * percent) / 100 - (gutterHeight / 2 + gutterMargin)
       );
     },
-    [gutterHeight, gutterMargin, props.height, props.standalone],
+    [gutterHeight, gutterMargin, props.height, props.standalone, hHeight],
   );
 
   const [tableSectionHeight, setTableSectionHeight] = useState(
@@ -132,15 +144,11 @@ const ExploreChartPanel = props => {
   );
 
   useEffect(() => {
-    const recalcSizes = debounce(() => recalcPanelSizes(splitSizes), 200);
-
-    window.addEventListener('resize', recalcSizes);
-    return () => window.removeEventListener('resize', recalcSizes);
-  }, [props.standalone, recalcPanelSizes, splitSizes]);
+    recalcPanelSizes(splitSizes);
+  }, [recalcPanelSizes, splitSizes]);
 
   const onDragEnd = sizes => {
     setSplitSizes(sizes);
-    recalcPanelSizes(sizes);
   };
 
   const onCollapseChange = openPanelName => {
@@ -154,42 +162,46 @@ const ExploreChartPanel = props => {
       ];
     }
     setSplitSizes(splitSizes);
-    recalcPanelSizes(splitSizes);
   };
 
-  const renderChart = () => {
+  const renderChart = useCallback(() => {
     const { chart } = props;
     const newHeight = calcSectionHeight(splitSizes[0]) - CHART_PANEL_PADDING;
     return (
-      <ParentSize>
-        {({ width }) =>
-          width > 0 && (
-            <ChartContainer
-              width={Math.floor(width)}
-              height={newHeight}
-              annotationData={chart.annotationData}
-              chartAlert={chart.chartAlert}
-              chartStackTrace={chart.chartStackTrace}
-              chartId={chart.id}
-              chartStatus={chart.chartStatus}
-              triggerRender={props.triggerRender}
-              datasource={props.datasource}
-              errorMessage={props.errorMessage}
-              formData={props.form_data}
-              onQuery={props.onQuery}
-              owners={props?.slice?.owners}
-              queriesResponse={chart.queriesResponse}
-              refreshOverlayVisible={props.refreshOverlayVisible}
-              setControlValue={props.actions.setControlValue}
-              timeout={props.timeout}
-              triggerQuery={chart.triggerQuery}
-              vizType={props.vizType}
-            />
-          )
-        }
-      </ParentSize>
+      chartWidth > 0 && (
+        <ChartContainer
+          width={Math.floor(chartWidth)}
+          height={newHeight}
+          annotationData={chart.annotationData}
+          chartAlert={chart.chartAlert}
+          chartStackTrace={chart.chartStackTrace}
+          chartId={chart.id}
+          chartStatus={chart.chartStatus}
+          triggerRender={props.triggerRender}
+          datasource={props.datasource}
+          errorMessage={props.errorMessage}
+          formData={props.form_data}
+          onQuery={props.onQuery}
+          owners={props?.slice?.owners}
+          queriesResponse={chart.queriesResponse}
+          refreshOverlayVisible={props.refreshOverlayVisible}
+          setControlValue={props.actions.setControlValue}
+          timeout={props.timeout}
+          triggerQuery={chart.triggerQuery}
+          vizType={props.vizType}
+        />
+      )
     );
-  };
+  }, [calcSectionHeight, chartWidth, props, splitSizes]);
+
+  const panelBody = useMemo(
+    () => (
+      <div className="panel-body" ref={chartRef}>
+        {renderChart()}
+      </div>
+    ),
+    [chartRef, renderChart],
+  );
 
   if (props.standalone) {
     // dom manipulation hack to get rid of the boostrap theme's body background
@@ -222,14 +234,12 @@ const ExploreChartPanel = props => {
     [dimension]: `calc(${elementSize}% - ${gutterSize + gutterMargin}px)`,
   });
 
-  const panelBody = <div className="panel-body">{renderChart()}</div>;
-
   return (
     <Styles
       className="panel panel-default chart-container"
       style={{ height: props.height }}
     >
-      <div className="panel-heading" ref={panelHeadingRef}>
+      <div className="panel-heading" ref={headerRef}>
         {header}
       </div>
       {props.vizType === 'filter_box' ? (
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer.jsx b/superset-frontend/src/explore/components/ExploreViewContainer.jsx
index 6599890..1a701df 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer.jsx
@@ -23,6 +23,7 @@ import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 import { styled, t, supersetTheme, css } from '@superset-ui/core';
 import { debounce } from 'lodash';
+import { Resizable } from 're-resizable';
 
 import { useDynamicPluginContext } from 'src/components/DynamicPlugins';
 import { Global } from '@emotion/core';
@@ -81,10 +82,8 @@ const Styles = styled.div`
   border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
   .explore-column {
     display: flex;
-    flex: 0 0 ${({ theme }) => theme.gridUnit * 95}px;
     flex-direction: column;
     padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
-    max-width: ${({ theme }) => theme.gridUnit * 95}px;
     max-height: 100%;
   }
   .data-source-selection {
@@ -404,7 +403,11 @@ function ExploreViewContainer(props) {
           dashboardId={props.dashboardId}
         />
       )}
-      <div
+      <Resizable
+        defaultSize={{ width: 300 }}
+        minWidth={300}
+        maxWidth="33%"
+        enable={{ right: true }}
         className={
           isCollapsed ? 'no-show' : 'explore-column data-source-selection'
         }
@@ -430,7 +433,7 @@ function ExploreViewContainer(props) {
           controls={props.controls}
           actions={props.actions}
         />
-      </div>
+      </Resizable>
       {isCollapsed ? (
         <div
           className="sidebar"
@@ -452,7 +455,13 @@ function ExploreViewContainer(props) {
           <Icon name="dataset-physical" width={16} />
         </div>
       ) : null}
-      <div className="col-sm-3 explore-column controls-column">
+      <Resizable
+        defaultSize={{ width: 320 }}
+        minWidth={320}
+        maxWidth="33%"
+        enable={{ right: true }}
+        className="col-sm-3 explore-column controls-column"
+      >
         <QueryAndSaveBtns
           canAdd={!!(props.can_add || props.can_overwrite)}
           onQuery={onQuery}
@@ -470,7 +479,7 @@ function ExploreViewContainer(props) {
           datasource_type={props.datasource_type}
           isDatasourceMetaLoading={props.isDatasourceMetaLoading}
         />
-      </div>
+      </Resizable>
       <div
         className={`main-explore-content ${
           isCollapsed ? 'col-sm-9' : 'col-sm-7'