diff --git a/.pylintrc b/.pylintrc
index 53a9b2ffd2f1a..49b3984d1943d 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -81,7 +81,7 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
-disable=long-builtin,dict-view-method,intern-builtin,suppressed-message,no-absolute-import,unpacking-in-except,apply-builtin,delslice-method,indexing-exception,old-raise-syntax,print-statement,cmp-builtin,reduce-builtin,useless-suppression,coerce-method,input-builtin,cmp-method,raw_input-builtin,nonzero-method,backtick,basestring-builtin,setslice-method,reload-builtin,oct-method,map-builtin-not-iterating,execfile-builtin,old-octal-literal,zip-builtin-not-iterating,buffer-builtin,getslice-method,metaclass-assignment,xrange-builtin,long-suffix,round-builtin,range-builtin-not-iterating,next-method-called,parameter-unpacking,unicode-builtin,unichr-builtin,import-star-module-level,raising-string,filter-builtin-not-iterating,using-cmp-argument,coerce-builtin,file-builtin,old-division,hex-method,missing-docstring,too-many-lines,ungrouped-imports,import-outside-toplevel,raise-missing-from,super-with-arguments,bad-option-value,too-few-public-methods
+disable=long-builtin,dict-view-method,intern-builtin,suppressed-message,no-absolute-import,unpacking-in-except,apply-builtin,delslice-method,indexing-exception,old-raise-syntax,print-statement,cmp-builtin,reduce-builtin,useless-suppression,coerce-method,input-builtin,cmp-method,raw_input-builtin,nonzero-method,backtick,basestring-builtin,setslice-method,reload-builtin,oct-method,map-builtin-not-iterating,execfile-builtin,old-octal-literal,zip-builtin-not-iterating,buffer-builtin,getslice-method,metaclass-assignment,xrange-builtin,long-suffix,round-builtin,range-builtin-not-iterating,next-method-called,parameter-unpacking,unicode-builtin,unichr-builtin,import-star-module-level,raising-string,filter-builtin-not-iterating,using-cmp-argument,coerce-builtin,file-builtin,old-division,hex-method,missing-docstring,too-many-lines,ungrouped-imports,import-outside-toplevel,raise-missing-from,super-with-arguments,bad-option-value,too-few-public-methods,too-many-locals
[REPORTS]
diff --git a/Dockerfile b/Dockerfile
index 9d6a61bef9b50..6ca89e889fd7d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -84,8 +84,8 @@ ENV LANG=C.UTF-8 \
SUPERSET_HOME="/app/superset_home" \
SUPERSET_PORT=8088
-RUN useradd --user-group --no-create-home --no-log-init --shell /bin/bash superset \
- && mkdir -p ${SUPERSET_HOME} ${PYTHONPATH} \
+RUN mkdir -p ${PYTHONPATH} \
+ && useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash superset \
&& apt-get update -y \
&& apt-get install -y --no-install-recommends \
build-essential \
diff --git a/docker-compose.yml b/docker-compose.yml
index 94300592093fe..8b94f70172227 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,7 @@
# limitations under the License.
#
x-superset-image: &superset-image apache/superset:latest-dev
+x-superset-user: &superset-user root
x-superset-depends-on: &superset-depends-on
- db
- redis
@@ -55,7 +56,7 @@ services:
restart: unless-stopped
ports:
- 8088:8088
- user: "root"
+ user: *superset-user
depends_on: *superset-depends-on
volumes: *superset-volumes
environment:
@@ -95,7 +96,7 @@ services:
command: ["/app/docker/docker-init.sh"]
env_file: docker/.env
depends_on: *superset-depends-on
- user: "root"
+ user: *superset-user
volumes: *superset-volumes
environment:
CYPRESS_CONFIG: "${CYPRESS_CONFIG}"
@@ -115,7 +116,7 @@ services:
env_file: docker/.env
restart: unless-stopped
depends_on: *superset-depends-on
- user: "root"
+ user: *superset-user
volumes: *superset-volumes
superset-worker-beat:
@@ -125,7 +126,7 @@ services:
env_file: docker/.env
restart: unless-stopped
depends_on: *superset-depends-on
- user: "root"
+ user: *superset-user
volumes: *superset-volumes
superset-tests-worker:
@@ -141,7 +142,7 @@ services:
REDIS_HOST: localhost
network_mode: host
depends_on: *superset-depends-on
- user: "root"
+ user: *superset-user
volumes: *superset-volumes
volumes:
diff --git a/docker/docker-bootstrap.sh b/docker/docker-bootstrap.sh
index b934640efb0c0..57163ccfe4c4c 100755
--- a/docker/docker-bootstrap.sh
+++ b/docker/docker-bootstrap.sh
@@ -41,7 +41,7 @@ if [[ "${1}" == "worker" ]]; then
celery worker --app=superset.tasks.celery_app:app -Ofair -l INFO
elif [[ "${1}" == "beat" ]]; then
echo "Starting Celery beat..."
- celery beat --app=superset.tasks.celery_app:app --pidfile /tmp/celerybeat.pid -l INFO
+ celery beat --app=superset.tasks.celery_app:app --pidfile /tmp/celerybeat.pid -l INFO -s "${SUPERSET_HOME}"/celerybeat-schedule
elif [[ "${1}" == "app" ]]; then
echo "Starting web app..."
flask run -p 8088 --with-threads --reload --debugger --host=0.0.0.0
diff --git a/docs/installation.rst b/docs/installation.rst
index 8d28bd0b811f7..edfedce43965d 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -433,7 +433,7 @@ For other strategies, check the `superset/tasks/cache.py` file.
Caching Thumbnails
------------------
-This is an optional feature that can be turned on by activating it's feature flag on config:
+This is an optional feature that can be turned on by activating its feature flag on config:
.. code-block:: python
@@ -972,7 +972,7 @@ environment variable: ::
Event Logging
-------------
-Superset by default logs special action event on it's database. These log can be accessed on the UI navigating to
+Superset by default logs special action event on its database. These logs can be accessed on the UI navigating to
"Security" -> "Action Log". You can freely customize these logs by implementing your own event log class.
Example of a simple JSON to Stdout class::
@@ -1256,6 +1256,38 @@ in this dictionary are made available for users to use in their SQL.
'my_crazy_macro': lambda x: x*2,
}
+Default values for jinja templates can be specified via ``Parameters`` menu in the SQL Lab user interface.
+In the UI you can assign a set of parameters as JSON
+
+.. code-block:: JSON
+ {
+ "my_table": "foo"
+ }
+
+The parameters become available in your SQL (example:SELECT * FROM {{ my_table }} ) by using Jinja templating syntax.
+SQL Lab template parameters are stored with the dataset as TEMPLATE PARAMETERS.
+
+There is a special ``_filters`` parameter which can be used to test filters used in the jinja template.
+
+.. code-block:: JSON
+ {
+ "_filters": [ {
+ "col": "action_type",
+ "op": "IN",
+ "val": ["sell", "buy"]
+ } ]
+ }
+
+.. code-block:: python
+ SELECT action, count(*) as times
+ FROM logs
+ WHERE
+ action in ({{ "'" + "','".join(filter_values('action_type')) + "'" }})
+ GROUP BY action
+
+Note ``_filters`` is not stored with the dataset. It's only used within the SQL Lab UI.
+
+
Besides default Jinja templating, SQL lab also supports self-defined template
processor by setting the ``CUSTOM_TEMPLATE_PROCESSORS`` in your superset configuration.
The values in this dictionary overwrite the default Jinja template processors of the
@@ -1326,7 +1358,7 @@ The available validators and names can be found in `sql_validators/`.
**Scheduling queries**
You can optionally allow your users to schedule queries directly in SQL Lab.
-This is done by addding extra metadata to saved queries, which are then picked
+This is done by adding extra metadata to saved queries, which are then picked
up by an external scheduled (like [Apache Airflow](https://airflow.apache.org/)).
To allow scheduled queries, add the following to your `config.py`:
diff --git a/docs/src/pages/docs/Connecting to Databases/mysql.mdx b/docs/src/pages/docs/Connecting to Databases/mysql.mdx
index 241eacee49dec..3db94f7bf21d5 100644
--- a/docs/src/pages/docs/Connecting to Databases/mysql.mdx
+++ b/docs/src/pages/docs/Connecting to Databases/mysql.mdx
@@ -8,12 +8,12 @@ version: 1
## MySQL
-The recommended connector library for MySQL is [mysql-connector-python](https://pypi.org/project/mysql-connector-python/).
+The recommended connector library for MySQL is `[mysqlclient](https://pypi.org/project/mysqlclient/)`.
Here's the connection string:
```
-mysql+mysqlconnector://{username}:{password}@{host}/{database}
+mysql://{username}:{password}@{host}/{database}
```
Host:
@@ -21,3 +21,9 @@ Host:
- For On Prem: IP address or Host name
- For Docker running in OSX: `docker.for.mac.host.internal`
Port: `3306` by default
+
+One problem with `mysqlclient` is that it will fail to connect to newer MySQL databases using `caching_sha2_password` for authentication, since the plugin is not included in the client. In this case, you should use `[mysql-connector-python](https://pypi.org/project/mysql-connector-python/)` instead:
+
+```
+mysql+mysqlconnector://{username}:{password}@{host}/{database}
+```
diff --git a/docs/src/pages/docs/installation/configuring.mdx b/docs/src/pages/docs/installation/configuring.mdx
index c725bf5950311..53340a5c9cdad 100644
--- a/docs/src/pages/docs/installation/configuring.mdx
+++ b/docs/src/pages/docs/installation/configuring.mdx
@@ -134,7 +134,7 @@ OAUTH_PROVIDERS = [
'access_token_headers':{ # Additional headers for calls to access_token_url
'Authorization': 'Basic Base64EncodedClientIdAndSecret'
},
- 'base_url':'https://myAuthorizationServer/oauth2AuthorizationServer/',
+ 'api_base_url':'https://myAuthorizationServer/oauth2AuthorizationServer/',
'access_token_url':'https://myAuthorizationServer/oauth2AuthorizationServer/token',
'authorize_url':'https://myAuthorizationServer/oauth2AuthorizationServer/authorize'
}
diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml
index 4fd7f59cd8da6..d06eff1859fda 100644
--- a/helm/superset/Chart.yaml
+++ b/helm/superset/Chart.yaml
@@ -22,7 +22,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
-version: 0.1.2
+version: 0.1.3
dependencies:
- name: postgresql
version: 10.2.0
diff --git a/helm/superset/templates/deployment-beat.yaml b/helm/superset/templates/deployment-beat.yaml
index 128bc6d767dc6..0ec76da15e88c 100644
--- a/helm/superset/templates/deployment-beat.yaml
+++ b/helm/superset/templates/deployment-beat.yaml
@@ -96,6 +96,10 @@ spec:
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
+{{- if .Values.imagePullSecrets }}
+ imagePullSecrets:
+{{ toYaml .Values.imagePullSecrets | indent 8 }}
+ {{- end }}
volumes:
- name: superset-config
secret:
diff --git a/helm/superset/templates/deployment-worker.yaml b/helm/superset/templates/deployment-worker.yaml
index 4001e76e920a5..fe0ce20d8580f 100644
--- a/helm/superset/templates/deployment-worker.yaml
+++ b/helm/superset/templates/deployment-worker.yaml
@@ -94,6 +94,10 @@ spec:
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
+{{- if .Values.imagePullSecrets }}
+ imagePullSecrets:
+{{ toYaml .Values.imagePullSecrets | indent 8 }}
+ {{- end }}
volumes:
- name: superset-config
secret:
diff --git a/helm/superset/templates/deployment.yaml b/helm/superset/templates/deployment.yaml
index 2a611ca8f5bec..8e807daf15e82 100644
--- a/helm/superset/templates/deployment.yaml
+++ b/helm/superset/templates/deployment.yaml
@@ -106,6 +106,11 @@ spec:
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
+{{- if .Values.imagePullSecrets }}
+ imagePullSecrets:
+{{ toYaml .Values.imagePullSecrets | indent 8 }}
+ {{- end }}
+
volumes:
- name: superset-config
secret:
diff --git a/helm/superset/templates/init-job.yaml b/helm/superset/templates/init-job.yaml
index 9a4530d3c8ee2..b3c4fd42f67a2 100644
--- a/helm/superset/templates/init-job.yaml
+++ b/helm/superset/templates/init-job.yaml
@@ -59,6 +59,10 @@ spec:
command: {{ tpl (toJson .Values.init.command) . }}
resources:
{{ toYaml .Values.init.resources | indent 10 }}
+{{- if .Values.imagePullSecrets }}
+ imagePullSecrets:
+{{ toYaml .Values.imagePullSecrets | indent 8 }}
+ {{- end }}
volumes:
- name: superset-config
secret:
diff --git a/helm/superset/values.yaml b/helm/superset/values.yaml
index 1a25419d16606..58e9faaef5fab 100644
--- a/helm/superset/values.yaml
+++ b/helm/superset/values.yaml
@@ -106,6 +106,9 @@ image:
tag: latest
pullPolicy: IfNotPresent
+imagePullSecrets: []
+
+
service:
type: ClusterIP
port: 8088
diff --git a/requirements/base.txt b/requirements/base.txt
index 47c234686adb2..0f433e0d66474 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -27,7 +27,7 @@ backoff==1.10.0
# via apache-superset
billiard==3.6.3.0
# via celery
-bleach==3.2.1
+bleach==3.3.0
# via apache-superset
brotli==1.0.9
# via flask-compress
@@ -56,7 +56,7 @@ cron-descriptor==1.2.24
# via apache-superset
croniter==0.3.36
# via apache-superset
-cryptography==3.2.1
+cryptography==3.3.2
# via apache-superset
decorator==4.4.2
# via retry
diff --git a/setup.py b/setup.py
index bffd2eb44a4f1..30e4c5479f7ec 100644
--- a/setup.py
+++ b/setup.py
@@ -73,7 +73,7 @@ def get_git_sha():
"contextlib2",
"croniter>=0.3.28",
"cron-descriptor",
- "cryptography>=3.2.1",
+ "cryptography>=3.3.2",
"flask>=1.1.0, <2.0.0",
"flask-appbuilder>=3.3.0, <4.0.0",
"flask-caching",
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts
index 6afe2c61900a8..375f14dacdd82 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/filter.test.ts
@@ -70,8 +70,8 @@ describe('Dashboard filter', () => {
}
expect(requestFilter).deep.eq({
col: 'region',
- op: '==',
- val: 'South Asia',
+ op: 'IN',
+ val: ['South Asia'],
});
});
});
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts
index d4ec42e0df3cf..85b8f51dee6c7 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/tabs.test.ts
@@ -121,8 +121,8 @@ describe('Dashboard tabs', () => {
const requestParams = JSON.parse(requestBody.form_data as string);
expect(requestParams.extra_filters[0]).deep.eq({
col: 'region',
- op: '==',
- val: 'South Asia',
+ op: 'IN',
+ val: ['South Asia'],
});
});
});
@@ -136,8 +136,8 @@ describe('Dashboard tabs', () => {
const requestParams = JSON.parse(requestBody.form_data as string);
expect(requestParams.extra_filters[0]).deep.eq({
col: 'region',
- op: '==',
- val: 'South Asia',
+ op: 'IN',
+ val: ['South Asia'],
});
expect(requestParams.viz_type).eq(LINE_CHART.viz);
});
@@ -150,8 +150,8 @@ describe('Dashboard tabs', () => {
cy.wait('@v1ChartData').then(({ request }) => {
expect(request.body.queries[0].filters[0]).deep.eq({
col: 'region',
- op: '==',
- val: 'South Asia',
+ op: 'IN',
+ val: ['South Asia'],
});
});
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts
index 61bdd7c6d9260..caff4c03f6755 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts
@@ -61,7 +61,6 @@ describe('dashboard filters card view', () => {
cy.get('.Select__menu').contains('Published').click({ timeout: 5000 });
cy.get('[data-test="styled-card"]').should('have.length', 2);
cy.get('[data-test="styled-card"]')
- .first()
.contains('USA Births Names')
.should('be.visible');
cy.get('.Select__control').eq(1).click();
@@ -107,13 +106,12 @@ describe('dashboard filters list view', () => {
cy.get('[data-test="table-row"]').should('not.exist');
});
- xit('should filter by published correctly', () => {
+ it('should filter by published correctly', () => {
// filter by published
cy.get('.Select__control').eq(2).click();
cy.get('.Select__menu').contains('Published').click();
cy.get('[data-test="table-row"]').should('have.length', 2);
cy.get('[data-test="table-row"]')
- .first()
.contains('USA Births Names')
.should('be.visible');
cy.get('.Select__control').eq(2).click();
diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts
index e8c1542300113..26660bc5fa677 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts
@@ -29,7 +29,7 @@ describe('Visualization > Line', () => {
it('should show validator error when no metric', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
cy.visitChartByParams(JSON.stringify(formData));
- cy.get('.ant-alert-warning').contains(`"Metrics" cannot be empty`);
+ cy.get('.ant-alert-warning').contains(`Metrics: cannot be empty`);
});
it('should preload mathjs', () => {
@@ -43,7 +43,7 @@ describe('Visualization > Line', () => {
it('should not show validator error when metric added', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
cy.visitChartByParams(JSON.stringify(formData));
- cy.get('.ant-alert-warning').contains(`"Metrics" cannot be empty`);
+ cy.get('.ant-alert-warning').contains(`Metrics: cannot be empty`);
cy.get('.text-danger').contains('Metrics');
cy.get('[data-test=metrics]')
@@ -62,6 +62,8 @@ describe('Visualization > Line', () => {
});
it('should allow negative values in Y bounds', () => {
+ const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
+ cy.visitChartByParams(JSON.stringify(formData));
cy.get('#controlSections-tab-display').click();
cy.get('span').contains('Y Axis Bounds').scrollIntoView();
cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 });
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 3cb402ae5d764..2649abee8eb27 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -15,35 +15,35 @@
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/cache": "^11.1.3",
"@emotion/react": "^11.1.5",
- "@superset-ui/chart-controls": "^0.17.50",
- "@superset-ui/core": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-calendar": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-chord": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-country-map": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-event-flow": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-force-directed": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-heatmap": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-histogram": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-horizon": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-map-box": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-partition": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-rose": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-sankey": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-sunburst": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-treemap": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-world-map": "^0.17.50",
- "@superset-ui/legacy-preset-chart-big-number": "^0.17.50",
- "@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
- "@superset-ui/legacy-preset-chart-nvd3": "^0.17.50",
- "@superset-ui/plugin-chart-echarts": "^0.17.50",
- "@superset-ui/plugin-chart-pivot-table": "^0.17.50",
- "@superset-ui/plugin-chart-table": "^0.17.50",
- "@superset-ui/plugin-chart-word-cloud": "^0.17.50",
- "@superset-ui/preset-chart-xy": "^0.17.50",
+ "@superset-ui/chart-controls": "^0.17.53",
+ "@superset-ui/core": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-calendar": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-chord": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-country-map": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-event-flow": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-force-directed": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-heatmap": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-histogram": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-horizon": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-map-box": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-partition": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-rose": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-sankey": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-sunburst": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-treemap": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-world-map": "^0.17.53",
+ "@superset-ui/legacy-preset-chart-big-number": "^0.17.53",
+ "@superset-ui/legacy-preset-chart-deckgl": "^0.4.7",
+ "@superset-ui/legacy-preset-chart-nvd3": "^0.17.53",
+ "@superset-ui/plugin-chart-echarts": "^0.17.53",
+ "@superset-ui/plugin-chart-pivot-table": "^0.17.53",
+ "@superset-ui/plugin-chart-table": "^0.17.53",
+ "@superset-ui/plugin-chart-word-cloud": "^0.17.53",
+ "@superset-ui/preset-chart-xy": "^0.17.53",
"@vx/responsive": "^0.0.195",
"abortcontroller-polyfill": "^1.1.9",
"antd": "^4.9.4",
@@ -102,6 +102,7 @@
"react-markdown": "^4.3.1",
"react-redux": "^7.2.0",
"react-resize-detector": "^6.0.1-rc.1",
+ "react-reverse-portal": "^2.0.1",
"react-router-dom": "^5.1.2",
"react-search-input": "^0.11.3",
"react-select": "^3.1.0",
@@ -14102,11 +14103,11 @@
}
},
"node_modules/@superset-ui/chart-controls": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.17.50.tgz",
- "integrity": "sha512-VVX8YxwYDcaD6pxfcxjDvwVA9pr34rzINNYYmumY3gCyWkfUCMs2oB11naavAbXqDOx93pD9sSfkR8GUEACahQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.17.53.tgz",
+ "integrity": "sha512-PjIDka4/lUwXUNEGjkQOIMwVWF2WfknqM6pKFNDPO0/nG4S4faQk96z/ABOXp8GYwIbBshnmmbmW4TCrCQ10Xw==",
"dependencies": {
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/core": "0.17.53",
"lodash": "^4.17.15",
"prop-types": "^15.7.2"
},
@@ -14118,9 +14119,9 @@
}
},
"node_modules/@superset-ui/core": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.17.50.tgz",
- "integrity": "sha512-YWCWZOHqsvXjzIGG+gKLJESsoSaobGcvIUQyQ+RN9nmqFJezBIlHenbsDVnn7eHN1jMOBUYTwmv5p9AojLslRw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.17.53.tgz",
+ "integrity": "sha512-2bIRrK3Y+4ZSNu6drc1EzHTq6fO3aWfdjCh43ytju88nlADHheQXgwxEKnmjzI141qxiVL2+oSL2kC6pSTkW8A==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"@emotion/cache": "^11.1.3",
@@ -14247,12 +14248,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-calendar": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.17.50.tgz",
- "integrity": "sha512-jLIYTStx04Jd2jZv7u8FZ9u+3Zf0bd/c1GjToG2w4VnbDc73eTEqiMhyJPGlaZuKABWP2pigEanmbpR0OTAD/g==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.17.53.tgz",
+ "integrity": "sha512-NLevYzzhQyRgP+vdEfhJyDxJIBbGM/bJTJfFw1iRllny3WQax6iU/X5hUw/iWZqruVNkwSnUA39+EGcjU1aIjg==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-array": "^2.0.3",
"d3-selection": "^1.4.0",
"d3-tip": "^0.9.1",
@@ -14271,24 +14272,24 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-chord": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.17.50.tgz",
- "integrity": "sha512-XUK6LnUBuhYAHsyqGId80a7f+vzXgVuiZfbFTRJy4M/uPNdIBKfxX1t4kKudHIlqKNoSV2pFIVwh+4h4KpmadQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.17.53.tgz",
+ "integrity": "sha512-a3Y8b/1nSuFvzEzUDTVVmad5/YjTBhz0qU2rcVGrdKp2kzuSVXVVljdN7KVisDUNHhYqrttLM8RQrqGw9f7x1A==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.6.2",
"react": "^16.13.1"
}
},
"node_modules/@superset-ui/legacy-plugin-chart-country-map": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.17.50.tgz",
- "integrity": "sha512-toxr09cCUI4Wari215323T8PL5YddCtnvliKVRA4+8UEEU9bnh+gQDIh++UqXq51dAR63czyr4kmxbu//JLDEg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.17.53.tgz",
+ "integrity": "sha512-zTImQdeBT8raXnxafBIHvaVqOqKoECfyDwgFlPKhs4M7EXPG7U8/VLg0Oi2dCA7/SFZA/ASrJwc/KxW399vJhw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-array": "^2.0.3",
"prop-types": "^15.6.2"
@@ -14303,13 +14304,13 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-event-flow": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.17.50.tgz",
- "integrity": "sha512-6Zq8CmIMCnqgcJJ8XSqWIexTZBbUG6lZto9isVnxBXLKiGAau6vMOLlWPZjyWPJETProVMnAc+CQm+YRhLI1TQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.17.53.tgz",
+ "integrity": "sha512-QYL0Feyfu7ZH1GeQ9sfEaEgnW2IQG93sJnM29NO53CjSvdbbZItfU9v6xVnAo6jMwcam7JLNYRtuIPgJevNThw==",
"dependencies": {
"@data-ui/event-flow": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"prop-types": "^15.6.2"
},
"peerDependencies": {
@@ -14317,12 +14318,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-force-directed": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.17.50.tgz",
- "integrity": "sha512-4KvRGdA2974MekvZ87ei/H5rP6MQooHB4PndLriRqqwtfwNs7LDlN3o/SRagKFj/8xvTxrZfprF0Kt+TO6Dk6A==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.17.53.tgz",
+ "integrity": "sha512-F7hkrBxC7EWrClQ1jb7anzj1SmIjqXVMz2JKhzwEUk++Tafnn0mrB7Yo51u3twFFOY5bwn+KcI1NObzBRkXguQ==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.7.2"
},
@@ -14331,12 +14332,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-heatmap": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.17.50.tgz",
- "integrity": "sha512-B/qt/z2ISVkiBRPoGDo4TVsur7QgFG3OKtIzjx6k+8KoRC1oWqeA2zTJAi55lp0bch5Mo4iIUwoyEY45T9nzOA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.17.53.tgz",
+ "integrity": "sha512-NXx/E3AiTxkL+qwaj8B0IDrhWo6P5u5EuXXx1xaWqMTH18YomyeA9l4NBPwsjCfhAMqrEeT0hzeY2/WSoPq5KQ==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-svg-legend": "^1.x",
"d3-tip": "^0.9.1",
@@ -14344,14 +14345,14 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-histogram": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.17.50.tgz",
- "integrity": "sha512-AO2VbdJERQfSAVTsCVOkCjkzaHOzFhTgyUQpuCNwaMt6sV9yzKR/G3QHYxrfzTT/2DxDEjAX+uV26b821VFA8A==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.17.53.tgz",
+ "integrity": "sha512-EQ/VvG+qCec+IqnwYHA90iHAjkhnPNGkKbTuKlsRyL3ONfxg3n6L4EQOlAA0HvELKkFAZXBxh8TA8Qc3j+g4Fw==",
"dependencies": {
"@data-ui/histogram": "^0.0.84",
"@data-ui/theme": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@vx/legend": "^0.0.198",
"@vx/responsive": "^0.0.199",
"@vx/scale": "^0.0.197",
@@ -14420,12 +14421,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-horizon": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.17.50.tgz",
- "integrity": "sha512-rfWJtCIITXqkYMW5Ue0MniqKYSQILtdVq8KDAtX2h5KuttDSi/2/ahBdwUmAs4rcjeSHH2FCaJONMAfAV0u1eA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.17.53.tgz",
+ "integrity": "sha512-LsM4HOuOkiabRNxMUjjietbFx99admne59Mm5zQdsRPNEpN/EKEWu8R4G4crSSqxxzD9KVnveRPE7OD0n91k/A==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-array": "^2.0.3",
"d3-scale": "^3.0.1",
"prop-types": "^15.6.2"
@@ -14455,12 +14456,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-map-box": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.17.50.tgz",
- "integrity": "sha512-9ksPlfBRQHqWuoktnpnRtR0N7l8FbZ0caBvK9I5+zuWDv8/rc5sunjkmDmAAJg3GSWfn7NzNS0spSk6YsfmKOQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.17.53.tgz",
+ "integrity": "sha512-JuM77arnxECuSiHkdLMry4JruuVTAfTKTtR8F4qGOpiYiXzGEv4K+y12eqBe1o94ckJF43Esz9e1fdPLDkjqTw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"immutable": "^3.8.2",
"mapbox-gl": "^0.53.0",
"prop-types": "^15.6.2",
@@ -14481,12 +14482,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-paired-t-test": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.17.50.tgz",
- "integrity": "sha512-20jRj92fsGm+fRzeQ8WHP4iFdczO9wMg9jok9OWtZHGUW0Sa3+YpbCW48of9qnqqf1Um1rqZu6PdmgKwevz1zg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.17.53.tgz",
+ "integrity": "sha512-QkRVm0XGoOxqOX0nRvHnGon2gG8MmV+dbBBpmPkmspxCWKrn183Wzq5SiMlM4vgo2HaroWUIPuBgLBd7rYZtGw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"distributions": "^1.0.0",
"prop-types": "^15.6.2",
"reactable": "^1.1.0"
@@ -14496,12 +14497,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-parallel-coordinates": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.17.50.tgz",
- "integrity": "sha512-fo8ASuix28TqTPNlVTBWXPXBo99sVpxXaCkXpb4cSnO6F6V6B9Kv9vSKIfV6KZL6ul1wQTe/xLjt/lyyX5HjHg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.17.53.tgz",
+ "integrity": "sha512-NcwuEd+rXfmwPshPby0jEgnJnbYfKruM7l0Hb3lIw6iMTc1IV21d1CMftQPvYYdwagam0FapBO2YcSvnvj2rDw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.7.2"
},
@@ -14510,12 +14511,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-partition": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.17.50.tgz",
- "integrity": "sha512-cHwGtxftPk+j1BowOL12fTuM+eAWbAbfxGF4t+W0X9/ZX5KFpHMYR0cd836nPgfhTtO9sSc4/W34dP0QIQA9hQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.17.53.tgz",
+ "integrity": "sha512-CTzKjaKCdT/+bFlXUDD4nXC2CO7mXmIPJ2K/M94rY2G2gdAWRZJ1i2HlcvTP+RY/AItzZm3C+E7hYdAQ6toBkA==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-hierarchy": "^1.1.8",
"prop-types": "^15.6.2"
@@ -14525,24 +14526,24 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-pivot-table": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.17.50.tgz",
- "integrity": "sha512-KolczfBy36AnXO445UF8uUDTb9enw9qy7wURr7RcMeBzLG0xN0P8vxEG/OU4NQ3MbbBBi7lYbTuTEYdZ/5wQgw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.17.53.tgz",
+ "integrity": "sha512-bk7mttnZFGgGmWCfj0kO++65XsMNyQJch0dgfRRnLVTlSnY89/kGqszTKybbCZhsbx4T5bJ+bn6hZKAGH+FnUA==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"datatables.net-bs": "^1.10.15",
"prop-types": "^15.6.2"
}
},
"node_modules/@superset-ui/legacy-plugin-chart-rose": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.17.50.tgz",
- "integrity": "sha512-DW8FKdR/dXlVsavxARnQZTB+hZ7B/C8Kwh+Kwa8Ji+qCJ5p30cH0xGQ9oX5Lgc15L01MIew4sLYaFdE6i41DCA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.17.53.tgz",
+ "integrity": "sha512-ppvQuKAS0rMhniKenLXSKczmAsHX4igYc0bVZAvfFDmLNW3tnlmivL+zYSw/sQ9PAhjMGDbTBlSio1oJ+91wiA==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"nvd3": "1.8.6",
"prop-types": "^15.6.2"
@@ -14552,12 +14553,12 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-sankey": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.17.50.tgz",
- "integrity": "sha512-d4kPCsY7nLZWhJDU1oXOC9+jwFoBWGvCI5n+GldJyhiDwxrp9+SBTZCx3ubmhgeI1HETeAD7C99DQ/neT5ttAA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.17.53.tgz",
+ "integrity": "sha512-3tvMghg5WUAq40su8cZrjJHoc/TsK1WWx6UFu+j2mPOh/BJJZb8wh7A63X82ubLdyzEqdjxsEs9pzZWzs7kUHw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-sankey": "^0.4.2",
"prop-types": "^15.6.2"
@@ -14567,47 +14568,47 @@
}
},
"node_modules/@superset-ui/legacy-plugin-chart-sankey-loop": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.17.50.tgz",
- "integrity": "sha512-AMgrl2Te24H5VoveHOjeHyhaI6tQtKo3EGuMb+RAHcgaBv1YRSaMx5LsR90qJtXlxMK1t4kRwG7mhqlE4kEP4Q==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.17.53.tgz",
+ "integrity": "sha512-60aGflqOi5+XDE3BR/p+Pw0xVp7OHsjwroX77CwkwBtFkw1AFVWczaTJH6CYeeCJZXCLYjrbc5OFMuaxIJ+j+Q==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-sankey-diagram": "^0.7.3",
"d3-selection": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"node_modules/@superset-ui/legacy-plugin-chart-sunburst": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.17.50.tgz",
- "integrity": "sha512-ZFN4s74tVtxMbHV6rGylup1tWbI3nwOMqFccuP9m/Nk78B2IMuU2ljZxE/2Zd87gbpg5NOH1442yKcbqyYt0kw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.17.53.tgz",
+ "integrity": "sha512-t0z7XPsDtDpnZ+fIpn57w9Vi3oWQ7ximDdjmag1WGhC6+dwR3XxEpNcicI6P6xfNX078RT8Iz89PZQBtagAAkA==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.6.2"
}
},
"node_modules/@superset-ui/legacy-plugin-chart-treemap": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.17.50.tgz",
- "integrity": "sha512-1W9kH/2rcpo6bZNM4A00mHdSd8BCFuatf7Z45yHGwUnl1monnNcam0d5YxLMgR4NZMYCW76S5ltRPY+2+/weGg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.17.53.tgz",
+ "integrity": "sha512-LV16Qwiz7ahfhCmuWIGk6f54KpdRJDAyLtr/ifFi8a2AcoG27Lf7hZZ3mCI9Jl5X6c7LLBmvAHfxdbBnLGa8+g==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-hierarchy": "^1.1.8",
"d3-selection": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"node_modules/@superset-ui/legacy-plugin-chart-world-map": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.17.50.tgz",
- "integrity": "sha512-HIomXGPRxI3sejPhW/ktMI673Qd47VrcwkNjsjJSlz8MrlwAMGWNynEwEYAaTmKbm16hoesNN2JuZSVGBAHX4w==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.17.53.tgz",
+ "integrity": "sha512-gnDBTyWPctqucyQzAObH6N+3f9GUQq9qpQ4cNbtvpIoVgXowYA5Q5dIfXBPnq525t78o3eiWqclTYf2Xcd62Kw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-array": "^2.4.0",
"d3-color": "^1.4.1",
@@ -14632,13 +14633,13 @@
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
},
"node_modules/@superset-ui/legacy-preset-chart-big-number": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.17.50.tgz",
- "integrity": "sha512-5ibPRV7LMzhUvFQ1WQgeBrcCaj8vmEk2Ocnuk+Kxk5cFFeOjF6Y/x2XbKz1PBwK1KTB4qC9OT3O6vJcTWHJjLQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.17.53.tgz",
+ "integrity": "sha512-HUlE6IZUjFvPMiXCj1cdRiR4avFLVhT5qwIQZk1l30kycl8/73rTm37Y/syBfZMPrfCrIW3nyReqfcnAaNqw9g==",
"dependencies": {
"@data-ui/xy-chart": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/d3-color": "^1.2.2",
"@types/shortid": "^0.0.29",
"d3-color": "^1.2.3",
@@ -14649,9 +14650,9 @@
}
},
"node_modules/@superset-ui/legacy-preset-chart-deckgl": {
- "version": "0.4.6",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.4.6.tgz",
- "integrity": "sha512-xXGNj7WQHMA+QpeiHMrinwWhOwskD9ucXoe10AfFFgar9TwvCE6wpgRwnoyF0hjoaXnMqpYyFbzlucCf3WSfVQ==",
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.4.7.tgz",
+ "integrity": "sha512-TaAX1PlZ5DhsNelgoOjCfPBlFtHZDFcozJEIAV2qXzXUo6rfIgskqIq4X3VbMuYnngZw5of4hAtOH1+Tgv+Wmw==",
"dependencies": {
"@math.gl/web-mercator": "^3.2.2",
"@types/d3-array": "^2.0.0",
@@ -14671,16 +14672,21 @@
"underscore": "^1.8.3",
"urijs": "^1.18.10",
"xss": "^1.0.6"
+ },
+ "peerDependencies": {
+ "@superset-ui/chart-controls": "^0.17.12",
+ "@superset-ui/core": "^0.17.11",
+ "react": "^15 || ^16"
}
},
"node_modules/@superset-ui/legacy-preset-chart-nvd3": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.17.50.tgz",
- "integrity": "sha512-jy2c37BXMQnSUc+pE9ZhiNiPNQHxweO+RD+gQcyF8DJ6Dn+woPE2DCfqjAdfVpUKuI4Y/FOM2lFlZT4xwGzsiA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.17.53.tgz",
+ "integrity": "sha512-wTbQRCZDrnb16tLJzXYbIiCFbHddRJ3fo5DKsbv6MFNrfOLWWx1SjAZ5C60e57u33XpKdTE5jGpEuGdq7BZ55w==",
"dependencies": {
"@data-ui/xy-chart": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^2.0.6",
@@ -14697,12 +14703,12 @@
}
},
"node_modules/@superset-ui/plugin-chart-echarts": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.17.50.tgz",
- "integrity": "sha512-Og5QRwmYZ66zyUjBsBPBU9JzL1zTtgsyS/opGhMDCBGBJbWmAPi8j6kSO2sVSzrexoTYWtPbOO261uMKzuGZ0w==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.17.53.tgz",
+ "integrity": "sha512-XXKqhr2CwZfi02qW55d9SQnNmdewTsAJT6xePBjci0SXAZRmi/T8vRbq2OCDJ7mQ0de7kjVBydAuOEEU/Y554A==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/mathjs": "^6.0.7",
"d3-array": "^1.2.0",
"echarts": "^5.1.1",
@@ -14714,25 +14720,25 @@
}
},
"node_modules/@superset-ui/plugin-chart-pivot-table": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-pivot-table/-/plugin-chart-pivot-table-0.17.50.tgz",
- "integrity": "sha512-WEa5lgJ4LbPb2v0MdHcDbDpnSN3khGmjvo2UV4G3AiJ6A47DLje8fhiYHkFhW7HfarozROYrwtcI/Rab6DYEyw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-pivot-table/-/plugin-chart-pivot-table-0.17.53.tgz",
+ "integrity": "sha512-18CTaM1sRgK5laFwHlKV+1A7+l9YWwPAvb7XrMjS8CQq0T2aEqNSQm7KWByG+LEj2x86idM8gaWghNes27yVtQ==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
- "@superset-ui/react-pivottable": "^0.12.6"
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
+ "@superset-ui/react-pivottable": "^0.12.8"
},
"peerDependencies": {
"react": "^16.13.1"
}
},
"node_modules/@superset-ui/plugin-chart-table": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.17.50.tgz",
- "integrity": "sha512-L8z9dm/0qPkvo4F8+lLb6yF0x2e7MaRxkZO9RGC0A9HYiMWEUc2Bg9Z+/QmLV6B9mA8qgYQbAeiglfBKC9h7nQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.17.53.tgz",
+ "integrity": "sha512-PSeL/zQSTvQyztjUMMm4U4G6oEM3xk3wkC4HTpuLEpjQ7qyGme39M1JeCGvNG4pPZRm0nO4pU+0U/36oR0lAjw==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/d3-array": "^2.9.0",
"@types/react-table": "^7.0.29",
"d3-array": "^2.4.0",
@@ -14758,12 +14764,12 @@
}
},
"node_modules/@superset-ui/plugin-chart-word-cloud": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.17.50.tgz",
- "integrity": "sha512-IudEfIEy4JxJhmSrn0nctI4pIJY/KORi9FrT2ZiBRWTh1SmFhZNM2d9vQixsgXq1+bHN9J3JpZ3G0+4sFOtwsw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.17.53.tgz",
+ "integrity": "sha512-lQTr9UpeoGgX1B0SkjrvtL0zjgYVoJbm6RVv8ELG+efCG1oYAoIVgw2sahJI4zLqNiHcNeWqHUcu7NK06uc4mA==",
"dependencies": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/d3-cloud": "^1.2.1",
"@types/d3-scale": "^2.0.2",
"d3-cloud": "^1.2.5",
@@ -14796,14 +14802,14 @@
}
},
"node_modules/@superset-ui/preset-chart-xy": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.17.50.tgz",
- "integrity": "sha512-pUhfI1aojKKDcrFB0aInkL7WmLlKdcOBhddsJ4+c5UfxpQZGdQ9VR9i0PPyvs8dbF4bRWyJNs31h9RHt7NOVSw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.17.53.tgz",
+ "integrity": "sha512-nmqif4Zd7Tdx4hLoDiiRiNFUFn1kliumjp9RQK68eMaefWcl1vTMT7nPmyFvgUH5390HJygpC3up50+j5Bngkg==",
"dependencies": {
"@data-ui/theme": "^0.0.84",
"@data-ui/xy-chart": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@vx/axis": "^0.0.198",
"@vx/legend": "^0.0.198",
"@vx/scale": "^0.0.197",
@@ -14923,9 +14929,9 @@
}
},
"node_modules/@superset-ui/react-pivottable": {
- "version": "0.12.6",
- "resolved": "https://registry.npmjs.org/@superset-ui/react-pivottable/-/react-pivottable-0.12.6.tgz",
- "integrity": "sha512-2+81WL4ocv4VFzgkj3wOBcEgejnJfsJ2D08kMvFfeBt6fhqC35nkendeZMAjl4bFBmzSJIFS6H+agjoeOUyq5A==",
+ "version": "0.12.8",
+ "resolved": "https://registry.npmjs.org/@superset-ui/react-pivottable/-/react-pivottable-0.12.8.tgz",
+ "integrity": "sha512-7DRxX/w1uSQE1pibSe64t1o+fmiP7ZWT2FJkjK510bSJm8NUIPCXtmpK+NKtNZuCteE9sqE7bQxd54SSq2xWKw==",
"dependencies": {
"immutability-helper": "^3.1.1",
"prop-types": "^15.7.2",
@@ -44732,6 +44738,15 @@
"resize-observer-polyfill": "^1.5.1"
}
},
+ "node_modules/react-reverse-portal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.0.1.tgz",
+ "integrity": "sha512-sj/D9nSHspqV8i8hWkTSZ5Ohnrqk2A5fkDKw4Xe/zV4OfF1UYwmbzrxLdmNRdKkWgQwnXIxaa2E3FC7QYdZAeA==",
+ "peerDependencies": {
+ "react": "^16.0.0",
+ "react-dom": "^16.0.0"
+ }
+ },
"node_modules/react-router": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
@@ -66270,19 +66285,19 @@
}
},
"@superset-ui/chart-controls": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.17.50.tgz",
- "integrity": "sha512-VVX8YxwYDcaD6pxfcxjDvwVA9pr34rzINNYYmumY3gCyWkfUCMs2oB11naavAbXqDOx93pD9sSfkR8GUEACahQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/chart-controls/-/chart-controls-0.17.53.tgz",
+ "integrity": "sha512-PjIDka4/lUwXUNEGjkQOIMwVWF2WfknqM6pKFNDPO0/nG4S4faQk96z/ABOXp8GYwIbBshnmmbmW4TCrCQ10Xw==",
"requires": {
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/core": "0.17.53",
"lodash": "^4.17.15",
"prop-types": "^15.7.2"
}
},
"@superset-ui/core": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.17.50.tgz",
- "integrity": "sha512-YWCWZOHqsvXjzIGG+gKLJESsoSaobGcvIUQyQ+RN9nmqFJezBIlHenbsDVnn7eHN1jMOBUYTwmv5p9AojLslRw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/core/-/core-0.17.53.tgz",
+ "integrity": "sha512-2bIRrK3Y+4ZSNu6drc1EzHTq6fO3aWfdjCh43ytju88nlADHheQXgwxEKnmjzI141qxiVL2+oSL2kC6pSTkW8A==",
"requires": {
"@babel/runtime": "^7.1.2",
"@emotion/cache": "^11.1.3",
@@ -66394,12 +66409,12 @@
}
},
"@superset-ui/legacy-plugin-chart-calendar": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.17.50.tgz",
- "integrity": "sha512-jLIYTStx04Jd2jZv7u8FZ9u+3Zf0bd/c1GjToG2w4VnbDc73eTEqiMhyJPGlaZuKABWP2pigEanmbpR0OTAD/g==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-calendar/-/legacy-plugin-chart-calendar-0.17.53.tgz",
+ "integrity": "sha512-NLevYzzhQyRgP+vdEfhJyDxJIBbGM/bJTJfFw1iRllny3WQax6iU/X5hUw/iWZqruVNkwSnUA39+EGcjU1aIjg==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-array": "^2.0.3",
"d3-selection": "^1.4.0",
"d3-tip": "^0.9.1",
@@ -66417,24 +66432,24 @@
}
},
"@superset-ui/legacy-plugin-chart-chord": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.17.50.tgz",
- "integrity": "sha512-XUK6LnUBuhYAHsyqGId80a7f+vzXgVuiZfbFTRJy4M/uPNdIBKfxX1t4kKudHIlqKNoSV2pFIVwh+4h4KpmadQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-chord/-/legacy-plugin-chart-chord-0.17.53.tgz",
+ "integrity": "sha512-a3Y8b/1nSuFvzEzUDTVVmad5/YjTBhz0qU2rcVGrdKp2kzuSVXVVljdN7KVisDUNHhYqrttLM8RQrqGw9f7x1A==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.6.2",
"react": "^16.13.1"
}
},
"@superset-ui/legacy-plugin-chart-country-map": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.17.50.tgz",
- "integrity": "sha512-toxr09cCUI4Wari215323T8PL5YddCtnvliKVRA4+8UEEU9bnh+gQDIh++UqXq51dAR63czyr4kmxbu//JLDEg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-country-map/-/legacy-plugin-chart-country-map-0.17.53.tgz",
+ "integrity": "sha512-zTImQdeBT8raXnxafBIHvaVqOqKoECfyDwgFlPKhs4M7EXPG7U8/VLg0Oi2dCA7/SFZA/ASrJwc/KxW399vJhw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-array": "^2.0.3",
"prop-types": "^15.6.2"
@@ -66451,34 +66466,34 @@
}
},
"@superset-ui/legacy-plugin-chart-event-flow": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.17.50.tgz",
- "integrity": "sha512-6Zq8CmIMCnqgcJJ8XSqWIexTZBbUG6lZto9isVnxBXLKiGAau6vMOLlWPZjyWPJETProVMnAc+CQm+YRhLI1TQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-event-flow/-/legacy-plugin-chart-event-flow-0.17.53.tgz",
+ "integrity": "sha512-QYL0Feyfu7ZH1GeQ9sfEaEgnW2IQG93sJnM29NO53CjSvdbbZItfU9v6xVnAo6jMwcam7JLNYRtuIPgJevNThw==",
"requires": {
"@data-ui/event-flow": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-force-directed": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.17.50.tgz",
- "integrity": "sha512-4KvRGdA2974MekvZ87ei/H5rP6MQooHB4PndLriRqqwtfwNs7LDlN3o/SRagKFj/8xvTxrZfprF0Kt+TO6Dk6A==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-force-directed/-/legacy-plugin-chart-force-directed-0.17.53.tgz",
+ "integrity": "sha512-F7hkrBxC7EWrClQ1jb7anzj1SmIjqXVMz2JKhzwEUk++Tafnn0mrB7Yo51u3twFFOY5bwn+KcI1NObzBRkXguQ==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.7.2"
}
},
"@superset-ui/legacy-plugin-chart-heatmap": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.17.50.tgz",
- "integrity": "sha512-B/qt/z2ISVkiBRPoGDo4TVsur7QgFG3OKtIzjx6k+8KoRC1oWqeA2zTJAi55lp0bch5Mo4iIUwoyEY45T9nzOA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-heatmap/-/legacy-plugin-chart-heatmap-0.17.53.tgz",
+ "integrity": "sha512-NXx/E3AiTxkL+qwaj8B0IDrhWo6P5u5EuXXx1xaWqMTH18YomyeA9l4NBPwsjCfhAMqrEeT0hzeY2/WSoPq5KQ==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-svg-legend": "^1.x",
"d3-tip": "^0.9.1",
@@ -66486,14 +66501,14 @@
}
},
"@superset-ui/legacy-plugin-chart-histogram": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.17.50.tgz",
- "integrity": "sha512-AO2VbdJERQfSAVTsCVOkCjkzaHOzFhTgyUQpuCNwaMt6sV9yzKR/G3QHYxrfzTT/2DxDEjAX+uV26b821VFA8A==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-histogram/-/legacy-plugin-chart-histogram-0.17.53.tgz",
+ "integrity": "sha512-EQ/VvG+qCec+IqnwYHA90iHAjkhnPNGkKbTuKlsRyL3ONfxg3n6L4EQOlAA0HvELKkFAZXBxh8TA8Qc3j+g4Fw==",
"requires": {
"@data-ui/histogram": "^0.0.84",
"@data-ui/theme": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@vx/legend": "^0.0.198",
"@vx/responsive": "^0.0.199",
"@vx/scale": "^0.0.197",
@@ -66561,12 +66576,12 @@
}
},
"@superset-ui/legacy-plugin-chart-horizon": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.17.50.tgz",
- "integrity": "sha512-rfWJtCIITXqkYMW5Ue0MniqKYSQILtdVq8KDAtX2h5KuttDSi/2/ahBdwUmAs4rcjeSHH2FCaJONMAfAV0u1eA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-horizon/-/legacy-plugin-chart-horizon-0.17.53.tgz",
+ "integrity": "sha512-LsM4HOuOkiabRNxMUjjietbFx99admne59Mm5zQdsRPNEpN/EKEWu8R4G4crSSqxxzD9KVnveRPE7OD0n91k/A==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-array": "^2.0.3",
"d3-scale": "^3.0.1",
"prop-types": "^15.6.2"
@@ -66595,12 +66610,12 @@
}
},
"@superset-ui/legacy-plugin-chart-map-box": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.17.50.tgz",
- "integrity": "sha512-9ksPlfBRQHqWuoktnpnRtR0N7l8FbZ0caBvK9I5+zuWDv8/rc5sunjkmDmAAJg3GSWfn7NzNS0spSk6YsfmKOQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-map-box/-/legacy-plugin-chart-map-box-0.17.53.tgz",
+ "integrity": "sha512-JuM77arnxECuSiHkdLMry4JruuVTAfTKTtR8F4qGOpiYiXzGEv4K+y12eqBe1o94ckJF43Esz9e1fdPLDkjqTw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"immutable": "^3.8.2",
"mapbox-gl": "^0.53.0",
"prop-types": "^15.6.2",
@@ -66617,118 +66632,118 @@
}
},
"@superset-ui/legacy-plugin-chart-paired-t-test": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.17.50.tgz",
- "integrity": "sha512-20jRj92fsGm+fRzeQ8WHP4iFdczO9wMg9jok9OWtZHGUW0Sa3+YpbCW48of9qnqqf1Um1rqZu6PdmgKwevz1zg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-paired-t-test/-/legacy-plugin-chart-paired-t-test-0.17.53.tgz",
+ "integrity": "sha512-QkRVm0XGoOxqOX0nRvHnGon2gG8MmV+dbBBpmPkmspxCWKrn183Wzq5SiMlM4vgo2HaroWUIPuBgLBd7rYZtGw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"distributions": "^1.0.0",
"prop-types": "^15.6.2",
"reactable": "^1.1.0"
}
},
"@superset-ui/legacy-plugin-chart-parallel-coordinates": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.17.50.tgz",
- "integrity": "sha512-fo8ASuix28TqTPNlVTBWXPXBo99sVpxXaCkXpb4cSnO6F6V6B9Kv9vSKIfV6KZL6ul1wQTe/xLjt/lyyX5HjHg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-parallel-coordinates/-/legacy-plugin-chart-parallel-coordinates-0.17.53.tgz",
+ "integrity": "sha512-NcwuEd+rXfmwPshPby0jEgnJnbYfKruM7l0Hb3lIw6iMTc1IV21d1CMftQPvYYdwagam0FapBO2YcSvnvj2rDw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.7.2"
}
},
"@superset-ui/legacy-plugin-chart-partition": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.17.50.tgz",
- "integrity": "sha512-cHwGtxftPk+j1BowOL12fTuM+eAWbAbfxGF4t+W0X9/ZX5KFpHMYR0cd836nPgfhTtO9sSc4/W34dP0QIQA9hQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-partition/-/legacy-plugin-chart-partition-0.17.53.tgz",
+ "integrity": "sha512-CTzKjaKCdT/+bFlXUDD4nXC2CO7mXmIPJ2K/M94rY2G2gdAWRZJ1i2HlcvTP+RY/AItzZm3C+E7hYdAQ6toBkA==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-hierarchy": "^1.1.8",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-pivot-table": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.17.50.tgz",
- "integrity": "sha512-KolczfBy36AnXO445UF8uUDTb9enw9qy7wURr7RcMeBzLG0xN0P8vxEG/OU4NQ3MbbBBi7lYbTuTEYdZ/5wQgw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-pivot-table/-/legacy-plugin-chart-pivot-table-0.17.53.tgz",
+ "integrity": "sha512-bk7mttnZFGgGmWCfj0kO++65XsMNyQJch0dgfRRnLVTlSnY89/kGqszTKybbCZhsbx4T5bJ+bn6hZKAGH+FnUA==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"datatables.net-bs": "^1.10.15",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-rose": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.17.50.tgz",
- "integrity": "sha512-DW8FKdR/dXlVsavxARnQZTB+hZ7B/C8Kwh+Kwa8Ji+qCJ5p30cH0xGQ9oX5Lgc15L01MIew4sLYaFdE6i41DCA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-rose/-/legacy-plugin-chart-rose-0.17.53.tgz",
+ "integrity": "sha512-ppvQuKAS0rMhniKenLXSKczmAsHX4igYc0bVZAvfFDmLNW3tnlmivL+zYSw/sQ9PAhjMGDbTBlSio1oJ+91wiA==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"nvd3": "1.8.6",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-sankey": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.17.50.tgz",
- "integrity": "sha512-d4kPCsY7nLZWhJDU1oXOC9+jwFoBWGvCI5n+GldJyhiDwxrp9+SBTZCx3ubmhgeI1HETeAD7C99DQ/neT5ttAA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey/-/legacy-plugin-chart-sankey-0.17.53.tgz",
+ "integrity": "sha512-3tvMghg5WUAq40su8cZrjJHoc/TsK1WWx6UFu+j2mPOh/BJJZb8wh7A63X82ubLdyzEqdjxsEs9pzZWzs7kUHw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-sankey": "^0.4.2",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-sankey-loop": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.17.50.tgz",
- "integrity": "sha512-AMgrl2Te24H5VoveHOjeHyhaI6tQtKo3EGuMb+RAHcgaBv1YRSaMx5LsR90qJtXlxMK1t4kRwG7mhqlE4kEP4Q==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sankey-loop/-/legacy-plugin-chart-sankey-loop-0.17.53.tgz",
+ "integrity": "sha512-60aGflqOi5+XDE3BR/p+Pw0xVp7OHsjwroX77CwkwBtFkw1AFVWczaTJH6CYeeCJZXCLYjrbc5OFMuaxIJ+j+Q==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-sankey-diagram": "^0.7.3",
"d3-selection": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-sunburst": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.17.50.tgz",
- "integrity": "sha512-ZFN4s74tVtxMbHV6rGylup1tWbI3nwOMqFccuP9m/Nk78B2IMuU2ljZxE/2Zd87gbpg5NOH1442yKcbqyYt0kw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-sunburst/-/legacy-plugin-chart-sunburst-0.17.53.tgz",
+ "integrity": "sha512-t0z7XPsDtDpnZ+fIpn57w9Vi3oWQ7ximDdjmag1WGhC6+dwR3XxEpNcicI6P6xfNX078RT8Iz89PZQBtagAAkA==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-treemap": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.17.50.tgz",
- "integrity": "sha512-1W9kH/2rcpo6bZNM4A00mHdSd8BCFuatf7Z45yHGwUnl1monnNcam0d5YxLMgR4NZMYCW76S5ltRPY+2+/weGg==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-treemap/-/legacy-plugin-chart-treemap-0.17.53.tgz",
+ "integrity": "sha512-LV16Qwiz7ahfhCmuWIGk6f54KpdRJDAyLtr/ifFi8a2AcoG27Lf7hZZ3mCI9Jl5X6c7LLBmvAHfxdbBnLGa8+g==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3-hierarchy": "^1.1.8",
"d3-selection": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"@superset-ui/legacy-plugin-chart-world-map": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.17.50.tgz",
- "integrity": "sha512-HIomXGPRxI3sejPhW/ktMI673Qd47VrcwkNjsjJSlz8MrlwAMGWNynEwEYAaTmKbm16hoesNN2JuZSVGBAHX4w==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-plugin-chart-world-map/-/legacy-plugin-chart-world-map-0.17.53.tgz",
+ "integrity": "sha512-gnDBTyWPctqucyQzAObH6N+3f9GUQq9qpQ4cNbtvpIoVgXowYA5Q5dIfXBPnq525t78o3eiWqclTYf2Xcd62Kw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-array": "^2.4.0",
"d3-color": "^1.4.1",
@@ -66752,13 +66767,13 @@
}
},
"@superset-ui/legacy-preset-chart-big-number": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.17.50.tgz",
- "integrity": "sha512-5ibPRV7LMzhUvFQ1WQgeBrcCaj8vmEk2Ocnuk+Kxk5cFFeOjF6Y/x2XbKz1PBwK1KTB4qC9OT3O6vJcTWHJjLQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-big-number/-/legacy-preset-chart-big-number-0.17.53.tgz",
+ "integrity": "sha512-HUlE6IZUjFvPMiXCj1cdRiR4avFLVhT5qwIQZk1l30kycl8/73rTm37Y/syBfZMPrfCrIW3nyReqfcnAaNqw9g==",
"requires": {
"@data-ui/xy-chart": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/d3-color": "^1.2.2",
"@types/shortid": "^0.0.29",
"d3-color": "^1.2.3",
@@ -66766,9 +66781,9 @@
}
},
"@superset-ui/legacy-preset-chart-deckgl": {
- "version": "0.4.6",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.4.6.tgz",
- "integrity": "sha512-xXGNj7WQHMA+QpeiHMrinwWhOwskD9ucXoe10AfFFgar9TwvCE6wpgRwnoyF0hjoaXnMqpYyFbzlucCf3WSfVQ==",
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-deckgl/-/legacy-preset-chart-deckgl-0.4.7.tgz",
+ "integrity": "sha512-TaAX1PlZ5DhsNelgoOjCfPBlFtHZDFcozJEIAV2qXzXUo6rfIgskqIq4X3VbMuYnngZw5of4hAtOH1+Tgv+Wmw==",
"requires": {
"@math.gl/web-mercator": "^3.2.2",
"@types/d3-array": "^2.0.0",
@@ -66791,13 +66806,13 @@
}
},
"@superset-ui/legacy-preset-chart-nvd3": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.17.50.tgz",
- "integrity": "sha512-jy2c37BXMQnSUc+pE9ZhiNiPNQHxweO+RD+gQcyF8DJ6Dn+woPE2DCfqjAdfVpUKuI4Y/FOM2lFlZT4xwGzsiA==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/legacy-preset-chart-nvd3/-/legacy-preset-chart-nvd3-0.17.53.tgz",
+ "integrity": "sha512-wTbQRCZDrnb16tLJzXYbIiCFbHddRJ3fo5DKsbv6MFNrfOLWWx1SjAZ5C60e57u33XpKdTE5jGpEuGdq7BZ55w==",
"requires": {
"@data-ui/xy-chart": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^2.0.6",
@@ -66811,12 +66826,12 @@
}
},
"@superset-ui/plugin-chart-echarts": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.17.50.tgz",
- "integrity": "sha512-Og5QRwmYZ66zyUjBsBPBU9JzL1zTtgsyS/opGhMDCBGBJbWmAPi8j6kSO2sVSzrexoTYWtPbOO261uMKzuGZ0w==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-echarts/-/plugin-chart-echarts-0.17.53.tgz",
+ "integrity": "sha512-XXKqhr2CwZfi02qW55d9SQnNmdewTsAJT6xePBjci0SXAZRmi/T8vRbq2OCDJ7mQ0de7kjVBydAuOEEU/Y554A==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/mathjs": "^6.0.7",
"d3-array": "^1.2.0",
"echarts": "^5.1.1",
@@ -66825,22 +66840,22 @@
}
},
"@superset-ui/plugin-chart-pivot-table": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-pivot-table/-/plugin-chart-pivot-table-0.17.50.tgz",
- "integrity": "sha512-WEa5lgJ4LbPb2v0MdHcDbDpnSN3khGmjvo2UV4G3AiJ6A47DLje8fhiYHkFhW7HfarozROYrwtcI/Rab6DYEyw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-pivot-table/-/plugin-chart-pivot-table-0.17.53.tgz",
+ "integrity": "sha512-18CTaM1sRgK5laFwHlKV+1A7+l9YWwPAvb7XrMjS8CQq0T2aEqNSQm7KWByG+LEj2x86idM8gaWghNes27yVtQ==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
- "@superset-ui/react-pivottable": "^0.12.6"
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
+ "@superset-ui/react-pivottable": "^0.12.8"
}
},
"@superset-ui/plugin-chart-table": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.17.50.tgz",
- "integrity": "sha512-L8z9dm/0qPkvo4F8+lLb6yF0x2e7MaRxkZO9RGC0A9HYiMWEUc2Bg9Z+/QmLV6B9mA8qgYQbAeiglfBKC9h7nQ==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-table/-/plugin-chart-table-0.17.53.tgz",
+ "integrity": "sha512-PSeL/zQSTvQyztjUMMm4U4G6oEM3xk3wkC4HTpuLEpjQ7qyGme39M1JeCGvNG4pPZRm0nO4pU+0U/36oR0lAjw==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/d3-array": "^2.9.0",
"@types/react-table": "^7.0.29",
"d3-array": "^2.4.0",
@@ -66862,12 +66877,12 @@
}
},
"@superset-ui/plugin-chart-word-cloud": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.17.50.tgz",
- "integrity": "sha512-IudEfIEy4JxJhmSrn0nctI4pIJY/KORi9FrT2ZiBRWTh1SmFhZNM2d9vQixsgXq1+bHN9J3JpZ3G0+4sFOtwsw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/plugin-chart-word-cloud/-/plugin-chart-word-cloud-0.17.53.tgz",
+ "integrity": "sha512-lQTr9UpeoGgX1B0SkjrvtL0zjgYVoJbm6RVv8ELG+efCG1oYAoIVgw2sahJI4zLqNiHcNeWqHUcu7NK06uc4mA==",
"requires": {
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@types/d3-cloud": "^1.2.1",
"@types/d3-scale": "^2.0.2",
"d3-cloud": "^1.2.5",
@@ -66898,14 +66913,14 @@
}
},
"@superset-ui/preset-chart-xy": {
- "version": "0.17.50",
- "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.17.50.tgz",
- "integrity": "sha512-pUhfI1aojKKDcrFB0aInkL7WmLlKdcOBhddsJ4+c5UfxpQZGdQ9VR9i0PPyvs8dbF4bRWyJNs31h9RHt7NOVSw==",
+ "version": "0.17.53",
+ "resolved": "https://registry.npmjs.org/@superset-ui/preset-chart-xy/-/preset-chart-xy-0.17.53.tgz",
+ "integrity": "sha512-nmqif4Zd7Tdx4hLoDiiRiNFUFn1kliumjp9RQK68eMaefWcl1vTMT7nPmyFvgUH5390HJygpC3up50+j5Bngkg==",
"requires": {
"@data-ui/theme": "^0.0.84",
"@data-ui/xy-chart": "^0.0.84",
- "@superset-ui/chart-controls": "0.17.50",
- "@superset-ui/core": "0.17.50",
+ "@superset-ui/chart-controls": "0.17.53",
+ "@superset-ui/core": "0.17.53",
"@vx/axis": "^0.0.198",
"@vx/legend": "^0.0.198",
"@vx/scale": "^0.0.197",
@@ -67024,9 +67039,9 @@
}
},
"@superset-ui/react-pivottable": {
- "version": "0.12.6",
- "resolved": "https://registry.npmjs.org/@superset-ui/react-pivottable/-/react-pivottable-0.12.6.tgz",
- "integrity": "sha512-2+81WL4ocv4VFzgkj3wOBcEgejnJfsJ2D08kMvFfeBt6fhqC35nkendeZMAjl4bFBmzSJIFS6H+agjoeOUyq5A==",
+ "version": "0.12.8",
+ "resolved": "https://registry.npmjs.org/@superset-ui/react-pivottable/-/react-pivottable-0.12.8.tgz",
+ "integrity": "sha512-7DRxX/w1uSQE1pibSe64t1o+fmiP7ZWT2FJkjK510bSJm8NUIPCXtmpK+NKtNZuCteE9sqE7bQxd54SSq2xWKw==",
"requires": {
"immutability-helper": "^3.1.1",
"prop-types": "^15.7.2",
@@ -92323,6 +92338,12 @@
"resize-observer-polyfill": "^1.5.1"
}
},
+ "react-reverse-portal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.0.1.tgz",
+ "integrity": "sha512-sj/D9nSHspqV8i8hWkTSZ5Ohnrqk2A5fkDKw4Xe/zV4OfF1UYwmbzrxLdmNRdKkWgQwnXIxaa2E3FC7QYdZAeA==",
+ "requires": {}
+ },
"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 deae9ee89d24a..120644c2b81f0 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -67,35 +67,35 @@
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/cache": "^11.1.3",
"@emotion/react": "^11.1.5",
- "@superset-ui/chart-controls": "^0.17.50",
- "@superset-ui/core": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-calendar": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-chord": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-country-map": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-event-flow": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-force-directed": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-heatmap": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-histogram": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-horizon": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-map-box": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-partition": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-rose": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-sankey": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-sunburst": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-treemap": "^0.17.50",
- "@superset-ui/legacy-plugin-chart-world-map": "^0.17.50",
- "@superset-ui/legacy-preset-chart-big-number": "^0.17.50",
- "@superset-ui/legacy-preset-chart-deckgl": "^0.4.6",
- "@superset-ui/legacy-preset-chart-nvd3": "^0.17.50",
- "@superset-ui/plugin-chart-echarts": "^0.17.50",
- "@superset-ui/plugin-chart-pivot-table": "^0.17.50",
- "@superset-ui/plugin-chart-table": "^0.17.50",
- "@superset-ui/plugin-chart-word-cloud": "^0.17.50",
- "@superset-ui/preset-chart-xy": "^0.17.50",
+ "@superset-ui/chart-controls": "^0.17.53",
+ "@superset-ui/core": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-calendar": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-chord": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-country-map": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-event-flow": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-force-directed": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-heatmap": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-histogram": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-horizon": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-map-box": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-partition": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-rose": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-sankey": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-sunburst": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-treemap": "^0.17.53",
+ "@superset-ui/legacy-plugin-chart-world-map": "^0.17.53",
+ "@superset-ui/legacy-preset-chart-big-number": "^0.17.53",
+ "@superset-ui/legacy-preset-chart-deckgl": "^0.4.7",
+ "@superset-ui/legacy-preset-chart-nvd3": "^0.17.53",
+ "@superset-ui/plugin-chart-echarts": "^0.17.53",
+ "@superset-ui/plugin-chart-pivot-table": "^0.17.53",
+ "@superset-ui/plugin-chart-table": "^0.17.53",
+ "@superset-ui/plugin-chart-word-cloud": "^0.17.53",
+ "@superset-ui/preset-chart-xy": "^0.17.53",
"@vx/responsive": "^0.0.195",
"abortcontroller-polyfill": "^1.1.9",
"antd": "^4.9.4",
@@ -154,6 +154,7 @@
"react-markdown": "^4.3.1",
"react-redux": "^7.2.0",
"react-resize-detector": "^6.0.1-rc.1",
+ "react-reverse-portal": "^2.0.1",
"react-router-dom": "^5.1.2",
"react-search-input": "^0.11.3",
"react-select": "^3.1.0",
diff --git a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
index 66eaf20fdbb43..20f63a6fcabd9 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
@@ -50,16 +50,21 @@ jest.mock('src/dashboard/actions/dashboardState');
describe('DashboardBuilder', () => {
let favStarStub;
+ let focusedTabStub;
beforeAll(() => {
// this is invoked on mount, so we stub it instead of making a request
favStarStub = sinon
.stub(dashboardStateActions, 'fetchFaveStar')
.returns({ type: 'mock-action' });
+ focusedTabStub = sinon
+ .stub(dashboardStateActions, 'setLastFocusedTab')
+ .returns({ type: 'mock-action' });
});
afterAll(() => {
favStarStub.restore();
+ focusedTabStub.restore();
});
function setup(overrideState = {}, overrideStore) {
diff --git a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx
index efc1d8ca91aa5..2a1d0bc98b2bb 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/ChartHolder_spec.jsx
@@ -36,6 +36,7 @@ import { sliceId } from 'spec/fixtures/mockChartQueries';
import dashboardInfo from 'spec/fixtures/mockDashboardInfo';
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
import { sliceEntitiesForChart } from 'spec/fixtures/mockSliceEntities';
+import { nativeFiltersInfo } from '../../fixtures/mockNativeFilters';
describe('ChartHolder', () => {
const props = {
@@ -55,6 +56,7 @@ describe('ChartHolder', () => {
handleComponentDrop() {},
updateComponents() {},
deleteComponent() {},
+ nativeFilters: nativeFiltersInfo.filters,
};
function setup(overrideProps) {
diff --git a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
index 4b2dbd9a5316e..2f15359c7f192 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Tabs_spec.jsx
@@ -33,8 +33,10 @@ import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
import Tabs from 'src/dashboard/components/gridComponents/Tabs';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
+import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
import { dashboardLayoutWithTabs } from 'spec/fixtures/mockDashboardLayout';
import { mockStoreWithTabs } from 'spec/fixtures/mockStore';
+import { nativeFilters } from 'spec/fixtures/mockNativeFilters';
describe('Tabs', () => {
fetchMock.post('glob:*/r/shortner/', {});
@@ -59,6 +61,8 @@ describe('Tabs', () => {
deleteComponent() {},
updateComponents() {},
logEvent() {},
+ dashboardLayout: emptyDashboardLayout,
+ nativeFilters: nativeFilters.filters,
};
function setup(overrideProps) {
diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx
index 4b672b4a842a6..5c355109676d1 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx
@@ -110,7 +110,7 @@ describe('FiltersConfigModal', () => {
function addFilter() {
act(() => {
- wrapper.find('button[aria-label="Add tab"]').at(0).simulate('click');
+ wrapper.find('[aria-label="Add filter"]').at(0).simulate('click');
});
}
diff --git a/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx b/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx
index a9e1454b0e4fa..2ab5ea296bd59 100644
--- a/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx
+++ b/superset-frontend/spec/javascripts/datasource/DatasourceEditor_spec.jsx
@@ -86,6 +86,7 @@ describe('DatasourceEditor', () => {
nullable: true,
default: '',
primary_key: false,
+ is_dttm: true,
},
{
name: 'gender',
@@ -93,6 +94,7 @@ describe('DatasourceEditor', () => {
nullable: true,
default: '',
primary_key: false,
+ is_dttm: false,
},
{
name: 'new_column',
@@ -100,6 +102,7 @@ describe('DatasourceEditor', () => {
nullable: true,
default: '',
primary_key: false,
+ is_dttm: false,
},
];
diff --git a/superset-frontend/spec/javascripts/explore/controlUtils_spec.tsx b/superset-frontend/spec/javascripts/explore/controlUtils_spec.tsx
index 7c4a69d9aef89..f50495602342f 100644
--- a/superset-frontend/spec/javascripts/explore/controlUtils_spec.tsx
+++ b/superset-frontend/spec/javascripts/explore/controlUtils_spec.tsx
@@ -149,30 +149,14 @@ describe('controlUtils', () => {
expect(control).toBeNull();
});
- it('applies the default function for metrics', () => {
+ it('metrics control should be empty by default', () => {
const control = getControlState('metrics', 'table', state);
- expect(control?.default).toEqual(['first']);
+ expect(control?.default).toBeUndefined();
});
- it('applies the default function for metric', () => {
+ it('metric control should be empty by default', () => {
const control = getControlState('metric', 'table', state);
- expect(control?.default).toEqual('first');
- });
-
- it('applies the default function, prefers count if it exists', () => {
- const stateWithCount = {
- ...state,
- datasource: {
- ...(state.datasource as DatasourceMeta),
- metrics: [
- { metric_name: 'first' },
- { metric_name: 'second' },
- { metric_name: 'count' },
- ],
- },
- };
- const control = getControlState('metrics', 'table', stateWithCount);
- expect(control?.default).toEqual(['count']);
+ expect(control?.default).toBeUndefined();
});
it('should not apply mapStateToProps when initializing', () => {
@@ -180,7 +164,6 @@ describe('controlUtils', () => {
...state,
controls: undefined,
});
- expect(typeof control?.default).toBe('function');
expect(control?.value).toBe(undefined);
});
});
diff --git a/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx b/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx
index d55720637b637..8c07008815aeb 100644
--- a/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/TableElement_spec.jsx
@@ -43,7 +43,7 @@ describe('TableElement', () => {
it('renders with props', () => {
expect(React.isValidElement( )).toBe(true);
});
- it('has 4 IconTooltip elements', () => {
+ it('has 5 IconTooltip elements', () => {
const wrapper = mount(
@@ -55,14 +55,14 @@ describe('TableElement', () => {
},
},
);
- expect(wrapper.find(IconTooltip)).toHaveLength(4);
+ expect(wrapper.find(IconTooltip)).toHaveLength(5);
});
it('has 14 columns', () => {
const wrapper = shallow( );
expect(wrapper.find(ColumnElement)).toHaveLength(14);
});
it('mounts', () => {
- mount(
+ const wrapper = mount(
,
@@ -73,6 +73,8 @@ describe('TableElement', () => {
},
},
);
+
+ expect(wrapper.find(TableElement)).toHaveLength(1);
});
it('fades table', async () => {
const wrapper = mount(
@@ -86,13 +88,11 @@ describe('TableElement', () => {
},
},
);
- expect(wrapper.find(TableElement).state().hovered).toBe(false);
expect(wrapper.find('[data-test="fade"]').first().props().hovered).toBe(
false,
);
wrapper.find('.header-container').hostNodes().simulate('mouseEnter');
await waitForComponentToPaint(wrapper, 300);
- expect(wrapper.find(TableElement).state().hovered).toBe(true);
expect(wrapper.find('[data-test="fade"]').first().props().hovered).toBe(
true,
);
@@ -111,12 +111,22 @@ describe('TableElement', () => {
},
},
);
- expect(wrapper.find(TableElement).state().sortColumns).toBe(false);
+ expect(
+ wrapper.find(IconTooltip).at(2).hasClass('fa-sort-alpha-asc'),
+ ).toEqual(true);
+ expect(
+ wrapper.find(IconTooltip).at(2).hasClass('fa-sort-numeric-asc'),
+ ).toEqual(false);
wrapper.find('.header-container').hostNodes().simulate('click');
expect(wrapper.find(ColumnElement).first().props().column.name).toBe('id');
wrapper.find('.header-container').simulate('mouseEnter');
wrapper.find('.sort-cols').hostNodes().simulate('click');
- expect(wrapper.find(TableElement).state().sortColumns).toBe(true);
+ expect(
+ wrapper.find(IconTooltip).at(2).hasClass('fa-sort-numeric-asc'),
+ ).toEqual(true);
+ expect(
+ wrapper.find(IconTooltip).at(2).hasClass('fa-sort-alpha-asc'),
+ ).toEqual(false);
expect(wrapper.find(ColumnElement).first().props().column.name).toBe(
'active',
);
diff --git a/superset-frontend/src/SqlLab/components/QueryTable.jsx b/superset-frontend/src/SqlLab/components/QueryTable/index.jsx
similarity index 66%
rename from superset-frontend/src/SqlLab/components/QueryTable.jsx
rename to superset-frontend/src/SqlLab/components/QueryTable/index.jsx
index e9414e59c2169..b3bbed99cdba0 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable.jsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.jsx
@@ -22,16 +22,18 @@ import moment from 'moment';
import Card from 'src/components/Card';
import ProgressBar from 'src/components/ProgressBar';
import Label from 'src/components/Label';
-import { t, css } from '@superset-ui/core';
+import { t, styled } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import TableView from 'src/components/TableView';
import Button from 'src/components/Button';
import { fDuration } from 'src/modules/dates';
-import { IconTooltip } from '../../components/IconTooltip';
-import ResultSet from './ResultSet';
-import ModalTrigger from '../../components/ModalTrigger';
-import HighlightedSql from './HighlightedSql';
-import QueryStateLabel from './QueryStateLabel';
+import Icons from 'src/components/Icons';
+import Icon from 'src/components/Icon';
+import { Tooltip } from 'src/components/Tooltip';
+import ResultSet from '../ResultSet';
+import ModalTrigger from '../../../components/ModalTrigger';
+import HighlightedSql from '../HighlightedSql';
+import { StaticPosition, verticalAlign, StyledTooltip } from './styles';
const propTypes = {
columns: PropTypes.array,
@@ -53,16 +55,78 @@ const openQuery = id => {
window.open(url);
};
-const StaticPosition = css`
- position: static;
+const statusAttributes = {
+ success: {
+ color: ({ theme }) => theme.colors.success.base,
+ config: {
+ name: 'check',
+ label: t('Success'),
+ status: 'success',
+ },
+ },
+ failed: {
+ color: ({ theme }) => theme.colors.error.base,
+ config: {
+ name: 'x-small',
+ label: t('Failed'),
+ status: 'failed',
+ },
+ },
+ stopped: {
+ color: ({ theme }) => theme.colors.error.base,
+ config: {
+ name: 'x-small',
+ label: t('Failed'),
+ status: 'failed',
+ },
+ },
+ running: {
+ color: ({ theme }) => theme.colors.primary.base,
+ config: {
+ name: 'running',
+ label: t('Running'),
+ status: 'running',
+ },
+ },
+ timed_out: {
+ color: ({ theme }) => theme.colors.grayscale.light1,
+ config: {
+ name: 'offline',
+ label: t('Offline'),
+ status: 'offline',
+ },
+ },
+ scheduled: {
+ name: 'queued',
+ label: t('Scheduled'),
+ status: 'queued',
+ },
+ pending: {
+ name: 'queued',
+ label: t('Scheduled'),
+ status: 'queued',
+ },
+};
+
+const StatusIcon = styled(Icon, {
+ shouldForwardProp: prop => prop !== 'status',
+})`
+ color: ${({ status, theme }) =>
+ statusAttributes[status]?.color || theme.colors.grayscale.base};
`;
const QueryTable = props => {
+ const setHeaders = column => {
+ if (column === 'sql') {
+ return column.toUpperCase();
+ }
+ return column.charAt(0).toUpperCase().concat(column.slice(1));
+ };
const columns = useMemo(
() =>
props.columns.map(column => ({
accessor: column,
- Header: column,
+ Header: () => setHeaders(column),
disableSortBy: true,
})),
[props.columns],
@@ -176,44 +240,59 @@ const QueryTable = props => {
q.ctas && q.tempTable && q.tempTable.includes('.') ? '' : q.schema;
q.output = [schemaUsed, q.tempTable].filter(v => v).join('.');
}
- q.progress = (
-
- );
- let errorTooltip;
- if (q.errorMessage) {
- errorTooltip = (
-
-
-
+ q.progress =
+ q.state === 'success' ? (
+
+ ) : (
+
);
- }
q.state = (
-
-
- {errorTooltip}
-
+
+
+
+
+
);
q.actions = (
- restoreSql(query)}
tooltip={t(
'Overwrite text in the editor with a query on this table',
)}
placement="top"
- />
-
+
+
+ openQueryInNewTab(query)}
tooltip={t('Run query in a new tab')}
placement="top"
- />
-
+
+
+ removeQuery(query)}
- />
+ >
+
+
);
return q;
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/styles.ts b/superset-frontend/src/SqlLab/components/QueryTable/styles.ts
new file mode 100644
index 0000000000000..2800f09945636
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/QueryTable/styles.ts
@@ -0,0 +1,41 @@
+/**
+ * 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 { styled, css } from '@superset-ui/core';
+import { IconTooltip } from '../../../components/IconTooltip';
+
+export const StaticPosition = css`
+ position: static;
+`;
+
+export const verticalAlign = css`
+ vertical-align: 0em;
+ svg {
+ height: 0.9em;
+ }
+`;
+
+export const StyledTooltip = styled(IconTooltip)`
+ padding-right: ${({ theme }) => theme.gridUnit * 2}px;
+ span {
+ color: ${({ theme }) => theme.colors.grayscale.base};
+ &: hover {
+ color: ${({ theme }) => theme.colors.primary.base};
+ }
+ }
+`;
diff --git a/superset-frontend/src/SqlLab/components/ResultSet.tsx b/superset-frontend/src/SqlLab/components/ResultSet.tsx
index a8fe019c7e6a0..b5a6e4faaa0a5 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet.tsx
@@ -271,9 +271,23 @@ export default class ResultSet extends React.PureComponent<
return;
}
- const { schema, sql, dbId, templateParams } = this.props.query;
+ const { schema, sql, dbId } = this.props.query;
+ let { templateParams } = this.props.query;
const selectedColumns = this.props.query?.results?.selected_columns || [];
+ // The filters param is only used to test jinja templates.
+ // Remove the special filters entry from the templateParams
+ // before saving the dataset.
+ if (templateParams) {
+ const p = JSON.parse(templateParams);
+ /* eslint-disable-next-line no-underscore-dangle */
+ if (p._filters) {
+ /* eslint-disable-next-line no-underscore-dangle */
+ delete p._filters;
+ templateParams = JSON.stringify(p);
+ }
+ }
+
this.props.actions
.createDatasource({
schema,
@@ -527,7 +541,7 @@ export default class ResultSet extends React.PureComponent<
let limitMessage;
const limitReached = results?.displayLimitReached;
const limit = queryLimit || results.query.limit;
- const isAdmin = !!this.props.user?.roles.Admin;
+ const isAdmin = !!this.props.user?.roles?.Admin;
const displayMaxRowsReachedMessage = {
withAdmin: t(
`The number of results displayed is limited to %(rows)d by the configuration DISPLAY_MAX_ROWS. `,
diff --git a/superset-frontend/src/SqlLab/components/TableElement.jsx b/superset-frontend/src/SqlLab/components/TableElement.jsx
index e44cccf0d695e..384771a96e83e 100644
--- a/superset-frontend/src/SqlLab/components/TableElement.jsx
+++ b/superset-frontend/src/SqlLab/components/TableElement.jsx
@@ -16,16 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Collapse from 'src/components/Collapse';
import Card from 'src/components/Card';
import ButtonGroup from 'src/components/ButtonGroup';
-import shortid from 'shortid';
import { t, styled } from '@superset-ui/core';
import { debounce } from 'lodash';
import { Tooltip } from 'src/components/Tooltip';
+import Icons from 'src/components/Icons';
import CopyToClipboard from '../../components/CopyToClipboard';
import { IconTooltip } from '../../components/IconTooltip';
import ColumnElement from './ColumnElement';
@@ -56,44 +56,26 @@ const Fade = styled.div`
opacity: ${props => (props.hovered ? 1 : 0)};
`;
-class TableElement extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- sortColumns: false,
- hovered: false,
- };
- this.toggleSortColumns = this.toggleSortColumns.bind(this);
- this.removeTable = this.removeTable.bind(this);
- this.setHover = debounce(this.setHover.bind(this), 100);
- }
+const TableElement = props => {
+ const [sortColumns, setSortColumns] = useState(false);
+ const [hovered, setHovered] = useState(false);
- setHover(hovered) {
- this.setState({ hovered });
- }
+ const { table, actions, isActive } = props;
- popSelectStar() {
- const qe = {
- id: shortid.generate(),
- title: this.props.table.name,
- dbId: this.props.table.dbId,
- autorun: true,
- sql: this.props.table.selectStar,
- };
- this.props.actions.addQueryEditor(qe);
- }
+ const setHover = hovered => {
+ debounce(() => setHovered(hovered), 100)();
+ };
- removeTable() {
- this.props.actions.removeDataPreview(this.props.table);
- this.props.actions.removeTable(this.props.table);
- }
+ const removeTable = () => {
+ actions.removeDataPreview(table);
+ actions.removeTable(table);
+ };
- toggleSortColumns() {
- this.setState(prevState => ({ sortColumns: !prevState.sortColumns }));
- }
+ const toggleSortColumns = () => {
+ setSortColumns(prevState => !prevState);
+ };
- renderWell() {
- const { table } = this.props;
+ const renderWell = () => {
let header;
if (table.partitions) {
let partitionQuery;
@@ -126,12 +108,11 @@ class TableElement extends React.PureComponent {
);
}
return header;
- }
+ };
- renderControls() {
+ const renderControls = () => {
let keyLink;
- const { table } = this.props;
- if (table.indexes && table.indexes.length > 0) {
+ if (table?.indexes?.length) {
keyLink = (
+
}
text={table.selectStar}
shouldShowText={false}
- tooltipText={t('Copy SELECT statement to the clipboard')}
/>
)}
{table.view && (
@@ -187,56 +170,52 @@ class TableElement extends React.PureComponent {
)}
);
- }
+ };
- renderHeader() {
- const { table } = this.props;
- return (
- this.setHover(true)}
- onMouseLeave={() => this.setHover(false)}
+ const renderHeader = () => (
+
setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ >
+
-
-
- {table.name}
-
-
+
+ {table.name}
+
+
-
- {table.isMetadataLoading || table.isExtraMetadataLoading ? (
-
- ) : (
- e.stopPropagation()}
- >
- {this.renderControls()}
-
- )}
-
+
+ {table.isMetadataLoading || table.isExtraMetadataLoading ? (
+
+ ) : (
+ e.stopPropagation()}
+ >
+ {renderControls()}
+
+ )}
- );
- }
+
+ );
- renderBody() {
- const { table } = this.props;
+ const renderBody = () => {
let cols;
if (table.columns) {
cols = table.columns.slice();
- if (this.state.sortColumns) {
+ if (sortColumns) {
cols.sort((a, b) => {
const colA = a.name.toUpperCase();
const colB = b.name.toUpperCase();
@@ -253,33 +232,54 @@ class TableElement extends React.PureComponent {
const metadata = (
this.setHover(true)}
- onMouseLeave={() => this.setHover(false)}
+ onMouseEnter={() => setHover(true)}
+ onMouseLeave={() => setHover(false)}
css={{ paddingTop: 6 }}
>
- {this.renderWell()}
+ {renderWell()}
- {cols &&
- cols.map(col => )}
+ {cols?.map(col => (
+
+ ))}
);
return metadata;
- }
+ };
- render() {
- return (
-
- {this.renderBody()}
-
- );
- }
-}
+ const collapseExpandIcon = () => (
+
+
+
+ );
+
+ return (
+
+ {renderBody()}
+
+ );
+};
TableElement.propTypes = propTypes;
TableElement.defaultProps = defaultProps;
diff --git a/superset-frontend/src/SqlLab/main.less b/superset-frontend/src/SqlLab/main.less
index 93bf91dc4e5ba..7e5cf6eebb4f1 100644
--- a/superset-frontend/src/SqlLab/main.less
+++ b/superset-frontend/src/SqlLab/main.less
@@ -414,6 +414,7 @@ div.tablePopover {
display: flex;
flex: 1;
align-items: center;
+ width: 100%;
.table-name {
white-space: nowrap;
diff --git a/superset-frontend/src/components/Form/FormItem.tsx b/superset-frontend/src/components/Form/FormItem.tsx
index ab301a883e543..9b529dbd5d141 100644
--- a/superset-frontend/src/components/Form/FormItem.tsx
+++ b/superset-frontend/src/components/Form/FormItem.tsx
@@ -35,7 +35,7 @@ const StyledItem = styled(Form.Item)`
&::after {
display: inline-block;
color: ${theme.colors.error.base};
- font-size: ${theme.typography.sizes.m}px;
+ font-size: ${theme.typography.sizes.s}px;
content: '*';
}
}
diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
index 8569b554a020d..75df2bb088cbb 100644
--- a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
+++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
@@ -25,17 +25,18 @@ import FormLabel from './FormLabel';
export interface LabeledErrorBoundInputProps {
label?: string;
validationMethods:
- | { onBlur: (value: any) => string }
- | { onChange: (value: any) => string };
+ | { onBlur: (value: any) => void }
+ | { onChange: (value: any) => void };
errorMessage: string | null;
helpText?: string;
required?: boolean;
id?: string;
+ classname?: string;
[x: string]: any;
}
const StyledInput = styled(Input)`
- margin: 8px 0;
+ margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
`;
const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css`
@@ -60,6 +61,12 @@ const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css`
}
}`}
`;
+const StyledFormGroup = styled('div')`
+ margin-bottom: ${({ theme }) => theme.gridUnit * 5}px;
+ .ant-form-item {
+ margin-bottom: 0;
+ }
+`;
const LabeledErrorBoundInput = ({
label,
@@ -68,9 +75,10 @@ const LabeledErrorBoundInput = ({
helpText,
required = false,
id,
+ className,
...props
}: LabeledErrorBoundInputProps) => (
- <>
+
{label}
@@ -83,7 +91,7 @@ const LabeledErrorBoundInput = ({
>
- >
+
);
export default LabeledErrorBoundInput;
diff --git a/superset-frontend/src/components/IconButton/IconButton.stories.tsx b/superset-frontend/src/components/IconButton/IconButton.stories.tsx
new file mode 100644
index 0000000000000..45435e70cc8ec
--- /dev/null
+++ b/superset-frontend/src/components/IconButton/IconButton.stories.tsx
@@ -0,0 +1,58 @@
+/**
+ * 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 React from 'react';
+import IconButton, { IconButtonProps } from '.';
+
+export default {
+ title: 'IconButton',
+ component: IconButton,
+};
+
+export const InteractiveIconButton = (args: IconButtonProps) => (
+
+);
+
+InteractiveIconButton.args = {
+ buttonText: 'This is the IconButton text',
+ altText: 'This is an example of non-default alt text',
+ href: 'https://preset.io/',
+ target: '_blank',
+};
+
+InteractiveIconButton.argTypes = {
+ icon: {
+ defaultValue: '/images/icons/sql.svg',
+ control: {
+ type: 'select',
+ options: [
+ '/images/icons/sql.svg',
+ '/images/icons/server.svg',
+ '/images/icons/image.svg',
+ 'Click to see example alt text',
+ ],
+ },
+ },
+};
diff --git a/superset-frontend/src/components/IconButton/IconButton.test.jsx b/superset-frontend/src/components/IconButton/IconButton.test.jsx
new file mode 100644
index 0000000000000..40490011953fa
--- /dev/null
+++ b/superset-frontend/src/components/IconButton/IconButton.test.jsx
@@ -0,0 +1,38 @@
+/**
+ * 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 React from 'react';
+import { render, screen } from 'spec/helpers/testing-library';
+import IconButton from 'src/components/IconButton';
+
+const defaultProps = {
+ buttonText: 'This is the IconButton text',
+ icon: '/images/icons/sql.svg',
+};
+
+describe('IconButton', () => {
+ it('renders an IconButton', () => {
+ render(
);
+
+ const icon = screen.getByRole('img');
+ const buttonText = screen.getByText(/this is the iconbutton text/i);
+
+ expect(icon).toBeVisible();
+ expect(buttonText).toBeVisible();
+ });
+});
diff --git a/superset-frontend/src/components/IconButton/index.tsx b/superset-frontend/src/components/IconButton/index.tsx
new file mode 100644
index 0000000000000..e7f9c2d89d528
--- /dev/null
+++ b/superset-frontend/src/components/IconButton/index.tsx
@@ -0,0 +1,123 @@
+/**
+ * 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 React from 'react';
+import { styled } from '@superset-ui/core';
+import Button from 'src/components/Button';
+import { ButtonProps as AntdButtonProps } from 'antd/lib/button';
+
+export interface IconButtonProps extends AntdButtonProps {
+ buttonText: string;
+ icon: string;
+ altText?: string;
+}
+
+const StyledButton = styled(Button)`
+ height: auto;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+`;
+const StyledImage = styled.div`
+ margin: ${({ theme }) => theme.gridUnit * 8}px 0;
+ padding: ${({ theme }) => theme.gridUnit * 4}px;
+
+ &:first-of-type {
+ margin-right: 0;
+ }
+
+ img {
+ width: fit-content;
+
+ &:first-of-type {
+ margin-right: 0;
+ }
+ }
+`;
+
+const StyledInner = styled.div`
+ max-height: calc(1.5em * 2);
+ overflow: hidden;
+ padding-right: 1rem;
+ position: relative;
+ white-space: break-spaces;
+
+ &::before {
+ content: '...';
+ inset-block-end: 0; /* "bottom" */
+ inset-inline-end: 8px; /* "right" */
+ position: absolute;
+ }
+
+ &::after {
+ background-color: ${({ theme }) => theme.colors.grayscale.light4};
+ content: '';
+ height: 1rem;
+ inset-inline-end: 8px; /* "right" */
+ position: absolute;
+ top: 4px;
+ width: 1rem;
+ }
+`;
+
+const StyledBottom = styled.div`
+ padding: ${({ theme }) => theme.gridUnit * 6}px
+ ${({ theme }) => theme.gridUnit * 4}px;
+ border-radius: 0 0 ${({ theme }) => theme.borderRadius}px
+ ${({ theme }) => theme.borderRadius}px;
+ background-color: ${({ theme }) => theme.colors.grayscale.light4};
+ width: 100%;
+ line-height: 1.5em;
+ overflow: hidden;
+ white-space: no-wrap;
+ text-overflow: ellipsis;
+
+ &:first-of-type {
+ margin-right: 0;
+ }
+`;
+
+const IconButton = styled(
+ ({ icon, altText, buttonText, ...props }: IconButtonProps) => (
+
+
+
+
+
+ {buttonText}
+
+
+ ),
+)`
+ text-transform: none;
+ background-color: ${({ theme }) => theme.colors.grayscale.light5};
+ font-weight: ${({ theme }) => theme.typography.weights.normal};
+ color: ${({ theme }) => theme.colors.grayscale.dark2};
+ border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ margin: 0;
+ width: 100%;
+
+ &:hover,
+ &:focus {
+ background-color: ${({ theme }) => theme.colors.grayscale.light5};
+ color: ${({ theme }) => theme.colors.grayscale.dark2};
+ border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ }
+`;
+
+export default IconButton;
diff --git a/superset-frontend/src/components/Menu/Menu.tsx b/superset-frontend/src/components/Menu/Menu.tsx
index 6733d6a059228..0875b85f8a661 100644
--- a/superset-frontend/src/components/Menu/Menu.tsx
+++ b/superset-frontend/src/components/Menu/Menu.tsx
@@ -27,6 +27,7 @@ import { Row, Col, Grid } from 'antd';
import Icon from 'src/components/Icon';
import RightMenu from './MenuRight';
import { Languages } from './LanguagePicker';
+import { URL_PARAMS } from '../../constants';
interface BrandProps {
path: string;
@@ -158,7 +159,7 @@ export function Menu({
return () => window.removeEventListener('resize', windowResize);
}, []);
- const standalone = getUrlParam('standalone', 'boolean');
+ const standalone = getUrlParam(URL_PARAMS.standalone);
if (standalone) return <>>;
const renderSubMenu = ({
diff --git a/superset-frontend/src/components/Menu/MenuRight.tsx b/superset-frontend/src/components/Menu/MenuRight.tsx
index 9263c1c621259..a722834bdf442 100644
--- a/superset-frontend/src/components/Menu/MenuRight.tsx
+++ b/superset-frontend/src/components/Menu/MenuRight.tsx
@@ -27,7 +27,7 @@ import { NavBarProps, MenuObjectProps } from './Menu';
export const dropdownItems = [
{
label: t('SQL query'),
- url: '/superset/sqllab',
+ url: '/superset/sqllab?new=true',
icon: 'fa-fw fa-search',
},
{
diff --git a/superset-frontend/src/components/Tabs/Tabs.tsx b/superset-frontend/src/components/Tabs/Tabs.tsx
index f5a1d148f36a5..704d84b5220f0 100644
--- a/superset-frontend/src/components/Tabs/Tabs.tsx
+++ b/superset-frontend/src/components/Tabs/Tabs.tsx
@@ -66,11 +66,6 @@ const StyledTabs = ({
.ant-tabs-nav-list {
width: 100%;
}
-
- .ant-tabs-tab {
- width: 0;
- margin-right: 0;
- }
`};
.ant-tabs-tab-btn {
diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts
index ad7683681b939..bfa7033697cbc 100644
--- a/superset-frontend/src/constants.ts
+++ b/superset-frontend/src/constants.ts
@@ -23,9 +23,19 @@ export const BOOL_TRUE_DISPLAY = 'True';
export const BOOL_FALSE_DISPLAY = 'False';
export const URL_PARAMS = {
- standalone: 'standalone',
- preselectFilters: 'preselect_filters',
-};
+ standalone: {
+ name: 'standalone',
+ type: 'number',
+ },
+ preselectFilters: {
+ name: 'preselect_filters',
+ type: 'object',
+ },
+ showFilters: {
+ name: 'show_filters',
+ type: 'boolean',
+ },
+} as const;
/**
* Faster debounce delay for inputs without expensive operation.
diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js
index ac2ead0d74330..d0b39e2ff14d7 100644
--- a/superset-frontend/src/dashboard/actions/dashboardState.js
+++ b/superset-frontend/src/dashboard/actions/dashboardState.js
@@ -344,6 +344,11 @@ export function setDirectPathToChild(path) {
return { type: SET_DIRECT_PATH, path };
}
+export const SET_LAST_FOCUSED_TAB = 'SET_LAST_FOCUSED_TAB';
+export function setLastFocusedTab(tabId) {
+ return { type: SET_LAST_FOCUSED_TAB, tabId };
+}
+
export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD';
export function setFocusedFilterField(chartId, column) {
return { type: SET_FOCUSED_FILTER_FIELD, chartId, column };
diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js
index 2928df65737c2..62751033ad672 100644
--- a/superset-frontend/src/dashboard/actions/hydrate.js
+++ b/superset-frontend/src/dashboard/actions/hydrate.js
@@ -27,7 +27,6 @@ import {
import { chart } from 'src/chart/chartReducer';
import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities';
import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters';
-import { getParam } from 'src/modules/utils';
import { applyDefaultFormData } from 'src/explore/store';
import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import findPermission, {
@@ -54,6 +53,8 @@ import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFro
import getLocationHash from 'src/dashboard/util/getLocationHash';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
+import { URL_PARAMS } from 'src/constants';
+import { getUrlParam } from 'src/utils/urlUtils';
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import extractUrlParams from '../util/extractUrlParams';
@@ -77,9 +78,9 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
});
try {
// allow request parameter overwrite dashboard metadata
- preselectFilters = JSON.parse(
- getParam('preselect_filters') || metadata.default_filters,
- );
+ preselectFilters =
+ getUrlParam(URL_PARAMS.preselectFilters) ||
+ JSON.parse(metadata.default_filters);
} catch (e) {
//
}
@@ -376,6 +377,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
lastModifiedTime: dashboardData.changed_on,
+ lastFocusedTabId: null,
},
dashboardLayout,
},
diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts
index 23ce35d241de8..e01a5a67244ef 100644
--- a/superset-frontend/src/dashboard/actions/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts
@@ -85,11 +85,19 @@ export const setFilterConfiguration = (
endpoint: `/api/v1/dashboard/${id}`,
});
+ const mergedFilterConfig = filterConfig.map(filter => {
+ const oldFilter = oldFilters[filter.id];
+ if (!oldFilter) {
+ return filter;
+ }
+ return { ...oldFilter, ...filter };
+ });
+
try {
const response = await updateDashboard({
json_metadata: JSON.stringify({
...metadata,
- native_filter_configuration: filterConfig,
+ native_filter_configuration: mergedFilterConfig,
}),
});
dispatch(
@@ -99,12 +107,20 @@ export const setFilterConfiguration = (
);
dispatch({
type: SET_FILTER_CONFIG_COMPLETE,
- filterConfig,
+ filterConfig: mergedFilterConfig,
});
- dispatch(setDataMaskForFilterConfigComplete(filterConfig, oldFilters));
+ dispatch(
+ setDataMaskForFilterConfigComplete(mergedFilterConfig, oldFilters),
+ );
} catch (err) {
- dispatch({ type: SET_FILTER_CONFIG_FAIL, filterConfig });
- dispatch({ type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL, filterConfig });
+ dispatch({
+ type: SET_FILTER_CONFIG_FAIL,
+ filterConfig: mergedFilterConfig,
+ });
+ dispatch({
+ type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
+ filterConfig: mergedFilterConfig,
+ });
}
};
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 877cea9c3fa23..807fe46b75dea 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -120,7 +120,9 @@ const DashboardBuilder: FC
= () => {
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
(canEdit || (!canEdit && filterValues.length !== 0));
- const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(true);
+ const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
+ getUrlParam(URL_PARAMS.showFilters) ?? true,
+ );
const toggleDashboardFiltersOpen = (visible?: boolean) => {
setDashboardFiltersOpen(visible ?? !dashboardFiltersOpen);
@@ -152,7 +154,7 @@ const DashboardBuilder: FC = () => {
: undefined;
const hideDashboardHeader =
- getUrlParam(URL_PARAMS.standalone, 'number') ===
+ getUrlParam(URL_PARAMS.standalone) ===
DashboardStandaloneMode.HIDE_NAV_AND_TITLE;
const barTopOffset =
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
index 9c58d853e4c1f..00c5d73ae5b71 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
@@ -21,7 +21,7 @@
import { ParentSize } from '@vx/responsive';
import Tabs from 'src/components/Tabs';
import React, { FC, useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
import { DashboardLayout, LayoutItem, RootState } from 'src/dashboard/types';
@@ -30,6 +30,10 @@ import {
DASHBOARD_ROOT_DEPTH,
} from 'src/dashboard/util/constants';
import { getRootLevelTabIndex } from './utils';
+import { Filters } from '../../reducers/types';
+import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
+import { findTabsWithChartsInScope } from '../nativeFilters/utils';
+import { setFilterConfiguration } from '../../actions/nativeFilters';
type DashboardContainerProps = {
topLevelTabs?: LayoutItem;
@@ -39,6 +43,9 @@ const DashboardContainer: FC = ({ topLevelTabs }) => {
const dashboardLayout = useSelector(
state => state.dashboardLayout.present,
);
+ const nativeFilters = useSelector(
+ state => state.nativeFilters.filters,
+ );
const directPathToChild = useSelector(
state => state.dashboardState.directPathToChild,
);
@@ -46,10 +53,37 @@ const DashboardContainer: FC = ({ topLevelTabs }) => {
getRootLevelTabIndex(dashboardLayout, directPathToChild),
);
+ const dispatch = useDispatch();
+
useEffect(() => {
setTabIndex(getRootLevelTabIndex(dashboardLayout, directPathToChild));
}, [getLeafComponentIdFromPath(directPathToChild)]);
+ // recalculate charts and tabs in scopes of native filters only when a scope or dashboard layout changes
+ const nativeFiltersValues = Object.values(nativeFilters);
+ const scopes = nativeFiltersValues.map(filter => filter.scope);
+ useEffect(() => {
+ nativeFiltersValues.forEach(filter => {
+ const filterScope = filter.scope;
+ const chartsInScope = getChartIdsInFilterScope({
+ filterScope: {
+ scope: filterScope.rootPath,
+ // @ts-ignore
+ immune: filterScope.excluded,
+ },
+ });
+ const tabsInScope = findTabsWithChartsInScope(
+ dashboardLayout,
+ chartsInScope,
+ );
+ Object.assign(filter, {
+ chartsInScope,
+ tabsInScope: Array.from(tabsInScope),
+ });
+ });
+ dispatch(setFilterConfiguration(nativeFiltersValues));
+ }, [JSON.stringify(scopes), JSON.stringify(dashboardLayout)]);
+
const childIds: string[] = topLevelTabs
? topLevelTabs.children
: [DASHBOARD_GRID_ID];
diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
index 7560f858f58a2..9d7b84799ba5e 100644
--- a/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
+++ b/superset-frontend/src/dashboard/components/FiltersBadge/selectors.ts
@@ -192,20 +192,15 @@ export const selectNativeIndicatorsForChart = (
const getStatus = ({
value,
- isAffectedByScope,
column,
type = DataMaskType.NativeFilters,
}: {
value: any;
- isAffectedByScope: boolean;
column?: string;
type?: DataMaskType;
}): IndicatorStatus => {
// a filter is only considered unset if it's value is null
const hasValue = value !== null;
- if (!isAffectedByScope) {
- return IndicatorStatus.Unset;
- }
if (type === DataMaskType.CrossFilters && hasValue) {
return IndicatorStatus.CrossFilterApplied;
}
@@ -223,16 +218,18 @@ export const selectNativeIndicatorsForChart = (
let nativeFilterIndicators: any = [];
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) {
- nativeFilterIndicators = Object.values(nativeFilters.filters).map(
- nativeFilter => {
- const isAffectedByScope = getTreeCheckedItems(
- nativeFilter.scope,
- dashboardLayout,
- ).some(
+ nativeFilterIndicators = Object.values(nativeFilters.filters)
+ .filter(nativeFilter =>
+ getTreeCheckedItems(nativeFilter.scope, dashboardLayout).some(
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
- );
+ ),
+ )
+ .map(nativeFilter => {
const column = nativeFilter.targets[0]?.column?.name;
- let value = dataMask[nativeFilter.id]?.filterState?.value ?? null;
+ let value =
+ dataMask[nativeFilter.id]?.filterState?.label ??
+ dataMask[nativeFilter.id]?.filterState?.value ??
+ null;
if (!Array.isArray(value) && value !== null) {
value = [value];
}
@@ -240,26 +237,28 @@ export const selectNativeIndicatorsForChart = (
column,
name: nativeFilter.name,
path: [nativeFilter.id],
- status: getStatus({ value, isAffectedByScope, column }),
+ status: getStatus({ value, column }),
value,
};
- },
- );
+ });
}
let crossFilterIndicators: any = [];
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
crossFilterIndicators = Object.values(chartConfiguration)
- .map(chartConfig => {
- const scope = chartConfig?.crossFilters?.scope;
- const isAffectedByScope = getTreeCheckedItems(
- scope,
+ .filter(chartConfig =>
+ getTreeCheckedItems(
+ chartConfig?.crossFilters?.scope,
dashboardLayout,
).some(
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
- );
-
- let value = dataMask[chartConfig.id]?.filterState?.value ?? null;
+ ),
+ )
+ .map(chartConfig => {
+ let value =
+ dataMask[chartConfig.id]?.filterState?.label ??
+ dataMask[chartConfig.id]?.filterState?.value ??
+ null;
if (!Array.isArray(value) && value !== null) {
value = [value];
}
@@ -270,7 +269,6 @@ export const selectNativeIndicatorsForChart = (
path: [`${chartConfig.id}`],
status: getStatus({
value,
- isAffectedByScope,
type: DataMaskType.CrossFilters,
}),
value,
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
index 9b9aefe03b6f6..4ed5fd623d153 100644
--- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx
@@ -168,7 +168,7 @@ class HeaderActionsDropdown extends React.PureComponent {
window.location.pathname,
getActiveFilters(),
window.location.hash,
- getUrlParam(URL_PARAMS.standalone, 'number'),
+ !getUrlParam(URL_PARAMS.standalone),
);
window.location.replace(url);
break;
diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx
index f4868d0e6a08e..102acae84aa02 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -85,37 +85,46 @@ const defaultProps = {
* If ChartHolder were a function component, this could be implemented as a hook instead.
*/
const FilterFocusHighlight = React.forwardRef(
- ({ chartId, focusedFilterScope, ...otherProps }, ref) => {
+ ({ chartId, focusedFilterScope, nativeFilters, ...otherProps }, ref) => {
const theme = useTheme();
- if (!focusedFilterScope) return
;
+ const focusedNativeFilterId = nativeFilters.focusedFilterId;
+ if (!(focusedFilterScope || focusedNativeFilterId))
+ return
;
// we use local styles here instead of a conditionally-applied class,
// because adding any conditional class to this container
// causes performance issues in Chrome.
// default to the "de-emphasized" state
- let styles = { opacity: 0.3, pointerEvents: 'none' };
+ const unfocusedChartStyles = { opacity: 0.3, pointerEvents: 'none' };
+ const focusedChartStyles = {
+ borderColor: theme.colors.primary.light2,
+ opacity: 1,
+ boxShadow: `0px 0px ${theme.gridUnit * 2}px ${
+ theme.colors.primary.light2
+ }`,
+ pointerEvents: 'auto',
+ };
- if (
+ if (focusedNativeFilterId) {
+ if (
+ nativeFilters.filters[focusedNativeFilterId].chartsInScope.includes(
+ chartId,
+ )
+ ) {
+ return
;
+ }
+ } else if (
chartId === focusedFilterScope.chartId ||
getChartIdsInFilterScope({
filterScope: focusedFilterScope.scope,
}).includes(chartId)
) {
- // apply the "highlighted" state if this chart
- // contains a filter being focused, or is in scope of a focused filter.
- styles = {
- borderColor: theme.colors.primary.light2,
- opacity: 1,
- boxShadow: `0px 0px ${theme.gridUnit * 2}px ${
- theme.colors.primary.light2
- }`,
- pointerEvents: 'auto',
- };
+ return
;
}
// inline styles are used here due to a performance issue when adding/changing a class, which causes a reflow
- return
;
+ return
;
},
);
@@ -233,6 +242,7 @@ class ChartHolder extends React.Component {
isComponentVisible,
dashboardId,
focusedFilterScope,
+ nativeFilters,
} = this.props;
// inherit the size of parent columns
@@ -291,6 +301,7 @@ class ChartHolder extends React.Component {
{
editMode: false,
isComponentVisible: true,
dashboardId: 123,
+ nativeFilters: nativeFiltersInfo.filters,
};
const renderWrapper = (props = defaultProps, state = mockState) =>
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
index 73cdca34ed155..faa1e046eff7a 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
@@ -18,6 +18,7 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
+import { styled } from '@superset-ui/core';
import DashboardComponent from '../../containers/DashboardComponent';
import DragDroppable from '../dnd/DragDroppable';
@@ -62,6 +63,17 @@ const defaultProps = {
onResizeStop() {},
};
+const TabTitleContainer = styled.div`
+ ${({ isHighlighted, theme: { gridUnit, colors } }) => `
+ padding: ${gridUnit}px ${gridUnit * 2}px;
+ margin: ${-gridUnit}px ${gridUnit * -2}px;
+ transition: box-shadow 0.2s ease-in-out;
+ ${
+ isHighlighted && `box-shadow: 0 0 ${gridUnit}px ${colors.primary.light1};`
+ }
+ `}
+`;
+
export default class Tab extends React.PureComponent {
constructor(props) {
super(props);
@@ -192,6 +204,7 @@ export default class Tab extends React.PureComponent {
editMode,
filters,
isFocused,
+ isHighlighted,
} = this.props;
return (
@@ -205,7 +218,11 @@ export default class Tab extends React.PureComponent {
editMode={editMode}
>
{({ dropIndicatorProps, dragSourceRef }) => (
-
+
}
-
+
)}
);
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
index 427145b89eb18..c41abf83a4b08 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
@@ -31,7 +31,11 @@ import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex';
import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath';
import { componentShape } from '../../util/propShapes';
-import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
+import {
+ NEW_TAB_ID,
+ DASHBOARD_ROOT_ID,
+ DASHBOARD_GRID_ID,
+} from '../../util/constants';
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
import { TAB_TYPE } from '../../util/componentTypes';
@@ -268,11 +272,28 @@ class Tabs extends React.PureComponent {
renderHoverMenu,
isComponentVisible: isCurrentTabVisible,
editMode,
+ nativeFilters,
+ dashboardLayout,
+ lastFocusedTabId,
+ setLastFocusedTab,
} = this.props;
const { children: tabIds } = tabsComponent;
const { tabIndex: selectedTabIndex, activeKey } = this.state;
+ // On dashboards with top level tabs, set initial focus to the active top level tab
+ const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ const isTopLevelTabs = rootChildId !== DASHBOARD_GRID_ID;
+ if (isTopLevelTabs && !lastFocusedTabId) {
+ setLastFocusedTab(activeKey);
+ }
+
+ let tabsToHighlight;
+ if (nativeFilters.focusedFilterId) {
+ tabsToHighlight =
+ nativeFilters.filters[nativeFilters.focusedFilterId].tabsInScope;
+ }
return (
{tabIds.map((tabId, tabIndex) => (
}
>
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
index f35dcd4df10b3..49ec405f25bdf 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
@@ -20,11 +20,12 @@
import userEvent from '@testing-library/user-event';
import React from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import { nativeFiltersInfo } from 'spec/javascripts/dashboard/fixtures/mockNativeFilters';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
-
+import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
import Tabs from './Tabs';
jest.mock('src/dashboard/containers/DashboardComponent', () =>
@@ -110,6 +111,8 @@ const createProps = () => ({
onChangeTab: jest.fn(),
deleteComponent: jest.fn(),
updateComponents: jest.fn(),
+ dashboardLayout: emptyDashboardLayout,
+ nativeFilters: nativeFiltersInfo.filters,
});
beforeEach(() => {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl/index.tsx
index 1ac1fb3a5f5a2..91908823badb2 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/CascadeFilterControl/index.tsx
@@ -22,8 +22,10 @@ import Icon from 'src/components/Icon';
import FilterControl from 'src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl';
import { CascadeFilter } from 'src/dashboard/components/nativeFilters/FilterBar/CascadeFilters/types';
import { Filter } from 'src/dashboard/components/nativeFilters/types';
+import { DataMaskStateWithId } from 'src/dataMask/types';
export interface CascadeFilterControlProps {
+ dataMaskSelected?: DataMaskStateWithId;
filter: CascadeFilter;
directPathToChild?: string[];
onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;
@@ -45,6 +47,7 @@ const StyledCaretIcon = styled(Icon)`
`;
const CascadeFilterControl: React.FC = ({
+ dataMaskSelected,
filter,
directPathToChild,
onFilterSelectionChange,
@@ -53,6 +56,7 @@ const CascadeFilterControl: React.FC = ({
= ({
{filter.cascadeChildren?.map(childFilter => (
= ({
+ dataMaskSelected,
filter,
visible,
onVisibleChange,
@@ -83,9 +82,7 @@ const CascadePopover: React.FC = ({
directPathToChild,
}) => {
const [currentPathToChild, setCurrentPathToChild] = useState();
- const dataMask = useSelector(
- state => state.dataMask[filter.id] ?? getInitialDataMask(filter.id),
- );
+ const dataMask = dataMaskSelected[filter.id];
useEffect(() => {
setCurrentPathToChild(directPathToChild);
@@ -98,7 +95,7 @@ const CascadePopover: React.FC = ({
const getActiveChildren = useCallback(
(filter: CascadeFilter): CascadeFilter[] | null => {
const children = filter.cascadeChildren || [];
- const currentValue = dataMask.filterState?.value;
+ const currentValue = dataMask?.filterState?.value;
const activeChildren = children.flatMap(
childFilter => getActiveChildren(childFilter) || [],
@@ -147,6 +144,7 @@ const CascadePopover: React.FC = ({
if (!filter.cascadeChildren?.length) {
return (
= ({
const content = (
= ({
{activeFilters.map(activeFilter => (
{
userEvent.click(screen.getByTestId(getTestId('collapsable')));
userEvent.click(screen.getByTestId(getTestId('create-filter')));
// select filter
- userEvent.click(screen.getByText('Select filter'));
- userEvent.click(screen.getByText('Time filter'));
+ userEvent.click(screen.getByText('Value'));
+ userEvent.click(screen.getByText('Time range'));
userEvent.type(screen.getByTestId(getModalTestId('name-input')), FILTER_NAME);
userEvent.click(screen.getByText('Save'));
await screen.findByText('All Filters (1)');
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
index 157f50e09d449..14dc65b90655e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
@@ -42,6 +42,7 @@ const StyledFilterControlContainer = styled.div`
`;
const FilterControl: React.FC = ({
+ dataMaskSelected,
filter,
icon,
onFilterSelectionChange,
@@ -57,6 +58,7 @@ const FilterControl: React.FC = ({
{icon}
theme.gridUnit * 4}px;
@@ -33,7 +41,7 @@ const Wrapper = styled.div`
type FilterControlsProps = {
directPathToChild?: string[];
- dataMaskSelected: DataMaskState;
+ dataMaskSelected: DataMaskStateWithId;
onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;
};
@@ -44,7 +52,18 @@ const FilterControls: FC = ({
}) => {
const [visiblePopoverId, setVisiblePopoverId] = useState(null);
const filters = useFilters();
+ const dashboardLayout = useDashboardLayout();
+ const lastFocusedTabId = useSelector(
+ state => state.dashboardState?.lastFocusedTabId,
+ );
const filterValues = Object.values(filters);
+ const portalNodes = React.useMemo(() => {
+ const nodes = new Array(filterValues.length);
+ for (let i = 0; i < filterValues.length; i += 1) {
+ nodes[i] = portals.createHtmlPortalNode();
+ }
+ return nodes;
+ }, [filterValues.length]);
const cascadeFilters = useMemo(() => {
const filtersWithValue = filterValues.map(filter => ({
@@ -53,22 +72,91 @@ const FilterControls: FC = ({
}));
return buildCascadeFiltersTree(filtersWithValue);
}, [filterValues, dataMaskSelected]);
+ const cascadeFilterIds = new Set(cascadeFilters.map(item => item.id));
+
+ let filtersInScope: CascadeFilter[] = [];
+ const filtersOutOfScope: CascadeFilter[] = [];
+ const dashboardHasTabs = Object.values(dashboardLayout).some(
+ element => element.type === TAB_TYPE,
+ );
+ const showCollapsePanel = dashboardHasTabs && cascadeFilters.length > 0;
+ if (!lastFocusedTabId || !dashboardHasTabs) {
+ filtersInScope = cascadeFilters;
+ } else {
+ cascadeFilters.forEach((filter, index) => {
+ if (cascadeFilters[index].tabsInScope?.includes(lastFocusedTabId)) {
+ filtersInScope.push(filter);
+ } else {
+ filtersOutOfScope.push(filter);
+ }
+ });
+ }
return (
- {cascadeFilters.map(filter => (
-
- setVisiblePopoverId(visible ? filter.id : null)
- }
- filter={filter}
- onFilterSelectionChange={onFilterSelectionChange}
- directPathToChild={directPathToChild}
- />
- ))}
+ {portalNodes
+ .filter((node, index) => cascadeFilterIds.has(filterValues[index].id))
+ .map((node, index) => (
+
+
+ setVisiblePopoverId(visible ? cascadeFilters[index].id : null)
+ }
+ filter={cascadeFilters[index]}
+ onFilterSelectionChange={onFilterSelectionChange}
+ directPathToChild={directPathToChild}
+ />
+
+ ))}
+ {filtersInScope.map(filter => {
+ const index = filterValues.findIndex(f => f.id === filter.id);
+ return ;
+ })}
+ {showCollapsePanel && (
+ css`
+ &.ant-collapse {
+ margin-top: ${filtersInScope.length > 0
+ ? theme.gridUnit * 6
+ : 0}px;
+ & > .ant-collapse-item {
+ & > .ant-collapse-header {
+ padding-left: 0;
+ padding-bottom: ${theme.gridUnit * 2}px;
+
+ & > .ant-collapse-arrow {
+ right: ${theme.gridUnit}px;
+ }
+ }
+
+ & .ant-collapse-content-box {
+ padding: ${theme.gridUnit * 4}px 0 0;
+ }
+ }
+ }
+ `}
+ >
+
+ {filtersOutOfScope.map(filter => {
+ const index = cascadeFilters.findIndex(f => f.id === filter.id);
+ return ;
+ })}
+
+
+ )}
);
};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index 200edfa35c66d..495f50f437bf4 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -47,16 +47,20 @@ import { useCascadingFilters } from './state';
const FilterItem = styled.div`
min-height: ${({ theme }) => theme.gridUnit * 11}px;
padding-bottom: ${({ theme }) => theme.gridUnit * 3}px;
+ & > div > div {
+ height: auto;
+ }
`;
const FilterValue: React.FC = ({
+ dataMaskSelected,
filter,
directPathToChild,
onFilterSelectionChange,
}) => {
const { id, targets, filterType, adhoc_filters, time_range } = filter;
const metadata = getChartMetadataRegistry().get(filterType);
- const cascadingFilters = useCascadingFilters(id);
+ const cascadingFilters = useCascadingFilters(id, dataMaskSelected);
const [state, setState] = useState([]);
const [error, setError] = useState('');
const [formData, setFormData] = useState>({});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts
index 7be5835780895..1aaf772722c16 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/state.ts
@@ -18,22 +18,27 @@
*/
import { useSelector } from 'react-redux';
import { NativeFiltersState } from 'src/dashboard/reducers/types';
+import { DataMaskStateWithId } from 'src/dataMask/types';
+import { ExtraFormData } from '@superset-ui/core';
import { mergeExtraFormData } from '../../utils';
-import { useNativeFiltersDataMask } from '../state';
// eslint-disable-next-line import/prefer-default-export
-export function useCascadingFilters(id: string) {
+export function useCascadingFilters(
+ id: string,
+ dataMaskSelected?: DataMaskStateWithId,
+): ExtraFormData {
const { filters } = useSelector(
state => state.nativeFilters,
);
const filter = filters[id];
const cascadeParentIds: string[] = filter?.cascadeParentIds ?? [];
let cascadedFilters = {};
- const nativeFiltersDataMask = useNativeFiltersDataMask();
cascadeParentIds.forEach(parentId => {
- const parentState = nativeFiltersDataMask[parentId] || {};
- const { extraFormData: parentExtra = {} } = parentState;
- cascadedFilters = mergeExtraFormData(cascadedFilters, parentExtra);
+ const parentState = dataMaskSelected?.[parentId];
+ cascadedFilters = mergeExtraFormData(
+ cascadedFilters,
+ parentState?.extraFormData,
+ );
});
return cascadedFilters;
}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts
index 0b39dd210be5e..93bf76d8c2946 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/types.ts
@@ -18,9 +18,11 @@
*/
import React from 'react';
import { DataMask } from '@superset-ui/core';
+import { DataMaskStateWithId } from 'src/dataMask/types';
import { Filter } from '../../types';
export interface FilterProps {
+ dataMaskSelected?: DataMaskStateWithId;
filter: Filter & {
dataMask?: DataMask;
};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
index 50c16e95018f5..44a4f82aed723 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
@@ -19,7 +19,7 @@
/* eslint-disable no-param-reassign */
import { HandlerFunction, styled, t } from '@superset-ui/core';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import cx from 'classnames';
import Icon from 'src/components/Icon';
@@ -37,11 +37,7 @@ import { testWithId } from 'src/utils/testUtils';
import { Filter } from 'src/dashboard/components/nativeFilters/types';
import Loading from 'src/components/Loading';
import { getInitialDataMask } from 'src/dataMask/reducer';
-import {
- getOnlyExtraFormData,
- mapParentFiltersToChildren,
- TabIds,
-} from './utils';
+import { getOnlyExtraFormData, TabIds } from './utils';
import FilterSets from './FilterSets';
import {
useNativeFiltersDataMask,
@@ -175,10 +171,6 @@ const FilterBar: React.FC = ({
const filterValues = Object.values(filters);
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
- const cascadeChildren = useMemo(
- () => mapParentFiltersToChildren(filterValues),
- [filterValues],
- );
useEffect(() => {
setDataMaskSelected(() => dataMaskApplied);
@@ -190,15 +182,6 @@ const FilterBar: React.FC = ({
) => {
setIsFilterSetChanged(tab !== TabIds.AllFilters);
setDataMaskSelected(draft => {
- const children = cascadeChildren[filter.id] || [];
- // force instant updating on initialization or for parent filters when dataMaskSelected has filter
- if (
- dataMaskSelected[filter.id] &&
- (filter.isInstant || children.length > 0)
- ) {
- dispatch(updateDataMask(filter.id, dataMask));
- }
-
draft[filter.id] = {
...(getInitialDataMask(filter.id) as DataMaskWithId),
...dataMask,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx
index bab37da734089..3d687961eaf8e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTabs.tsx
@@ -24,7 +24,7 @@ import Icon from 'src/components/Icon';
import { FilterRemoval } from './types';
import { REMOVAL_DELAY_SECS } from './utils';
-export const FILTER_WIDTH = 200;
+export const FILTER_WIDTH = 180;
export const StyledSpan = styled.span`
cursor: pointer;
@@ -42,11 +42,9 @@ export const StyledFilterTitle = styled.span`
export const StyledAddFilterBox = styled.div`
color: ${({ theme }) => theme.colors.primary.dark1};
- text-align: left;
- padding: ${({ theme }) => theme.gridUnit * 2}px 0;
- margin: ${({ theme }) => theme.gridUnit * 3}px 0 0
- ${({ theme }) => -theme.gridUnit * 2}px;
- border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light1};
+ padding: ${({ theme }) => theme.gridUnit * 2}px;
+ border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ cursor: pointer;
&:hover {
color: ${({ theme }) => theme.colors.primary.base};
@@ -89,12 +87,19 @@ const FilterTabsContainer = styled(LineEditableTabs)`
& > .ant-tabs-content-holder {
border-left: 1px solid ${theme.colors.grayscale.light2};
- margin-right: ${theme.gridUnit * 4}px;
+ padding-right: ${theme.gridUnit * 4}px;
+ overflow-x: hidden;
+ overflow-y: auto;
}
+
& > .ant-tabs-content-holder ~ .ant-tabs-content-holder {
border: none;
}
+ &.ant-tabs-card > .ant-tabs-nav .ant-tabs-ink-bar {
+ visibility: hidden;
+ }
+
&.ant-tabs-left
> .ant-tabs-content-holder
> .ant-tabs-content
@@ -104,10 +109,13 @@ const FilterTabsContainer = styled(LineEditableTabs)`
}
.ant-tabs-nav-list {
- padding-top: ${theme.gridUnit * 4}px;
- padding-right: ${theme.gridUnit * 2}px;
- padding-bottom: ${theme.gridUnit * 4}px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding-top: ${theme.gridUnit * 2}px;
+ padding-right: ${theme.gridUnit}px;
+ padding-bottom: ${theme.gridUnit * 3}px;
padding-left: ${theme.gridUnit * 3}px;
+ width: 270px;
}
// extra selector specificity:
@@ -135,6 +143,24 @@ const FilterTabsContainer = styled(LineEditableTabs)`
justify-content: space-between;
text-transform: unset;
}
+
+ .ant-tabs-nav-more {
+ display: none;
+ }
+
+ .ant-tabs-extra-content {
+ width: 100%;
+ }
+ `}
+`;
+
+const StyledHeader = styled.div`
+ ${({ theme }) => `
+ color: ${theme.colors.grayscale.dark1};
+ font-size: ${theme.typography.sizes.l}px;
+ padding-top: ${theme.gridUnit * 4}px;
+ padding-right: ${theme.gridUnit * 4}px;
+ padding-left: ${theme.gridUnit * 4}px;
`}
`;
@@ -160,16 +186,37 @@ const FilterTabs: FC = ({
children,
}) => (
- {' '}
- {t('Add filter')}
-
- }
+ hideAdd
+ tabBarExtraContent={{
+ left: {t('Filters')} ,
+ right: (
+ {
+ onEdit('', 'add');
+ setTimeout(() => {
+ const element = document.getElementById('native-filters-tabs');
+ if (element) {
+ const navList = element.getElementsByClassName(
+ 'ant-tabs-nav-list',
+ )[0];
+ navList.scrollTop = navList.scrollHeight;
+ }
+ }, 0);
+ }}
+ >
+ {' '}
+
+ {t('Add filter')}
+
+
+ ),
+ }}
>
{filterIds.map(id => (
void;
+}
+
+const StyledContainer = styled.div<{ checked: boolean }>`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ min-height: ${({ theme }) => theme.gridUnit * 10}px;
+ padding-top: ${({ theme }) => theme.gridUnit * 2 + 2}px;
+
+ .checkbox {
+ margin-bottom: ${({ theme, checked }) => (checked ? theme.gridUnit : 0)}px;
+ }
+
+ & > div {
+ margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
+ }
+`;
+
+const CollapsibleControl = (props: CollapsibleControlProps) => {
+ const { checked = false, title, children, onChange } = props;
+ const [isChecked, setIsChecked] = useState(checked);
+ return (
+
+ {
+ const value = e.target.checked;
+ setIsChecked(value);
+ if (onChange) {
+ onChange(value);
+ }
+ }}
+ >
+ {title}
+
+ {isChecked && children}
+
+ );
+};
+
+export { CollapsibleControl, CollapsibleControlProps };
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ControlItems.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ControlItems.tsx
deleted file mode 100644
index ae20a8886035c..0000000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ControlItems.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * 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 {
- CustomControlItem,
- InfoTooltipWithTrigger,
-} from '@superset-ui/chart-controls';
-import React, { FC } from 'react';
-import { Checkbox } from 'src/common/components';
-import { FormInstance } from 'antd/lib/form';
-import { getChartControlPanelRegistry, t } from '@superset-ui/core';
-import { Tooltip } from 'src/components/Tooltip';
-import { getControlItems, setNativeFilterFieldValues } from './utils';
-import { NativeFiltersForm, NativeFiltersFormItem } from '../types';
-import { StyledCheckboxFormItem } from './FiltersConfigForm';
-import { Filter } from '../../types';
-
-type ControlItemsProps = {
- disabled: boolean;
- filterId: string;
- forceUpdate: Function;
- filterToEdit?: Filter;
- form: FormInstance;
- formFilter?: NativeFiltersFormItem;
-};
-
-const ControlItems: FC = ({
- disabled,
- forceUpdate,
- form,
- filterId,
- filterToEdit,
- formFilter,
-}) => {
- const filterType = formFilter?.filterType;
-
- if (!filterType) return null;
-
- const controlPanelRegistry = getChartControlPanelRegistry();
- const controlItems =
- getControlItems(controlPanelRegistry.get(filterType)) ?? [];
- return (
- <>
- {controlItems
- .filter(
- (controlItem: CustomControlItem) =>
- controlItem?.config?.renderTrigger,
- )
- .map(controlItem => (
-
-
- {
- if (!controlItem.config.resetConfig) {
- forceUpdate();
- return;
- }
- setNativeFilterFieldValues(form, filterId, {
- defaultDataMask: null,
- });
- forceUpdate();
- }}
- >
- {controlItem.config.label}{' '}
- {controlItem.config.description && (
-
- )}
-
-
-
- ))}
- >
- );
-};
-export default ControlItems;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx
index 0d05add39e707..524ab83cd79ec 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DefaultValue.tsx
@@ -73,6 +73,7 @@ const DefaultValue: FC = ({
chartType={formFilter?.filterType}
hooks={{ setDataMask }}
enableNoResults={enableNoResults}
+ filterState={formFilter.defaultDataMask?.filterState}
/>
);
};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 85e642c87d433..76fa33bdc7097 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -32,9 +32,17 @@ import {
Metric,
} from '@superset-ui/chart-controls';
import { FormInstance } from 'antd/lib/form';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useState,
+ useMemo,
+ forwardRef,
+ useImperativeHandle,
+} from 'react';
import { useSelector } from 'react-redux';
-import { Checkbox, Form, Input } from 'src/common/components';
+import { FormItem } from 'src/components/Form';
+import { Checkbox, Input } from 'src/common/components';
import { Select } from 'src/components/Select';
import SupersetResourceSelect, {
cachedSupersetGet,
@@ -45,7 +53,6 @@ import { addDangerToast } from 'src/messageToasts/actions';
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
import SelectControl from 'src/explore/components/controls/SelectControl';
import Collapse from 'src/components/Collapse';
-import Button from 'src/components/Button';
import { getChartDataRequest } from 'src/chart/chartAction';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { waitForAsyncData } from 'src/middleware/asyncEvent';
@@ -60,10 +67,11 @@ import {
import { useBackendFormUpdate } from './state';
import { getFormData } from '../../utils';
import { Filter } from '../../types';
-import ControlItems from './ControlItems';
+import getControlItemsMap from './getControlItemsMap';
import FilterScope from './FilterScope/FilterScope';
import RemovedFilter from './RemovedFilter';
import DefaultValue from './DefaultValue';
+import { CollapsibleControl } from './CollapsibleControl';
import {
CASCADING_FILTERS,
getFiltersConfigModalTestId,
@@ -77,19 +85,42 @@ const StyledContainer = styled.div`
justify-content: space-between;
`;
-const StyledDatasetContainer = styled.div`
+const StyledRowContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
+ width: 100%;
`;
-export const StyledFormItem = styled(Form.Item)`
+export const StyledFormItem = styled(FormItem)`
width: 49%;
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
+
+ & .ant-form-item-label {
+ padding-bottom: 0px;
+ }
+
+ & .ant-form-item-control-input {
+ min-height: ${({ theme }) => theme.gridUnit * 10}px;
+ }
`;
-export const StyledCheckboxFormItem = styled(Form.Item)`
- margin-bottom: 0;
+export const StyledRowFormItem = styled(FormItem)`
+ margin-bottom: 0px;
+ padding-bottom: 0px;
+ min-width: 50%;
+
+ & .ant-form-item-label {
+ padding-bottom: 0px;
+ }
+
+ .ant-form-item-control-input-content > div > div {
+ height: auto;
+ }
+
+ & .ant-form-item-control-input {
+ min-height: ${({ theme }) => theme.gridUnit * 10}px;
+ }
`;
export const StyledLabel = styled.span`
@@ -98,7 +129,7 @@ export const StyledLabel = styled.span`
text-transform: uppercase;
`;
-const CleanFormItem = styled(Form.Item)`
+const CleanFormItem = styled(FormItem)`
margin-bottom: 0;
`;
@@ -120,6 +151,10 @@ const StyledCollapse = styled(Collapse)`
border: 0px;
}
+ .ant-collapse-content-box {
+ padding-top: ${({ theme }) => theme.gridUnit * 2}px;
+ }
+
&.ant-collapse > .ant-collapse-item {
border: 0px;
border-radius: 0px;
@@ -127,9 +162,29 @@ const StyledCollapse = styled(Collapse)`
`;
const StyledTabs = styled(Tabs)`
+ .ant-tabs-nav {
+ position: sticky;
+ top: 0px;
+ background: white;
+ z-index: 1;
+ }
+
.ant-tabs-nav-list {
padding: 0px;
}
+
+ .ant-form-item-label {
+ padding-bottom: 0px;
+ }
+`;
+
+const StyledAsterisk = styled.span`
+ color: ${({ theme }) => theme.colors.error.base};
+ font-family: SimSun, sans-serif;
+ margin-right: ${({ theme }) => theme.gridUnit - 1}px;
+ &:before {
+ content: '*';
+ }
`;
const FilterTabs = {
@@ -169,24 +224,51 @@ const FILTERS_WITHOUT_COLUMN = [
'filter_timecolumn',
'filter_groupby',
];
+
const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
+const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
+
+// TODO: Rename the filter plugins and remove this mapping
+const FILTER_TYPE_NAME_MAPPING = {
+ [t('Select filter')]: t('Value'),
+ [t('Range filter')]: t('Numerical range'),
+ [t('Time filter')]: t('Time range'),
+ [t('Time column')]: t('Time column'),
+ [t('Time grain')]: t('Time grain'),
+ [t('Group By')]: t('Group by'),
+};
+
/**
* The configuration form for a specific filter.
* Assigns field values to `filters[filterId]` in the form.
*/
-export const FiltersConfigForm: React.FC = ({
- filterId,
- filterToEdit,
- removed,
- restoreFilter,
- form,
- parentFilters,
-}) => {
+const FiltersConfigForm = (
+ {
+ filterId,
+ filterToEdit,
+ removed,
+ restoreFilter,
+ form,
+ parentFilters,
+ }: FiltersConfigFormProps,
+ ref: React.RefObject,
+) => {
const [metrics, setMetrics] = useState([]);
+ const [activeTabKey, setActiveTabKey] = useState(
+ FilterTabs.configuration.key,
+ );
+ const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
+ string | string[]
+ >(FilterPanels.basic.key);
+ const [hasDefaultValue, setHasDefaultValue] = useState(
+ !!filterToEdit?.defaultDataMask?.filterState?.value,
+ );
const forceUpdate = useForceUpdate();
const [datasetDetails, setDatasetDetails] = useState>();
- const formFilter = form.getFieldValue('filters')?.[filterId] || {};
+ const defaultFormFilter = useMemo(() => {}, []);
+ const formFilter =
+ form.getFieldValue('filters')?.[filterId] || defaultFormFilter;
const nativeFilterItems = getChartMetadataRegistry().items;
const nativeFilterVizTypes = Object.entries(nativeFilterItems)
// @ts-ignore
@@ -228,6 +310,12 @@ export const FiltersConfigForm: React.FC = ({
}
}, [datasetId, hasColumn]);
+ useImperativeHandle(ref, () => ({
+ changeTab(tab: 'configuration' | 'scoping') {
+ setActiveTabKey(tab);
+ },
+ }));
+
const hasMetrics = hasColumn && !!metrics.length;
const hasFilledDataset =
@@ -243,7 +331,7 @@ export const FiltersConfigForm: React.FC = ({
useBackendFormUpdate(form, filterId);
- const refreshHandler = () => {
+ const refreshHandler = useCallback(() => {
if (!hasDataset || !formFilter?.dataset?.value) {
forceUpdate();
return;
@@ -287,7 +375,7 @@ export const FiltersConfigForm: React.FC = ({
forceUpdate();
}
});
- };
+ }, [filterId, forceUpdate, form, formFilter, hasDataset]);
const defaultDatasetSelectOptions = Object.values(loadedDatasets).map(
datasetToSelectOption,
@@ -304,6 +392,19 @@ export const FiltersConfigForm: React.FC = ({
...formFilter,
});
+ useEffect(() => {
+ if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
+ refreshHandler();
+ }
+ }, [
+ hasDataset,
+ hasFilledDataset,
+ hasDefaultValue,
+ formFilter,
+ isDataDirty,
+ refreshHandler,
+ ]);
+
const onDatasetSelectError = useCallback(
({ error, message }: ClientErrorObject) => {
let errorText = message || error || t('An error has occurred');
@@ -315,301 +416,463 @@ export const FiltersConfigForm: React.FC = ({
[],
);
- if (removed) {
- return restoreFilter(filterId)} />;
- }
-
const parentFilterOptions = parentFilters.map(filter => ({
value: filter.id,
label: filter.title,
}));
+ const parentFilter = parentFilterOptions.find(
+ ({ value }) => value === filterToEdit?.cascadeParentIds[0],
+ );
+
+ const hasParentFilter = !!parentFilter;
+
+ const hasPreFilter =
+ !!filterToEdit?.adhoc_filters || !!filterToEdit?.time_range;
+
+ const hasSorting =
+ typeof filterToEdit?.controlValues?.sortAscending === 'boolean';
+
const showDefaultValue = !hasDataset || (!isDataDirty && hasFilledDataset);
+ const controlItems = formFilter
+ ? getControlItemsMap({
+ disabled: false,
+ forceUpdate,
+ form,
+ filterId,
+ filterType: formFilter.filterType,
+ filterToEdit,
+ })
+ : {};
+
+ const onSortChanged = (value: boolean | undefined) => {
+ const previous = form.getFieldValue('filters')?.[filterId].controlValues;
+ setNativeFilterFieldValues(form, filterId, {
+ controlValues: {
+ ...previous,
+ sortAscending: value,
+ },
+ });
+ forceUpdate();
+ };
+
+ let hasCheckedAdvancedControl = hasParentFilter || hasPreFilter || hasSorting;
+ if (!hasCheckedAdvancedControl) {
+ hasCheckedAdvancedControl = Object.keys(controlItems)
+ .filter(key => !BASIC_CONTROL_ITEMS.includes(key))
+ .some(key => controlItems[key].checked);
+ }
+
+ useEffect(() => {
+ const activeFilterPanelKey = [FilterPanels.basic.key];
+ if (hasCheckedAdvancedControl) {
+ activeFilterPanelKey.push(FilterPanels.advanced.key);
+ }
+ setActiveFilterPanelKey(activeFilterPanelKey);
+ }, [hasCheckedAdvancedControl]);
+
+ if (removed) {
+ return restoreFilter(filterId)} />;
+ }
+
return (
- <>
-
-
-
- {t('Filter name')}}
- initialValue={filterToEdit?.name}
- rules={[{ required: !removed, message: t('Name is required') }]}
- >
-
-
+ setActiveTabKey(activeKey)}
+ centered
+ >
+
+
+ {t('Filter name')}}
+ initialValue={filterToEdit?.name}
+ rules={[{ required: !removed, message: t('Name is required') }]}
+ >
+
+
+ {t('Filter Type')}}
+ {...getFiltersConfigModalTestId('filter-type')}
+ >
+ {
+ // @ts-ignore
+ const name = nativeFilterItems[filterType]?.value.name;
+ const mappedName = name
+ ? FILTER_TYPE_NAME_MAPPING[name]
+ : undefined;
+ return {
+ value: filterType,
+ label: mappedName || name,
+ };
+ })}
+ onChange={({ value }: { value: string }) => {
+ setNativeFilterFieldValues(form, filterId, {
+ filterType: value,
+ defaultDataMask: null,
+ });
+ forceUpdate();
+ }}
+ />
+
+
+ {hasDataset && (
+
{t('Filter Type')}}
- {...getFiltersConfigModalTestId('filter-type')}
+ name={['filters', filterId, 'dataset']}
+ initialValue={{ value: initialDatasetId }}
+ label={{t('Dataset')} }
+ rules={[
+ { required: !removed, message: t('Dataset is required') },
+ ]}
+ {...getFiltersConfigModalTestId('datasource-input')}
>
- ({
- value: filterType,
- // @ts-ignore
- label: nativeFilterItems[filterType]?.value.name,
- }))}
- onChange={({ value }: { value: string }) => {
- setNativeFilterFieldValues(form, filterId, {
- filterType: value,
- defaultDataMask: null,
- });
+ {
+ // We need reset column when dataset changed
+ if (datasetId && e?.value !== datasetId) {
+ setNativeFilterFieldValues(form, filterId, {
+ defaultDataMask: null,
+ column: null,
+ });
+ }
forceUpdate();
}}
/>
-
- {hasDataset && (
-
+ {hasColumn && (
{t('Dataset')}}
+ // don't show the column select unless we have a dataset
+ // style={{ display: datasetId == null ? undefined : 'none' }}
+ name={['filters', filterId, 'column']}
+ initialValue={initColumn}
+ label={{t('Column')} }
rules={[
- { required: !removed, message: t('Dataset is required') },
+ { required: !removed, message: t('Field is required') },
]}
- {...getFiltersConfigModalTestId('datasource-input')}
+ data-test="field-input"
>
- {
- // We need reset column when dataset changed
- if (datasetId && e?.value !== datasetId) {
- setNativeFilterFieldValues(form, filterId, {
- defaultDataMask: null,
- column: null,
- });
- }
+ {
+ // We need reset default value when when column changed
+ setNativeFilterFieldValues(form, filterId, {
+ defaultDataMask: null,
+ });
forceUpdate();
}}
/>
- {hasColumn && (
- {t('Column')}}
- rules={[
- { required: !removed, message: t('Field is required') },
- ]}
- data-test="field-input"
- >
- {
- // We need reset default value when when column changed
+ )}
+
+ )}
+ setActiveFilterPanelKey(key)}
+ expandIconPosition="right"
+ >
+
+ {hasFilledDataset && (
+
+ )}
+
+ setHasDefaultValue(value)}
+ >
+ {t('Default Value')}}
+ required
+ rules={[
+ {
+ required: true,
+ },
+ {
+ validator: (rule, value) => {
+ const hasValue = !!value.filterState?.value;
+ if (hasValue) {
+ return Promise.resolve();
+ }
+ return Promise.reject(
+ new Error(t('Default value is required')),
+ );
+ },
+ },
+ ]}
+ >
+ {showDefaultValue ? (
+ {
setNativeFilterFieldValues(form, filterId, {
- defaultDataMask: null,
+ defaultDataMask: dataMask,
});
+ form.validateFields([
+ ['filters', filterId, 'defaultDataMask'],
+ ]);
forceUpdate();
}}
+ filterId={filterId}
+ hasDataset={hasDataset}
+ form={form}
+ formData={newFormData}
+ enableNoResults={enableNoResults}
/>
-
- )}
-
- )}
-
+ ) : (
+ t('Fill all required fields to enable "Default Value"')
+ )}
+
+
+ {Object.keys(controlItems)
+ .filter(key => BASIC_CONTROL_ITEMS.includes(key))
+ .map(key => controlItems[key].element)}
+
+
+ {t('Apply changes instantly')}
+
+
+
+ {((hasDataset && hasAdditionalFilters) || hasMetrics) && (
- {hasFilledDataset && (
-
- )}
-
{isCascadingFilter && (
- {t('Parent filter')}}
- initialValue={parentFilterOptions.find(
- ({ value }) => value === filterToEdit?.cascadeParentIds[0],
- )}
- data-test="parent-filter-input"
+ {
+ if (checked) {
+ // execute after render
+ setTimeout(
+ () =>
+ form.validateFields([
+ ['filters', filterId, 'parentFilter'],
+ ]),
+ 0,
+ );
+ }
+ }}
>
-
-
+ {t('Parent filter')}}
+ initialValue={parentFilter}
+ data-test="parent-filter-input"
+ required
+ rules={[
+ {
+ required: true,
+ message: t('Parent filter is required'),
+ },
+ ]}
+ >
+
+
+
)}
-
- }>
- {hasDataset && hasFilledDataset && (
-
- {isDataDirty ? t('Populate') : t('Refresh')}
-
- )}
-
- {t('Default Value')}}
+ {Object.keys(controlItems)
+ .filter(key => !BASIC_CONTROL_ITEMS.includes(key))
+ .map(key => controlItems[key].element)}
+ {hasDataset && hasAdditionalFilters && (
+ {
+ if (checked) {
+ // execute after render
+ setTimeout(
+ () =>
+ form.validateFields([
+ ['filters', filterId, 'adhoc_filters'],
+ ]),
+ 0,
+ );
+ }
+ }}
>
- {showDefaultValue ? (
- {
+
+ c.filterable,
+ ) || []
+ }
+ savedMetrics={datasetDetails?.metrics || []}
+ datasource={datasetDetails}
+ onChange={(filters: AdhocFilter[]) => {
setNativeFilterFieldValues(form, filterId, {
- defaultDataMask: dataMask,
+ adhoc_filters: filters,
});
forceUpdate();
}}
- filterId={filterId}
- hasDataset={hasDataset}
- form={form}
- formData={newFormData}
- enableNoResults={enableNoResults}
+ label={
+
+
+ {t('Pre-filter')}
+
+ }
/>
- ) : hasFilledDataset ? (
- t('Click "Populate" to get "Default Value" ->')
- ) : (
- t('Fill all required fields to enable "Default Value"')
- )}
-
-
-
-
- {t('Apply changes instantly')}
-
-
-
-
- {((hasDataset && hasAdditionalFilters) || hasMetrics) && (
-
- {hasDataset && hasAdditionalFilters && (
- <>
+
+ {t('Time range')}}
+ initialValue={filterToEdit?.time_range || 'No filter'}
+ >
+ {
+ setNativeFilterFieldValues(form, filterId, {
+ time_range: timeRange,
+ });
+ forceUpdate();
+ }}
+ />
+
+
+ )}
+ {formFilter?.filterType !== 'filter_range' && (
+ onSortChanged(checked || undefined)}
+ checked={hasSorting}
+ >
+
{t('Sort type')}}
>
- c.filterable,
- ) || []
+
+ onSortChanged(value)
}
- savedMetrics={datasetDetails?.metrics || []}
- datasource={datasetDetails}
- onChange={(filters: AdhocFilter[]) => {
- setNativeFilterFieldValues(form, filterId, {
- adhoc_filters: filters,
- });
- forceUpdate();
- }}
- label={{t('Adhoc filters')} }
/>
- {t('Time range')}}
- initialValue={filterToEdit?.time_range || 'No filter'}
- >
- {
- setNativeFilterFieldValues(form, filterId, {
- time_range: timeRange,
- });
- forceUpdate();
- }}
- />
-
- >
- )}
- {hasMetrics && (
- {t('Sort Metric')}}
- data-test="field-input"
- >
- ({
- value: metric.metric_name,
- label: metric.verbose_name ?? metric.metric_name,
- }))}
- onChange={(value: string | null): void => {
- if (value !== undefined) {
- setNativeFilterFieldValues(form, filterId, {
- sortMetric: value,
- });
- forceUpdate();
- }
- }}
- />
-
- )}
-
- )}
-
-
-
-
- setNativeFilterFieldValues(form, filterId, values)
- }
- pathToFormValue={['filters', filterId]}
- forceUpdate={forceUpdate}
- scope={filterToEdit?.scope}
- formScope={formFilter?.scope}
- formScoping={formFilter?.scoping}
- />
-
-
- >
+ {hasMetrics && (
+ {t('Sort Metric')}}
+ data-test="field-input"
+ >
+ ({
+ value: metric.metric_name,
+ label: metric.verbose_name ?? metric.metric_name,
+ }))}
+ onChange={(value: string | null): void => {
+ if (value !== undefined) {
+ setNativeFilterFieldValues(form, filterId, {
+ sortMetric: value,
+ });
+ forceUpdate();
+ }
+ }}
+ />
+
+ )}
+
+
+ )}
+
+ )}
+
+
+
+
+ setNativeFilterFieldValues(form, filterId, values)
+ }
+ pathToFormValue={['filters', filterId]}
+ forceUpdate={forceUpdate}
+ scope={filterToEdit?.scope}
+ formScope={formFilter?.scope}
+ formScoping={formFilter?.scoping}
+ />
+
+
);
};
-export default FiltersConfigForm;
+export default forwardRef(
+ FiltersConfigForm,
+);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ControlItems.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
similarity index 61%
rename from superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ControlItems.test.tsx
rename to superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
index 04cfe503c7f7d..fcff22da0ee74 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ControlItems.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
@@ -19,23 +19,57 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
+import { Filter } from 'src/dashboard/components/nativeFilters/types';
+import { FormInstance } from 'src/common/components';
import { getControlItems, setNativeFilterFieldValues } from './utils';
-
-import ControlItems from './ControlItems';
+import getControlItemsMap, { ControlItemsProps } from './getControlItemsMap';
jest.mock('./utils', () => ({
getControlItems: jest.fn(),
setNativeFilterFieldValues: jest.fn(),
}));
-const createProps = () => ({
+const formMock: FormInstance = {
+ __INTERNAL__: { itemRef: () => () => {} },
+ scrollToField: () => {},
+ getFieldInstance: () => {},
+ getFieldValue: () => {},
+ getFieldsValue: () => {},
+ getFieldError: () => [],
+ getFieldsError: () => [],
+ isFieldsTouched: () => false,
+ isFieldTouched: () => false,
+ isFieldValidating: () => false,
+ isFieldsValidating: () => false,
+ resetFields: () => {},
+ setFields: () => {},
+ setFieldsValue: () => {},
+ validateFields: () => Promise.resolve(),
+ submit: () => {},
+};
+
+const filterMock: Filter = {
+ cascadeParentIds: [],
+ defaultDataMask: {},
+ isInstant: false,
+ id: 'mock',
+ name: 'mock',
+ scope: {
+ rootPath: [],
+ excluded: [],
+ },
+ filterType: '',
+ targets: [{}],
+ controlValues: {},
+};
+
+const createProps: () => ControlItemsProps = () => ({
+ disabled: false,
forceUpdate: jest.fn(),
- form: 'form',
+ form: formMock,
filterId: 'filterId',
- filterToEdit: '',
- formFilter: {
- filterType: 'filterType',
- },
+ filterToEdit: filterMock,
+ filterType: 'filterType',
});
const createControlItems = () => [
@@ -49,33 +83,36 @@ beforeEach(() => {
jest.clearAllMocks();
});
+function renderControlItems(
+ controlItemsMap: ReturnType,
+) {
+ return render(
+ <>{Object.values(controlItemsMap).map(value => value.element)}>,
+ );
+}
+
test('Should render null when has no "formFilter"', () => {
- const defaultProps = createProps();
- const props = {
- forceUpdate: defaultProps.forceUpdate,
- form: defaultProps.form,
- filterId: defaultProps.filterId,
- };
- const { container } = render( );
+ const props = createProps();
+ const controlItemsMap = getControlItemsMap(props);
+ const { container } = renderControlItems(controlItemsMap);
expect(container.children).toHaveLength(0);
});
test('Should render null when has no "formFilter.filterType" is falsy value', () => {
- const defaultProps = createProps();
- const props = {
- forceUpdate: defaultProps.forceUpdate,
- form: defaultProps.form,
- filterId: defaultProps.filterId,
- formFilter: { name: 'name', filterType: 'filterType' },
- };
- const { container } = render( );
+ const props = createProps();
+ const controlItemsMap = getControlItemsMap({
+ ...props,
+ filterType: 'filterType',
+ });
+ const { container } = renderControlItems(controlItemsMap);
expect(container.children).toHaveLength(0);
});
test('Should render null empty when "getControlItems" return []', () => {
const props = createProps();
(getControlItems as jest.Mock).mockReturnValue([]);
- const { container } = render( );
+ const controlItemsMap = getControlItemsMap(props);
+ const { container } = renderControlItems(controlItemsMap);
expect(container.children).toHaveLength(0);
});
@@ -83,8 +120,8 @@ test('Should render null empty when "controlItems" are falsy', () => {
const props = createProps();
const controlItems = [null, false, {}, { config: { renderTrigger: false } }];
(getControlItems as jest.Mock).mockReturnValue(controlItems);
-
- const { container } = render( );
+ const controlItemsMap = getControlItemsMap(props);
+ const { container } = renderControlItems(controlItemsMap);
expect(container.children).toHaveLength(0);
});
@@ -96,16 +133,16 @@ test('Should render render ControlItems', () => {
{ name: 'name_2', config: { renderTrigger: true } },
];
(getControlItems as jest.Mock).mockReturnValue(controlItems);
-
- render( );
+ const controlItemsMap = getControlItemsMap(props);
+ renderControlItems(controlItemsMap);
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
});
test('Clickin on checkbox', () => {
const props = createProps();
(getControlItems as jest.Mock).mockReturnValue(createControlItems());
- render( );
-
+ const controlItemsMap = getControlItemsMap(props);
+ renderControlItems(controlItemsMap);
expect(props.forceUpdate).not.toBeCalled();
expect(setNativeFilterFieldValues).not.toBeCalled();
userEvent.click(screen.getByRole('checkbox'));
@@ -118,8 +155,8 @@ test('Clickin on checkbox when resetConfig:flase', () => {
(getControlItems as jest.Mock).mockReturnValue([
{ name: 'name_1', config: { renderTrigger: true, resetConfig: false } },
]);
- render( );
-
+ const controlItemsMap = getControlItemsMap(props);
+ renderControlItems(controlItemsMap);
expect(props.forceUpdate).not.toBeCalled();
expect(setNativeFilterFieldValues).not.toBeCalled();
userEvent.click(screen.getByRole('checkbox'));
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
new file mode 100644
index 0000000000000..15e877ee57478
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -0,0 +1,113 @@
+/**
+ * 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 {
+ CustomControlItem,
+ InfoTooltipWithTrigger,
+} from '@superset-ui/chart-controls';
+import React from 'react';
+import { Checkbox } from 'src/common/components';
+import { FormInstance } from 'antd/lib/form';
+import { getChartControlPanelRegistry, t } from '@superset-ui/core';
+import { Tooltip } from 'src/components/Tooltip';
+import { getControlItems, setNativeFilterFieldValues } from './utils';
+import { NativeFiltersForm } from '../types';
+import { StyledRowFormItem } from './FiltersConfigForm';
+import { Filter } from '../../types';
+
+export interface ControlItemsProps {
+ disabled: boolean;
+ forceUpdate: Function;
+ form: FormInstance;
+ filterId: string;
+ filterType: string;
+ filterToEdit?: Filter;
+}
+
+export default function getControlItemsMap({
+ disabled,
+ forceUpdate,
+ form,
+ filterId,
+ filterType,
+ filterToEdit,
+}: ControlItemsProps) {
+ const controlPanelRegistry = getChartControlPanelRegistry();
+ const controlItems =
+ getControlItems(controlPanelRegistry.get(filterType)) ?? [];
+ const map: Record<
+ string,
+ { element: React.ReactNode; checked: boolean }
+ > = {};
+
+ controlItems
+ .filter(
+ (controlItem: CustomControlItem) =>
+ controlItem?.config?.renderTrigger &&
+ controlItem.name !== 'sortAscending',
+ )
+ .forEach(controlItem => {
+ const initialValue =
+ filterToEdit?.controlValues?.[controlItem.name] ??
+ controlItem?.config?.default;
+ const element = (
+
+
+ {
+ if (!controlItem.config.resetConfig) {
+ forceUpdate();
+ return;
+ }
+ setNativeFilterFieldValues(form, filterId, {
+ defaultDataMask: null,
+ });
+ forceUpdate();
+ }}
+ >
+ {controlItem.config.label}{' '}
+ {controlItem.config.description && (
+
+ )}
+
+
+
+ );
+ map[controlItem.name] = { element, checked: initialValue };
+ });
+ return map;
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
index c1017b6fe0249..c1f39c0cd9311 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState, useRef } from 'react';
import { uniq } from 'lodash';
import { t, styled } from '@superset-ui/core';
import { Form } from 'src/common/components';
@@ -27,6 +27,7 @@ import { useFilterConfigMap, useFilterConfiguration } from '../state';
import { FilterRemoval, NativeFiltersForm } from './types';
import { FilterConfiguration } from '../types';
import {
+ validateForm,
createHandleSave,
createHandleTabEdit,
generateFilterId,
@@ -46,7 +47,7 @@ const StyledModalWrapper = styled(StyledModal)`
export const StyledModalBody = styled.div`
display: flex;
- height: 500px;
+ height: 700px;
flex-direction: row;
.filters-list {
width: ${({ theme }) => theme.gridUnit * 50}px;
@@ -89,6 +90,8 @@ export function FiltersConfigModal({
}: FiltersConfigModalProps) {
const [form] = Form.useForm();
+ const configFormRef = useRef();
+
// the filter config from redux state, this does not change until modal is closed.
const filterConfig = useFilterConfiguration();
const filterConfigMap = useFilterConfigMap();
@@ -180,23 +183,40 @@ export function FiltersConfigModal({
filterIds
.filter(filterId => filterId !== id && !removedFilters[filterId])
.filter(filterId =>
- CASCADING_FILTERS.includes(formValues.filters[filterId]?.filterType),
+ CASCADING_FILTERS.includes(
+ formValues.filters[filterId]
+ ? formValues.filters[filterId].filterType
+ : filterConfigMap[filterId]?.filterType,
+ ),
)
.map(id => ({
id,
title: getFilterTitle(id),
}));
- const handleSave = createHandleSave(
- form,
- currentFilterId,
- filterConfigMap,
- filterIds,
- removedFilters,
- setCurrentFilterId,
- resetForm,
- onSave,
- );
+ const handleSave = async () => {
+ const values: NativeFiltersForm | null = await validateForm(
+ form,
+ currentFilterId,
+ filterConfigMap,
+ filterIds,
+ removedFilters,
+ setCurrentFilterId,
+ );
+
+ if (values) {
+ createHandleSave(
+ filterConfigMap,
+ filterIds,
+ removedFilters,
+ resetForm,
+ onSave,
+ values,
+ )();
+ } else {
+ configFormRef.current.changeTab('configuration');
+ }
+ };
const handleConfirmCancel = () => {
resetForm();
@@ -215,7 +235,7 @@ export function FiltersConfigModal({
{(id: string) => (
,
- currentFilterId: string,
filterConfigMap: Record,
filterIds: string[],
removedFilters: Record,
- setCurrentFilterId: Function,
resetForm: Function,
saveForm: Function,
+ values: NativeFiltersForm,
) => async () => {
- const values: NativeFiltersForm | null = await validateForm(
- form,
- currentFilterId,
- filterConfigMap,
- filterIds,
- removedFilters,
- setCurrentFilterId,
- );
- if (values === null) return;
-
const newFilterConfig: FilterConfiguration = filterIds
.filter(id => !removedFilters[id])
.map(id => {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/state.ts
index 83003d67c9e16..027bf2ac968c4 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/state.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/state.ts
@@ -19,6 +19,7 @@
import { useSelector } from 'react-redux';
import { useMemo } from 'react';
import { Filter, FilterConfiguration } from './types';
+import { DashboardLayout } from '../../types';
const defaultFilterConfiguration: Filter[] = [];
@@ -45,3 +46,9 @@ export function useFilterConfigMap() {
[filterConfig],
);
}
+
+export function useDashboardLayout() {
+ return useSelector(
+ state => state.dashboardLayout?.present,
+ );
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/types.ts
index 07e347c437b82..ac772dcd73491 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts
@@ -56,6 +56,8 @@ export interface Filter {
sortMetric?: string | null;
adhoc_filters?: AdhocFilter[];
time_range?: string;
+ tabsInScope?: string[];
+ chartsInScope?: number[];
}
export type FilterConfiguration = Filter[];
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts
new file mode 100644
index 0000000000000..b38f4bce1681a
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts
@@ -0,0 +1,126 @@
+/**
+ * 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 { Behavior, FeatureFlag } from '@superset-ui/core';
+import * as featureFlags from 'src/featureFlags';
+import { nativeFilterGate } from './utils';
+
+let isFeatureEnabledMock: jest.MockInstance;
+
+describe('nativeFilterGate', () => {
+ describe('with all feature flags disabled', () => {
+ beforeAll(() => {
+ isFeatureEnabledMock = jest
+ .spyOn(featureFlags, 'isFeatureEnabled')
+ .mockImplementation(() => false);
+ });
+
+ afterAll(() => {
+ // @ts-ignore
+ isFeatureEnabledMock.restore();
+ });
+
+ it('should return true for regular chart', () => {
+ expect(nativeFilterGate([])).toEqual(true);
+ });
+
+ it('should return true for cross filter chart', () => {
+ expect(nativeFilterGate([Behavior.INTERACTIVE_CHART])).toEqual(true);
+ });
+
+ it('should return false for native filter chart with cross filter support', () => {
+ expect(
+ nativeFilterGate([Behavior.NATIVE_FILTER, Behavior.INTERACTIVE_CHART]),
+ ).toEqual(false);
+ });
+
+ it('should return false for native filter behavior', () => {
+ expect(nativeFilterGate([Behavior.NATIVE_FILTER])).toEqual(false);
+ });
+ });
+
+ describe('with only native filters feature flag enabled', () => {
+ beforeAll(() => {
+ isFeatureEnabledMock = jest
+ .spyOn(featureFlags, 'isFeatureEnabled')
+ .mockImplementation(
+ (featureFlag: FeatureFlag) =>
+ featureFlag === FeatureFlag.DASHBOARD_NATIVE_FILTERS,
+ );
+ });
+
+ afterAll(() => {
+ // @ts-ignore
+ isFeatureEnabledMock.restore();
+ });
+
+ it('should return true for regular chart', () => {
+ expect(nativeFilterGate([])).toEqual(true);
+ });
+
+ it('should return true for cross filter chart', () => {
+ expect(nativeFilterGate([Behavior.INTERACTIVE_CHART])).toEqual(true);
+ });
+
+ it('should return false for native filter chart with cross filter support', () => {
+ expect(
+ nativeFilterGate([Behavior.NATIVE_FILTER, Behavior.INTERACTIVE_CHART]),
+ ).toEqual(false);
+ });
+
+ it('should return false for native filter behavior', () => {
+ expect(nativeFilterGate([Behavior.NATIVE_FILTER])).toEqual(false);
+ });
+ });
+
+ describe('with native filters and experimental feature flag enabled', () => {
+ beforeAll(() => {
+ isFeatureEnabledMock = jest
+ .spyOn(featureFlags, 'isFeatureEnabled')
+ .mockImplementation((featureFlag: FeatureFlag) =>
+ [
+ FeatureFlag.DASHBOARD_CROSS_FILTERS,
+ FeatureFlag.DASHBOARD_FILTERS_EXPERIMENTAL,
+ ].includes(featureFlag),
+ );
+ });
+
+ afterAll(() => {
+ // @ts-ignore
+ isFeatureEnabledMock.restore();
+ });
+
+ it('should return true for regular chart', () => {
+ expect(nativeFilterGate([])).toEqual(true);
+ });
+
+ it('should return true for cross filter chart', () => {
+ expect(nativeFilterGate([Behavior.INTERACTIVE_CHART])).toEqual(true);
+ });
+
+ it('should return true for native filter chart with cross filter support', () => {
+ expect(
+ nativeFilterGate([Behavior.NATIVE_FILTER, Behavior.INTERACTIVE_CHART]),
+ ).toEqual(true);
+ });
+
+ it('should return false for native filter behavior', () => {
+ expect(nativeFilterGate([Behavior.NATIVE_FILTER])).toEqual(false);
+ });
+ });
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts
index 776a3a3234fd2..24264cb4e725e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts
@@ -24,12 +24,16 @@ import {
EXTRA_FORM_DATA_APPEND_KEYS,
EXTRA_FORM_DATA_OVERRIDE_KEYS,
AdhocFilter,
+ FeatureFlag,
} from '@superset-ui/core';
-import { Charts } from 'src/dashboard/types';
+import { Charts, DashboardLayout } from 'src/dashboard/types';
import { RefObject } from 'react';
import { DataMaskStateWithId } from 'src/dataMask/types';
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
+import { isFeatureEnabled } from 'src/featureFlags';
import { Filter } from './types';
+import { CHART_TYPE, TAB_TYPE } from '../../util/componentTypes';
+import { DASHBOARD_GRID_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
export const getFormData = ({
datasetId,
@@ -131,3 +135,82 @@ export function getExtraFormData(
});
return extraFormData;
}
+
+export function nativeFilterGate(behaviors: Behavior[]): boolean {
+ return (
+ !behaviors.includes(Behavior.NATIVE_FILTER) ||
+ (isFeatureEnabled(FeatureFlag.DASHBOARD_FILTERS_EXPERIMENTAL) &&
+ isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) &&
+ behaviors.includes(Behavior.INTERACTIVE_CHART))
+ );
+}
+
+const isComponentATab = (
+ dashboardLayout: DashboardLayout,
+ componentId: string,
+) => dashboardLayout[componentId].type === TAB_TYPE;
+
+const findTabsWithChartsInScopeHelper = (
+ dashboardLayout: DashboardLayout,
+ chartsInScope: number[],
+ componentId: string,
+ tabIds: string[],
+ tabsToHighlight: Set,
+) => {
+ if (
+ dashboardLayout[componentId].type === CHART_TYPE &&
+ chartsInScope.includes(dashboardLayout[componentId].meta.chartId)
+ ) {
+ tabIds.forEach(tabsToHighlight.add, tabsToHighlight);
+ }
+ if (
+ dashboardLayout[componentId].children.length === 0 ||
+ (isComponentATab(dashboardLayout, componentId) &&
+ tabsToHighlight.has(componentId))
+ ) {
+ return;
+ }
+ dashboardLayout[componentId].children.forEach(childId =>
+ findTabsWithChartsInScopeHelper(
+ dashboardLayout,
+ chartsInScope,
+ childId,
+ isComponentATab(dashboardLayout, childId) ? [...tabIds, childId] : tabIds,
+ tabsToHighlight,
+ ),
+ );
+};
+
+export const findTabsWithChartsInScope = (
+ dashboardLayout: DashboardLayout,
+ chartsInScope: number[],
+) => {
+ const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
+ const rootChildId = dashboardRoot.children[0];
+ const hasTopLevelTabs = rootChildId !== DASHBOARD_GRID_ID;
+ const tabsInScope = new Set();
+ if (hasTopLevelTabs) {
+ dashboardLayout[rootChildId].children?.forEach(tabId =>
+ findTabsWithChartsInScopeHelper(
+ dashboardLayout,
+ chartsInScope,
+ tabId,
+ [tabId],
+ tabsInScope,
+ ),
+ );
+ } else {
+ Object.values(dashboardLayout)
+ .filter(element => element.type === TAB_TYPE)
+ .forEach(element =>
+ findTabsWithChartsInScopeHelper(
+ dashboardLayout,
+ chartsInScope,
+ element.id,
+ [element.id],
+ tabsInScope,
+ ),
+ );
+ }
+ return tabsInScope;
+};
diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
index f47a8734ab4e4..387e67c926298 100644
--- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx
@@ -35,7 +35,10 @@ import {
updateComponents,
handleComponentDrop,
} from '../actions/dashboardLayout';
-import { setDirectPathToChild } from '../actions/dashboardState';
+import {
+ setDirectPathToChild,
+ setLastFocusedTab,
+} from '../actions/dashboardState';
const propTypes = {
id: PropTypes.string,
@@ -79,19 +82,6 @@ function selectFocusedFilterScope(dashboardState, dashboardFilters) {
};
}
-function selectFocusedNativeFilterScope(nativeFilters) {
- if (!nativeFilters.focusedFilterId) return null;
- const id = nativeFilters.focusedFilterId;
- const focusedFilterScope = nativeFilters.filters[id].scope;
- return {
- chartId: id,
- scope: {
- scope: focusedFilterScope.rootPath,
- immune: focusedFilterScope.excluded,
- },
- };
-}
-
function mapStateToProps(
{
dashboardLayout: undoableLayout,
@@ -107,17 +97,21 @@ function mapStateToProps(
const component = dashboardLayout[id];
const props = {
component,
+ dashboardLayout,
parentComponent: dashboardLayout[parentId],
editMode: dashboardState.editMode,
undoLength: undoableLayout.past.length,
redoLength: undoableLayout.future.length,
filters: getActiveFilters(),
directPathToChild: dashboardState.directPathToChild,
+ lastFocusedTabId: dashboardState.lastFocusedTabId,
directPathLastUpdated: dashboardState.directPathLastUpdated,
dashboardId: dashboardInfo.id,
- focusedFilterScope:
- selectFocusedFilterScope(dashboardState, dashboardFilters) ||
- selectFocusedNativeFilterScope(nativeFilters),
+ nativeFilters,
+ focusedFilterScope: selectFocusedFilterScope(
+ dashboardState,
+ dashboardFilters,
+ ),
};
// rows and columns need more data about their child dimensions
@@ -147,6 +141,7 @@ function mapDispatchToProps(dispatch) {
updateComponents,
handleComponentDrop,
setDirectPathToChild,
+ setLastFocusedTab,
logEvent,
},
dispatch,
diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js
index 6f162084708f4..842916e1cc31e 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardState.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardState.js
@@ -35,6 +35,7 @@ import {
SET_DIRECT_PATH,
SET_FOCUSED_FILTER_FIELD,
UNSET_FOCUSED_FILTER_FIELD,
+ SET_LAST_FOCUSED_TAB,
} from '../actions/dashboardState';
import { HYDRATE_DASHBOARD } from '../actions/hydrate';
@@ -133,6 +134,12 @@ export default function dashboardStateReducer(state = {}, action) {
directPathLastUpdated: Date.now(),
};
},
+ [SET_LAST_FOCUSED_TAB]() {
+ return {
+ ...state,
+ lastFocusedTabId: action.tabId,
+ };
+ },
[SET_FOCUSED_FILTER_FIELD]() {
return {
...state,
diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts
index a37cc0f938702..9a15b1da7ef03 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -43,7 +43,11 @@ export type Chart = ChartState & {
export type DashboardLayout = { [key: string]: LayoutItem };
export type DashboardLayoutState = { present: DashboardLayout };
-export type DashboardState = { editMode: boolean; directPathToChild: string[] };
+export type DashboardState = {
+ editMode: boolean;
+ directPathToChild: string[];
+ lastFocusedTabId: string | null;
+};
export type DashboardInfo = {
common: {
flash_messages: string[];
diff --git a/superset-frontend/src/dashboard/util/getDashboardUrl.ts b/superset-frontend/src/dashboard/util/getDashboardUrl.ts
index d3cf06c668b9a..7eb817a0957a9 100644
--- a/superset-frontend/src/dashboard/util/getDashboardUrl.ts
+++ b/superset-frontend/src/dashboard/util/getDashboardUrl.ts
@@ -30,12 +30,12 @@ export default function getDashboardUrl(
// convert flattened { [id_column]: values } object
// to nested filter object
newSearchParams.set(
- URL_PARAMS.preselectFilters,
+ URL_PARAMS.preselectFilters.name,
JSON.stringify(serializeActiveFilterValues(filters)),
);
if (standalone) {
- newSearchParams.set(URL_PARAMS.standalone, standalone.toString());
+ newSearchParams.set(URL_PARAMS.standalone.name, standalone.toString());
}
const hashSection = hash ? `#${hash}` : '';
diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx
index 8cf17e550a1ab..94d3cd976769f 100644
--- a/superset-frontend/src/datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/datasource/DatasourceEditor.jsx
@@ -379,6 +379,7 @@ class DatasourceEditor extends React.PureComponent {
}
setColumns(obj) {
+ // update calculatedColumns or databaseColumns
this.setState(obj, this.validateAndChange);
}
@@ -414,13 +415,18 @@ class DatasourceEditor extends React.PureComponent {
type: col.type,
groupby: true,
filterable: true,
+ is_dttm: col.is_dttm,
});
results.added.push(col.name);
- } else if (currentCol.type !== col.type) {
+ } else if (
+ currentCol.type !== col.type ||
+ currentCol.is_dttm !== col.is_dttm
+ ) {
// modified column
finalColumns.push({
...currentCol,
type: col.type,
+ is_dttm: col.is_dttm,
});
results.modified.push(col.name);
} else {
diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index 161407ab80127..2fd44df9b8aad 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -92,6 +92,13 @@ const Styles = styled.div`
.Select__menu {
max-width: 100%;
}
+ .type-label {
+ margin-right: ${({ theme }) => theme.gridUnit * 3}px;
+ width: ${({ theme }) => theme.gridUnit * 7}px;
+ display: inline-block;
+ text-align: center;
+ font-weight: ${({ theme }) => theme.typography.weights.bold};
+ }
`;
const ControlPanelsTabs = styled(Tabs)`
diff --git a/superset-frontend/src/explore/components/DataTablesPane/index.tsx b/superset-frontend/src/explore/components/DataTablesPane/index.tsx
index 389e0213c6cd5..f2b33f28e071c 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/index.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/index.tsx
@@ -107,12 +107,14 @@ export const DataTablesPane = ({
onCollapseChange,
chartStatus,
ownState,
+ errorMessage,
}: {
queryFormData: Record;
tableSectionHeight: number;
chartStatus: string;
ownState?: JsonObject;
onCollapseChange: (openPanelName: string) => void;
+ errorMessage?: JSX.Element;
}) => {
const [data, setData] = useState<{
[RESULT_TYPES.results]?: Record[];
@@ -196,6 +198,17 @@ export const DataTablesPane = ({
useEffect(() => {
if (panelOpen && isRequestPending[RESULT_TYPES.results]) {
+ if (errorMessage) {
+ setIsRequestPending(prevState => ({
+ ...prevState,
+ [RESULT_TYPES.results]: false,
+ }));
+ setIsLoading(prevIsLoading => ({
+ ...prevIsLoading,
+ [RESULT_TYPES.results]: false,
+ }));
+ return;
+ }
if (chartStatus === 'loading') {
setIsLoading(prevIsLoading => ({
...prevIsLoading,
@@ -220,7 +233,14 @@ export const DataTablesPane = ({
}));
getData(RESULT_TYPES.samples);
}
- }, [panelOpen, isRequestPending, getData, activeTabKey, chartStatus]);
+ }, [
+ panelOpen,
+ isRequestPending,
+ getData,
+ activeTabKey,
+ chartStatus,
+ errorMessage,
+ ]);
const filteredData = {
[RESULT_TYPES.results]: useFilteredTableData(
@@ -262,6 +282,9 @@ export const DataTablesPane = ({
/>
);
}
+ if (errorMessage) {
+ return {errorMessage} ;
+ }
return null;
};
diff --git a/superset-frontend/src/explore/components/EmbedCodeButton.jsx b/superset-frontend/src/explore/components/EmbedCodeButton.jsx
index 24b62ecd11616..46eed0bb0cda3 100644
--- a/superset-frontend/src/explore/components/EmbedCodeButton.jsx
+++ b/superset-frontend/src/explore/components/EmbedCodeButton.jsx
@@ -69,7 +69,7 @@ export default class EmbedCodeButton extends React.Component {
generateEmbedHTML() {
const srcLink = `${window.location.origin + getURIDirectory()}?r=${
this.state.shortUrlId
- }&${URL_PARAMS.standalone}=1&height=${this.state.height}`;
+ }&${URL_PARAMS.standalone.name}=1&height=${this.state.height}`;
return (
'
-
+
);
};
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index e2ee33d5cca69..4f2211ebe728d 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -17,35 +17,35 @@
* under the License.
*/
import {
- SupersetClient,
getChartMetadataRegistry,
- t,
styled,
+ SupersetClient,
+ t,
} from '@superset-ui/core';
import React, { useMemo, useState } from 'react';
import rison from 'rison';
import { uniqBy } from 'lodash';
-import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
+import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
- createFetchRelated,
createErrorHandler,
+ createFetchRelated,
handleBulkChartExport,
handleChartDelete,
} from 'src/views/CRUD/utils';
import {
- useListViewResource,
- useFavoriteStatus,
useChartEditModal,
+ useFavoriteStatus,
+ useListViewResource,
} from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import FaveStar from 'src/components/FaveStar';
import ListView, {
- ListViewProps,
Filter,
+ FilterOperator,
Filters,
+ ListViewProps,
SelectOption,
- FilterOperator,
} from 'src/components/ListView';
import { getFromLocalStorage } from 'src/utils/localStorageHelpers';
import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -54,6 +54,7 @@ import ImportModelsModal from 'src/components/ImportModal/index';
import Chart from 'src/types/Chart';
import { Tooltip } from 'src/components/Tooltip';
import Icons from 'src/components/Icons';
+import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
import ChartCard from './ChartCard';
const PAGE_SIZE = 25;
@@ -454,6 +455,7 @@ function ChartList(props: ChartListProps) {
unfilteredLabel: t('All'),
selects: registry
.keys()
+ .filter(k => nativeFilterGate(registry.get(k)?.behaviors || []))
.map(k => ({ label: registry.get(k)?.name || k, value: k }))
.sort((a, b) => {
if (!a.label || !b.label) {
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
index d1105e7062229..fda65baa27038 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
@@ -29,8 +29,9 @@ import { Tooltip } from 'src/components/Tooltip';
import Icons from 'src/components/Icons';
import ListView, { FilterOperator, Filters } from 'src/components/ListView';
import { commonMenuData } from 'src/views/CRUD/data/common';
-import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
import ImportModelsModal from 'src/components/ImportModal/index';
+import DatabaseModal from './DatabaseModal';
+
import { DatabaseObject } from './types';
const PAGE_SIZE = 25;
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx
index f0542e481c121..5c7c729da5893 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx
@@ -17,11 +17,14 @@
* under the License.
*/
import React, { FormEvent } from 'react';
-import cx from 'classnames';
+import { SupersetTheme, JsonObject } from '@superset-ui/core';
import { InputProps } from 'antd/lib/input';
-import { FormLabel, FormItem } from 'src/components/Form';
-import { Input } from 'src/common/components';
-import { StyledFormHeader, formScrollableStyles } from './styles';
+import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
+import {
+ StyledFormHeader,
+ formScrollableStyles,
+ validatedFormStyles,
+} from './styles';
import { DatabaseForm } from '../types';
export const FormFieldOrder = [
@@ -33,64 +36,137 @@ export const FormFieldOrder = [
'database_name',
];
-const CHANGE_METHOD = {
- onChange: 'onChange',
- onPropertiesChange: 'onPropertiesChange',
-};
+interface FieldPropTypes {
+ required: boolean;
+ changeMethods: { onParametersChange: (value: any) => string } & {
+ onChange: (value: any) => string;
+ };
+ validationErrors: JsonObject | null;
+ getValidation: () => void;
+}
+
+const hostField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+}: FieldPropTypes) => (
+
+);
+const portField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+}: FieldPropTypes) => (
+
+);
+const databaseField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+}: FieldPropTypes) => (
+
+);
+const usernameField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+}: FieldPropTypes) => (
+
+);
+const passwordField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+}: FieldPropTypes) => (
+
+);
+const displayField = ({
+ required,
+ changeMethods,
+ getValidation,
+ validationErrors,
+}: FieldPropTypes) => (
+
+);
const FORM_FIELD_MAP = {
- host: {
- description: 'Host',
- type: 'text',
- className: 'w-50',
- placeholder: 'e.g. 127.0.0.1',
- changeMethod: CHANGE_METHOD.onPropertiesChange,
- },
- port: {
- description: 'Port',
- type: 'text',
- className: 'w-50',
- placeholder: 'e.g. 5432',
- changeMethod: CHANGE_METHOD.onPropertiesChange,
- },
- database: {
- description: 'Database name',
- type: 'text',
- label:
- 'Copy the name of the PostgreSQL database you are trying to connect to.',
- placeholder: 'e.g. world_population',
- changeMethod: CHANGE_METHOD.onPropertiesChange,
- },
- username: {
- description: 'Username',
- type: 'text',
- placeholder: 'e.g. Analytics',
- changeMethod: CHANGE_METHOD.onPropertiesChange,
- },
- password: {
- description: 'Password',
- type: 'text',
- placeholder: 'e.g. ********',
- changeMethod: CHANGE_METHOD.onPropertiesChange,
- },
- database_name: {
- description: 'Display Name',
- type: 'text',
- label: 'Pick a nickname for this database to display as in Superset.',
- changeMethod: CHANGE_METHOD.onChange,
- },
- query: {
- additionalProperties: {},
- description: 'Additional parameters',
- type: 'object',
- changeMethod: CHANGE_METHOD.onPropertiesChange,
- },
+ host: hostField,
+ port: portField,
+ database: databaseField,
+ username: usernameField,
+ password: passwordField,
+ database_name: displayField,
};
const DatabaseConnectionForm = ({
dbModel: { name, parameters },
onParametersChange,
onChange,
+ validationErrors,
+ getValidation,
}: {
dbModel: DatabaseForm;
onParametersChange: (
@@ -99,6 +175,8 @@ const DatabaseConnectionForm = ({
onChange: (
event: FormEvent | { target: HTMLInputElement },
) => void;
+ validationErrors: JsonObject | null;
+ getValidation: () => void;
}) => (
<>
@@ -107,52 +185,30 @@ const DatabaseConnectionForm = ({
Need help? Learn more about connecting to {name}.
-
+
[
+ formScrollableStyles,
+ validatedFormStyles(theme),
+ ]}
+ >
{parameters &&
FormFieldOrder.filter(
(key: string) =>
Object.keys(parameters.properties).includes(key) ||
key === 'database_name',
- ).map(field => {
- const {
- className,
- description,
- type,
- placeholder,
- label,
- changeMethod,
- } = FORM_FIELD_MAP[field];
- const onEdit =
- changeMethod === CHANGE_METHOD.onChange
- ? onChange
- : onParametersChange;
- return (
-
-
- {description}
-
-
- {label}
-
- );
- })}
+ ).map(field =>
+ FORM_FIELD_MAP[field]({
+ required: parameters.required.includes(field),
+ changeMethods: { onParametersChange, onChange },
+ validationErrors,
+ getValidation,
+ key: field,
+ }),
+ )}
>
);
-
export const FormFieldMap = FORM_FIELD_MAP;
export default DatabaseConnectionForm;
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx
index 174ef60930471..c046c25011115 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx
@@ -292,12 +292,12 @@ const ExtraOptions = ({
checked={!!db?.impersonate_user}
onChange={onInputChange}
labelText={t(
- 'Impersonate Logged In User (Presto, Hive, and GSheets)',
+ 'Impersonate Logged In User (Presto, Trino, Hive, and GSheets)',
)}
/>
| null {
const trimmedState = {
...(state || {}),
- database_name: state?.database_name?.trim() || '',
- sqlalchemy_uri: state?.sqlalchemy_uri || '',
};
switch (action.type) {
@@ -163,9 +161,7 @@ function dbReducer(
};
case ActionType.fetched:
return {
- parameters: {
- engine: trimmedState.parameters?.engine,
- },
+ engine: trimmedState.engine,
configuration_method: trimmedState.configuration_method,
...action.payload,
};
@@ -196,13 +192,16 @@ const DatabaseModal: FunctionComponent = ({
>(dbReducer, null);
const [tabKey, setTabKey] = useState(DEFAULT_TAB_KEY);
const [availableDbs, getAvailableDbs] = useAvailableDatabases();
+ const [validationErrors, getValidation] = useDatabaseValidation();
const [hasConnectedDb, setHasConnectedDb] = useState(false);
+ const [dbName, setDbName] = useState('');
const conf = useCommonConf();
const isEditMode = !!databaseId;
const useSqlAlchemyForm =
db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
const useTabLayout = isEditMode || useSqlAlchemyForm;
+
// Database fetch logic
const {
state: { loading: dbLoading, resource: dbFetched },
@@ -248,14 +247,16 @@ const DatabaseModal: FunctionComponent = ({
// don't pass parameters if using the sqlalchemy uri
delete update.parameters;
}
- updateResource(db.id as number, update as DatabaseObject).then(result => {
- if (result) {
- if (onDatabaseAdd) {
- onDatabaseAdd();
- }
- onClose();
+ const result = await updateResource(
+ db.id as number,
+ update as DatabaseObject,
+ );
+ if (result) {
+ if (onDatabaseAdd) {
+ onDatabaseAdd();
}
- });
+ onClose();
+ }
} else if (db) {
// Create
const dbId = await createResource(update as DatabaseObject);
@@ -300,7 +301,6 @@ const DatabaseModal: FunctionComponent = ({
setDB({
type: ActionType.dbSelected,
payload: {
- parameters: {},
configuration_method: CONFIGURATION_METHOD.SQLALCHEMY_URI,
}, // todo hook this up to step 1
});
@@ -316,6 +316,9 @@ const DatabaseModal: FunctionComponent = ({
type: ActionType.fetched,
payload: dbFetched,
});
+ // keep a copy of the name separate for display purposes
+ // because it shouldn't change when the form is updated
+ setDbName(dbFetched.database_name);
}
}, [dbFetched]);
@@ -326,7 +329,7 @@ const DatabaseModal: FunctionComponent = ({
const dbModel: DatabaseForm =
availableDbs?.databases?.find(
(available: { engine: string | undefined }) =>
- available.engine === db?.parameters?.engine,
+ available.engine === db?.engine,
) || {};
const disableSave =
@@ -362,12 +365,12 @@ const DatabaseModal: FunctionComponent = ({
}
>
{isEditMode ? (
-
+
{db?.backend}
- {db?.database_name}
-
+ {dbName}
+
) : (
-
+
Enter Primary Credentials
Need help? Learn how to connect your database{' '}
@@ -380,7 +383,7 @@ const DatabaseModal: FunctionComponent = ({
.
-
+
)}
= ({
value: target.value,
})
}
+ getValidation={() => getValidation(db)}
+ validationErrors={validationErrors}
/>
css`
margin-left: ${theme.gridUnit * 8}px;
}
}
- .text-danger {
- color: ${theme.colors.error.base};
- font-size: ${theme.typography.sizes.s - 1}px;
- strong {
- font-weight: normal;
- }
- }
}
.control-label {
color: ${theme.colors.grayscale.dark1};
@@ -181,16 +173,20 @@ export const formStyles = (theme: SupersetTheme) => css`
font-size: ${theme.typography.sizes.s - 1}px;
margin-top: ${theme.gridUnit * 1.5}px;
}
- .ant-modal-body {
- padding-top: 0;
- margin-bottom: 0;
- }
.ant-tabs-content-holder {
overflow: auto;
max-height: 475px;
}
`;
+export const validatedFormStyles = (theme: SupersetTheme) => css`
+ label {
+ color: ${theme.colors.grayscale.dark1};
+ font-size: ${theme.typography.sizes.s - 1}px;
+ margin-bottom: 0;
+ }
+`;
+
export const StyledInputContainer = styled.div`
margin-bottom: ${({ theme }) => theme.gridUnit * 6}px;
&.mb-0 {
@@ -310,23 +306,13 @@ export const buttonLinkStyles = css`
text-transform: initial;
`;
-export const EditHeader = styled.div`
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding: 0px;
- margin: ${({ theme }) => theme.gridUnit * 4}px
- ${({ theme }) => theme.gridUnit * 4}px
- ${({ theme }) => theme.gridUnit * 9}px;
-`;
-
-export const CreateHeader = styled.div`
+export const TabHeader = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
padding: 0px;
margin: 0 ${({ theme }) => theme.gridUnit * 4}px
- ${({ theme }) => theme.gridUnit * 6}px;
+ ${({ theme }) => theme.gridUnit * 8}px;
`;
export const CreateHeaderTitle = styled.div`
@@ -343,12 +329,12 @@ export const CreateHeaderSubtitle = styled.div`
export const EditHeaderTitle = styled.div`
color: ${({ theme }) => theme.colors.grayscale.light1};
- font-size: ${({ theme }) => theme.typography.sizes.s}px;
+ font-size: ${({ theme }) => theme.typography.sizes.s - 1}px;
text-transform: uppercase;
`;
export const EditHeaderSubtitle = styled.div`
color: ${({ theme }) => theme.colors.grayscale.dark1};
- font-size: ${({ theme }) => theme.typography.sizes.xl}px;
+ font-size: ${({ theme }) => theme.typography.sizes.l}px;
font-weight: bold;
`;
diff --git a/superset-frontend/src/views/CRUD/data/database/types.ts b/superset-frontend/src/views/CRUD/data/database/types.ts
index 2c386b5796a60..1baac06f9855a 100644
--- a/superset-frontend/src/views/CRUD/data/database/types.ts
+++ b/superset-frontend/src/views/CRUD/data/database/types.ts
@@ -30,8 +30,9 @@ export type DatabaseObject = {
created_by?: null | DatabaseUser;
changed_on_delta_humanized?: string;
changed_on?: string;
- parameters?: { database_name?: string; engine?: string };
+ parameters?: { database_name?: string };
configuration_method: CONFIGURATION_METHOD;
+ engine?: string;
// Performance
cache_timeout?: string;
diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
index 63bbb9be10583..ea184acce4ea1 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
@@ -20,7 +20,6 @@ import React, { FunctionComponent, useState } from 'react';
import { styled, t } from '@superset-ui/core';
import { useSingleViewResource } from 'src/views/CRUD/hooks';
import { isEmpty, isNil } from 'lodash';
-import Icon from 'src/components/Icon';
import Modal from 'src/components/Modal';
import TableSelector from 'src/components/TableSelector';
import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -39,10 +38,6 @@ interface DatasetModalProps {
show: boolean;
}
-const StyledIcon = styled(Icon)`
- margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0;
-`;
-
const TableSelectorContainer = styled.div`
padding-bottom: 340px;
width: 65%;
@@ -105,12 +100,7 @@ const DatasetModal: FunctionComponent = ({
onHide={onHide}
primaryButtonName={t('Add')}
show={show}
- title={
- <>
-
- {t('Add dataset')}
- >
- }
+ title={t('Add dataset')}
>
(
+ null,
+ );
+ const getValidation = useCallback(
+ (database: Partial | null) => {
+ SupersetClient.post({
+ endpoint: '/api/v1/database/validate_parameters',
+ body: JSON.stringify(database),
+ headers: { 'Content-Type': 'application/json' },
+ })
+ .then(() => {
+ setValidationErrors(null);
+ })
+ .catch(e => {
+ if (typeof e.json === 'function') {
+ e.json().then(({ errors = [] }: JsonObject) => {
+ const parsedErrors = errors
+ .filter(
+ (error: { error_type: string }) =>
+ error.error_type !== 'CONNECTION_MISSING_PARAMETERS_ERROR',
+ )
+ .reduce(
+ (
+ obj: {},
+ {
+ extra,
+ message,
+ }: {
+ extra: { invalid?: string[] };
+ message: string;
+ },
+ ) => {
+ // if extra.invalid doesn't exist then the
+ // error can't be mapped to a parameter
+ // so leave it alone
+ if (extra.invalid) {
+ return { ...obj, [extra.invalid[0]]: message };
+ }
+ return obj;
+ },
+ {},
+ );
+ setValidationErrors(parsedErrors);
+ });
+ } else {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ });
+ },
+ [setValidationErrors],
+ );
+
+ return [validationErrors, getValidation] as const;
+}
diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx
index cbe95664e8593..8a5a867fd3fc0 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, SupersetClient } from '@superset-ui/core';
+import { t, SupersetClient, ensureIsArray } from '@superset-ui/core';
import {
BOOL_FALSE_DISPLAY,
@@ -158,10 +158,11 @@ class FilterBox extends React.PureComponent {
if (options !== null) {
if (Array.isArray(options)) {
vals = options.map(opt => (typeof opt === 'string' ? opt : opt.value));
- } else if (options.value) {
- vals = options.value;
+ } else if (Object.values(TIME_FILTER_MAP).includes(fltr)) {
+ vals = options.value ?? options;
} else {
- vals = options;
+ // must use array member for legacy extra_filters's value
+ vals = ensureIsArray(options.value ?? options);
}
}
@@ -222,10 +223,10 @@ class FilterBox extends React.PureComponent {
? [
{
clause: 'WHERE',
- comparator: null,
- expressionType: 'SQL',
- // TODO: Evaluate SQL Injection risk
- sqlExpression: `lower(${key}) like '%${input}%'`,
+ expressionType: 'SIMPLE',
+ subject: key,
+ operator: 'ILIKE',
+ comparator: `%${input}%`,
},
]
: null,
diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js
index 03ea7a26027fa..ee4d806a49798 100644
--- a/superset-frontend/src/visualizations/presets/MainPreset.js
+++ b/superset-frontend/src/visualizations/presets/MainPreset.js
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Preset } from '@superset-ui/core';
+import { isFeatureEnabled, Preset } from '@superset-ui/core';
import {
BigNumberChartPlugin,
BigNumberTotalChartPlugin,
@@ -76,9 +76,16 @@ import {
import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table';
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin';
+import { FeatureFlag } from '../../featureFlags';
export default class MainPreset extends Preset {
constructor() {
+ const experimentalplugins = isFeatureEnabled(
+ FeatureFlag.DASHBOARD_FILTERS_EXPERIMENTAL,
+ )
+ ? [new GroupByFilterPlugin().configure({ key: 'filter_groupby' })]
+ : [];
+
super({
name: 'Legacy charts',
presets: [new DeckGLChartPreset()],
@@ -135,8 +142,8 @@ export default class MainPreset extends Preset {
new TimeFilterPlugin().configure({ key: 'filter_time' }),
new TimeColumnFilterPlugin().configure({ key: 'filter_timecolumn' }),
new TimeGrainFilterPlugin().configure({ key: 'filter_timegrain' }),
- new GroupByFilterPlugin().configure({ key: 'filter_groupby' }),
new EchartsTreeChartPlugin().configure({ key: 'tree_chart' }),
+ ...experimentalplugins,
],
});
}
diff --git a/superset/charts/api.py b/superset/charts/api.py
index e3f3936f93a6c..bb92b3da20efa 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -63,7 +63,7 @@
screenshot_query_schema,
thumbnail_query_schema,
)
-from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import NoValidFilesFoundError
from superset.commands.importers.v1.utils import get_contents_from_bundle
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.exceptions import QueryObjectValidationError
@@ -977,7 +977,6 @@ def favorite_status(self, **kwargs: Any) -> Response:
@expose("/import/", methods=["POST"])
@protect()
- @safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.import_",
@@ -1003,7 +1002,7 @@ def import_(self) -> Response:
type: string
overwrite:
description: overwrite existing databases?
- type: bool
+ type: boolean
responses:
200:
description: Chart import result
@@ -1029,6 +1028,9 @@ def import_(self) -> Response:
with ZipFile(upload) as bundle:
contents = get_contents_from_bundle(bundle)
+ if not contents:
+ raise NoValidFilesFoundError()
+
passwords = (
json.loads(request.form["passwords"])
if "passwords" in request.form
@@ -1039,12 +1041,5 @@ def import_(self) -> Response:
command = ImportChartsCommand(
contents, passwords=passwords, overwrite=overwrite
)
- try:
- command.run()
- return self.response(200, message="OK")
- except CommandInvalidError as exc:
- logger.warning("Import chart failed")
- return self.response_422(message=exc.normalized_messages())
- except Exception as exc: # pylint: disable=broad-except
- logger.exception("Import chart failed")
- return self.response_500(message=str(exc))
+ command.run()
+ return self.response(200, message="OK")
diff --git a/superset/commands/importers/exceptions.py b/superset/commands/importers/exceptions.py
index e79cc1c5d24b0..c1beb8eb5377d 100644
--- a/superset/commands/importers/exceptions.py
+++ b/superset/commands/importers/exceptions.py
@@ -21,3 +21,8 @@
class IncorrectVersionError(CommandException):
status = 422
message = "Import has incorrect version"
+
+
+class NoValidFilesFoundError(CommandException):
+ status = 400
+ message = "No valid import files were found"
diff --git a/superset/config.py b/superset/config.py
index 7f26ac27c4916..a6427b528d775 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -383,6 +383,9 @@ def _try_json_readsha( # pylint: disable=unused-argument
# for report with type 'report' still send with email and slack message with
# screenshot and link
"ALERTS_ATTACH_REPORTS": True,
+ # Enabling FORCE_DATABASE_CONNECTIONS_SSL forces all database connections to be
+ # encrypted before being saved into superset metastore.
+ "FORCE_DATABASE_CONNECTIONS_SSL": False,
}
# Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars.
@@ -1028,16 +1031,12 @@ def CSV_TO_HIVE_UPLOAD_DIRECTORY_FUNC(
WEBDRIVER_AUTH_FUNC = None
# Any config options to be passed as-is to the webdriver
-WEBDRIVER_CONFIGURATION: Dict[Any, Any] = {}
+WEBDRIVER_CONFIGURATION: Dict[Any, Any] = {"service_log_path": "/dev/null"}
# Additional args to be passed as arguments to the config object
# Note: these options are Chrome-specific. For FF, these should
# only include the "--headless" arg
-WEBDRIVER_OPTION_ARGS = [
- "--force-device-scale-factor=2.0",
- "--high-dpi-support=2.0",
- "--headless",
-]
+WEBDRIVER_OPTION_ARGS = ["--headless", "--marionette"]
# The base URL to query for accessing the user interface
WEBDRIVER_BASEURL = "http://0.0.0.0:8080/"
@@ -1073,13 +1072,13 @@ def CSV_TO_HIVE_UPLOAD_DIRECTORY_FUNC(
# A list of preferred databases, in order. These databases will be
# displayed prominently in the "Add Database" dialog. You should
-# use the "engine" attribute of the corresponding DB engine spec in
-# `superset/db_engine_specs/`.
+# use the "engine_name" attribute of the corresponding DB engine spec
+# in `superset/db_engine_specs/`.
PREFERRED_DATABASES: List[str] = [
- # "postgresql",
- # "presto",
- # "mysql",
- # "sqlite",
+ # "PostgreSQL",
+ # "Presto",
+ # "MySQL",
+ # "SQLite",
# etc.
]
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index bc8012b86466f..2f7b6d1498d1c 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -230,6 +230,8 @@ def db_engine_spec(self) -> Type[BaseEngineSpec]:
@property
def type_generic(self) -> Optional[utils.GenericDataType]:
+ if self.is_dttm:
+ return GenericDataType.TEMPORAL
column_spec = self.db_engine_spec.get_column_spec(self.type)
return column_spec.generic_type if column_spec else None
@@ -309,7 +311,8 @@ def dttm_sql_literal(
],
) -> str:
"""Convert datetime object to a SQL expression string"""
- sql = self.db_engine_spec.convert_dttm(self.type, dttm) if self.type else None
+ dttm_type = self.type or ("DATETIME" if self.is_dttm else None)
+ sql = self.db_engine_spec.convert_dttm(dttm_type, dttm) if dttm_type else None
if sql:
return sql
@@ -635,7 +638,9 @@ def external_metadata(self) -> List[Dict[str, str]]:
db_engine_spec = self.db_engine_spec
if self.sql:
engine = self.database.get_sqla_engine(schema=self.schema)
- sql = self.get_template_processor().process_template(self.sql)
+ sql = self.get_template_processor().process_template(
+ self.sql, **self.template_params_dict
+ )
parsed_query = ParsedQuery(sql)
if not db_engine_spec.is_readonly_query(parsed_query):
raise SupersetSecurityException(
@@ -676,13 +681,25 @@ def external_metadata(self) -> List[Dict[str, str]]:
for col in cols:
try:
if isinstance(col["type"], TypeEngine):
- col["type"] = db_engine_spec.column_datatype_to_string(
+ db_type = db_engine_spec.column_datatype_to_string(
col["type"], db_dialect
)
+ type_spec = db_engine_spec.get_column_spec(db_type)
+ col.update(
+ {
+ "type": db_type,
+ "type_generic": type_spec.generic_type
+ if type_spec
+ else None,
+ "is_dttm": type_spec.is_dttm if type_spec else None,
+ }
+ )
# Broad exception catch, because there are multiple possible exceptions
# from different drivers that fall outside CompileError
except Exception: # pylint: disable=broad-except
- col["type"] = "UNKNOWN"
+ col.update(
+ {"type": "UNKNOWN", "generic_type": None, "is_dttm": None,}
+ )
return cols
@property
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 8c21e0b8048d1..632f7f2c6464e 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -32,7 +32,7 @@
from superset import is_feature_enabled, thumbnail_cache
from superset.charts.schemas import ChartEntityResponseSchema
-from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import NoValidFilesFoundError
from superset.commands.importers.v1.utils import get_contents_from_bundle
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.dashboards.commands.bulk_delete import BulkDeleteDashboardCommand
@@ -43,7 +43,6 @@
DashboardCreateFailedError,
DashboardDeleteFailedError,
DashboardForbiddenError,
- DashboardImportError,
DashboardInvalidError,
DashboardNotFoundError,
DashboardUpdateFailedError,
@@ -888,7 +887,6 @@ def favorite_status(self, **kwargs: Any) -> Response:
@expose("/import/", methods=["POST"])
@protect()
- @safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.import_",
@@ -914,7 +912,7 @@ def import_(self) -> Response:
type: string
overwrite:
description: overwrite existing databases?
- type: bool
+ type: boolean
responses:
200:
description: Dashboard import result
@@ -944,6 +942,9 @@ def import_(self) -> Response:
upload.seek(0)
contents = {upload.filename: upload.read()}
+ if not contents:
+ raise NoValidFilesFoundError()
+
passwords = (
json.loads(request.form["passwords"])
if "passwords" in request.form
@@ -954,12 +955,5 @@ def import_(self) -> Response:
command = ImportDashboardsCommand(
contents, passwords=passwords, overwrite=overwrite
)
- try:
- command.run()
- return self.response(200, message="OK")
- except CommandInvalidError as exc:
- logger.warning("Import dashboard failed")
- return self.response_422(message=exc.normalized_messages())
- except DashboardImportError as exc:
- logger.exception("Import dashboard failed")
- return self.response_500(message=str(exc))
+ command.run()
+ return self.response(200, message="OK")
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 0b0d29718d3c6..d64238baf4a3f 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -28,7 +28,7 @@
from sqlalchemy.exc import NoSuchTableError, OperationalError, SQLAlchemyError
from superset import app, event_logger
-from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import NoValidFilesFoundError
from superset.commands.importers.v1.utils import get_contents_from_bundle
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.databases.commands.create import CreateDatabaseCommand
@@ -38,7 +38,6 @@
DatabaseCreateFailedError,
DatabaseDeleteDatasetsExistFailedError,
DatabaseDeleteFailedError,
- DatabaseImportError,
DatabaseInvalidError,
DatabaseNotFoundError,
DatabaseUpdateFailedError,
@@ -749,7 +748,6 @@ def export(self, **kwargs: Any) -> Response:
@expose("/import/", methods=["POST"])
@protect()
- @safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.import_",
@@ -775,7 +773,7 @@ def import_(self) -> Response:
type: string
overwrite:
description: overwrite existing databases?
- type: bool
+ type: boolean
responses:
200:
description: Database import result
@@ -801,6 +799,9 @@ def import_(self) -> Response:
with ZipFile(upload) as bundle:
contents = get_contents_from_bundle(bundle)
+ if not contents:
+ raise NoValidFilesFoundError()
+
passwords = (
json.loads(request.form["passwords"])
if "passwords" in request.form
@@ -811,15 +812,8 @@ def import_(self) -> Response:
command = ImportDatabasesCommand(
contents, passwords=passwords, overwrite=overwrite
)
- try:
- command.run()
- return self.response(200, message="OK")
- except CommandInvalidError as exc:
- logger.warning("Import database failed")
- return self.response_422(message=exc.normalized_messages())
- except DatabaseImportError as exc:
- logger.error("Import database failed", exc_info=True)
- return self.response_500(message=str(exc))
+ command.run()
+ return self.response(200, message="OK")
@expose("//function_names/", methods=["GET"])
@protect()
@@ -886,14 +880,26 @@ def available(self) -> Response:
name:
description: Name of the database
type: string
+ engine:
+ description: Name of the SQLAlchemy engine
+ type: string
+ available_drivers:
+ description: Installed drivers for the engine
+ type: array
+ items:
+ type: string
+ default_driver:
+ description: Default driver for the engine
+ type: string
preferred:
description: Is the database preferred?
- type: bool
+ type: boolean
sqlalchemy_uri_placeholder:
description: Example placeholder for the SQLAlchemy URI
type: string
parameters:
description: JSON schema defining the needed parameters
+ type: object
400:
$ref: '#/components/responses/400'
500:
@@ -901,15 +907,22 @@ def available(self) -> Response:
"""
preferred_databases: List[str] = app.config.get("PREFERRED_DATABASES", [])
available_databases = []
- for engine_spec in get_available_engine_specs():
+ for engine_spec, drivers in get_available_engine_specs().items():
payload: Dict[str, Any] = {
"name": engine_spec.engine_name,
"engine": engine_spec.engine,
- "preferred": engine_spec.engine in preferred_databases,
+ "available_drivers": sorted(drivers),
+ "preferred": engine_spec.engine_name in preferred_databases,
}
- if hasattr(engine_spec, "parameters_json_schema") and hasattr(
- engine_spec, "sqlalchemy_uri_placeholder"
+ if hasattr(engine_spec, "default_driver"):
+ payload["default_driver"] = engine_spec.default_driver # type: ignore
+
+ # show configuration parameters for DBs that support it
+ if (
+ hasattr(engine_spec, "parameters_json_schema")
+ and hasattr(engine_spec, "sqlalchemy_uri_placeholder")
+ and getattr(engine_spec, "default_driver") in drivers
):
payload[
"parameters"
@@ -920,13 +933,25 @@ def available(self) -> Response:
available_databases.append(payload)
- available_databases.sort(
- key=lambda payload: preferred_databases.index(payload["engine"])
- if payload["engine"] in preferred_databases
- else len(preferred_databases)
+ # sort preferred first
+ response = sorted(
+ (payload for payload in available_databases if payload["preferred"]),
+ key=lambda payload: preferred_databases.index(payload["name"]),
+ )
+
+ # add others
+ response.extend(
+ sorted(
+ (
+ payload
+ for payload in available_databases
+ if not payload["preferred"]
+ ),
+ key=lambda payload: payload["name"],
+ )
)
- return self.response(200, databases=available_databases)
+ return self.response(200, databases=response)
@expose("/validate_parameters", methods=["POST"])
@protect()
diff --git a/superset/databases/commands/validate.py b/superset/databases/commands/validate.py
index 93e32ef71fb42..2f084f6324085 100644
--- a/superset/databases/commands/validate.py
+++ b/superset/databases/commands/validate.py
@@ -78,7 +78,9 @@ def run(self) -> None:
)
# perform initial validation
- errors = engine_spec.validate_parameters(self._properties["parameters"])
+ errors = engine_spec.validate_parameters(
+ self._properties.get("parameters", None)
+ )
if errors:
raise InvalidParametersError(errors)
@@ -90,7 +92,7 @@ def run(self) -> None:
# try to connect
sqlalchemy_uri = engine_spec.build_sqlalchemy_uri(
- self._properties["parameters"], # type: ignore
+ self._properties.get("parameters", None), # type: ignore
encrypted_extra,
)
if self._model and sqlalchemy_uri == self._model.safe_sqlalchemy_uri():
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index e95296740beb1..1df799c5fda0d 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -233,6 +233,12 @@ class DatabaseParametersSchemaMixin:
values=fields.Raw(),
description="DB-specific parameters for configuration",
)
+ configuration_method = EnumField(
+ ConfigurationMethod,
+ by_value=True,
+ description=configuration_method_description,
+ missing=ConfigurationMethod.SQLALCHEMY_FORM,
+ )
# pylint: disable=no-self-use, unused-argument
@pre_load
@@ -252,7 +258,8 @@ def build_sqlalchemy_uri(
# frontend is not passing engine inside parameters
engine = data.pop("engine", None) or parameters.pop("engine", None)
- if parameters:
+ configuration_method = data.get("configuration_method")
+ if configuration_method == ConfigurationMethod.DYNAMIC_FORM:
if not engine:
raise ValidationError(
[
@@ -269,19 +276,31 @@ def build_sqlalchemy_uri(
)
engine_spec = engine_specs[engine]
- if hasattr(engine_spec, "build_sqlalchemy_uri"):
- serialized_encrypted_extra = data.get("encrypted_extra", "{}")
- try:
- encrypted_extra = json.loads(serialized_encrypted_extra)
- except json.decoder.JSONDecodeError:
- encrypted_extra = {}
-
- data[
- "sqlalchemy_uri"
- ] = engine_spec.build_sqlalchemy_uri( # type: ignore
- parameters, encrypted_extra
+ if not hasattr(engine_spec, "build_sqlalchemy_uri") or not hasattr(
+ engine_spec, "parameters_schema"
+ ):
+ raise ValidationError(
+ [
+ _(
+ 'Engine spec "InvalidEngine" does not support '
+ "being configured via individual parameters."
+ )
+ ]
)
+ # validate parameters
+ parameters = engine_spec.parameters_schema.load(parameters) # type: ignore
+
+ serialized_encrypted_extra = data.get("encrypted_extra", "{}")
+ try:
+ encrypted_extra = json.loads(serialized_encrypted_extra)
+ except json.decoder.JSONDecodeError:
+ encrypted_extra = {}
+
+ data["sqlalchemy_uri"] = engine_spec.build_sqlalchemy_uri( # type: ignore
+ parameters, encrypted_extra
+ )
+
return data
@@ -307,6 +326,12 @@ class DatabaseValidateParametersSchema(Schema):
allow_none=True,
validate=server_cert_validator,
)
+ configuration_method = EnumField(
+ ConfigurationMethod,
+ by_value=True,
+ allow_none=True,
+ description=configuration_method_description,
+ )
class DatabasePostSchema(Schema, DatabaseParametersSchemaMixin):
@@ -325,11 +350,6 @@ class Meta: # pylint: disable=too-few-public-methods
allow_ctas = fields.Boolean(description=allow_ctas_description)
allow_cvas = fields.Boolean(description=allow_cvas_description)
allow_dml = fields.Boolean(description=allow_dml_description)
- configuration_method = EnumField(
- ConfigurationMethod,
- by_value=True,
- description=configuration_method_description,
- )
force_ctas_schema = fields.String(
description=force_ctas_schema_description,
allow_none=True,
@@ -367,12 +387,6 @@ class Meta: # pylint: disable=too-few-public-methods
description=cache_timeout_description, allow_none=True
)
expose_in_sqllab = fields.Boolean(description=expose_in_sqllab_description)
- configuration_method = EnumField(
- ConfigurationMethod,
- by_value=True,
- allow_none=True,
- description=configuration_method_description,
- )
allow_run_async = fields.Boolean(description=allow_run_async_description)
allow_csv_upload = fields.Boolean(description=allow_csv_upload_description)
allow_ctas = fields.Boolean(description=allow_ctas_description)
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index c35a0333cb781..312369c609e3e 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -29,7 +29,7 @@
from marshmallow import ValidationError
from superset import event_logger, is_feature_enabled
-from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import NoValidFilesFoundError
from superset.commands.importers.v1.utils import get_contents_from_bundle
from superset.connectors.sqla.models import SqlaTable
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
@@ -42,7 +42,6 @@
DatasetCreateFailedError,
DatasetDeleteFailedError,
DatasetForbiddenError,
- DatasetImportError,
DatasetInvalidError,
DatasetNotFoundError,
DatasetRefreshFailedError,
@@ -267,7 +266,7 @@ def put(self, pk: int) -> Response:
name: pk
- in: query
schema:
- type: bool
+ type: boolean
name: override_columns
requestBody:
description: Dataset schema
@@ -655,7 +654,6 @@ def bulk_delete(self, **kwargs: Any) -> Response:
@expose("/import/", methods=["POST"])
@protect()
- @safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.import_",
@@ -681,7 +679,7 @@ def import_(self) -> Response:
type: string
overwrite:
description: overwrite existing datasets?
- type: bool
+ type: boolean
responses:
200:
description: Dataset import result
@@ -711,6 +709,9 @@ def import_(self) -> Response:
upload.seek(0)
contents = {upload.filename: upload.read()}
+ if not contents:
+ raise NoValidFilesFoundError()
+
passwords = (
json.loads(request.form["passwords"])
if "passwords" in request.form
@@ -721,12 +722,5 @@ def import_(self) -> Response:
command = ImportDatasetsCommand(
contents, passwords=passwords, overwrite=overwrite
)
- try:
- command.run()
- return self.response(200, message="OK")
- except CommandInvalidError as exc:
- logger.warning("Import dataset failed")
- return self.response_422(message=exc.normalized_messages())
- except DatasetImportError as exc:
- logger.error("Import dataset failed", exc_info=True)
- return self.response_500(message=str(exc))
+ command.run()
+ return self.response(200, message="OK")
diff --git a/superset/db_engine_specs/__init__.py b/superset/db_engine_specs/__init__.py
index a4e083cf6ed00..f4ced6f323127 100644
--- a/superset/db_engine_specs/__init__.py
+++ b/superset/db_engine_specs/__init__.py
@@ -30,12 +30,15 @@
import inspect
import logging
import pkgutil
+from collections import defaultdict
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, List, Set, Type
import sqlalchemy.databases
+import sqlalchemy.dialects
from pkg_resources import iter_entry_points
+from sqlalchemy.engine.default import DefaultDialect
from superset.db_engine_specs.base import BaseEngineSpec
@@ -85,12 +88,31 @@ def get_engine_specs() -> Dict[str, Type[BaseEngineSpec]]:
return engine_specs_map
-def get_available_engine_specs() -> List[Type[BaseEngineSpec]]:
+def get_available_engine_specs() -> Dict[Type[BaseEngineSpec], Set[str]]:
+ """
+ Return available engine specs and installed drivers for them.
+ """
+ drivers: Dict[str, Set[str]] = defaultdict(set)
+
# native SQLAlchemy dialects
- backends: Set[str] = {
- getattr(sqlalchemy.databases, attr).dialect.name
- for attr in sqlalchemy.databases.__all__
- }
+ for attr in sqlalchemy.databases.__all__:
+ dialect = getattr(sqlalchemy.dialects, attr)
+ for attribute in dialect.__dict__.values():
+ if (
+ hasattr(attribute, "dialect")
+ and inspect.isclass(attribute.dialect)
+ and issubclass(attribute.dialect, DefaultDialect)
+ ):
+ try:
+ attribute.dialect.dbapi()
+ except ModuleNotFoundError:
+ continue
+ except Exception as ex: # pylint: disable=broad-except
+ logger.warning(
+ "Unable to load dialect %s: %s", attribute.dialect, ex
+ )
+ continue
+ drivers[attr].add(attribute.dialect.driver)
# installed 3rd-party dialects
for ep in iter_entry_points("sqlalchemy.dialects"):
@@ -99,7 +121,11 @@ def get_available_engine_specs() -> List[Type[BaseEngineSpec]]:
except Exception: # pylint: disable=broad-except
logger.warning("Unable to load SQLAlchemy dialect: %s", dialect)
else:
- backends.add(dialect.name)
+ drivers[dialect.name].add(dialect.driver)
engine_specs = get_engine_specs()
- return [engine_specs[backend] for backend in backends if backend in engine_specs]
+ return {
+ engine_specs[backend]: drivers
+ for backend, drivers in drivers.items()
+ if backend in engine_specs
+ }
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 97321b8b9a70a..75d0953260c6e 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -1328,7 +1328,7 @@ class BasicParametersMixin:
individual parameters, instead of the full SQLAlchemy URI. This
mixin is for the most common pattern of URI:
- drivername://user:password@host:port/dbname[?key=value&key=value...]
+ engine+driver://user:password@host:port/dbname[?key=value&key=value...]
"""
@@ -1336,11 +1336,11 @@ class BasicParametersMixin:
parameters_schema = BasicParametersSchema()
# recommended driver name for the DB engine spec
- drivername = ""
+ default_driver = ""
# placeholder with the SQLAlchemy URI template
sqlalchemy_uri_placeholder = (
- "drivername://user:password@host:port/dbname[?key=value&key=value...]"
+ "engine+driver://user:password@host:port/dbname[?key=value&key=value...]"
)
# query parameter to enable encryption in the database connection
@@ -1361,7 +1361,7 @@ def build_sqlalchemy_uri(
return str(
URL(
- cls.drivername,
+ f"{cls.engine}+{cls.default_driver}".rstrip("+"), # type: ignore
username=parameters.get("username"),
password=parameters.get("password"),
host=parameters["host"],
@@ -1372,7 +1372,9 @@ def build_sqlalchemy_uri(
)
@classmethod
- def get_parameters_from_uri(cls, uri: str) -> BasicParametersType:
+ def get_parameters_from_uri(
+ cls, uri: str, encrypted_extra: Optional[Dict[str, Any]] = None
+ ) -> BasicParametersType:
url = make_url(uri)
encryption = all(
item in url.query.items() for item in cls.encryption_parameters.items()
@@ -1400,7 +1402,7 @@ def validate_parameters(
errors: List[SupersetError] = []
required = {"host", "port", "username", "database"}
- present = {key for key in parameters if parameters[key]} # type: ignore
+ present = {key for key in parameters if parameters.get(key, ())} # type: ignore
missing = sorted(required - present)
if missing:
@@ -1413,7 +1415,7 @@ def validate_parameters(
),
)
- host = parameters["host"]
+ host = parameters.get("host", None)
if not host:
return errors
if not is_hostname_valid(host):
@@ -1427,7 +1429,7 @@ def validate_parameters(
)
return errors
- port = parameters["port"]
+ port = parameters.get("port", None)
if not port:
return errors
if not is_port_open(host, port):
diff --git a/superset/db_engine_specs/bigquery.py b/superset/db_engine_specs/bigquery.py
index a7ce77c580f6e..7320eab6db9db 100644
--- a/superset/db_engine_specs/bigquery.py
+++ b/superset/db_engine_specs/bigquery.py
@@ -49,7 +49,7 @@
class BigQueryParametersSchema(Schema):
credentials_info = EncryptedField(
- description="Contents of BigQuery JSON credentials.",
+ required=True, description="Contents of BigQuery JSON credentials.",
)
@@ -67,7 +67,7 @@ class BigQueryEngineSpec(BaseEngineSpec):
max_column_name_length = 128
parameters_schema = BigQueryParametersSchema()
- drivername = engine
+ default_driver = "bigquery"
sqlalchemy_uri_placeholder = "bigquery://{project_id}"
# BigQuery doesn't maintain context when running multiple statements in the
@@ -307,11 +307,13 @@ def df_to_sql(
@classmethod
def build_sqlalchemy_uri(
- cls, _: BigQueryParametersType, encrypted_extra: Optional[Dict[str, str]] = None
+ cls, _: BigQueryParametersType, encrypted_extra: Optional[Dict[str, Any]] = None
) -> str:
if encrypted_extra:
- project_id = encrypted_extra.get("project_id")
- return f"{cls.drivername}://{project_id}"
+ project_id = encrypted_extra.get("credentials_info", {}).get("project_id")
+
+ if project_id:
+ return f"{cls.engine}+{cls.default_driver}://{project_id}"
raise SupersetGenericDBErrorException(
message="Big Query encrypted_extra is not available.",
diff --git a/superset/db_engine_specs/cockroachdb.py b/superset/db_engine_specs/cockroachdb.py
index 80b547bd8d1ae..8c83bd793d23b 100644
--- a/superset/db_engine_specs/cockroachdb.py
+++ b/superset/db_engine_specs/cockroachdb.py
@@ -20,4 +20,4 @@
class CockroachDbEngineSpec(PostgresEngineSpec):
engine = "cockroachdb"
engine_name = "CockroachDB"
- drivername = "cockroach"
+ default_driver = ""
diff --git a/superset/db_engine_specs/mysql.py b/superset/db_engine_specs/mysql.py
index 870dbe853d590..4bb5979706b5b 100644
--- a/superset/db_engine_specs/mysql.py
+++ b/superset/db_engine_specs/mysql.py
@@ -58,11 +58,10 @@ class MySQLEngineSpec(BaseEngineSpec, BasicParametersMixin):
engine_name = "MySQL"
max_column_name_length = 64
- drivername = "mysql+mysqldb"
+ default_driver = "mysqldb"
sqlalchemy_uri_placeholder = (
"mysql://user:password@host:port/dbname[?key=value&key=value...]"
)
-
encryption_parameters = {"ssl": "1"}
column_type_mappings: Tuple[
diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py
index 5c8ff40365a6e..513e01eb0cd75 100644
--- a/superset/db_engine_specs/postgres.py
+++ b/superset/db_engine_specs/postgres.py
@@ -159,9 +159,9 @@ class PostgresEngineSpec(PostgresBaseEngineSpec, BasicParametersMixin):
engine = "postgresql"
engine_aliases = {"postgres"}
- drivername = "postgresql+psycopg2"
+ default_driver = "psycopg2"
sqlalchemy_uri_placeholder = (
- "postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...]"
+ "postgresql://user:password@host:port/dbname[?key=value&key=value...]"
)
# https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBQ-SSL-CERTIFICATES
encryption_parameters = {"sslmode": "verify-ca"}
diff --git a/superset/db_engine_specs/redshift.py b/superset/db_engine_specs/redshift.py
index beff1f694b837..6b79007526a10 100644
--- a/superset/db_engine_specs/redshift.py
+++ b/superset/db_engine_specs/redshift.py
@@ -50,6 +50,7 @@ class RedshiftEngineSpec(PostgresBaseEngineSpec, BasicParametersMixin):
engine = "redshift"
engine_name = "Amazon Redshift"
max_column_name_length = 127
+ default_driver = "psycopg2"
sqlalchemy_uri_placeholder = (
"redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]"
diff --git a/superset/db_engine_specs/trino.py b/superset/db_engine_specs/trino.py
index 791d248ce3ebf..7c60e622855b9 100644
--- a/superset/db_engine_specs/trino.py
+++ b/superset/db_engine_specs/trino.py
@@ -15,10 +15,10 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
-from typing import Optional
+from typing import Any, Dict, Optional
from urllib import parse
-from sqlalchemy.engine.url import URL
+from sqlalchemy.engine.url import make_url, URL
from superset.db_engine_specs.base import BaseEngineSpec
from superset.utils import core as utils
@@ -69,3 +69,37 @@ def adjust_database_uri(
selected_schema = parse.quote(selected_schema, safe="")
database = database.split("/")[0] + "/" + selected_schema
uri.database = database
+
+ @classmethod
+ def update_impersonation_config(
+ cls, connect_args: Dict[str, Any], uri: str, username: Optional[str],
+ ) -> None:
+ """
+ Update a configuration dictionary
+ that can set the correct properties for impersonating users
+ :param connect_args: config to be updated
+ :param uri: URI string
+ :param impersonate_user: Flag indicating if impersonation is enabled
+ :param username: Effective username
+ :return: None
+ """
+ url = make_url(uri)
+ backend_name = url.get_backend_name()
+
+ # Must be Trino connection, enable impersonation, and set optional param
+ # auth=LDAP|KERBEROS
+ # Set principal_username=$effective_username
+ if backend_name == "trino" and username is not None:
+ connect_args["user"] = username
+
+ @classmethod
+ def modify_url_for_impersonation(
+ cls, url: URL, impersonate_user: bool, username: Optional[str]
+ ) -> None:
+ """
+ Modify the SQL Alchemy URL object with the user to impersonate if applicable.
+ :param url: SQLAlchemy URL object
+ :param impersonate_user: Flag indicating if impersonation is enabled
+ :param username: Effective username
+ """
+ # Do nothing and let update_impersonation_config take care of impersonation
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index b6fb8552c5537..43e64e4be72ea 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -297,7 +297,7 @@ def get_filters(self, column: str, remove_filter: bool = False) -> List[Filter]:
filters: List[Filter] = []
for flt in form_data.get("adhoc_filters", []):
- val: Union[str, List[str]] = flt.get("comparator")
+ val: Union[Any, List[Any]] = flt.get("comparator")
op: str = flt["operator"].upper() if "operator" in flt else None
# fltOpName: str = flt.get("filterOptionName")
if (
diff --git a/superset/models/core.py b/superset/models/core.py
index 540974f9de375..ee8e822477a8f 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -246,7 +246,8 @@ def parameters(self) -> Dict[str, Any]:
self.db_engine_spec, "get_parameters_from_uri"
):
uri = make_url(self.sqlalchemy_uri_decrypted)
- return {**parameters, **self.db_engine_spec.get_parameters_from_uri(uri)} # type: ignore
+ encrypted_extra = self.get_encrypted_extra()
+ return {**parameters, **self.db_engine_spec.get_parameters_from_uri(uri, encrypted_extra=encrypted_extra)} # type: ignore
return parameters
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index aef2288422cb5..97b123e344421 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -26,7 +26,7 @@
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
-from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import NoValidFilesFoundError
from superset.commands.importers.v1.utils import get_contents_from_bundle
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.databases.filters import DatabaseFilter
@@ -263,7 +263,6 @@ def export(self, **kwargs: Any) -> Response:
@expose("/import/", methods=["POST"])
@protect()
- @safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.import_",
@@ -289,7 +288,7 @@ def import_(self) -> Response:
type: string
overwrite:
description: overwrite existing saved queries?
- type: bool
+ type: boolean
responses:
200:
description: Saved Query import result
@@ -315,6 +314,9 @@ def import_(self) -> Response:
with ZipFile(upload) as bundle:
contents = get_contents_from_bundle(bundle)
+ if not contents:
+ raise NoValidFilesFoundError()
+
passwords = (
json.loads(request.form["passwords"])
if "passwords" in request.form
@@ -325,12 +327,5 @@ def import_(self) -> Response:
command = ImportSavedQueriesCommand(
contents, passwords=passwords, overwrite=overwrite
)
- try:
- command.run()
- return self.response(200, message="OK")
- except CommandInvalidError as exc:
- logger.warning("Import Saved Query failed")
- return self.response_422(message=exc.normalized_messages())
- except Exception as exc: # pylint: disable=broad-except
- logger.exception("Import Saved Query failed")
- return self.response_500(message=str(exc))
+ command.run()
+ return self.response(200, message="OK")
diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py
index 30fce841b793a..42625e66a2795 100644
--- a/superset/utils/date_parser.py
+++ b/superset/utils/date_parser.py
@@ -79,7 +79,8 @@ def parse_human_datetime(human_readable: str) -> datetime:
if re.search(x_periods, human_readable, re.IGNORECASE):
raise TimeRangeUnclearError(human_readable)
try:
- dttm = parse(human_readable)
+ default = datetime(year=datetime.now().year, month=1, day=1)
+ dttm = parse(human_readable, default=default)
except (ValueError, OverflowError) as ex:
cal = parsedatetime.Calendar()
parsed_dttm, parsed_flags = cal.parseDT(human_readable)
diff --git a/superset/views/utils.py b/superset/views/utils.py
index 767490ca7317c..a3dd432e4f377 100644
--- a/superset/views/utils.py
+++ b/superset/views/utils.py
@@ -127,13 +127,16 @@ def loads_request_json(request_json_data: str) -> Dict[Any, Any]:
def get_form_data( # pylint: disable=too-many-locals
slice_id: Optional[int] = None, use_slice_data: bool = False
) -> Tuple[Dict[str, Any], Optional[Slice]]:
- form_data = {}
+ form_data: Dict[str, Any] = {}
# chart data API requests are JSON
request_json_data = (
request.json["queries"][0]
if request.is_json and "queries" in request.json
else None
)
+
+ add_sqllab_custom_filters(form_data)
+
request_form_data = request.form.get("form_data")
request_args_data = request.args.get("form_data")
if request_json_data:
@@ -196,6 +199,26 @@ def get_form_data( # pylint: disable=too-many-locals
return form_data, slc
+def add_sqllab_custom_filters(form_data: Dict[Any, Any]) -> Any:
+ """
+ SQLLab can include a "filters" attribute in the templateParams.
+ The filters attribute is a list of filters to include in the
+ request. Useful for testing templates in SQLLab.
+ """
+ try:
+ data = json.loads(request.data)
+ if isinstance(data, dict):
+ params_str = data.get("templateParams")
+ if isinstance(params_str, str):
+ params = json.loads(params_str)
+ if isinstance(params, dict):
+ filters = params.get("_filters")
+ if filters:
+ form_data.update({"filters": filters})
+ except (TypeError, json.JSONDecodeError):
+ data = {}
+
+
def get_datasource_info(
datasource_id: Optional[int], datasource_type: Optional[str], form_data: FormData
) -> Tuple[int, Optional[str]]:
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index 33927bbe1e7a2..b43abfb6641da 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -1633,9 +1633,22 @@ def test_import_chart_overwrite(self):
assert rv.status_code == 422
assert response == {
- "message": {
- "charts/imported_chart.yaml": "Chart already exists and `overwrite=true` was not passed"
- }
+ "errors": [
+ {
+ "message": "Error importing chart",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "charts/imported_chart.yaml": "Chart already exists and `overwrite=true` was not passed",
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": "Issue 1010 - Superset encountered an error while running a command.",
+ }
+ ],
+ },
+ }
+ ]
}
# import with overwrite flag
@@ -1691,7 +1704,25 @@ def test_import_chart_invalid(self):
assert rv.status_code == 422
assert response == {
- "message": {"metadata.yaml": {"type": ["Must be equal to Slice."]}}
+ "errors": [
+ {
+ "message": "Error importing chart",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "metadata.yaml": {"type": ["Must be equal to Slice."]},
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered an "
+ "error while running a command."
+ ),
+ }
+ ],
+ },
+ }
+ ]
}
@pytest.mark.usefixtures(
diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py
index 02be01e32f010..7bec82bb93a70 100644
--- a/tests/dashboards/api_tests.py
+++ b/tests/dashboards/api_tests.py
@@ -626,6 +626,14 @@ def create_dashboard_import(self):
buf.seek(0)
return buf
+ def create_invalid_dashboard_import(self):
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ with bundle.open("sql/dump.sql", "w") as fp:
+ fp.write("CREATE TABLE foo (bar INT)".encode())
+ buf.seek(0)
+ return buf
+
def test_delete_dashboard(self):
"""
Dashboard API: Test delete
@@ -1392,6 +1400,42 @@ def test_import_dashboard(self):
db.session.delete(database)
db.session.commit()
+ def test_import_dashboard_invalid_file(self):
+ """
+ Dashboard API: Test import invalid dashboard file
+ """
+ self.login(username="admin")
+ uri = "api/v1/dashboard/import/"
+
+ buf = self.create_invalid_dashboard_import()
+ form_data = {
+ "formData": (buf, "dashboard_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 400
+ assert response == {
+ "errors": [
+ {
+ "message": "No valid import files were found",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered an "
+ "error while running a command."
+ ),
+ }
+ ]
+ },
+ }
+ ]
+ }
+
def test_import_dashboard_v0_export(self):
num_dashboards = db.session.query(Dashboard).count()
@@ -1449,9 +1493,25 @@ def test_import_dashboard_overwrite(self):
assert rv.status_code == 422
assert response == {
- "message": {
- "dashboards/imported_dashboard.yaml": "Dashboard already exists and `overwrite=true` was not passed"
- }
+ "errors": [
+ {
+ "message": "Error importing dashboard",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "dashboards/imported_dashboard.yaml": "Dashboard already exists and `overwrite=true` was not passed",
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered an "
+ "error while running a command."
+ ),
+ }
+ ],
+ },
+ }
+ ]
}
# import with overwrite flag
@@ -1515,7 +1575,25 @@ def test_import_dashboard_invalid(self):
assert rv.status_code == 422
assert response == {
- "message": {"metadata.yaml": {"type": ["Must be equal to Dashboard."]}}
+ "errors": [
+ {
+ "message": "Error importing dashboard",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "metadata.yaml": {"type": ["Must be equal to Dashboard."]},
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered "
+ "an error while running a command."
+ ),
+ }
+ ],
+ },
+ }
+ ]
}
def test_get_all_related_roles(self):
diff --git a/tests/databases/api_tests.py b/tests/databases/api_tests.py
index 9ad0b1970e4c5..fad93ade8ee83 100644
--- a/tests/databases/api_tests.py
+++ b/tests/databases/api_tests.py
@@ -510,7 +510,7 @@ def test_update_database(self):
self.login(username="admin")
database_data = {
"database_name": "test-database-updated",
- "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
+ "configuration_method": ConfigurationMethod.SQLALCHEMY_FORM,
}
uri = f"api/v1/database/{test_database.id}"
rv = self.client.put(uri, json=database_data)
@@ -1208,9 +1208,25 @@ def test_import_database_overwrite(self):
assert rv.status_code == 422
assert response == {
- "message": {
- "databases/imported_database.yaml": "Database already exists and `overwrite=true` was not passed"
- }
+ "errors": [
+ {
+ "message": "Error importing database",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "databases/imported_database.yaml": "Database already exists and `overwrite=true` was not passed",
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered an "
+ "error while running a command."
+ ),
+ }
+ ],
+ },
+ }
+ ]
}
# import with overwrite flag
@@ -1263,7 +1279,25 @@ def test_import_database_invalid(self):
assert rv.status_code == 422
assert response == {
- "message": {"metadata.yaml": {"type": ["Must be equal to Database."]}}
+ "errors": [
+ {
+ "message": "Error importing database",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "metadata.yaml": {"type": ["Must be equal to Database."]},
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered an "
+ "error while running a command."
+ ),
+ }
+ ],
+ },
+ }
+ ]
}
def test_import_database_masked_password(self):
@@ -1300,11 +1334,27 @@ def test_import_database_masked_password(self):
assert rv.status_code == 422
assert response == {
- "message": {
- "databases/imported_database.yaml": {
- "_schema": ["Must provide a password for the database"]
+ "errors": [
+ {
+ "message": "Error importing database",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "databases/imported_database.yaml": {
+ "_schema": ["Must provide a password for the database"]
+ },
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered an "
+ "error while running a command."
+ ),
+ }
+ ],
+ },
}
- }
+ ]
}
def test_import_database_masked_password_provided(self):
@@ -1372,16 +1422,14 @@ def test_function_names(self, mock_get_function_names):
@mock.patch("superset.databases.api.get_available_engine_specs")
@mock.patch("superset.databases.api.app")
def test_available(self, app, get_available_engine_specs):
- app.config = {
- "PREFERRED_DATABASES": ["postgresql", "biqquery", "mysql", "redshift"]
+ app.config = {"PREFERRED_DATABASES": ["PostgreSQL", "Google BigQuery"]}
+ get_available_engine_specs.return_value = {
+ PostgresEngineSpec: {"psycopg2"},
+ BigQueryEngineSpec: {"bigquery"},
+ MySQLEngineSpec: {"mysqlconnector", "mysqldb"},
+ RedshiftEngineSpec: {"psycopg2"},
+ HanaEngineSpec: {""},
}
- get_available_engine_specs.return_value = [
- PostgresEngineSpec,
- BigQueryEngineSpec,
- MySQLEngineSpec,
- RedshiftEngineSpec,
- HanaEngineSpec,
- ]
self.login(username="admin")
uri = "api/v1/database/available/"
@@ -1392,6 +1440,8 @@ def test_available(self, app, get_available_engine_specs):
assert response == {
"databases": [
{
+ "available_drivers": ["psycopg2"],
+ "default_driver": "psycopg2",
"engine": "postgresql",
"name": "PostgreSQL",
"parameters": {
@@ -1433,11 +1483,32 @@ def test_available(self, app, get_available_engine_specs):
"type": "object",
},
"preferred": True,
- "sqlalchemy_uri_placeholder": "postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...]",
+ "sqlalchemy_uri_placeholder": "postgresql://user:password@host:port/dbname[?key=value&key=value...]",
},
{
- "engine": "mysql",
- "name": "MySQL",
+ "available_drivers": ["bigquery"],
+ "default_driver": "bigquery",
+ "engine": "bigquery",
+ "name": "Google BigQuery",
+ "parameters": {
+ "properties": {
+ "credentials_info": {
+ "description": "Contents of BigQuery JSON credentials.",
+ "type": "string",
+ "x-encrypted-extra": True,
+ }
+ },
+ "required": ["credentials_info"],
+ "type": "object",
+ },
+ "preferred": True,
+ "sqlalchemy_uri_placeholder": "bigquery://{project_id}",
+ },
+ {
+ "available_drivers": ["psycopg2"],
+ "default_driver": "psycopg2",
+ "engine": "redshift",
+ "name": "Amazon Redshift",
"parameters": {
"properties": {
"database": {
@@ -1476,12 +1547,14 @@ def test_available(self, app, get_available_engine_specs):
"required": ["database", "host", "port", "username"],
"type": "object",
},
- "preferred": True,
- "sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]",
+ "preferred": False,
+ "sqlalchemy_uri_placeholder": "redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]",
},
{
- "engine": "redshift",
- "name": "Amazon Redshift",
+ "available_drivers": ["mysqlconnector", "mysqldb"],
+ "default_driver": "mysqldb",
+ "engine": "mysql",
+ "name": "MySQL",
"parameters": {
"properties": {
"database": {
@@ -1520,26 +1593,48 @@ def test_available(self, app, get_available_engine_specs):
"required": ["database", "host", "port", "username"],
"type": "object",
},
+ "preferred": False,
+ "sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]",
+ },
+ {
+ "available_drivers": [""],
+ "engine": "hana",
+ "name": "SAP HANA",
+ "preferred": False,
+ },
+ ]
+ }
+
+ @mock.patch("superset.databases.api.get_available_engine_specs")
+ @mock.patch("superset.databases.api.app")
+ def test_available_no_default(self, app, get_available_engine_specs):
+ app.config = {"PREFERRED_DATABASES": ["MySQL"]}
+ get_available_engine_specs.return_value = {
+ MySQLEngineSpec: {"mysqlconnector"},
+ HanaEngineSpec: {""},
+ }
+
+ self.login(username="admin")
+ uri = "api/v1/database/available/"
+
+ rv = self.client.get(uri)
+ response = json.loads(rv.data.decode("utf-8"))
+ assert rv.status_code == 200
+ assert response == {
+ "databases": [
+ {
+ "available_drivers": ["mysqlconnector"],
+ "default_driver": "mysqldb",
+ "engine": "mysql",
+ "name": "MySQL",
"preferred": True,
- "sqlalchemy_uri_placeholder": "redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]",
},
{
- "engine": "bigquery",
- "name": "Google BigQuery",
- "parameters": {
- "properties": {
- "credentials_info": {
- "description": "Contents of BigQuery JSON credentials.",
- "type": "string",
- "x-encrypted-extra": True,
- }
- },
- "type": "object",
- },
+ "available_drivers": [""],
+ "engine": "hana",
+ "name": "SAP HANA",
"preferred": False,
- "sqlalchemy_uri_placeholder": "bigquery://{project_id}",
},
- {"engine": "hana", "name": "SAP HANA", "preferred": False},
]
}
diff --git a/tests/databases/schema_tests.py b/tests/databases/schema_tests.py
index 6e10e21786491..1f8ca067f6b0d 100644
--- a/tests/databases/schema_tests.py
+++ b/tests/databases/schema_tests.py
@@ -21,6 +21,7 @@
from superset.databases.schemas import DatabaseParametersSchemaMixin
from superset.db_engine_specs.base import BasicParametersMixin
+from superset.models.core import ConfigurationMethod
class DummySchema(Schema, DatabaseParametersSchemaMixin):
@@ -28,7 +29,8 @@ class DummySchema(Schema, DatabaseParametersSchemaMixin):
class DummyEngine(BasicParametersMixin):
- drivername = "dummy"
+ engine = "dummy"
+ default_driver = "dummy"
class InvalidEngine:
@@ -39,31 +41,34 @@ class InvalidEngine:
def test_database_parameters_schema_mixin(get_engine_specs):
get_engine_specs.return_value = {"dummy_engine": DummyEngine}
payload = {
+ "engine": "dummy_engine",
+ "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
"parameters": {
- "engine": "dummy_engine",
"username": "username",
"password": "password",
"host": "localhost",
"port": 12345,
"database": "dbname",
- }
+ },
}
schema = DummySchema()
result = schema.load(payload)
assert result == {
- "sqlalchemy_uri": "dummy://username:password@localhost:12345/dbname"
+ "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
+ "sqlalchemy_uri": "dummy+dummy://username:password@localhost:12345/dbname",
}
def test_database_parameters_schema_mixin_no_engine():
payload = {
+ "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
"parameters": {
"username": "username",
"password": "password",
"host": "localhost",
"port": 12345,
"dbname": "dbname",
- }
+ },
}
schema = DummySchema()
try:
@@ -80,14 +85,15 @@ def test_database_parameters_schema_mixin_no_engine():
def test_database_parameters_schema_mixin_invalid_engine(get_engine_specs):
get_engine_specs.return_value = {}
payload = {
+ "engine": "dummy_engine",
+ "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
"parameters": {
- "engine": "dummy_engine",
"username": "username",
"password": "password",
"host": "localhost",
"port": 12345,
"dbname": "dbname",
- }
+ },
}
schema = DummySchema()
try:
@@ -102,14 +108,15 @@ def test_database_parameters_schema_mixin_invalid_engine(get_engine_specs):
def test_database_parameters_schema_no_mixin(get_engine_specs):
get_engine_specs.return_value = {"invalid_engine": InvalidEngine}
payload = {
+ "engine": "invalid_engine",
+ "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
"parameters": {
- "engine": "invalid_engine",
"username": "username",
"password": "password",
"host": "localhost",
"port": 12345,
"database": "dbname",
- }
+ },
}
schema = DummySchema()
try:
@@ -123,3 +130,24 @@ def test_database_parameters_schema_no_mixin(get_engine_specs):
)
]
}
+
+
+@mock.patch("superset.databases.schemas.get_engine_specs")
+def test_database_parameters_schema_mixin_invalid_type(get_engine_specs):
+ get_engine_specs.return_value = {"dummy_engine": DummyEngine}
+ payload = {
+ "engine": "dummy_engine",
+ "configuration_method": ConfigurationMethod.DYNAMIC_FORM,
+ "parameters": {
+ "username": "username",
+ "password": "password",
+ "host": "localhost",
+ "port": "badport",
+ "database": "dbname",
+ },
+ }
+ schema = DummySchema()
+ try:
+ schema.load(payload)
+ except ValidationError as err:
+ assert err.messages == {"port": ["Not a valid integer."]}
diff --git a/tests/datasets/api_tests.py b/tests/datasets/api_tests.py
index b82ef75d41499..ea0e17277a931 100644
--- a/tests/datasets/api_tests.py
+++ b/tests/datasets/api_tests.py
@@ -1543,9 +1543,22 @@ def test_import_dataset_overwrite(self):
assert rv.status_code == 422
assert response == {
- "message": {
- "datasets/imported_dataset.yaml": "Dataset already exists and `overwrite=true` was not passed"
- }
+ "errors": [
+ {
+ "message": "Error importing dataset",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "datasets/imported_dataset.yaml": "Dataset already exists and `overwrite=true` was not passed",
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": "Issue 1010 - Superset encountered an error while running a command.",
+ }
+ ],
+ },
+ }
+ ]
}
# import with overwrite flag
@@ -1599,7 +1612,25 @@ def test_import_dataset_invalid(self):
assert rv.status_code == 422
assert response == {
- "message": {"metadata.yaml": {"type": ["Must be equal to SqlaTable."]}}
+ "errors": [
+ {
+ "message": "Error importing dataset",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "metadata.yaml": {"type": ["Must be equal to SqlaTable."]},
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": (
+ "Issue 1010 - Superset encountered "
+ "an error while running a command."
+ ),
+ }
+ ],
+ },
+ }
+ ]
}
def test_import_dataset_invalid_v0_validation(self):
@@ -1628,4 +1659,20 @@ def test_import_dataset_invalid_v0_validation(self):
response = json.loads(rv.data.decode("utf-8"))
assert rv.status_code == 422
- assert response == {"message": "Could not process entity"}
+ assert response == {
+ "errors": [
+ {
+ "message": "Could not find a valid command to import file",
+ "error_type": "GENERIC_COMMAND_ERROR",
+ "level": "warning",
+ "extra": {
+ "issue_codes": [
+ {
+ "code": 1010,
+ "message": "Issue 1010 - Superset encountered an error while running a command.",
+ }
+ ]
+ },
+ }
+ ]
+ }
diff --git a/tests/datasource_tests.py b/tests/datasource_tests.py
index 438079fc5be85..4ae3957f77480 100644
--- a/tests/datasource_tests.py
+++ b/tests/datasource_tests.py
@@ -73,6 +73,25 @@ def test_external_metadata_for_virtual_table(self):
session.delete(table)
session.commit()
+ def test_external_metadata_for_virtual_table_template_params(self):
+ self.login(username="admin")
+ session = db.session
+ table = SqlaTable(
+ table_name="dummy_sql_table_with_template_params",
+ database=get_example_database(),
+ sql="select {{ foo }} as intcol",
+ template_params=json.dumps({"foo": "123"}),
+ )
+ session.add(table)
+ session.commit()
+
+ table = self.get_table_by_name("dummy_sql_table_with_template_params")
+ url = f"/datasource/external_metadata/table/{table.id}/"
+ resp = self.get_json_resp(url)
+ assert {o.get("name") for o in resp} == {"intcol"}
+ session.delete(table)
+ session.commit()
+
def test_external_metadata_for_malicious_virtual_table(self):
self.login(username="admin")
table = SqlaTable(
diff --git a/tests/model_tests.py b/tests/model_tests.py
index da30d30a41e67..83b826ff800ca 100644
--- a/tests/model_tests.py
+++ b/tests/model_tests.py
@@ -157,6 +157,33 @@ def test_impersonate_user_presto(self, mocked_create_engine):
"password": "original_user_password",
}
+ @mock.patch("superset.models.core.create_engine")
+ def test_impersonate_user_trino(self, mocked_create_engine):
+ uri = "trino://localhost"
+ principal_user = "logged_in_user"
+
+ model = Database(database_name="test_database", sqlalchemy_uri=uri)
+
+ model.impersonate_user = True
+ model.get_sqla_engine(user_name=principal_user)
+ call_args = mocked_create_engine.call_args
+
+ assert str(call_args[0][0]) == "trino://localhost"
+
+ assert call_args[1]["connect_args"] == {
+ "user": "logged_in_user",
+ }
+
+ uri = "trino://original_user:original_user_password@localhost"
+ model = Database(database_name="test_database", sqlalchemy_uri=uri)
+ model.impersonate_user = True
+ model.get_sqla_engine(user_name=principal_user)
+ call_args = mocked_create_engine.call_args
+
+ assert str(call_args[0][0]) == "trino://original_user@localhost"
+
+ assert call_args[1]["connect_args"] == {"user": "logged_in_user"}
+
@mock.patch("superset.models.core.create_engine")
def test_impersonate_user_hive(self, mocked_create_engine):
uri = "hive://localhost"
diff --git a/tests/sqla_models_tests.py b/tests/sqla_models_tests.py
index 69dc4c737f1c3..a759270809c25 100644
--- a/tests/sqla_models_tests.py
+++ b/tests/sqla_models_tests.py
@@ -102,6 +102,10 @@ def test_db_column_types(self):
self.assertEqual(col.is_numeric, db_col_type == GenericDataType.NUMERIC)
self.assertEqual(col.is_string, db_col_type == GenericDataType.STRING)
+ for str_type, db_col_type in test_cases.items():
+ col = TableColumn(column_name="foo", type=str_type, table=tbl, is_dttm=True)
+ self.assertTrue(col.is_temporal)
+
@patch("superset.jinja_context.g")
def test_extra_cache_keys(self, flask_g):
flask_g.user.username = "abc"
diff --git a/tests/utils/date_parser_tests.py b/tests/utils/date_parser_tests.py
index 2d5cb2264de62..ad87f40dc8218 100644
--- a/tests/utils/date_parser_tests.py
+++ b/tests/utils/date_parser_tests.py
@@ -35,6 +35,10 @@
def mock_parse_human_datetime(s):
if s == "now":
return datetime(2016, 11, 7, 9, 30, 10)
+ elif s == "2018":
+ return datetime(2018, 1, 1)
+ elif s == "2018-9":
+ return datetime(2018, 9, 1)
elif s == "today":
return datetime(2016, 11, 7)
elif s == "yesterday":
@@ -153,6 +157,14 @@ def test_datetime_eval(self):
expected = datetime(2016, 11, 7)
self.assertEqual(result, expected)
+ result = datetime_eval("datetime('2018')")
+ expected = datetime(2018, 1, 1)
+ self.assertEqual(result, expected)
+
+ result = datetime_eval("datetime('2018-9')")
+ expected = datetime(2018, 9, 1)
+ self.assertEqual(result, expected)
+
# Parse compact arguments spelling
result = datetime_eval("dateadd(datetime('today'),1,year,)")
expected = datetime(2017, 11, 7)