diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ea027a84896..fc6d3a84395 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,10 +33,10 @@ # Notify PMC members of changes to extension-related files -/superset-core/ @michael-s-molina @villebro -/superset-extensions-cli/ @michael-s-molina @villebro -/superset/core/ @michael-s-molina @villebro -/superset/extensions/ @michael-s-molina @villebro -/superset-frontend/src/packages/superset-core/ @michael-s-molina @villebro -/superset-frontend/src/core/ @michael-s-molina @villebro -/superset-frontend/src/extensions/ @michael-s-molina @villebro +/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje +/superset-extensions-cli/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje +/superset/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje +/superset/extensions/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje +/superset-frontend/src/packages/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje +/superset-frontend/src/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje +/superset-frontend/src/extensions/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje diff --git a/UPDATING.md b/UPDATING.md index 08cd483775e..f34c46debf0 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -23,6 +23,8 @@ This file documents any backwards-incompatible changes in Superset and assists people when migrating to a new version. ## Next +- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading. +- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments. - [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact. - [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files). - [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`: diff --git a/docs/docs/configuration/configuring-superset.mdx b/docs/docs/configuration/configuring-superset.mdx index 1a453adfe32..0a50b07fe69 100644 --- a/docs/docs/configuration/configuring-superset.mdx +++ b/docs/docs/configuration/configuring-superset.mdx @@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager ] ``` -### Keycloak-Specific Configuration using Flask-OIDC - -If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option. - -Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency. - -The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file. - -```python -from flask_appbuilder.security.manager import AUTH_OID -from superset.security import SupersetSecurityManager -from flask_oidc import OpenIDConnect -from flask_appbuilder.security.views import AuthOIDView -from flask_login import login_user -from urllib.parse import quote -from flask_appbuilder.views import ModelView, SimpleFormView, expose -from flask import ( - redirect, - request -) -import logging - -class OIDCSecurityManager(SupersetSecurityManager): - - def __init__(self, appbuilder): - super(OIDCSecurityManager, self).__init__(appbuilder) - if self.auth_type == AUTH_OID: - self.oid = OpenIDConnect(self.appbuilder.get_app) - self.authoidview = AuthOIDCView - -class AuthOIDCView(AuthOIDView): - - @expose('/login/', methods=['GET', 'POST']) - def login(self, flag=True): - sm = self.appbuilder.sm - oidc = sm.oid - - @self.appbuilder.sm.oid.require_login - def handle_login(): - user = sm.auth_user_oid(oidc.user_getfield('email')) - - if user is None: - info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email']) - user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'), - info.get('email'), sm.find_role('Gamma')) - - login_user(user, remember=False) - return redirect(self.appbuilder.get_url_for_index) - - return handle_login() - - @expose('/logout/', methods=['GET', 'POST']) - def logout(self): - oidc = self.appbuilder.sm.oid - - oidc.logout() - super(AuthOIDCView, self).logout() - redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login - - return redirect( - oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url)) -``` - -Then add to your `superset_config.py` file: - -```python -from keycloak_security_manager import OIDCSecurityManager -from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH -import os - -AUTH_TYPE = AUTH_OID -SECRET_KEY: 'SomethingNotEntirelySecret' -OIDC_CLIENT_SECRETS = '/path/to/client_secret.json' -OIDC_ID_TOKEN_COOKIE_SECURE = False -OIDC_OPENID_REALM: '' -OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post' -CUSTOM_SECURITY_MANAGER = OIDCSecurityManager - -# Will allow user self registration, allowing to create Flask users from Authorized User -AUTH_USER_REGISTRATION = True - -# The default user self registration role -AUTH_USER_REGISTRATION_ROLE = 'Public' -``` - -Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`: - -```json -{ - "": { - "issuer": "https:///realms/", - "auth_uri": "https:///realms//protocol/openid-connect/auth", - "client_id": "https://", - "client_secret": "", - "redirect_uris": [ - "https:///oauth-authorized/" - ], - "userinfo_uri": "https:///realms//protocol/openid-connect/userinfo", - "token_uri": "https:///realms//protocol/openid-connect/token", - "token_introspection_uri": "https:///realms//protocol/openid-connect/token/introspect" - } -} -``` - ## LDAP Authentication FAB supports authenticating user credentials against an LDAP server. diff --git a/docs/docs/contributing/development.mdx b/docs/docs/contributing/development.mdx index c811177624b..6e22055d0d4 100644 --- a/docs/docs/contributing/development.mdx +++ b/docs/docs/contributing/development.mdx @@ -620,10 +620,10 @@ See [how tos](/docs/contributing/howtos#linting) :::tip `act` compatibility of Superset's GHAs is not fully tested. Running `act` locally may or may not -work for different actions, and may require fine tunning and local secret-handling. +work for different actions, and may require fine tuning and local secret-handling. For those more intricate GHAs that are tricky to run locally, we recommend iterating directly on GHA's infrastructure, by pushing directly on a branch and monitoring GHA logs. -For more targetted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI. +For more targeted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI. ::: For automation and CI/CD, Superset makes extensive use of GitHub Actions (GHA). You diff --git a/docs/docs/contributing/howtos.mdx b/docs/docs/contributing/howtos.mdx index 9a23cbc075a..e243a1fcf5c 100644 --- a/docs/docs/contributing/howtos.mdx +++ b/docs/docs/contributing/howtos.mdx @@ -232,7 +232,7 @@ CYPRESS_CONFIG=true docker compose up --build ``` `docker compose` will get to work and expose a Cypress-ready Superset app. This app uses a different database schema (`superset_cypress`) to keep it isolated from -your other dev environmen(s)t, a specific set of examples, and a set of configurations that +your other dev environment(s), a specific set of examples, and a set of configurations that aligns with the expectations within the end-to-end tests. Also note that it's served on a different port than the default port for the backend (`8088`). @@ -627,7 +627,7 @@ feature flag to `true`, you can add the following line to the PR body/descriptio FEATURE_TAGGING_SYSTEM=true ``` -Simarly, it's possible to disable feature flags with: +Similarly, it's possible to disable feature flags with: ``` FEATURE_TAGGING_SYSTEM=false diff --git a/docs/docs/installation/docker-compose.mdx b/docs/docs/installation/docker-compose.mdx index 74145ff0b5b..9727b97eaeb 100644 --- a/docs/docs/installation/docker-compose.mdx +++ b/docs/docs/installation/docker-compose.mdx @@ -282,5 +282,5 @@ address. When running `docker compose up`, docker will build what is required behind the scene, but may use the docker cache if assets already exist. Running `docker compose build` prior to `docker compose up` or the equivalent shortcut `docker compose up --build` ensures that your -docker images matche the definition in the repository. This should only apply to the main +docker images match the definition in the repository. This should only apply to the main docker-compose.yml file (default) and not to the alternative methods defined above. diff --git a/docs/package.json b/docs/package.json index 556936239c9..a2c6234d916 100644 --- a/docs/package.json +++ b/docs/package.json @@ -73,7 +73,7 @@ "@eslint/js": "^9.32.0", "@types/react": "^19.1.8", "@typescript-eslint/eslint-plugin": "^8.37.0", - "@typescript-eslint/parser": "^8.37.0", + "@typescript-eslint/parser": "^8.42.0", "eslint": "^9.34.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.3", diff --git a/docs/versioned_docs/version-6.0.0/configuration/configuring-superset.mdx b/docs/versioned_docs/version-6.0.0/configuration/configuring-superset.mdx index 1a453adfe32..0a50b07fe69 100644 --- a/docs/versioned_docs/version-6.0.0/configuration/configuring-superset.mdx +++ b/docs/versioned_docs/version-6.0.0/configuration/configuring-superset.mdx @@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager ] ``` -### Keycloak-Specific Configuration using Flask-OIDC - -If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option. - -Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency. - -The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file. - -```python -from flask_appbuilder.security.manager import AUTH_OID -from superset.security import SupersetSecurityManager -from flask_oidc import OpenIDConnect -from flask_appbuilder.security.views import AuthOIDView -from flask_login import login_user -from urllib.parse import quote -from flask_appbuilder.views import ModelView, SimpleFormView, expose -from flask import ( - redirect, - request -) -import logging - -class OIDCSecurityManager(SupersetSecurityManager): - - def __init__(self, appbuilder): - super(OIDCSecurityManager, self).__init__(appbuilder) - if self.auth_type == AUTH_OID: - self.oid = OpenIDConnect(self.appbuilder.get_app) - self.authoidview = AuthOIDCView - -class AuthOIDCView(AuthOIDView): - - @expose('/login/', methods=['GET', 'POST']) - def login(self, flag=True): - sm = self.appbuilder.sm - oidc = sm.oid - - @self.appbuilder.sm.oid.require_login - def handle_login(): - user = sm.auth_user_oid(oidc.user_getfield('email')) - - if user is None: - info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email']) - user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'), - info.get('email'), sm.find_role('Gamma')) - - login_user(user, remember=False) - return redirect(self.appbuilder.get_url_for_index) - - return handle_login() - - @expose('/logout/', methods=['GET', 'POST']) - def logout(self): - oidc = self.appbuilder.sm.oid - - oidc.logout() - super(AuthOIDCView, self).logout() - redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login - - return redirect( - oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url)) -``` - -Then add to your `superset_config.py` file: - -```python -from keycloak_security_manager import OIDCSecurityManager -from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH -import os - -AUTH_TYPE = AUTH_OID -SECRET_KEY: 'SomethingNotEntirelySecret' -OIDC_CLIENT_SECRETS = '/path/to/client_secret.json' -OIDC_ID_TOKEN_COOKIE_SECURE = False -OIDC_OPENID_REALM: '' -OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post' -CUSTOM_SECURITY_MANAGER = OIDCSecurityManager - -# Will allow user self registration, allowing to create Flask users from Authorized User -AUTH_USER_REGISTRATION = True - -# The default user self registration role -AUTH_USER_REGISTRATION_ROLE = 'Public' -``` - -Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`: - -```json -{ - "": { - "issuer": "https:///realms/", - "auth_uri": "https:///realms//protocol/openid-connect/auth", - "client_id": "https://", - "client_secret": "", - "redirect_uris": [ - "https:///oauth-authorized/" - ], - "userinfo_uri": "https:///realms//protocol/openid-connect/userinfo", - "token_uri": "https:///realms//protocol/openid-connect/token", - "token_introspection_uri": "https:///realms//protocol/openid-connect/token/introspect" - } -} -``` - ## LDAP Authentication FAB supports authenticating user credentials against an LDAP server. diff --git a/docs/versioned_docs/version-6.0.0/contributing/development.mdx b/docs/versioned_docs/version-6.0.0/contributing/development.mdx index ee38ca76349..4f58c29ae61 100644 --- a/docs/versioned_docs/version-6.0.0/contributing/development.mdx +++ b/docs/versioned_docs/version-6.0.0/contributing/development.mdx @@ -620,10 +620,10 @@ See [how tos](/docs/contributing/howtos#linting) :::tip `act` compatibility of Superset's GHAs is not fully tested. Running `act` locally may or may not -work for different actions, and may require fine tunning and local secret-handling. +work for different actions, and may require fine tuning and local secret-handling. For those more intricate GHAs that are tricky to run locally, we recommend iterating directly on GHA's infrastructure, by pushing directly on a branch and monitoring GHA logs. -For more targetted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI. +For more targeted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI. ::: For automation and CI/CD, Superset makes extensive use of GitHub Actions (GHA). You diff --git a/docs/versioned_docs/version-6.0.0/contributing/howtos.mdx b/docs/versioned_docs/version-6.0.0/contributing/howtos.mdx index 9a23cbc075a..e243a1fcf5c 100644 --- a/docs/versioned_docs/version-6.0.0/contributing/howtos.mdx +++ b/docs/versioned_docs/version-6.0.0/contributing/howtos.mdx @@ -232,7 +232,7 @@ CYPRESS_CONFIG=true docker compose up --build ``` `docker compose` will get to work and expose a Cypress-ready Superset app. This app uses a different database schema (`superset_cypress`) to keep it isolated from -your other dev environmen(s)t, a specific set of examples, and a set of configurations that +your other dev environment(s), a specific set of examples, and a set of configurations that aligns with the expectations within the end-to-end tests. Also note that it's served on a different port than the default port for the backend (`8088`). @@ -627,7 +627,7 @@ feature flag to `true`, you can add the following line to the PR body/descriptio FEATURE_TAGGING_SYSTEM=true ``` -Simarly, it's possible to disable feature flags with: +Similarly, it's possible to disable feature flags with: ``` FEATURE_TAGGING_SYSTEM=false diff --git a/docs/versioned_docs/version-6.0.0/installation/docker-compose.mdx b/docs/versioned_docs/version-6.0.0/installation/docker-compose.mdx index 74145ff0b5b..9727b97eaeb 100644 --- a/docs/versioned_docs/version-6.0.0/installation/docker-compose.mdx +++ b/docs/versioned_docs/version-6.0.0/installation/docker-compose.mdx @@ -282,5 +282,5 @@ address. When running `docker compose up`, docker will build what is required behind the scene, but may use the docker cache if assets already exist. Running `docker compose build` prior to `docker compose up` or the equivalent shortcut `docker compose up --build` ensures that your -docker images matche the definition in the repository. This should only apply to the main +docker images match the definition in the repository. This should only apply to the main docker-compose.yml file (default) and not to the alternative methods defined above. diff --git a/docs/yarn.lock b/docs/yarn.lock index 679d24faba3..1543c3541ff 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -4102,7 +4102,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.40.0", "@typescript-eslint/parser@^8.37.0": +"@typescript-eslint/parser@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.40.0.tgz#1bc9f3701ced29540eb76ff2d95ce0d52ddc7e69" integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw== @@ -4113,6 +4113,17 @@ "@typescript-eslint/visitor-keys" "8.40.0" debug "^4.3.4" +"@typescript-eslint/parser@^8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.42.0.tgz#20ea66f4867981fb5bb62cbe1454250fc4a440ab" + integrity sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg== + dependencies: + "@typescript-eslint/scope-manager" "8.42.0" + "@typescript-eslint/types" "8.42.0" + "@typescript-eslint/typescript-estree" "8.42.0" + "@typescript-eslint/visitor-keys" "8.42.0" + debug "^4.3.4" + "@typescript-eslint/project-service@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz#1b7ba6079ff580c3215882fe75a43e5d3ed166b9" @@ -4122,6 +4133,15 @@ "@typescript-eslint/types" "^8.40.0" debug "^4.3.4" +"@typescript-eslint/project-service@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.42.0.tgz#636eb3418b6c42c98554dce884943708bf41a583" + integrity sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.42.0" + "@typescript-eslint/types" "^8.42.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz#2fbfcc8643340d8cd692267e61548b946190be8a" @@ -4130,11 +4150,24 @@ "@typescript-eslint/types" "8.40.0" "@typescript-eslint/visitor-keys" "8.40.0" +"@typescript-eslint/scope-manager@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz#36016757bc85b46ea42bae47b61f9421eddedde3" + integrity sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw== + dependencies: + "@typescript-eslint/types" "8.42.0" + "@typescript-eslint/visitor-keys" "8.42.0" + "@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz#8e8fdb9b988854aedd04abdde3239c4bdd2d26e4" integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw== +"@typescript-eslint/tsconfig-utils@8.42.0", "@typescript-eslint/tsconfig-utils@^8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz#21a3e74396fd7443ff930bc41b27789ba7e9236e" + integrity sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ== + "@typescript-eslint/type-utils@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz#a7e4a1f0815dd0ba3e4eef945cc87193ca32c422" @@ -4146,11 +4179,16 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0": +"@typescript-eslint/types@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.40.0.tgz#0b580fdf643737aa5c01285314b5c6e9543846a9" integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg== +"@typescript-eslint/types@8.42.0", "@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@^8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.42.0.tgz#ae15c09cebda20473772902033328e87372db008" + integrity sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw== + "@typescript-eslint/typescript-estree@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz#295149440ce7da81c790a4e14e327599a3a1e5c9" @@ -4167,6 +4205,22 @@ semver "^7.6.0" ts-api-utils "^2.1.0" +"@typescript-eslint/typescript-estree@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz#593c3af87d4462252c0d7239d1720b84a1b56864" + integrity sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ== + dependencies: + "@typescript-eslint/project-service" "8.42.0" + "@typescript-eslint/tsconfig-utils" "8.42.0" + "@typescript-eslint/types" "8.42.0" + "@typescript-eslint/visitor-keys" "8.42.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + "@typescript-eslint/utils@8.40.0": version "8.40.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.40.0.tgz#8d0c6430ed2f5dc350784bb0d8be514da1e54054" @@ -4185,6 +4239,14 @@ "@typescript-eslint/types" "8.40.0" eslint-visitor-keys "^4.2.1" +"@typescript-eslint/visitor-keys@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz#87c6caaa1ac307bc73a87c1fc469f88f0162f27e" + integrity sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ== + dependencies: + "@typescript-eslint/types" "8.42.0" + eslint-visitor-keys "^4.2.1" + "@ungap/structured-clone@^1.0.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" diff --git a/pyproject.toml b/pyproject.toml index ef848653092..9d83616441a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "cryptography>=42.0.4, <45.0.0", "deprecation>=2.1.0, <2.2.0", "flask>=2.2.5, <3.0.0", - "flask-appbuilder>=4.8.1, <5.0.0", + "flask-appbuilder>=5.0.0,<6", "flask-caching>=2.1.0, <3", "flask-compress>=1.13, <2.0", "flask-talisman>=1.0.0, <2.0", diff --git a/requirements/base.txt b/requirements/base.txt index 4357d92701c..d62d7f09750 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -114,11 +114,9 @@ flask==2.3.3 # flask-session # flask-sqlalchemy # flask-wtf -flask-appbuilder==4.8.1 - # via - # apache-superset (pyproject.toml) - # apache-superset-core -flask-babel==2.0.0 +flask-appbuilder==5.0.0 + # via apache-superset (pyproject.toml) +flask-babel==3.1.0 # via flask-appbuilder flask-caching==2.3.1 # via apache-superset (pyproject.toml) diff --git a/requirements/development.txt b/requirements/development.txt index f9b292c8b95..978fecc8b69 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -208,12 +208,11 @@ flask==2.3.3 # flask-sqlalchemy # flask-testing # flask-wtf -flask-appbuilder==4.8.1 +flask-appbuilder==5.0.0 # via # -c requirements/base-constraint.txt # apache-superset - # apache-superset-core -flask-babel==2.0.0 +flask-babel==3.1.0 # via # -c requirements/base-constraint.txt # flask-appbuilder diff --git a/superset-core/pyproject.toml b/superset-core/pyproject.toml index 7c6a760caae..0511509caa1 100644 --- a/superset-core/pyproject.toml +++ b/superset-core/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "flask-appbuilder>=4.5.3, <5.0.0", + "flask-appbuilder>=5.0.0,<6", ] [project.urls] diff --git a/superset-embedded-sdk/package-lock.json b/superset-embedded-sdk/package-lock.json index 500c1263f8a..590d6ca1faf 100644 --- a/superset-embedded-sdk/package-lock.json +++ b/superset-embedded-sdk/package-lock.json @@ -19,7 +19,6 @@ "@babel/preset-typescript": "^7.24.7", "@types/jest": "^29.5.12", "@types/node": "^22.5.4", - "axios": "^1.7.7", "babel-loader": "^9.1.3", "jest": "^29.7.0", "tscw-config": "^1.1.2", @@ -3233,24 +3232,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -3644,19 +3625,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3843,18 +3811,6 @@ "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -4045,15 +4001,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4063,20 +4010,6 @@ "node": ">=8" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.19", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz", @@ -4137,57 +4070,12 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -4628,42 +4516,6 @@ "node": ">=8" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -4717,30 +4569,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4750,19 +4578,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4825,18 +4640,6 @@ "node": ">=4" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4865,45 +4668,6 @@ "node": ">=4" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7092,15 +6856,6 @@ "tmpl": "1.0.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7414,12 +7169,6 @@ "node": ">= 6" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -10724,23 +10473,6 @@ "sprintf-js": "~1.0.2" } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", - "dev": true, - "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -11023,16 +10755,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -11158,15 +10880,6 @@ "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", "dev": true }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -11299,29 +11012,12 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, - "dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "requires": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - } - }, "electron-to-chromium": { "version": "1.5.19", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz", @@ -11365,45 +11061,12 @@ "is-arrayish": "^0.2.1" } }, - "es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true - }, "es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, - "es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "requires": { - "es-errors": "^1.3.0" - } - }, - "es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "requires": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - } - }, "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -11712,25 +11375,6 @@ "path-exists": "^4.0.0" } }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true - }, - "form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - } - }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -11768,40 +11412,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - } - }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, - "get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "requires": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - } - }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -11844,12 +11460,6 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, - "gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true - }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -11871,30 +11481,6 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, - "has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true - }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.3" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "requires": { - "function-bind": "^1.1.2" - } - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -13505,12 +13091,6 @@ "tmpl": "1.0.5" } }, - "math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -13742,12 +13322,6 @@ "sisteransi": "^1.0.5" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", diff --git a/superset-embedded-sdk/package.json b/superset-embedded-sdk/package.json index 336b0b926da..884e3595332 100644 --- a/superset-embedded-sdk/package.json +++ b/superset-embedded-sdk/package.json @@ -43,7 +43,6 @@ "@babel/preset-typescript": "^7.24.7", "@types/jest": "^29.5.12", "@types/node": "^22.5.4", - "axios": "^1.7.7", "babel-loader": "^9.1.3", "jest": "^29.7.0", "tscw-config": "^1.1.2", diff --git a/superset-embedded-sdk/release-if-necessary.js b/superset-embedded-sdk/release-if-necessary.js index 632f8cd4b87..83196ad821d 100644 --- a/superset-embedded-sdk/release-if-necessary.js +++ b/superset-embedded-sdk/release-if-necessary.js @@ -18,7 +18,6 @@ */ const { execSync } = require('child_process'); -const axios = require('axios'); const { name, version } = require('./package.json'); function log(...args) { @@ -36,9 +35,7 @@ function logError(...args) { // npm commands output a bunch of garbage in the edge cases, // and require sending semi-validated strings to the command line, // so let's just use good old http. - const { status } = await axios.get(packageUrl, { - validateStatus: (status) => true // we literally just want the status so any status is valid - }); + const { status } = await fetch(packageUrl); if (status === 200) { log('version already exists on npm, exiting'); diff --git a/superset-frontend/packages/superset-core/src/api/core.ts b/superset-frontend/packages/superset-core/src/api/core.ts index d46e198ecd7..03c5f7c358d 100644 --- a/superset-frontend/packages/superset-core/src/api/core.ts +++ b/superset-frontend/packages/superset-core/src/api/core.ts @@ -31,12 +31,32 @@ import { Contributions } from './contributions'; /** * Represents a database column with its name and data type. */ -export declare interface Column { - /** The name of the column */ +export type Column = { + /** + * Label of the column + */ name: string; - /** The data type of the column (e.g., 'INTEGER', 'VARCHAR', 'TIMESTAMP') */ + + /** + * Column name defined + */ + column_name: string; + + /** + * The data type of the column (e.g., 'INTEGER', 'VARCHAR', 'TIMESTAMP') + */ type: string; -} + + /** + * Generic data type format + */ + type_generic: GenericDataType; + + /** + * True if the column is date format + */ + is_dttm: boolean; +}; /** * Represents a database table with its name and column definitions. @@ -76,6 +96,45 @@ export declare interface Database { schemas: Schema[]; } +// Keep in sync with superset/errors.py +export type ErrorLevel = 'info' | 'warning' | 'error'; + +/** + * Superset error object structure. + * Contains details about an error that occurred within Superset. + */ +export type SupersetError = { + /** + * Error types, see enum of SupersetErrorType in superset/errors.py + */ + error_type: string; + + /** + * Extra properties based on the error types + */ + extra: Record; + + /** + * Level of the error type + */ + level: ErrorLevel; + + /** + * Detail description for the error + */ + message: string; +}; + +/** + * Generic data types, see enum of the same name in superset/utils/core.py. + */ +export enum GenericDataType { + Numeric = 0, + String = 1, + Temporal = 2, + Boolean = 3, +} + /** * Represents a type which can release resources, such * as event listening or a timer. diff --git a/superset-frontend/packages/superset-core/src/api/sqlLab.ts b/superset-frontend/packages/superset-core/src/api/sqlLab.ts index 997ec578090..a1abe66b839 100644 --- a/superset-frontend/packages/superset-core/src/api/sqlLab.ts +++ b/superset-frontend/packages/superset-core/src/api/sqlLab.ts @@ -29,7 +29,7 @@ * - Global APIs: Functions and events available across the entire SQL Lab interface */ -import { Event, Database } from './core'; +import { Event, Database, SupersetError, Column } from './core'; /** * Represents an SQL editor instance within a SQL Lab tab. @@ -111,6 +111,126 @@ export interface Tab { panels: Panel[]; } +export enum CTASMethod { + Table = 'TABLE', + View = 'VIEW', +} + +export interface CTAS { + /** + * Create method for CTAS creation request. + */ + method: CTASMethod; + + /** + * Temporary table name for creation using a CTAS query. + */ + tempTable: string | null; +} + +export interface QueryContext { + /** + * Unique query ID on client side. + */ + clientId: string; + + /** + * Contains CTAS if the query requests table creation. + */ + ctas: CTAS | null; + + /** + * Requested row limit for the query. + */ + requestedLimit: number | null; + + /** + * True if the query execution result will be/was delivered asynchronously + */ + runAsync?: boolean; + + /** + * Start datetime for the query in a numerical timestamp + */ + startDttm: number; + + /** + * The tab instance associated with the request query + */ + tab: Tab; + + /** + * A key-value JSON associated with Jinja template variables + */ + templateParameters: Record; +} + +export interface QueryErrorResultContext extends QueryContext { + /** + * Finished datetime for the query in a numerical timestamp + */ + endDttm: number; + + /** + * Error message returned from DB engine + */ + errorMessage: string; + + /** + * Error details in a SupersetError structure + */ + errors: SupersetError[] | null; + + /** + * Executed SQL after parsing Jinja templates. + */ + executedSql: string | null; +} + +export interface QueryResultContext extends QueryContext { + /** + * Actual number of rows returned by the query. + */ + appliedLimit: number; + + /** + * Major factor that is determining the row limit of the query results. + */ + appliedLimitingFactor: string; + + /** + * Finished datetime for the query in a numerical timestamp. + */ + endDttm: number; + + /** + * Executed SQL after parsing Jinja templates. + */ + executedSql: string; + + /** + * Remote query id stored in backend. + */ + remoteId: number; + + /** + * Query result data and metadata. + */ + result: QueryResult; +} + +export interface QueryResult { + /** + * Column metadata associated with the query result. + */ + columns: Column[]; + + /** + * Query result data. + */ + data: Record[]; +} + /** * Tab-scoped Events and Functions * @@ -240,30 +360,30 @@ export declare const onDidChangeTabTitle: Event; /** * Event fired when a query starts running in the current tab. - * Provides the editor state at the time of query execution. + * Provides the query request state at the time of query execution. * * @example * ```typescript - * onDidQueryRun.event((editor) => { - * console.log('Query started on database:', editor.databaseId); - * console.log('Query content:', editor.content); + * onDidQueryRun.event((query) => { + * console.log('Query started on database:', query.tab.editor.databaseId); + * console.log('Query content:', query.tab.editor.content); * }); * ``` */ -export declare const onDidQueryRun: Event; +export declare const onDidQueryRun: Event; /** * Event fired when a running query is stopped in the current tab. - * Provides the editor state when the query was stopped. + * Provides the query request state when the query was stopped. * * @example * ```typescript - * onDidQueryStop.event((editor) => { - * console.log('Query stopped for database:', editor.databaseId); + * onDidQueryStop.event((query) => { + * console.log('Query stopped for database:', query.tab.editor.databaseId); * }); * ``` */ -export declare const onDidQueryStop: Event; +export declare const onDidQueryStop: Event; /** * Event fired when a query fails in the current tab. @@ -273,12 +393,12 @@ export declare const onDidQueryStop: Event; * * @example * ```typescript - * onDidQueryFail.event((error) => { - * console.error('Query failed:', error); + * onDidQueryFail.event((result) => { + * console.error('Query failed:', result.errorMessage); * }); * ``` */ -export declare const onDidQueryFail: Event; +export declare const onDidQueryFail: Event; /** * Event fired when a query succeeds in the current tab. @@ -288,12 +408,13 @@ export declare const onDidQueryFail: Event; * * @example * ```typescript - * onDidQuerySuccess.event((result) => { - * console.log('Query succeeded:', result); + * onDidQuerySuccess.event((query) => { + * console.log('Query succeeded:', query.result.data); + * console.log('Query executed content:', query.executedSql); * }); * ``` */ -export declare const onDidQuerySuccess: Event; +export declare const onDidQuerySuccess: Event; /** * Global Events and Functions diff --git a/superset-frontend/packages/superset-ui-core/src/components/EmptyState/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/EmptyState/index.tsx index 05e39eed9b7..c8251be11a7 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/EmptyState/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/EmptyState/index.tsx @@ -60,7 +60,7 @@ const EmptyStateContainer = styled.div` flex-direction: column; width: 100%; height: 100%; - color: ${theme.colorTextQuaternary}; + color: ${theme.colorTextTertiary}; align-items: center; justify-content: center; padding: ${theme.sizeUnit * 4}px; @@ -84,7 +84,7 @@ const EmptyStateContainer = styled.div` const Title = styled.p<{ size: EmptyStateSize }>` ${({ theme, size }) => css` font-size: ${size === 'large' ? theme.fontSizeLG : theme.fontSize}px; - color: ${theme.colorTextQuaternary}; + color: ${theme.colorTextTertiary}; margin-top: ${size === 'large' ? theme.sizeUnit * 4 : theme.sizeUnit * 2}px; font-weight: ${theme.fontWeightStrong}; `} @@ -93,7 +93,7 @@ const Title = styled.p<{ size: EmptyStateSize }>` const Description = styled.p<{ size: EmptyStateSize }>` ${({ theme, size }) => css` font-size: ${size === 'large' ? theme.fontSize : theme.fontSizeSM}px; - color: ${theme.colorTextQuaternary}; + color: ${theme.colorTextTertiary}; margin-top: ${theme.sizeUnit * 2}px; `} `; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 00d323bdae8..010c0cbb9ae 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -34,6 +34,7 @@ export enum FeatureFlag { DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION', DashboardRbac = 'DASHBOARD_RBAC', DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT', + DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED', /** @deprecated */ DrillToDetail = 'DRILL_TO_DETAIL', DrillBy = 'DRILL_BY', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts index 72ea077a8bd..40b2697accd 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts @@ -158,6 +158,7 @@ const config: ControlPanelConfig = { }, }, ], + ['zoomable'], ], }, ], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts index 511d083d479..ba7f0ca0166 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts @@ -73,6 +73,7 @@ export default function transformProps( yAxisTitleMargin, yAxisTitlePosition, sliceId, + zoomable, } = formData as BoxPlotQueryFormData; const refs: Refs = {}; const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); @@ -284,6 +285,26 @@ export default function transformProps( }, }, series, + toolbox: { + show: zoomable, + feature: { + dataZoom: { + title: { + zoom: 'zoom area', + back: 'restore zoom', + }, + }, + }, + }, + dataZoom: zoomable + ? [ + { + type: 'inside', + zoomOnMouseWheel: false, + moveOnMouseWheel: true, + }, + ] + : [], }; return { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 7a20c9393de..a4b0ca7c42c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -42,6 +42,7 @@ import { xAxisBounds, xAxisLabelRotation, xAxisLabelInterval, + forceMaxInterval, } from '../controls'; const { @@ -358,6 +359,7 @@ const config: ControlPanelConfig = { ['x_axis_time_format'], [xAxisLabelRotation], [xAxisLabelInterval], + [forceMaxInterval], [{t('Tooltip')}], [ { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index e80a5df36d7..29be1a39fc9 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -214,6 +214,7 @@ export default function transformProps( sortSeriesAscending, sortSeriesAscendingB, timeGrainSqla, + forceMaxInterval, percentageThreshold, showQueryIdentifiers = false, metrics = [], @@ -583,11 +584,17 @@ export default function transformProps( }, minorTick: { show: minorTicks }, minInterval: - xAxisType === AxisType.Time && timeGrainSqla + xAxisType === AxisType.Time && timeGrainSqla && !forceMaxInterval ? TIMEGRAIN_TO_TIMESTAMP[ timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP ] : 0, + maxInterval: + xAxisType === AxisType.Time && timeGrainSqla && forceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : undefined, ...getMinAndMaxFromBounds( xAxisType, truncateXAxis, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index d73a1c2c076..fe1bee9e41a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -57,6 +57,7 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & { truncateYAxis: boolean; truncateYAxisSecondary: boolean; timeGrainSqla?: TimeGranularity; + forceMaxInterval?: boolean; tooltipTimeFormat?: string; zoomable: boolean; richTooltip: boolean; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index f3e40cb1dc7..5b51242490d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -41,6 +41,7 @@ import { truncateXAxis, xAxisBounds, minorTicks, + forceMaxInterval, } from '../../controls'; import { AreaChartStackControlOptions } from '../../constants'; @@ -185,6 +186,7 @@ const config: ControlPanelConfig = { ], [xAxisLabelRotation], [xAxisLabelInterval], + [forceMaxInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index a2032d3967f..9a26c3e8a64 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -39,6 +39,7 @@ import { xAxisBounds, xAxisLabelRotation, xAxisLabelInterval, + forceMaxInterval, } from '../../../controls'; import { OrientationType } from '../../types'; @@ -364,6 +365,7 @@ const config: ControlPanelConfig = { ...createAxisControl('x'), [truncateXAxis], [xAxisBounds], + [forceMaxInterval], ...richTooltipSection, [{t('Y Axis')}], ...createAxisControl('y'), diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx index 2553fe48d39..148bc966b1a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx @@ -42,6 +42,7 @@ import { xAxisBounds, xAxisLabelRotation, xAxisLabelInterval, + forceMaxInterval, } from '../../../controls'; const { @@ -173,6 +174,7 @@ const config: ControlPanelConfig = { ], [xAxisLabelRotation], [xAxisLabelInterval], + [forceMaxInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index b9bca7ca5c5..bf23eadd61f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -41,6 +41,7 @@ import { xAxisBounds, xAxisLabelRotation, xAxisLabelInterval, + forceMaxInterval, } from '../../../controls'; const { @@ -116,6 +117,7 @@ const config: ControlPanelConfig = { ], [xAxisLabelRotation], [xAxisLabelInterval], + [forceMaxInterval], // eslint-disable-next-line react/jsx-key ...richTooltipSection, // eslint-disable-next-line react/jsx-key diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx index d25fbbd5669..00ea73742fd 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx @@ -41,6 +41,7 @@ import { xAxisBounds, xAxisLabelRotation, xAxisLabelInterval, + forceMaxInterval, } from '../../../controls'; const { @@ -115,6 +116,7 @@ const config: ControlPanelConfig = { ], [xAxisLabelRotation], [xAxisLabelInterval], + [forceMaxInterval], // eslint-disable-next-line react/jsx-key ...richTooltipSection, // eslint-disable-next-line react/jsx-key diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index a59ec06b34d..cf2a8598e1a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -39,6 +39,7 @@ import { xAxisBounds, xAxisLabelRotation, xAxisLabelInterval, + forceMaxInterval, } from '../../controls'; const { @@ -167,6 +168,7 @@ const config: ControlPanelConfig = { ], [xAxisLabelRotation], [xAxisLabelInterval], + [forceMaxInterval], ...richTooltipSection, // eslint-disable-next-line react/jsx-key [{t('Y Axis')}], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index abde2dabbe1..4e07a8410df 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -168,6 +168,7 @@ export default function transformProps( sortSeriesType, sortSeriesAscending, timeGrainSqla, + forceMaxInterval, timeCompare, timeShiftColor, stack, @@ -510,11 +511,17 @@ export default function transformProps( }, minorTick: { show: minorTicks }, minInterval: - xAxisType === AxisType.Time && timeGrainSqla + xAxisType === AxisType.Time && timeGrainSqla && !forceMaxInterval ? TIMEGRAIN_TO_TIMESTAMP[ timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP ] : 0, + maxInterval: + xAxisType === AxisType.Time && timeGrainSqla && forceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : undefined, ...getMinAndMaxFromBounds( xAxisType, truncateXAxis, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index 76f8b6387f9..72ea97019ef 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -85,6 +85,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { xAxisForceCategorical?: boolean; xAxisTimeFormat?: string; timeGrainSqla?: TimeGranularity; + forceMaxInterval?: boolean; xAxisBounds: [number | undefined | null, number | undefined | null]; yAxisBounds: [number | undefined | null, number | undefined | null]; zoomable: boolean; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index 071e748903f..488ae5ef2e0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -338,6 +338,21 @@ export const xAxisLabelInterval = { }, }; +export const forceMaxInterval = { + name: 'force_max_interval', + config: { + type: 'CheckboxControl', + label: t('Force Time Grain as Max Interval'), + renderTrigger: true, + default: false, + description: t( + 'Forces selected Time Grain as the maximum interval for X Axis Labels', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.time_grain_sqla?.value), + }, +}; + export const seriesOrderSection: ControlSetRow[] = [ [{t('Series Order')}], [sortSeriesType], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts index cd76f27c4d0..a8ef2737cd3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts @@ -54,7 +54,9 @@ const getCrossFilterDataMask = values = [value]; } - const groupbyValues = values.map(value => labelMap[value]); + const groupbyValues = values + .map(value => labelMap[value]) + .filter(Boolean) as string[][]; return { dataMask: { @@ -122,6 +124,9 @@ export const contextMenuEventHandler = const drillFilters: BinaryQueryObjectFilterClause[] = []; if (groupby.length > 0) { const values = labelMap[e.name]; + if (!values) { + return; + } groupby.forEach((dimension, i) => { drillFilters.push({ col: dimension, diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts index c982cb1084d..db814cc1092 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts @@ -31,6 +31,7 @@ describe('BoxPlot transformProps', () => { whiskerOptions: 'Tukey', yAxisFormat: 'SMART_NUMBER', viz_type: 'my_chart', + zoomable: true, }; const chartProps = new ChartProps({ formData, @@ -75,6 +76,13 @@ describe('BoxPlot transformProps', () => { width: 800, height: 600, echartOptions: expect.objectContaining({ + dataZoom: expect.arrayContaining([ + { + moveOnMouseWheel: true, + type: 'inside', + zoomOnMouseWheel: false, + }, + ]), series: expect.arrayContaining([ expect.objectContaining({ name: 'boxplot', diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts index f474eb67b34..8b722af5435 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts @@ -17,23 +17,7 @@ * under the License. */ import { debounce } from 'lodash'; -import { formatSelectOptions } from '@superset-ui/chart-controls'; import { Constants } from '@superset-ui/core/components'; -import { t } from '@superset-ui/core'; - -export const PAGE_SIZE_OPTIONS = formatSelectOptions([ - [0, t('page_size.all')], - 1, - 2, - 3, - 4, - 5, - 10, - 20, - 50, - 100, - 200, -]); export const debounceFunc = debounce( (func: (val: string) => void, source: string) => func(source), diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index cd570c68731..050a61fdbd5 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -89,6 +89,7 @@ import { formatColumnValue } from './utils/formatValue'; import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts'; import { updateTableOwnState } from './DataTable/utils/externalAPIs'; import getScrollBarSize from './DataTable/utils/getScrollBarSize'; +import DateWithFormatter from './utils/DateWithFormatter'; type ValueRange = [number, number]; @@ -198,7 +199,7 @@ function SearchInput({ {t('Search')} ( }, className: [ className, - value == null ? 'dt-is-null' : '', + value == null || + (value instanceof DateWithFormatter && value.input == null) + ? 'dt-is-null' + : '', isActiveFilterValue(key, value) ? ' dt-is-active-filter' : '', ].join(' '), tabIndex: 0, diff --git a/superset-frontend/plugins/plugin-chart-table/src/consts.ts b/superset-frontend/plugins/plugin-chart-table/src/consts.ts index 58f60d049f5..e8836228f86 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/consts.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/consts.ts @@ -20,7 +20,7 @@ import { formatSelectOptions } from '@superset-ui/chart-controls'; import { t } from '@superset-ui/core'; export const PAGE_SIZE_OPTIONS = formatSelectOptions([ - [0, t('page_size.all')], + [0, t('All')], 10, 20, 50, diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index f0745695fbe..019a1fe3ef9 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -272,10 +272,14 @@ export function logFailedQuery(query, errors) { }; } +export function createQueryFailedAction(query, msg, link, errors) { + return { type: QUERY_FAILED, query, msg, link, errors }; +} + export function queryFailed(query, msg, link, errors) { return function (dispatch) { dispatch(logFailedQuery(query, errors)); - dispatch({ type: QUERY_FAILED, query, msg, link, errors }); + dispatch(createQueryFailedAction(query, msg, link, errors)); }; } @@ -391,6 +395,7 @@ export function runQueryFromSqlEditor( dbId: qe.dbId, sql: qe.selectedText || qe.sql, sqlEditorId: qe.tabViewId ?? qe.id, + immutableId: qe.immutableId, tab: qe.name, catalog: qe.catalog, schema: qe.schema, @@ -533,6 +538,7 @@ export function addQueryEditor(queryEditor) { const newQueryEditor = { ...queryEditor, id: nanoid(11), + immutableId: nanoid(11), loaded: true, inLocalStorage: true, }; diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index 4f344c973b9..0280a283a66 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -441,6 +441,7 @@ describe('async actions', () => { queryLimit: undefined, maxRow: undefined, id: 'abcd', + immutableId: 'abcd', templateParams: undefined, inLocalStorage: true, loaded: true, @@ -570,6 +571,7 @@ describe('async actions', () => { type: actions.ADD_QUERY_EDITOR, queryEditor: { ...queryEditor, + immutableId: 'abcd', inLocalStorage: true, loaded: true, }, @@ -597,6 +599,7 @@ describe('async actions', () => { type: actions.ADD_QUERY_EDITOR, queryEditor: { id: 'abcd', + immutableId: 'abcd', sql: expect.stringContaining('SELECT ...'), name: `Untitled Query 7`, dbId: defaultQueryEditor.dbId, @@ -753,6 +756,7 @@ describe('async actions', () => { queryEditor: { ...queryEditor, id: 'abcd', + immutableId: 'abcd', loaded: true, inLocalStorage: true, }, diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx index c7de1208b53..f941fb45873 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx @@ -33,7 +33,7 @@ import useInterval from 'src/SqlLab/utils/useInterval'; import { refreshQueries, clearInactiveQueries, - logFailedQuery, + queryFailed, } from 'src/SqlLab/actions/sqlLab'; import type { DatabaseObject } from 'src/features/databases/types'; @@ -119,7 +119,14 @@ function QueryAutoRefresh({ !failedQueries.current.has(id) && state === QueryState.Failed ) { - dispatch(logFailedQuery(query, query.extra?.errors)); + dispatch( + queryFailed( + query, + query.errorMessage, + query.extra?.errors?.[0]?.extra?.link, + query.extra?.errors, + ), + ); failedQueries.current.set(id, true); } }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx index 945c0f13151..1ace7435ef6 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx @@ -41,7 +41,7 @@ import { import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar'; import ResultSet from 'src/SqlLab/components/ResultSet'; import { api } from 'src/hooks/apiResources/queryApi'; -import setupExtensions from 'src/setup/setupExtensions'; +import setupCodeOverrides from 'src/setup/setupCodeOverrides'; import type { Action, Middleware, Store } from 'redux'; import SqlEditor, { Props } from '.'; @@ -353,7 +353,7 @@ describe('SqlEditor', () => { <>sqleditor.extension.form extension component )); - setupExtensions(); + setupCodeOverrides(); const { findByText } = setup(mockedProps, store); expect( await findByText('sqleditor.extension.form extension component'), diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index 9ac8bed6e78..3ebc1db5980 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -188,6 +188,7 @@ export const table = { export const defaultQueryEditor = { version: LatestQueryEditorVersion, id: 'dfsadfs', + immutableId: 'immutable-id', autorun: false, dbId: 1, latestQueryId: null, @@ -204,6 +205,7 @@ export const defaultQueryEditor = { export const extraQueryEditor1 = { ...defaultQueryEditor, id: 'diekd23', + immutableId: 'immutable-id', sql: 'SELECT *\nFROM\nWHERE\nLIMIT', name: 'Untitled Query 2', selectedText: 'SELECT', @@ -212,6 +214,7 @@ export const extraQueryEditor1 = { export const extraQueryEditor2 = { ...defaultQueryEditor, id: 'owkdi998', + immutableId: 'immutable-id', sql: '', name: 'Untitled Query 3', }; @@ -219,6 +222,7 @@ export const extraQueryEditor2 = { export const extraQueryEditor3 = { ...defaultQueryEditor, id: 'kvk23', + immutableId: 'immutable-id', sql: '', name: 'Untitled Query 4', tabViewId: 37, diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.ts index 91386e3fe67..7d9f0fddaca 100644 --- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts +++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts @@ -17,6 +17,7 @@ * under the License. */ import { t } from '@superset-ui/core'; +import { nanoid } from 'nanoid'; import type { BootstrapData } from 'src/types/bootstrapTypes'; import type { InitialState } from 'src/hooks/apiResources/sqlLab'; import { @@ -55,6 +56,7 @@ export default function getInitialState({ let queryEditors: Record = {}; const defaultQueryEditor = { version: LatestQueryEditorVersion, + immutableId: nanoid(11), loaded: true, name: t('Untitled query'), sql: '', @@ -78,6 +80,7 @@ export default function getInitialState({ queryEditor = { version: activeTab.extra_json?.version ?? QueryEditorVersion.V1, id: id.toString(), + immutableId: activeTab.extra_json?.immutableId ?? nanoid(11), loaded: true, name: activeTab.label, sql: activeTab.sql || '', @@ -100,6 +103,7 @@ export default function getInitialState({ queryEditor = { ...defaultQueryEditor, id: id.toString(), + immutableId: nanoid(11), loaded: false, name: label, dbId: undefined, diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts index 9665a88fae8..5532f155b74 100644 --- a/superset-frontend/src/SqlLab/types.ts +++ b/superset-frontend/src/SqlLab/types.ts @@ -49,6 +49,7 @@ export interface CursorPosition { export interface QueryEditor { version: QueryEditorVersion; id: string; + immutableId: string; dbId?: number; name: string; title?: string; // keep it optional for backward compatibility diff --git a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts index e8e1e156739..683b082b38c 100644 --- a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts +++ b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts @@ -45,6 +45,7 @@ const PERSISTENT_QUERY_EDITOR_KEYS = new Set([ 'dbId', 'height', 'id', + 'immutableId', 'latestQueryId', 'northPercent', 'queryLimit', diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx similarity index 98% rename from superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx rename to superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx index 060f6149a12..20fbc37be92 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/ChangeDatasourceModal.test.jsx @@ -22,7 +22,7 @@ import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; import sinon from 'sinon'; import mockDatasource from 'spec/fixtures/mockDatasource'; -import { ChangeDatasourceModal } from '.'; +import ChangeDatasourceModal from '.'; const mockStore = configureStore([thunk]); const store = mockStore({}); diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/index.tsx similarity index 99% rename from superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx rename to superset-frontend/src/components/Datasource/ChangeDatasourceModal/index.tsx index 5acd1b4d3d4..41c70a6f4ca 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal/index.tsx @@ -55,7 +55,7 @@ import { } from 'src/features/datasets/constants'; import withToasts from 'src/components/MessageToasts/withToasts'; import { InputRef } from 'antd'; -import type { Datasource, ChangeDatasourceModalProps } from './types'; +import type { Datasource, ChangeDatasourceModalProps } from '../types'; const CONFIRM_WARNING_MESSAGE = t( 'Warning! Changing the dataset may break the chart if the metadata does not exist.', diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx similarity index 99% rename from superset-frontend/src/components/Datasource/DatasourceModal.test.jsx rename to superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx index b614ff87db9..d4045a3de6f 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx @@ -29,7 +29,7 @@ import fetchMock from 'fetch-mock'; import sinon from 'sinon'; import { SupersetClient } from '@superset-ui/core'; import mockDatasource from 'spec/fixtures/mockDatasource'; -import { DatasourceModal } from '.'; +import DatasourceModal from '.'; // Define your constants here const SAVE_ENDPOINT = 'glob:*/api/v1/dataset/7'; diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx similarity index 98% rename from superset-frontend/src/components/Datasource/DatasourceModal.tsx rename to superset-frontend/src/components/Datasource/DatasourceModal/index.tsx index 6e551a9a156..ecbd8144ddf 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx @@ -45,9 +45,11 @@ import { import withToasts from 'src/components/MessageToasts/withToasts'; import { ErrorMessageWithStackTrace } from 'src/components'; import type { DatasetObject } from 'src/features/datasets/types'; -import type { DatasourceModalProps } from './types'; +import type { DatasourceModalProps } from '../types'; -const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor')); +const DatasourceEditor = AsyncEsmComponent( + () => import('../components/DatasourceEditor'), +); const StyledDatasourceModal = styled(Modal)` .modal-content { diff --git a/superset-frontend/src/components/Datasource/CollectionTable.test.tsx b/superset-frontend/src/components/Datasource/components/CollectionTable/CollectionTable.test.tsx similarity index 96% rename from superset-frontend/src/components/Datasource/CollectionTable.test.tsx rename to superset-frontend/src/components/Datasource/components/CollectionTable/CollectionTable.test.tsx index 8736a7e5569..205e9882e02 100644 --- a/superset-frontend/src/components/Datasource/CollectionTable.test.tsx +++ b/superset-frontend/src/components/Datasource/components/CollectionTable/CollectionTable.test.tsx @@ -19,7 +19,7 @@ import { render } from 'spec/helpers/testing-library'; import mockDatasource from 'spec/fixtures/mockDatasource'; -import CollectionTable from './CollectionTable'; +import CollectionTable from '.'; const props = { collection: mockDatasource['7__table'].columns, diff --git a/superset-frontend/src/components/Datasource/CollectionTable.tsx b/superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx similarity index 99% rename from superset-frontend/src/components/Datasource/CollectionTable.tsx rename to superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx index 59f3d07fb46..50169092f48 100644 --- a/superset-frontend/src/components/Datasource/CollectionTable.tsx +++ b/superset-frontend/src/components/Datasource/components/CollectionTable/index.tsx @@ -28,13 +28,13 @@ import Table, { type TablePaginationConfig, TableSize, } from '@superset-ui/core/components/Table'; -import Fieldset from './Fieldset'; -import { recurseReactClone } from './utils'; +import Fieldset from '../Fieldset'; +import { recurseReactClone } from '../../utils'; import { type CRUDCollectionProps, type CRUDCollectionState, type Sort, -} from './types'; +} from '../../types'; const CrudButtonWrapper = styled.div` text-align: right; diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx similarity index 94% rename from superset-frontend/src/components/Datasource/DatasourceEditor.jsx rename to superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx index b55a511bb36..30b499bed5c 100644 --- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx @@ -69,11 +69,12 @@ import { resetDatabaseState, } from 'src/database/actions'; import Mousetrap from 'mousetrap'; -import { DatabaseSelector } from '../DatabaseSelector'; -import CollectionTable from './CollectionTable'; -import Fieldset from './Fieldset'; -import Field from './Field'; -import { fetchSyncedColumns, updateColumns } from './utils'; +import { DatabaseSelector } from '../../../DatabaseSelector'; +import CollectionTable from '../CollectionTable'; +import Fieldset from '../Fieldset'; +import Field from '../Field'; +import { fetchSyncedColumns, updateColumns } from '../../utils'; +import DatasetUsageTab from './components/DatasetUsageTab'; const extensionsRegistry = getExtensionsRegistry(); @@ -142,7 +143,7 @@ const StyledLabelWrapper = styled.div` } `; -const StyledColumnsTabWrapper = styled.div` +const StyledTableTabWrapper = styled.div` .table > tbody > tr > td { vertical-align: middle; } @@ -177,6 +178,7 @@ const TABS_KEYS = { METRICS: 'METRICS', COLUMNS: 'COLUMNS', CALCULATED_COLUMNS: 'CALCULATED_COLUMNS', + USAGE: 'USAGE', SETTINGS: 'SETTINGS', SPATIAL: 'SPATIAL', }; @@ -658,6 +660,8 @@ class DatasourceEditor extends PureComponent { datasourceType: props.datasource.sql ? DATASOURCE_TYPES.virtual.key : DATASOURCE_TYPES.physical.key, + usageCharts: [], + usageChartsCount: 0, }; this.onChange = this.onChange.bind(this); @@ -671,6 +675,7 @@ class DatasourceEditor extends PureComponent { this.validateAndChange = this.validateAndChange.bind(this); this.handleTabSelect = this.handleTabSelect.bind(this); this.formatSql = this.formatSql.bind(this); + this.fetchUsageData = this.fetchUsageData.bind(this); this.currencies = ensureIsArray(props.currencies).map(currencyCode => ({ value: currencyCode, label: `${getCurrencySymbol({ @@ -844,6 +849,89 @@ class DatasourceEditor extends PureComponent { } } + async fetchUsageData( + page = 1, + pageSize = 25, + sortColumn = 'changed_on_delta_humanized', + sortDirection = 'desc', + ) { + const { datasource } = this.state; + try { + const queryParams = rison.encode({ + columns: [ + 'slice_name', + 'url', + 'certified_by', + 'certification_details', + 'description', + 'owners.first_name', + 'owners.last_name', + 'owners.id', + 'changed_on_delta_humanized', + 'changed_on', + 'changed_by.first_name', + 'changed_by.last_name', + 'changed_by.id', + 'dashboards.id', + 'dashboards.dashboard_title', + 'dashboards.url', + ], + filters: [ + { + col: 'datasource_id', + opr: 'eq', + value: datasource.id, + }, + ], + order_column: sortColumn, + order_direction: sortDirection, + page: page - 1, + page_size: pageSize, + }); + + const { json = {} } = await SupersetClient.get({ + endpoint: `/api/v1/chart/?q=${queryParams}`, + }); + + const charts = json?.result || []; + const ids = json?.ids || []; + + // Map chart IDs to chart objects + const chartsWithIds = charts.map((chart, index) => ({ + ...chart, + id: ids[index], + })); + + this.setState({ + usageCharts: chartsWithIds, + usageChartsCount: json?.count || 0, + }); + + return { + charts: chartsWithIds, + count: json?.count || 0, + ids, + }; + } catch (error) { + const { error: clientError, statusText } = + await getClientErrorObject(error); + this.props.addDangerToast( + clientError || + statusText || + t('An error occurred while fetching usage data'), + ); + this.setState({ + usageCharts: [], + usageChartsCount: 0, + }); + return { + charts: [], + count: 0, + ids: [], + }; + } + } + findDuplicates(arr, accessor) { const seen = {}; const dups = []; @@ -1684,7 +1772,7 @@ class DatasourceEditor extends PureComponent { /> ), children: ( - +