diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e2f6a79affc53..23b4fe42c58da 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,3 +17,6 @@ # Notify Helm Chart maintainers about changes in it /helm/superset/ @craig-rueda @dpgaspar @villebro + +# Notify E2E test maintainers of changes +/superset-frontend/cypress-base/ @jinghua-qa diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 493868eab966d..7320c23a1432b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,7 @@ updates: labels: - npm - dependabot + versioning-strategy: increase - package-ecosystem: "pip" directory: "/requirements/" @@ -21,9 +22,29 @@ updates: schedule: interval: "daily" open-pull-requests-limit: 0 + versioning-strategy: increase - package-ecosystem: "npm" directory: "/docs/" schedule: interval: "daily" open-pull-requests-limit: 0 + versioning-strategy: increase + + - package-ecosystem: "npm" + directory: "/superset-websocket/" + schedule: + interval: "daily" + labels: + - npm + - dependabot + versioning-strategy: increase + + - package-ecosystem: "npm" + directory: "/superset-websocket/utils/client-ws-app/" + schedule: + interval: "daily" + labels: + - npm + - dependabot + versioning-strategy: increase diff --git a/.pylintrc b/.pylintrc index dbc0eabaefcec..848767fe5dcb1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -134,7 +134,9 @@ include-naming-hint=no # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty +property-classes= + abc.abstractproperty, + sqlalchemy.ext.hybrid.hybrid_property # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ diff --git a/Makefile b/Makefile index 8dc7ffa8f569e..8304ccfb151b7 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ superset: superset load-examples # Install node packages - cd superset-frontend; npm install + cd superset-frontend; npm ci update: update-py update-js @@ -101,7 +101,7 @@ node-app: build-cypress: cd superset-frontend; npm run build-instrumented - cd superset-frontend/cypress-base; npm install + cd superset-frontend/cypress-base; npm ci open-cypress: if ! [ $(port) ]; then cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:9000 npm run cypress open; fi diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md index 5a5b722a06acf..4a14790ed5b64 100644 --- a/RESOURCES/INTHEWILD.md +++ b/RESOURCES/INTHEWILD.md @@ -48,6 +48,7 @@ Join our growing community! ### E-Commerce - [AiHello](https://www.aihello.com) [@ganeshkrishnan1] +- [Bazaar Technologies](https://www.bazaartech.com) [@umair-abro] - [Dragonpass](https://www.dragonpass.com.cn/) [@zhxjdwh] - [Fanatics](https://www.fanatics.com) [@coderfender] - [Fordeal](http://www.fordeal.com) [@Renkai] diff --git a/docs/docs/installation/installing-superset-from-scratch.mdx b/docs/docs/installation/installing-superset-from-scratch.mdx index 3a12c9db3ac13..5efdb3e8f1f28 100644 --- a/docs/docs/installation/installing-superset-from-scratch.mdx +++ b/docs/docs/installation/installing-superset-from-scratch.mdx @@ -64,7 +64,7 @@ We don't recommend using the system installed Python. Instead, first install the brew install readline pkg-config libffi openssl mysql postgres ``` -You should install a recent version of Python (the official docker image uses 3.8.12). We'd recommend using a Python version manager like [pyenv](https://github.com/pyenv/pyenv) (and also [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)). +You should install a recent version of Python (the official docker image uses 3.8.13). We'd recommend using a Python version manager like [pyenv](https://github.com/pyenv/pyenv) (and also [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)). Let's also make sure we have the latest version of `pip` and `setuptools`: diff --git a/docs/docs/installation/running-on-kubernetes.mdx b/docs/docs/installation/running-on-kubernetes.mdx index 755172a7ce34d..f7e568e52eb6d 100644 --- a/docs/docs/installation/running-on-kubernetes.mdx +++ b/docs/docs/installation/running-on-kubernetes.mdx @@ -63,7 +63,7 @@ superset-worker-75b48bbcc-qrq49 1/1 Running 0 4m12s The exact list will depend on some of your specific configuration overrides but you should generally expect: -- N `superset-xxxx-yyyy` and `superset-worker-xxxx-yyyy` pods (depending on your `replicaCount` value) +- N `superset-xxxx-yyyy` and `superset-worker-xxxx-yyyy` pods (depending on your `supersetNode.replicaCount` and `supersetWorker.replicaCount` values) - 1 `superset-postgresql-0` depending on your postgres settings - 1 `superset-redis-master-0` depending on your redis settings - 1 `superset-celerybeat-xxxx-yyyy` pod if you have `supersetCeleryBeat.enabled = true` in your values overrides diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index d57225a0d797c..f2b0030997e28 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -249,6 +249,15 @@ const CarouselSection = styled('div')` } `; +const StyledCredits = styled.div` + width: 100%; + height: 60px; + padding: 18px; + background-color: #282E4A; + text-align: center; + color: #FFFFFF; +`; + const StyledDatabaseImg = styled.img` width: ${(props) => props.width}; height: ${(props) => props.height}; @@ -461,8 +470,13 @@ export default function Home(): JSX.Element { - + + We use{' '} + + + + ); } diff --git a/docs/static/img/applitools.png b/docs/static/img/applitools.png new file mode 100644 index 0000000000000..66b7b1eb4f375 Binary files /dev/null and b/docs/static/img/applitools.png differ diff --git a/docs/yarn.lock b/docs/yarn.lock index 2c788f210c475..367ec2363c993 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3014,15 +3014,37 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" - integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" @@ -3032,6 +3054,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -3840,15 +3870,10 @@ acorn-walk@^8.0.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.0.4, acorn@^8.4.1: - version "8.5.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== - -acorn@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== address@^1.0.1, address@^1.1.2: version "1.1.2" @@ -4438,7 +4463,7 @@ btoa@^1.2.1: buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-indexof@^1.0.0: @@ -4790,7 +4815,7 @@ comma-separated-tokens@^1.0.0: commander@^2.20.0: version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@^5.1.0: @@ -10264,9 +10289,9 @@ source-map-js@^1.0.2: integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map-support@~0.5.20: - version "0.5.20" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10281,11 +10306,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz" @@ -10618,23 +10638,14 @@ terser-webpack-plugin@^5.3.1: source-map "^0.6.1" terser "^5.7.2" -terser@^5.10.0: - version "5.12.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" - integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ== +terser@^5.10.0, terser@^5.7.2: + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== dependencies: + "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.20" - -terser@^5.7.2: - version "5.9.0" - resolved "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz" - integrity sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" source-map-support "~0.5.20" text-table@^0.2.0: diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml index e22bc966e2796..078d11b717997 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.6.6 +version: 0.7.0 dependencies: - name: postgresql version: 11.1.22 diff --git a/helm/superset/templates/deployment-worker.yaml b/helm/superset/templates/deployment-worker.yaml index 2d2cb0ddb9526..71f90ace481a8 100644 --- a/helm/superset/templates/deployment-worker.yaml +++ b/helm/superset/templates/deployment-worker.yaml @@ -28,7 +28,7 @@ metadata: {{ toYaml .Values.supersetWorker.deploymentAnnotations | nindent 4 }} {{- end }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.supersetWorker.replicaCount }} selector: matchLabels: app: {{ template "superset.name" . }}-worker diff --git a/helm/superset/templates/deployment.yaml b/helm/superset/templates/deployment.yaml index 75156d8285c86..8d035a2d0f735 100644 --- a/helm/superset/templates/deployment.yaml +++ b/helm/superset/templates/deployment.yaml @@ -28,7 +28,7 @@ metadata: {{ toYaml .Values.supersetNode.deploymentAnnotations | nindent 4 }} {{- end }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.supersetNode.replicaCount }} selector: matchLabels: app: {{ template "superset.name" . }} diff --git a/helm/superset/values.schema.json b/helm/superset/values.schema.json index 33912c6718083..dc8a5863dc788 100644 --- a/helm/superset/values.schema.json +++ b/helm/superset/values.schema.json @@ -3,9 +3,6 @@ "type": "object", "additionalProperties": true, "properties": { - "replicaCount": { - "type": "integer" - }, "runAsUser": { "type": "integer" }, @@ -216,6 +213,9 @@ "type": "object", "additionalProperties": false, "properties": { + "replicaCount": { + "type": "integer" + }, "command": { "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.23.0/_definitions.json##/definitions/io.k8s.api.core.v1.Container/properties/command" }, @@ -287,6 +287,7 @@ } }, "required": [ + "replicaCount", "command", "connections", "env", @@ -297,6 +298,9 @@ "type": "object", "additionalProperties": false, "properties": { + "replicaCount": { + "type": "integer" + }, "command": { "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.23.0/_definitions.json##/definitions/io.k8s.api.core.v1.Container/properties/command" }, @@ -326,6 +330,7 @@ } }, "required": [ + "replicaCount", "command", "forceReload" ] @@ -594,7 +599,6 @@ } }, "required": [ - "replicaCount", "runAsUser", "serviceAccount", "bootstrapScript", diff --git a/helm/superset/values.yaml b/helm/superset/values.yaml index 1b2fe3ac04118..318890335dbb2 100644 --- a/helm/superset/values.yaml +++ b/helm/superset/values.yaml @@ -19,8 +19,6 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 1 - # User ID directive. This user must have enough permissions to run the bootstrap script # Runn containers as root is not recommended in production. Change this to another UID - e.g. 1000 to be more secure runAsUser: 0 @@ -224,6 +222,7 @@ hostAliases: [] ## ## Superset node configuration supersetNode: + replicaCount: 1 command: - "/bin/sh" - "-c" @@ -268,6 +267,7 @@ supersetNode: ## ## Superset worker configuration supersetWorker: + replicaCount: 1 command: - "/bin/sh" - "-c" diff --git a/requirements/integration.txt b/requirements/integration.txt index fb1d37cd53f91..0d4567381d054 100644 --- a/requirements/integration.txt +++ b/requirements/integration.txt @@ -7,6 +7,8 @@ # backports-entry-points-selectable==1.1.0 # via virtualenv +build==0.8.0 + # via pip-tools cfgv==3.3.0 # via pre-commit click==8.0.4 @@ -24,12 +26,14 @@ identify==2.2.13 nodeenv==1.6.0 # via pre-commit packaging==21.3 - # via tox + # via + # build + # tox pep517==0.11.0 - # via pip-tools + # via build pip-compile-multi==2.4.1 # via -r requirements/integration.in -pip-tools==6.2.0 +pip-tools==6.8.0 # via pip-compile-multi platformdirs==2.2.0 # via virtualenv @@ -52,10 +56,10 @@ toml==0.10.2 # pre-commit # tox tomli==1.2.1 - # via pep517 + # via build toposort==1.6 # via pip-compile-multi -tox==3.24.1 +tox==3.25.1 # via -r requirements/integration.in virtualenv==20.7.2 # via diff --git a/requirements/testing.txt b/requirements/testing.txt index 5c1c2f7fce345..f1ceac3b8b46f 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -128,7 +128,7 @@ rsa==4.7.2 # via google-auth statsd==3.3.0 # via -r requirements/testing.in -trino==0.313.0 +trino==0.315.0 # via apache-superset typing-inspect==0.7.1 # via libcst diff --git a/setup.cfg b/setup.cfg index 6f667677ec810..7f2f83c18e7db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,7 @@ disallow_untyped_calls = true disallow_untyped_defs = true ignore_missing_imports = true no_implicit_optional = true -warn_unused_ignores = false +warn_unused_ignores = true [mypy-superset.migrations.versions.*] ignore_errors = true diff --git a/setup.py b/setup.py index b62dceb47f078..ba16a2e58f67a 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,11 @@ def get_git_sha() -> str: zip_safe=False, entry_points={ "console_scripts": ["superset=superset.cli.main:superset"], + # the `postgres+psycopg2://` scheme was removed in SQLAlchemy 1.4, add an alias here + # to prevent breaking existing databases + "sqlalchemy.dialects": [ + "postgres.psycopg2=sqlalchemy.dialects.postgresql:dialect" + ], }, install_requires=[ "backoff>=1.8.0", @@ -162,9 +167,7 @@ def get_git_sha() -> str: "shillelagh": [ "shillelagh[datasetteapi,gsheetsapi,socrata,weatherapi]>=1.0.3, <2" ], - "snowflake": [ - "snowflake-sqlalchemy==1.2.4" - ], # PINNED! 1.2.5 introduced breaking changes requiring sqlalchemy>=1.4.0 + "snowflake": ["snowflake-sqlalchemy>=1.2.4, <2"], "spark": ["pyhive[hive]>=0.6.5", "tableschema", "thrift>=0.11.0, <1.0.0"], "teradata": ["teradatasql>=16.20.0.23"], "thumbnails": ["Pillow>=9.1.1, <10.0.0"], diff --git a/superset-embedded-sdk/src/index.ts b/superset-embedded-sdk/src/index.ts index 32b02641e00d2..317195522c88d 100644 --- a/superset-embedded-sdk/src/index.ts +++ b/superset-embedded-sdk/src/index.ts @@ -105,9 +105,10 @@ export async function embedDashboard({ iframe.sandbox.add("allow-scripts"); // obviously the iframe needs scripts iframe.sandbox.add("allow-presentation"); // for fullscreen charts iframe.sandbox.add("allow-downloads"); // for downloading charts as image + iframe.sandbox.add("allow-forms"); // for forms to submit + iframe.sandbox.add("allow-popups"); // for exporting charts as csv // add these ones if it turns out we need them: // iframe.sandbox.add("allow-top-navigation"); - // iframe.sandbox.add("allow-forms"); // add the event listener before setting src, to be 100% sure that we capture the load event iframe.addEventListener('load', () => { diff --git a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts index bc18f2c410f2c..fb3445fc6366f 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts @@ -28,7 +28,7 @@ import { HEALTH_POP_FORM_DATA_DEFAULTS } from './visualizations/shared.helper'; const apiURL = (endpoint: string, queryObject: Record) => `${endpoint}?q=${rison.encode(queryObject)}`; -describe.skip('Test explore links', () => { +describe('Test explore links', () => { beforeEach(() => { cy.login(); interceptChart({ legacy: true }).as('chartData'); 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 a4d99b850f3fc..b20f69a2140ba 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 @@ -40,7 +40,10 @@ describe('Visualization > Line', () => { cy.get('.panel-body').contains( `Add required control values to preview chart`, ); - cy.get('.text-danger').contains('Metrics'); + cy.get('[data-test="metrics-header"]').contains('Metrics'); + cy.get('[data-test="metrics-header"] [data-test="error-tooltip"]').should( + 'exist', + ); cy.get('[data-test=metrics]') .contains('Drop columns/metrics here or click') @@ -55,7 +58,11 @@ describe('Visualization > Line', () => { .type('sum{enter}'); cy.get('[data-test="AdhocMetricEdit#save"]').contains('Save').click(); - cy.get('.text-danger').should('not.exist'); + cy.get('[data-test="metrics-header"]').contains('Metrics'); + cy.get('[data-test="metrics-header"] [data-test="error-tooltip"]').should( + 'not.exist', + ); + cy.get('.ant-alert-warning').should('not.exist'); }); diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 927126632b7c1..1f5c937db791f 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -6489,6 +6489,58 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@lerna/add": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz", @@ -12817,13 +12869,14 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "devOptional": true, "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -12868,15 +12921,6 @@ } } }, - "node_modules/@storybook/builder-webpack5/node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "devOptional": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/@storybook/builder-webpack5/node_modules/webpack-dev-middleware": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", @@ -14694,13 +14738,14 @@ } }, "node_modules/@storybook/manager-webpack5/node_modules/terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "devOptional": true, "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -14745,15 +14790,6 @@ } } }, - "node_modules/@storybook/manager-webpack5/node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "devOptional": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/@storybook/manager-webpack5/node_modules/webpack-dev-middleware": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", @@ -38044,7 +38080,8 @@ "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true }, "node_modules/lodash.template": { "version": "4.5.0", @@ -49100,9 +49137,9 @@ } }, "node_modules/terser": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", - "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "dependencies": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -49354,13 +49391,13 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dependencies": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" }, "bin": { @@ -49370,17 +49407,6 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/terser-webpack-plugin/node_modules/webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", @@ -49390,16 +49416,6 @@ "source-map": "~0.6.1" } }, - "node_modules/terser-webpack-plugin/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/terser-webpack-plugin/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -49769,6 +49785,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -51211,7 +51228,8 @@ "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true }, "node_modules/webpack": { "version": "5.52.1", @@ -52047,12 +52065,13 @@ } }, "node_modules/webpack/node_modules/terser": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.8.0.tgz", - "integrity": "sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -52096,14 +52115,6 @@ } } }, - "node_modules/webpack/node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webpack/node_modules/watchpack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", @@ -55753,7 +55764,7 @@ }, "plugins/plugin-chart-handlebars": { "name": "@superset-ui/plugin-chart-handlebars", - "version": "0.0.0", + "version": "0.18.25", "license": "Apache-2.0", "dependencies": { "handlebars": "^4.7.7", @@ -60740,6 +60751,49 @@ } } }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@lerna/add": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz", @@ -65570,22 +65624,15 @@ } }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "devOptional": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "devOptional": true - } } }, "terser-webpack-plugin": { @@ -66876,22 +66923,15 @@ } }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "devOptional": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "devOptional": true - } } }, "terser-webpack-plugin": { @@ -85817,7 +85857,8 @@ "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true }, "lodash.template": { "version": "4.5.0", @@ -94503,9 +94544,9 @@ } }, "terser": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", - "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -94684,24 +94725,14 @@ } }, "terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "requires": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "requires": { - "whatwg-url": "^7.0.0" - } - } } }, "webpack-sources": { @@ -94713,16 +94744,6 @@ "source-map": "~0.6.1" } }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -95008,6 +95029,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, "requires": { "punycode": "^2.1.0" } @@ -96119,7 +96141,8 @@ "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true }, "webpack": { "version": "5.52.1", @@ -96327,20 +96350,14 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, "terser": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.8.0.tgz", - "integrity": "sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } } }, "terser-webpack-plugin": { diff --git a/superset-frontend/packages/superset-ui-core/src/api/types/core.ts b/superset-frontend/packages/superset-ui-core/src/api/types/core.ts new file mode 100644 index 0000000000000..9aeba85accc95 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/api/types/core.ts @@ -0,0 +1,31 @@ +/** + * 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. + */ + +// /superset/sqllab_viz +interface SqlLabPostRequest { + data: { + schema: string; + sql: string; + dbId: number; + templateParams?: string | undefined; + datasourceName: string; + metrics?: string[]; + columns?: string[]; + }; +} diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts index 964ff67a9b880..dc71c809ea760 100644 --- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts +++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/ExtensionsRegistry.ts @@ -38,6 +38,7 @@ type ReturningDisplayable

= (props: P) => string | React.ReactElement; export type Extensions = Partial<{ 'embedded.documentation.description': ReturningDisplayable; 'embedded.documentation.url': string; + 'dashboard.nav.right': React.ComponentType; 'navbar.right': React.ComponentType; 'welcome.banner': React.ComponentType; }>; diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-pivot-table/PivotTableStories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-pivot-table/PivotTableStories.tsx new file mode 100644 index 0000000000000..54903c013aa25 --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-pivot-table/PivotTableStories.tsx @@ -0,0 +1,74 @@ +/* + * 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 { withKnobs } from '@storybook/addon-knobs'; +import { SuperChart } from '@superset-ui/core'; +import { PivotTableChartPlugin } from '@superset-ui/plugin-chart-pivot-table'; +import { basicFormData, basicData } from './testData'; +import { withResizableChartDemo } from '../../../shared/components/ResizableChartDemo'; + +export default { + title: 'Chart Plugins/plugin-chart-pivot-table', + decorators: [withKnobs, withResizableChartDemo], +}; + +new PivotTableChartPlugin().configure({ key: 'pivot_table_v2' }).register(); + +export const basic = ({ width, height }) => ( + +); +basic.story = { + parameters: { + initialSize: { + width: 680, + height: 420, + }, + }, +}; + +export const MaximumAggregation = ({ width, height }) => ( + +); +basic.story = { + parameters: { + initialSize: { + width: 680, + height: 420, + }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-pivot-table/testData.ts b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-pivot-table/testData.ts new file mode 100644 index 0000000000000..0e6457d0c122e --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-pivot-table/testData.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. + */ +export const basicFormData = { + datasource: '1__table', + viz_type: 'pivot_table_v2', + granularity_sqla: 'ts', + groupbyColumns: ['location'], + groupbyRows: ['program_language'], + metrics: [ + { + expressionType: 'SIMPLE', + column: { + id: 1, + column_name: 'count', + description: null, + expression: null, + groupby: true, + is_dttm: false, + python_date_format: null, + type: 'BIGINT', + type_generic: 0, + }, + aggregate: 'SUM', + sqlExpression: null, + isNew: false, + hasCustomLabel: true, + label: 'Count', + }, + { + expressionType: 'SIMPLE', + column: { + id: 2, + column_name: 'ts', + description: null, + expression: "DATE_PARSE(ds || ' ' || hr, '%Y-%m-%d %H')", + groupby: true, + is_dttm: true, + type: 'TIMESTAMP', + type_generic: 2, + python_date_format: null, + }, + aggregate: 'MAX', + sqlExpression: null, + isNew: false, + hasCustomLabel: true, + label: 'Most Recent Data', + }, + ], + metricsLayout: 'COLUMNS', + order_desc: true, + aggregateFunction: 'Sum', + valueFormat: '~g', + date_format: 'smart_date', + rowOrder: 'key_a_to_z', + colOrder: 'key_a_to_z', +}; + +export const basicData = { + cache_key: 'f2cd2a37b6977e3619ce6c07d0027972', + cached_dttm: '2022-07-27T17:42:39', + cache_timeout: 129600, + applied_template_filters: [], + annotation_data: {}, + error: null, + is_cached: true, + query: 'SELECT \nFROM\nWHERE', + status: 'success', + stacktrace: null, + rowcount: 5, + from_dttm: 1658426268000, + to_dttm: 1659031068000, + colnames: ['location', 'program_language', 'Count', 'Most Recent Data'], + indexnames: [0, 1, 2, 3, 4], + coltypes: [1, 1, 0, 1], + data: [ + { + location: 'AMEA', + program_language: 'Javscript', + Count: 134, + 'Most Recent Data': '2022-07-25 13:00:00.000', + }, + { + location: 'ASIA', + program_language: 'python', + Count: 19, + 'Most Recent Data': '2022-07-25 16:00:00.000', + }, + { + location: 'ASIA', + program_language: 'Java', + Count: 7, + 'Most Recent Data': '2022-07-25 15:00:00.000', + }, + { + location: 'ASIA', + program_language: 'C++', + Count: 1, + 'Most Recent Data': '2022-07-25 02:00:00.000', + }, + { + location: 'ASIA', + program_language: 'PHP', + Count: 1, + 'Most Recent Data': '2022-07-24 00:00:00.000', + }, + ], + result_format: 'json', + applied_filters: [], + rejected_filters: [], +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts index faf6271302f86..07ca77547b4ff 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts @@ -125,8 +125,10 @@ export default function transformProps( if (compareIndex < sortedData.length) { const compareValue = sortedData[compareIndex][1]; // compare values must both be non-nulls - if (bigNumber !== null && compareValue !== null && compareValue !== 0) { - percentChange = (bigNumber - compareValue) / Math.abs(compareValue); + if (bigNumber !== null && compareValue !== null) { + percentChange = compareValue + ? (bigNumber - compareValue) / Math.abs(compareValue) + : 0; formattedSubheader = `${formatPercentChange( percentChange, )} ${compareSuffix}`; 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 5d30ee37e0a71..d5e8fcdee90af 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 @@ -328,6 +328,35 @@ const config: ControlPanelConfig = { row_limit: { default: rowLimit, }, + limit: { + rerender: ['timeseries_limit_metric', 'order_desc'], + }, + timeseries_limit_metric: { + label: t('Series Limit Sort By'), + description: t( + 'Metric used to order the limit if a series limit is present. ' + + 'If undefined reverts to the first metric (where appropriate).', + ), + visibility: ({ controls }) => Boolean(controls?.limit.value), + mapStateToProps: (state, controlState) => { + const timeserieslimitProps = + sharedControls.timeseries_limit_metric.mapStateToProps?.( + state, + controlState, + ) || {}; + timeserieslimitProps.value = state.controls?.limit?.value + ? controlState.value + : []; + return timeserieslimitProps; + }, + }, + order_desc: { + label: t('Series Limit Sort Descending'), + default: false, + description: t( + 'Whether to sort descending or ascending if a series limit is present', + ), + }, }, formDataOverrides: formData => ({ ...formData, 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 b6b23fc70141c..07f18c3d5e62f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -41,6 +41,7 @@ import { EchartsTimeseriesSeriesType, TimeseriesChartTransformedProps, OrientationType, + AxisType, } from './types'; import { DEFAULT_FORM_DATA } from './constants'; import { ForecastSeriesEnum, ForecastValue } from '../types'; @@ -337,13 +338,23 @@ export default function transformProps( rotate: xAxisLabelRotation, }, minInterval: - xAxisType === 'time' && timeGrainSqla + xAxisType === AxisType.time && timeGrainSqla ? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla] : 0, }; + + if (xAxisType === AxisType.time) { + /** + * Overriding default behavior (false) for time axis regardless of the granilarity. + * Not including this in the initial declaration above so if echarts changes the default + * behavior for other axist types we won't unintentionally override it + */ + xAxis.axisLabel.showMaxLabel = null; + } + let yAxis: any = { ...defaultYAxis, - type: logAxis ? 'log' : 'value', + type: logAxis ? AxisType.log : AxisType.value, min, max, minorTick: { show: true }, 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 946d41ec164d8..71729bd0f11d0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -94,3 +94,10 @@ export interface EchartsTimeseriesChartProps export type TimeseriesChartTransformedProps = EChartTransformedProps; + +export enum AxisType { + category = 'category', + value = 'value', + time = 'time', + log = 'log', +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index 7dd823f644792..ec956a9591764 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -31,7 +31,7 @@ import { export const NULL_STRING = ''; export const TIMESERIES_CONSTANTS = { - gridOffsetRight: 40, + gridOffsetRight: 20, gridOffsetLeft: 20, gridOffsetTop: 20, gridOffsetBottom: 20, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js index 9ae5888819777..9b47132936b4e 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js @@ -231,7 +231,7 @@ const baseAggregatorTemplates = { return { sum: 0, push(record) { - if (Number.isNaN(parseFloat(record[attr]))) { + if (Number.isNaN(Number(record[attr]))) { this.sum = record[attr]; } else { this.sum += parseFloat(record[attr]); @@ -259,7 +259,7 @@ const baseAggregatorTemplates = { push(record) { const x = record[attr]; if (['min', 'max'].includes(mode)) { - const coercedValue = parseFloat(x); + const coercedValue = Number(x); if (Number.isNaN(coercedValue)) { this.val = !this.val || @@ -308,7 +308,7 @@ const baseAggregatorTemplates = { strMap: {}, push(record) { const val = record[attr]; - const x = parseFloat(val); + const x = Number(val); if (Number.isNaN(x)) { this.strMap[val] = (this.strMap[val] || 0) + 1; @@ -354,7 +354,7 @@ const baseAggregatorTemplates = { s: 0.0, strValue: null, push(record) { - const x = parseFloat(record[attr]); + const x = Number(record[attr]); if (Number.isNaN(x)) { this.strValue = typeof record[attr] === 'string' ? record[attr] : this.strValue; @@ -405,10 +405,10 @@ const baseAggregatorTemplates = { sumNum: 0, sumDenom: 0, push(record) { - if (!Number.isNaN(parseFloat(record[num]))) { + if (!Number.isNaN(Number(record[num]))) { this.sumNum += parseFloat(record[num]); } - if (!Number.isNaN(parseFloat(record[denom]))) { + if (!Number.isNaN(Number(record[denom]))) { this.sumDenom += parseFloat(record[denom]); } }, diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index f56381bb96efd..8acc06199f66e 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import React, { CSSProperties, useCallback, useMemo, useState } from 'react'; +import React, { + CSSProperties, + useCallback, + useLayoutEffect, + useMemo, + useState, +} from 'react'; import { ColumnInstance, ColumnWithLooseAccessor, @@ -50,9 +56,15 @@ import Styles from './Styles'; import { formatColumnValue } from './utils/formatValue'; import { PAGE_SIZE_OPTIONS } from './consts'; import { updateExternalFormData } from './DataTable/utils/externalAPIs'; +import getScrollBarSize from './DataTable/utils/getScrollBarSize'; type ValueRange = [number, number]; +interface TableSize { + width: number; + height: number; +} + /** * Return sortType based on data type */ @@ -198,7 +210,10 @@ export default function TableChart( value => getTimeFormatterForGranularity(timeGrain)(value), [timeGrain], ); - + const [tableSize, setTableSize] = useState({ + width: 0, + height: 0, + }); // keep track of whether column order changed, so that column widths can too const [columnOrderToggle, setColumnOrderToggle] = useState(false); @@ -526,6 +541,41 @@ export default function TableChart( [setDataMask], ); + const handleSizeChange = useCallback( + ({ width, height }: { width: number; height: number }) => { + setTableSize({ width, height }); + }, + [], + ); + + useLayoutEffect(() => { + // After initial load the table should resize only when the new sizes + // Are not only scrollbar updates, otherwise, the table would twicth + const scrollBarSize = getScrollBarSize(); + const { width: tableWidth, height: tableHeight } = tableSize; + // Table is increasing its original size + if ( + width - tableWidth > scrollBarSize || + height - tableHeight > scrollBarSize + ) { + handleSizeChange({ + width: width - scrollBarSize, + height: height - scrollBarSize, + }); + } else if ( + tableWidth - width > scrollBarSize || + tableHeight - height > scrollBarSize + ) { + // Table is decreasing its original size + handleSizeChange({ + width, + height, + }); + } + }, [width, height, handleSizeChange, tableSize]); + + const { width: widthFromState, height: heightFromState } = tableSize; + return ( @@ -536,8 +586,8 @@ export default function TableChart( pageSize={pageSize} serverPaginationData={serverPaginationData} pageSizeOptions={pageSizeOptions} - width={width} - height={height} + width={widthFromState} + height={heightFromState} serverPagination={serverPagination} onServerPaginationChange={handleServerPaginationChange} onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)} diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index 6a43821b5cbcc..1435e0d796e21 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -112,7 +112,7 @@ const queryClientMapping = { id: 'remoteId', db_id: 'dbId', client_id: 'id', - label: 'title', + label: 'name', }; const queryServerMapping = invert(queryClientMapping); @@ -541,7 +541,7 @@ export function cloneQueryToNewTab(query, autorun) { qe => qe.id === tabHistory[tabHistory.length - 1], ); const queryEditor = { - title: t('Copy of %s', sourceQueryEditor.title), + name: t('Copy of %s', sourceQueryEditor.name), dbId: query.dbId ? query.dbId : null, schema: query.schema ? query.schema : null, autorun, @@ -629,7 +629,7 @@ export function switchQueryEditor(queryEditor, displayLimit) { const loadedQueryEditor = { id: json.id.toString(), loaded: true, - title: json.label, + name: json.label, sql: json.sql, selectedText: null, latestQueryId: json.latest_query?.id, @@ -834,24 +834,22 @@ export function queryEditorSetAutorun(queryEditor, autorun) { }; } -export function queryEditorSetTitle(queryEditor, title) { +export function queryEditorSetTitle(queryEditor, name) { return function (dispatch) { const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) ? SupersetClient.put({ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`), - postPayload: { label: title }, + postPayload: { label: name }, }) : Promise.resolve(); return sync - .then(() => - dispatch({ type: QUERY_EDITOR_SET_TITLE, queryEditor, title }), - ) + .then(() => dispatch({ type: QUERY_EDITOR_SET_TITLE, queryEditor, name })) .catch(() => dispatch( addDangerToast( t( - 'An error occurred while setting the tab title. Please contact your administrator.', + 'An error occurred while setting the tab name. Please contact your administrator.', ), ), ), @@ -873,7 +871,7 @@ export function saveQuery(query) { query, result: savedQuery, }); - dispatch(queryEditorSetTitle(query, query.title)); + dispatch(queryEditorSetTitle(query, query.name)); return savedQuery; }) .catch(() => @@ -908,7 +906,7 @@ export function updateSavedQuery(query) { }) .then(() => { dispatch(addSuccessToast(t('Your query was updated'))); - dispatch(queryEditorSetTitle(query, query.title)); + dispatch(queryEditorSetTitle(query, query.name)); }) .catch(() => dispatch(addDangerToast(t('Your query could not be updated'))), @@ -965,7 +963,7 @@ export function queryEditorSetQueryLimit(queryEditor, queryLimit) { dispatch( addDangerToast( t( - 'An error occurred while setting the tab title. Please contact your administrator.', + 'An error occurred while setting the tab name. Please contact your administrator.', ), ), ), @@ -1264,7 +1262,7 @@ export function popStoredQuery(urlId) { .then(({ json }) => dispatch( addQueryEditor({ - title: json.title ? json.title : t('Shared query'), + name: json.name ? json.name : t('Shared query'), dbId: json.dbId ? parseInt(json.dbId, 10) : null, schema: json.schema ? json.schema : null, autorun: json.autorun ? json.autorun : false, @@ -1302,7 +1300,7 @@ export function popQuery(queryId) { dbId: queryData.database.id, schema: queryData.schema, sql: queryData.sql, - title: `Copy of ${queryData.tab_name}`, + name: `Copy of ${queryData.tab_name}`, autorun: false, }; return dispatch(addQueryEditor(queryEditorProps)); @@ -1318,7 +1316,7 @@ export function popDatasourceQuery(datasourceKey, sql) { .then(({ json }) => dispatch( addQueryEditor({ - title: `Query ${json.name}`, + name: `Query ${json.name}`, dbId: json.database.id, schema: json.schema, autorun: sql !== undefined, diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index f9e972d2a453e..1c4509b3a1ad4 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -38,8 +38,8 @@ describe('async actions', () => { latestQueryId: null, selectedText: null, sql: 'SELECT *\nFROM\nWHERE', - title: 'Untitled Query 1', - schemaOptions: [{ value: 'main', label: 'main', title: 'main' }], + name: 'Untitled Query 1', + schemaOptions: [{ value: 'main', label: 'main', name: 'main' }], }; let dispatch; @@ -290,7 +290,7 @@ describe('async actions', () => { const state = { sqlLab: { tabHistory: [id], - queryEditors: [{ id, title: 'Dummy query editor' }], + queryEditors: [{ id, name: 'Dummy query editor' }], }, }; const store = mockStore(state); @@ -350,7 +350,7 @@ describe('async actions', () => { const state = { sqlLab: { tabHistory: [id], - queryEditors: [{ id, title: 'Dummy query editor' }], + queryEditors: [{ id, name: 'Dummy query editor' }], }, }; const store = mockStore(state); @@ -358,7 +358,7 @@ describe('async actions', () => { { type: actions.ADD_QUERY_EDITOR, queryEditor: { - title: 'Copy of Dummy query editor', + name: 'Copy of Dummy query editor', dbId: 1, schema: null, autorun: true, @@ -617,17 +617,17 @@ describe('async actions', () => { it('updates the tab state in the backend', () => { expect.assertions(2); - const title = 'title'; + const name = 'name'; const store = mockStore({}); const expectedActions = [ { type: actions.QUERY_EDITOR_SET_TITLE, queryEditor, - title, + name, }, ]; return store - .dispatch(actions.queryEditorSetTitle(queryEditor, title)) + .dispatch(actions.queryEditorSetTitle(queryEditor, name)) .then(() => { expect(store.getActions()).toEqual(expectedActions); expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1); diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index f68fe224da1f8..9e1df28b4b28f 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { CSSProperties } from 'react'; +import React from 'react'; import ButtonGroup from 'src/components/ButtonGroup'; import Alert from 'src/components/Alert'; import Button from 'src/components/Button'; @@ -54,8 +54,6 @@ enum LIMITING_FACTOR { NOT_LIMITED = 'NOT_LIMITED', } -const LOADING_STYLES: CSSProperties = { position: 'relative', minHeight: 100 }; - interface ResultSetProps { showControls?: boolean; actions: Record; @@ -80,6 +78,17 @@ interface ResultSetState { alertIsOpen: boolean; } +const ResultlessStyles = styled.div` + position: relative; + minheight: 100px; + [role='alert'] { + margin-top: ${({ theme }) => theme.gridUnit * 2}px; + } + .sql-result-track-job { + margin-top: ${({ theme }) => theme.gridUnit * 2}px; + } +`; + // Making text render line breaks/tabs as is as monospace, // but wrapping text too so text doesn't overflow const MonospaceDiv = styled.div` @@ -107,13 +116,6 @@ const ResultSetButtons = styled.div` padding-right: ${({ theme }) => 2 * theme.gridUnit}px; `; -const ResultSetErrorMessage = styled.div` - padding-top: ${({ theme }) => 4 * theme.gridUnit}px; - .sql-result-track-job { - margin-top: ${({ theme }) => 2 * theme.gridUnit}px; - } -`; - export default class ResultSet extends React.PureComponent< ResultSetProps, ResultSetState @@ -185,7 +187,7 @@ export default class ResultSet extends React.PureComponent< popSelectStar(tempSchema: string | null, tempTable: string) { const qe = { id: shortid.generate(), - title: tempTable, + name: tempTable, autorun: false, dbId: this.props.query.dbId, sql: `SELECT * FROM ${tempSchema ? `${tempSchema}.` : ''}${tempTable}`, @@ -278,11 +280,8 @@ export default class ResultSet extends React.PureComponent< this.props.database?.allows_virtual_table_explore && ( this.setState({ showSaveDatasetModal: true })} + onClick={this.createExploreResultsOnClick} /> - // In order to use the new workflow for a query powered chart, replace the - // above function with: - // onClick={this.createExploreResultsOnClick} )} {this.props.csv && ( + ) : ( + setShowSave(true)} + overlay={overlayMenu} + icon={ + + } + trigger={['click']} + > + {t('Save')} + + ); +} diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx index b7f3ad8a8a42d..7e838a44e3e26 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx @@ -17,35 +17,18 @@ * under the License. */ import React from 'react'; +import * as reactRedux from 'react-redux'; import { - ISaveableDatasource, - SaveDatasetModal, -} from 'src/SqlLab/components/SaveDatasetModal'; -import { render, screen } from 'spec/helpers/testing-library'; -import { DatasourceType } from '@superset-ui/core'; - -const testQuery: ISaveableDatasource = { - name: 'unimportant', - dbId: 1, - sql: 'SELECT *', - columns: [ - { - name: 'Column 1', - type: DatasourceType.Query, - is_dttm: false, - }, - { - name: 'Column 3', - type: DatasourceType.Query, - is_dttm: false, - }, - { - name: 'Column 2', - type: DatasourceType.Query, - is_dttm: true, - }, - ], -}; + render, + screen, + waitFor, + within, + cleanup, +} from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'fetch-mock'; +import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; +import { user, testQuery, mockdatasets } from 'src/SqlLab/fixtures'; const mockedProps = { visible: true, @@ -55,8 +38,20 @@ const mockedProps = { datasource: testQuery, }; -describe('SaveDatasetModal RTL', () => { - it('renders a "Save as new" field', () => { +fetchMock.get('glob:*/api/v1/dataset?*', { + result: mockdatasets, + dataset_count: 3, +}); + +// Mock the user +const useSelectorMock = jest.spyOn(reactRedux, 'useSelector'); +beforeEach(() => { + useSelectorMock.mockClear(); + cleanup(); +}); + +describe('SaveDatasetModal', () => { + it('renders a "Save as new" field', async () => { render(, { useRedux: true }); const saveRadioBtn = screen.getByRole('radio', { @@ -73,7 +68,7 @@ describe('SaveDatasetModal RTL', () => { expect(inputFieldText).toBeVisible(); }); - it('renders an "Overwrite existing" field', () => { + it('renders an "Overwrite existing" field', async () => { render(, { useRedux: true }); const overwriteRadioBtn = screen.getByRole('radio', { @@ -89,15 +84,65 @@ describe('SaveDatasetModal RTL', () => { expect(placeholderText).toBeVisible(); }); - it('renders a save button', () => { + it('renders a close button', async () => { render(, { useRedux: true }); + expect(screen.getByRole('button', { name: /close/i })).toBeVisible(); + }); + + it('renders a save button when "Save as new" is selected', async () => { + render(, { useRedux: true }); + + // "Save as new" is selected when the modal opens by default expect(screen.getByRole('button', { name: /save/i })).toBeVisible(); }); - it('renders a close button', () => { + it('renders an overwrite button when "Overwrite existing" is selected', async () => { render(, { useRedux: true }); - expect(screen.getByRole('button', { name: /close/i })).toBeVisible(); + // Click the overwrite radio button to reveal the overwrite confirmation and back buttons + const overwriteRadioBtn = screen.getByRole('radio', { + name: /overwrite existing/i, + }); + userEvent.click(overwriteRadioBtn); + + expect(screen.getByRole('button', { name: /overwrite/i })).toBeVisible(); + }); + + it('renders the overwrite button as disabled until an existing dataset is selected, confirms overwrite', async () => { + useSelectorMock.mockReturnValue({ ...user }); + render(, { useRedux: true }); + + // Click the overwrite radio button + const overwriteRadioBtn = screen.getByRole('radio', { + name: /overwrite existing/i, + }); + await waitFor(async () => { + userEvent.click(overwriteRadioBtn); + }); + + // Overwrite confirmation button should be disabled at this point + const overwriteConfirmationBtn = screen.getByRole('button', { + name: /overwrite/i, + }); + expect(overwriteConfirmationBtn).toBeDisabled(); + + // Click the select component + const select = screen.getByRole('combobox', { name: /existing dataset/i })!; + await waitFor(async () => userEvent.click(select)); + + // Select the first "existing dataset" from the listbox + const option = within( + document.querySelector('.rc-virtual-list')!, + ).getByText('coolest table 0')!; + userEvent.click(option); + + // Overwrite button should now be enabled + expect(overwriteConfirmationBtn).toBeEnabled(); + + // Check Overwrite confirmation functionality + userEvent.click(overwriteConfirmationBtn); + expect(screen.getByRole('button', { name: /back/i })).toBeVisible(); + expect(screen.getByRole('button', { name: /overwrite/i })).toBeVisible(); }); }); diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx index 1189740d63fd0..6d6acf8af9787 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx @@ -186,6 +186,11 @@ export const SaveDatasetModal: FunctionComponent = ({ ...(formData || {}), }; const handleOverwriteDataset = async () => { + // if user wants to overwrite a dataset we need to prompt them + if (!shouldOverwriteDataset) { + setShouldOverwriteDataset(true); + return; + } const [, key] = await Promise.all([ updateDataset( datasource?.dbId, @@ -217,6 +222,7 @@ export const SaveDatasetModal: FunctionComponent = ({ setShouldOverwriteDataset(false); setDatasetName(getDefaultDatasetName()); + onHide(); }; const loadDatasetOverwriteOptions = useCallback( @@ -257,12 +263,6 @@ export const SaveDatasetModal: FunctionComponent = ({ ); const handleSaveInDataset = () => { - // if user wants to overwrite a dataset we need to prompt them - if (newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET) { - setShouldOverwriteDataset(true); - return; - } - const selectedColumns = datasource?.columns ?? []; // The filters param is only used to test jinja templates. @@ -346,7 +346,7 @@ export const SaveDatasetModal: FunctionComponent = ({ onHide={onHide} footer={ <> - {!shouldOverwriteDataset && ( + {newOrOverwrite === DatasetRadioState.SAVE_NEW && ( )} - {shouldOverwriteDataset && ( + {newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET && ( <> - + {shouldOverwriteDataset && ( + + )} + + setShowSaveDatasetModal(false)} + buttonTextOnSave={t('Save & Explore')} + buttonTextOnOverwrite={t('Overwrite & Explore')} + datasource={getDatasourceAsSaveableDataset(query)} + /> ( const defaultProps = { queryEditor: { dbId: 0, - title: 'query title', + name: 'query title', schema: 'query_schema', autorun: false, sql: 'SELECT * FROM ...', diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx index 5f473c642fd31..c521cb5dc75d9 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx @@ -49,8 +49,8 @@ function ShareSqlLabQuery({ const theme = useTheme(); const getCopyUrlForKvStore = (callback: Function) => { - const { dbId, title, schema, autorun, sql } = queryEditor; - const sharedQuery = { dbId, title, schema, autorun, sql }; + const { dbId, name, schema, autorun, sql } = queryEditor; + const sharedQuery = { dbId, name, schema, autorun, sql }; return storeQuery(sharedQuery) .then(shortUrl => { diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index d40ca65f2f665..bdda237158ae7 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -345,10 +345,10 @@ class SqlEditor extends React.PureComponent { key: userOS === 'Windows' ? 'ctrl+q' : 'ctrl+t', descr: t('New tab'), func: () => { - const title = newQueryTabName(this.props.queryEditors || []); + const name = newQueryTabName(this.props.queryEditors || []); this.props.addQueryEditor({ ...this.props.queryEditor, - title, + name, }); }, }, @@ -463,7 +463,7 @@ class SqlEditor extends React.PureComponent { dbId: qe.dbId, sql: qe.selectedText ? qe.selectedText : qe.sql, sqlEditorId: qe.id, - tab: qe.title, + tab: qe.name, schema: qe.schema, tempTable: ctas ? this.state.ctas : '', templateParams: qe.templateParams, @@ -584,7 +584,7 @@ class SqlEditor extends React.PureComponent { {scheduledQueriesConf && ( diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx index 9f8e5bcf1ae25..5de36bc99a455 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx @@ -55,7 +55,7 @@ describe('TabbedSqlEditors', () => { schema: null, selectedText: null, sql: 'SELECT ds...', - title: 'Untitled Query', + name: 'Untitled Query', }, ]; const queries = { @@ -177,7 +177,7 @@ describe('TabbedSqlEditors', () => { wrapper.instance().newQueryEditor(); expect( - wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].title, + wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name, ).toContain('Untitled Query'); }); it('should properly increment query tab name', () => { @@ -186,7 +186,7 @@ describe('TabbedSqlEditors', () => { wrapper.instance().newQueryEditor(); expect( - wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].title, + wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name, ).toContain('Untitled Query 2'); }); it('should duplicate query editor', () => { diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx index 494ef9cba0ef7..c97080c6e95a5 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx @@ -167,7 +167,7 @@ class TabbedSqlEditors extends React.PureComponent { } } const newQueryEditor = { - title: query.title, + name: query.name, dbId, schema: query.schema, autorun: query.autorun, @@ -266,7 +266,7 @@ class TabbedSqlEditors extends React.PureComponent { const newTitle = newQueryTabName(this.props.queryEditors || []); const qe = { - title: newTitle, + name: newTitle, dbId: activeQueryEditor && activeQueryEditor.dbId ? activeQueryEditor.dbId @@ -376,7 +376,7 @@ class TabbedSqlEditors extends React.PureComponent { const tabHeader = ( - {qe.title} {' '} + {qe.name} {' '} ); return ( diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index 58b21edd9cf73..ea0fbd1bb8d1e 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -20,6 +20,7 @@ import sinon from 'sinon'; import * as actions from 'src/SqlLab/actions/sqlLab'; import { ColumnKeyTypeType } from 'src/SqlLab/components/ColumnElement'; import { DatasourceType, QueryResponse, QueryState } from '@superset-ui/core'; +import { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal'; export const mockedActions = sinon.stub({ ...actions }); @@ -181,12 +182,12 @@ export const defaultQueryEditor = { latestQueryId: null, selectedText: null, sql: 'SELECT *\nFROM\nWHERE', - title: 'Untitled Query 1', + name: 'Untitled Query 1', schemaOptions: [ { value: 'main', label: 'main', - title: 'main', + name: 'main', }, ], }; @@ -670,3 +671,40 @@ export const query = { ctas: false, cached: false, }; + +export const testQuery: ISaveableDatasource = { + name: 'unimportant', + dbId: 1, + sql: 'SELECT *', + columns: [ + { + name: 'Column 1', + type: DatasourceType.Query, + is_dttm: false, + }, + { + name: 'Column 3', + type: DatasourceType.Query, + is_dttm: false, + }, + { + name: 'Column 2', + type: DatasourceType.Query, + is_dttm: true, + }, + ], +}; + +export const mockdatasets = [...new Array(3)].map((_, i) => ({ + changed_by_name: 'user', + kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual + changed_by_url: 'changed_by_url', + changed_by: 'user', + changed_on: new Date().toISOString(), + database_name: `db ${i}`, + explore_url: `/explore/?dataset_type=table&dataset_id=${i}`, + id: i, + schema: `schema ${i}`, + table_name: `coolest table ${i}`, + owners: [{ username: 'admin', userId: 1 }], +})); diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.js b/superset-frontend/src/SqlLab/reducers/getInitialState.js index d5f02029d0ccf..21c367844d7fa 100644 --- a/superset-frontend/src/SqlLab/reducers/getInitialState.js +++ b/superset-frontend/src/SqlLab/reducers/getInitialState.js @@ -41,7 +41,7 @@ export default function getInitialState({ const defaultQueryEditor = { id: null, loaded: true, - title: t('Untitled query'), + name: t('Untitled query'), sql: 'SELECT *\nFROM\nWHERE', selectedText: null, latestQueryId: null, @@ -73,7 +73,7 @@ export default function getInitialState({ queryEditor = { id: id.toString(), loaded: true, - title: activeTab.label, + name: activeTab.label, sql: activeTab.sql || undefined, selectedText: undefined, latestQueryId: activeTab.latest_query @@ -99,7 +99,7 @@ export default function getInitialState({ ...defaultQueryEditor, id: id.toString(), loaded: false, - title: label, + name: label, }; } queryEditors.push(queryEditor); diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index a9cef50870540..f87c8ce9c1043 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -48,7 +48,7 @@ export default function sqlLabReducer(state = {}, action) { existing, { remoteId: result.remoteId, - title: query.title, + name: query.name, }, 'id', ); @@ -71,7 +71,7 @@ export default function sqlLabReducer(state = {}, action) { ); const qe = { remoteId: progenitor.remoteId, - title: t('Copy of %s', progenitor.title), + name: t('Copy of %s', progenitor.name), dbId: action.query.dbId ? action.query.dbId : null, schema: action.query.schema ? action.query.schema : null, autorun: true, @@ -467,7 +467,7 @@ export default function sqlLabReducer(state = {}, action) { }, [actions.QUERY_EDITOR_SET_TITLE]() { return alterInArr(state, 'queryEditors', action.queryEditor, { - title: action.title, + name: action.name, }); }, [actions.QUERY_EDITOR_SET_SQL]() { diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js index c986fc5ac06f1..4c92e1324740f 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js @@ -96,14 +96,14 @@ describe('sqlLabReducer', () => { expect(newState.queryEditors[1].autorun).toBe(true); }); it('should not fail while setting title', () => { - const title = 'a new title'; + const title = 'Untitled Query 1'; const action = { type: actions.QUERY_EDITOR_SET_TITLE, queryEditor: qe, title, }; newState = sqlLabReducer(newState, action); - expect(newState.queryEditors[1].title).toBe(title); + expect(newState.queryEditors[0].name).toBe(title); }); it('should not fail while setting Sql', () => { const sql = 'SELECT nothing from dev_null'; diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts index 0d54f97476676..8bb3e73d28870 100644 --- a/superset-frontend/src/SqlLab/types.ts +++ b/superset-frontend/src/SqlLab/types.ts @@ -21,6 +21,10 @@ import { SupersetError } from 'src/components/ErrorMessage/types'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { ToastType } from 'src/components/MessageToasts/types'; import { RootState } from 'src/dashboard/types'; +import { DropdownButtonProps } from 'src/components/DropdownButton'; +import { ButtonProps } from 'src/components/Button'; + +export type QueryButtonProps = DropdownButtonProps | ButtonProps; // Object as Dictionary (associative array) with Query id as the key and type Query as the value export type QueryDictionary = { @@ -29,7 +33,7 @@ export type QueryDictionary = { export interface QueryEditor { dbId?: number; - title: string; + name: string; schema: string; autorun: boolean; sql: string; diff --git a/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts b/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts index d0d98c3cd5e29..6f2af5ebb7376 100644 --- a/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts +++ b/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts @@ -36,8 +36,8 @@ describe('newQueryTabName', () => { it('should return next available number if there are unsaved editors', () => { const untitledQueryText = 'Untitled Query'; const unsavedEditors = [ - { ...emptyEditor, title: `${untitledQueryText} 1` }, - { ...emptyEditor, title: `${untitledQueryText} 2` }, + { ...emptyEditor, name: `${untitledQueryText} 1` }, + { ...emptyEditor, name: `${untitledQueryText} 2` }, ]; const nextTitle = newQueryTabName(unsavedEditors); diff --git a/superset-frontend/src/SqlLab/utils/newQueryTabName.ts b/superset-frontend/src/SqlLab/utils/newQueryTabName.ts index 3815226cd4ba5..c068fdd3d5cbb 100644 --- a/superset-frontend/src/SqlLab/utils/newQueryTabName.ts +++ b/superset-frontend/src/SqlLab/utils/newQueryTabName.ts @@ -31,10 +31,10 @@ export const newQueryTabName = ( if (queryEditors.length > 0) { const mappedUntitled = queryEditors.filter(qe => - qe.title.match(untitledQueryRegex), + qe.name.match(untitledQueryRegex), ); const untitledQueryNumbers = mappedUntitled.map( - qe => +qe.title.replace(untitledQuery, ''), + qe => +qe.name.replace(untitledQuery, ''), ); if (untitledQueryNumbers.length > 0) { // When there are query tabs open, and at least one is called "Untitled Query #" diff --git a/superset-frontend/src/components/Chart/Chart.jsx b/superset-frontend/src/components/Chart/Chart.jsx index 7d02bf3452e0d..893665ab15c7a 100644 --- a/superset-frontend/src/components/Chart/Chart.jsx +++ b/superset-frontend/src/components/Chart/Chart.jsx @@ -236,7 +236,6 @@ class Chart extends React.PureComponent { link={queryResponse ? queryResponse.link : null} source={dashboardId ? 'dashboard' : 'explore'} stackTrace={chartStackTrace} - errorMitigationFunction={this.toggleSaveDatasetModal} /> ); } diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index e972e95b002f1..1df7f78a3bea9 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -302,7 +302,6 @@ export default function DatabaseSelector({ disabled={!currentDb || readOnly} header={{t('Schema')}} labelInValue - lazyLoading={false} loading={loadingSchemas} name="select-schema" placeholder={t('Select schema or type schema name')} diff --git a/superset-frontend/src/components/Datasource/CollectionTable.tsx b/superset-frontend/src/components/Datasource/CollectionTable.tsx index 194d3765792c9..e0eb44e8453d2 100644 --- a/superset-frontend/src/components/Datasource/CollectionTable.tsx +++ b/superset-frontend/src/components/Datasource/CollectionTable.tsx @@ -33,6 +33,14 @@ interface CRUDCollectionProps { expandFieldset?: ReactNode; extraButtons?: ReactNode; itemGenerator?: () => any; + itemCellProps?: (( + val: unknown, + label: string, + record: any, + ) => React.DetailedHTMLProps< + React.TdHTMLAttributes, + HTMLTableCellElement + >)[]; itemRenderers?: (( val: unknown, onChange: () => void, @@ -335,6 +343,12 @@ export default class CRUDCollection extends React.PureComponent< ); } + getCellProps(record: any, col: any) { + const cellPropsFn = this.props.itemCellProps?.[col]; + const val = record[col]; + return cellPropsFn ? cellPropsFn(val, this.getLabel(col), record) : {}; + } + renderCell(record: any, col: any) { const renderer = this.props.itemRenderers && this.props.itemRenderers[col]; const val = record[col]; @@ -366,7 +380,9 @@ export default class CRUDCollection extends React.PureComponent< } tds = tds.concat( tableColumns.map(col => ( - {this.renderCell(record, col)} + + {this.renderCell(record, col)} + )), ); if (allowAddItem) { diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx index a64abaf0dc80a..bf524c22101f5 100644 --- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx @@ -235,6 +235,7 @@ function ColumnCollectionTable({ } /> @@ -848,7 +849,11 @@ class DatasourceEditor extends React.PureComponent { fieldKey="description" label={t('Description')} control={ - + } /> } /> @@ -901,6 +907,7 @@ class DatasourceEditor extends React.PureComponent { controlId="extra" language="json" offerEditInModal={false} + resize="vertical" /> } /> @@ -1081,6 +1088,7 @@ class DatasourceEditor extends React.PureComponent { minLines={20} maxLines={20} readOnly={!this.state.isEditMode} + resize="both" /> } /> @@ -1233,6 +1241,7 @@ class DatasourceEditor extends React.PureComponent { controlId="warning_markdown" language="markdown" offerEditInModal={false} + resize="vertical" /> } /> @@ -1247,6 +1256,11 @@ class DatasourceEditor extends React.PureComponent { verbose_name: '', expression: '', })} + itemCellProps={{ + expression: () => ({ + width: '240px', + }), + }} itemRenderers={{ metric_name: (v, onChange, _, record) => ( @@ -1276,6 +1290,8 @@ class DatasourceEditor extends React.PureComponent { language="sql" offerEditInModal={false} minLines={5} + textAreaStyles={{ minWidth: '200px', maxWidth: '450px' }} + resize="both" /> ), description: (v, onChange, label) => ( diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx index f2a223a49fd8d..c6293f66a3fbd 100644 --- a/superset-frontend/src/components/DropdownButton/index.tsx +++ b/superset-frontend/src/components/DropdownButton/index.tsx @@ -52,10 +52,9 @@ const StyledDropdownButton = styled.div` border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light5}; content: ''; display: block; - height: 23px; + height: ${({ theme }) => theme.gridUnit * 8}px; margin: 0; position: absolute; - top: ${({ theme }) => theme.gridUnit * 0.75}px; width: ${({ theme }) => theme.gridUnit * 0.25}px; } diff --git a/superset-frontend/src/components/GenericLink/GenericLink.test.tsx b/superset-frontend/src/components/GenericLink/GenericLink.test.tsx new file mode 100644 index 0000000000000..c8f2ba5f5f41e --- /dev/null +++ b/superset-frontend/src/components/GenericLink/GenericLink.test.tsx @@ -0,0 +1,59 @@ +/** + * 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 { GenericLink } from './GenericLink'; + +test('renders', () => { + render(Link to Explore, { + useRouter: true, + }); + expect(screen.getByText('Link to Explore')).toBeVisible(); +}); + +test('navigates to internal URL', () => { + render(Link to Explore, { + useRouter: true, + }); + const internalLink = screen.getByTestId('internal-link'); + expect(internalLink).toHaveAttribute('href', '/explore'); +}); + +test('navigates to external URL', () => { + render( + + Link to external website + , + { useRouter: true }, + ); + const externalLink = screen.getByTestId('external-link'); + expect(externalLink).toHaveAttribute('href', 'https://superset.apache.org/'); +}); + +test('navigates to external URL without host', () => { + render( + + Link to external website + , + { useRouter: true }, + ); + const externalLink = screen.getByTestId('external-link'); + expect(externalLink).toHaveAttribute('href', '//superset.apache.org/'); +}); diff --git a/superset-frontend/src/components/GenericLink/GenericLink.tsx b/superset-frontend/src/components/GenericLink/GenericLink.tsx new file mode 100644 index 0000000000000..2bc111d1b60f4 --- /dev/null +++ b/superset-frontend/src/components/GenericLink/GenericLink.tsx @@ -0,0 +1,52 @@ +/** + * 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 { Link, LinkProps } from 'react-router-dom'; +import { isUrlExternal, parseUrl } from 'src/utils/urlUtils'; + +export const GenericLink = ({ + to, + component, + replace, + innerRef, + children, + ...rest +}: React.PropsWithoutRef> & + React.RefAttributes) => { + if (typeof to === 'string' && isUrlExternal(to)) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +}; diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx b/superset-frontend/src/components/ListView/Filters/Select.tsx index 525061fd27411..ecda25a81ff63 100644 --- a/superset-frontend/src/components/ListView/Filters/Select.tsx +++ b/superset-frontend/src/components/ListView/Filters/Select.tsx @@ -26,6 +26,7 @@ import { t } from '@superset-ui/core'; import { Select } from 'src/components'; import { Filter, SelectOption } from 'src/components/ListView/types'; import { FormLabel } from 'src/components/Form'; +import AsyncSelect from 'src/components/Select/AsyncSelect'; import { FilterContainer, BaseFilter, FilterHandler } from './Base'; interface SelectFilterProps extends BaseFilter { @@ -86,19 +87,34 @@ function SelectFilter( return ( - {Header}} + labelInValue + onChange={onChange} + onClear={onClear} + options={selects} + placeholder={t('Select or type a value')} + showSearch + value={selectedOption} + /> + )} ); } diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx index dc6eff35d9426..8a50002c866f8 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx @@ -60,10 +60,16 @@ const loadOptions = async (search: string, page: number, pageSize: number) => { const start = page * pageSize; const deleteCount = start + pageSize < totalCount ? pageSize : totalCount - start; - const data = OPTIONS.filter(option => option.label.match(search)).splice( - start, - deleteCount, - ); + const searchValue = search.trim().toLowerCase(); + const optionFilterProps = ['label', 'value', 'gender']; + const data = OPTIONS.filter(option => + optionFilterProps.some(prop => { + const optionProp = option?.[prop] + ? String(option[prop]).trim().toLowerCase() + : ''; + return optionProp.includes(searchValue); + }), + ).splice(start, deleteCount); return { data, totalCount: OPTIONS.length, @@ -74,7 +80,7 @@ const defaultProps = { allowClear: true, ariaLabel: ARIA_LABEL, labelInValue: true, - options: OPTIONS, + options: loadOptions, pageSize: 10, showSearch: true, }; @@ -129,17 +135,31 @@ test('displays a header', async () => { expect(screen.getByText(headerText)).toBeInTheDocument(); }); -test('adds a new option if the value is not in the options', async () => { - const { rerender } = render( - , +test('adds a new option if the value is not in the options, when options are empty', async () => { + const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 })); + render( + , ); await open(); expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); + options.forEach((option, i) => + expect(option).toHaveTextContent(OPTIONS[i].label), + ); +}); - rerender( - , +test('adds a new option if the value is not in the options, when options have values', async () => { + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[1]], + totalCount: 1, + })); + render( + , ); await open(); + expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options).toHaveLength(2); options.forEach((option, i) => @@ -147,6 +167,20 @@ test('adds a new option if the value is not in the options', async () => { ); }); +test('does not add a new option if the value is already in the options', async () => { + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[0]], + totalCount: 1, + })); + render( + , + ); + await open(); + expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); +}); + test('inverts the selection', async () => { render(); await open(); @@ -155,8 +189,11 @@ test('inverts the selection', async () => { }); test('sort the options by label if no sort comparator is provided', async () => { - const unsortedOptions = [...OPTIONS].sort(() => Math.random()); - render(); + const loadUnsortedOptions = jest.fn(async () => ({ + data: [...OPTIONS].sort(() => Math.random()), + totalCount: 2, + })); + render(); await open(); const options = await findAllSelectOptions(); options.forEach((option, key) => @@ -250,20 +287,23 @@ test('searches for label or value', async () => { render(); const search = option.value; await type(search.toString()); + expect(await findSelectOption(option.label)).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options.length).toBe(1); expect(options[0]).toHaveTextContent(option.label); }); test('search order exact and startWith match first', async () => { - render(); + render(); + await open(); await type('Her'); + expect(await findSelectOption('Guilherme')).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options.length).toBe(4); - expect(options[0]?.textContent).toEqual('Her'); - expect(options[1]?.textContent).toEqual('Herme'); - expect(options[2]?.textContent).toEqual('Cher'); - expect(options[3]?.textContent).toEqual('Guilherme'); + expect(options[0]).toHaveTextContent('Her'); + expect(options[1]).toHaveTextContent('Herme'); + expect(options[2]).toHaveTextContent('Cher'); + expect(options[3]).toHaveTextContent('Guilherme'); }); test('ignores case when searching', async () => { @@ -273,17 +313,16 @@ test('ignores case when searching', async () => { }); test('same case should be ranked to the top', async () => { - render( - , - ); + const loadOptions = jest.fn(async () => ({ + data: [ + { value: 'Cac' }, + { value: 'abac' }, + { value: 'acbc' }, + { value: 'CAc' }, + ], + totalCount: 4, + })); + render(); await type('Ac'); const options = await findAllSelectOptions(); expect(options.length).toBe(4); @@ -294,7 +333,7 @@ test('same case should be ranked to the top', async () => { }); test('ignores special keys when searching', async () => { - render(); + render(); await type('{shift}'); expect(screen.queryByText(LOADING)).not.toBeInTheDocument(); }); @@ -303,11 +342,16 @@ test('searches for custom fields', async () => { render( , ); + await open(); await type('Liam'); + // Liam is on the second page. need to wait to fetch options + expect(await findSelectOption('Liam')).toBeInTheDocument(); let options = await findAllSelectOptions(); expect(options.length).toBe(1); expect(options[0]).toHaveTextContent('Liam'); await type('Female'); + // Olivia is on the second page. need to wait to fetch options + expect(await findSelectOption('Olivia')).toBeInTheDocument(); options = await findAllSelectOptions(); expect(options.length).toBe(6); expect(options[0]).toHaveTextContent('Ava'); @@ -317,7 +361,7 @@ test('searches for custom fields', async () => { expect(options[4]).toHaveTextContent('Nikole'); expect(options[5]).toHaveTextContent('Olivia'); await type('1'); - expect(screen.getByText(NO_DATA)).toBeInTheDocument(); + expect(await screen.findByText(NO_DATA)).toBeInTheDocument(); }); test('removes duplicated values', async () => { @@ -332,12 +376,15 @@ test('removes duplicated values', async () => { }); test('renders a custom label', async () => { - const options = [ - { label: 'John', value: 1, customLabel:

John

}, - { label: 'Liam', value: 2, customLabel:

Liam

}, - { label: 'Olivia', value: 3, customLabel:

Olivia

}, - ]; - render(); + const loadOptions = jest.fn(async () => ({ + data: [ + { label: 'John', value: 1, customLabel:

John

}, + { label: 'Liam', value: 2, customLabel:

Liam

}, + { label: 'Olivia', value: 3, customLabel:

Olivia

}, + ], + totalCount: 3, + })); + render(); await open(); expect(screen.getByRole('heading', { name: 'John' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Liam' })).toBeInTheDocument(); @@ -345,12 +392,15 @@ test('renders a custom label', async () => { }); test('searches for a word with a custom label', async () => { - const options = [ - { label: 'John', value: 1, customLabel:

John

}, - { label: 'Liam', value: 2, customLabel:

Liam

}, - { label: 'Olivia', value: 3, customLabel:

Olivia

}, - ]; - render(); + const loadOptions = jest.fn(async () => ({ + data: [ + { label: 'John', value: 1, customLabel:

John

}, + { label: 'Liam', value: 2, customLabel:

Liam

}, + { label: 'Olivia', value: 3, customLabel:

Olivia

}, + ], + totalCount: 3, + })); + render(); await type('Liam'); const selectOptions = await findAllSelectOptions(); expect(selectOptions.length).toBe(1); @@ -391,7 +441,11 @@ test('does not add a new option if allowNewOptions is false', async () => { }); test('adds the null option when selected in single mode', async () => { - render(); + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[0], NULL_OPTION], + totalCount: 2, + })); + render(); await open(); userEvent.click(await findSelectOption(NULL_OPTION.label)); const values = await findAllSelectValues(); @@ -399,12 +453,12 @@ test('adds the null option when selected in single mode', async () => { }); test('adds the null option when selected in multiple mode', async () => { + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[0], NULL_OPTION], + totalCount: 2, + })); render( - , + , ); await open(); userEvent.click(await findSelectOption(OPTIONS[0].label)); diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index 98f146f15f2ac..b95f2d8f0d1f6 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -55,7 +55,6 @@ type PickedSelectProps = Pick< | 'autoFocus' | 'disabled' | 'filterOption' - | 'labelInValue' | 'loading' | 'notFoundContent' | 'onChange' @@ -129,11 +128,10 @@ export interface AsyncSelectProps extends PickedSelectProps { optionFilterProps?: string[]; /** * It defines the options of the Select. - * The options can be static, an array of options. - * The options can also be async, a promise that returns + * The options are async, a promise that returns * an array of options. */ - options: OptionsType | OptionsPagePromise; + options: OptionsPagePromise; /** * It defines how many results should be included * in the query response. @@ -299,7 +297,6 @@ const AsyncSelect = ( filterOption = true, header = null, invertSelection = false, - labelInValue = false, lazyLoading = true, loading, mode = 'single', @@ -322,9 +319,7 @@ const AsyncSelect = ( }: AsyncSelectProps, ref: RefObject, ) => { - const isAsync = typeof options === 'function'; const isSingleMode = mode === 'single'; - const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch; const [selectValue, setSelectValue] = useState(value); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(loading); @@ -360,8 +355,8 @@ const AsyncSelect = ( sortSelectedFirst(a, b) || // Only apply the custom sorter in async mode because we should // preserve the options order as much as possible. - (isAsync ? sortComparator(a, b, '') : 0), - [isAsync, sortComparator, sortSelectedFirst], + sortComparator(a, b, ''), + [sortComparator, sortSelectedFirst], ); const initialOptions = useMemo( @@ -528,7 +523,6 @@ const AsyncSelect = ( setSelectOptions(newOptions); } if ( - isAsync && !allValuesLoaded && loadingEnabled && !fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize)) @@ -546,7 +540,7 @@ const AsyncSelect = ( vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7; const hasMoreData = page * pageSize + pageSize < totalCount; - if (!isLoading && isAsync && hasMoreData && thresholdReached) { + if (!isLoading && hasMoreData && thresholdReached) { const newPage = page + 1; fetchPage(inputValue, newPage); } @@ -575,30 +569,26 @@ const AsyncSelect = ( const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => { setIsDropdownVisible(isDropdownVisible); - if (isAsync) { - // loading is enabled when dropdown is open, - // disabled when dropdown is closed - if (loadingEnabled !== isDropdownVisible) { - setLoadingEnabled(isDropdownVisible); - } - // when closing dropdown, always reset loading state - if (!isDropdownVisible && isLoading) { - // delay is for the animation of closing the dropdown - // so the dropdown doesn't flash between "Loading..." and "No data" - // before closing. - setTimeout(() => { - setIsLoading(false); - }, 250); - } + // loading is enabled when dropdown is open, + // disabled when dropdown is closed + if (loadingEnabled !== isDropdownVisible) { + setLoadingEnabled(isDropdownVisible); + } + // when closing dropdown, always reset loading state + if (!isDropdownVisible && isLoading) { + // delay is for the animation of closing the dropdown + // so the dropdown doesn't flash between "Loading..." and "No data" + // before closing. + setTimeout(() => { + setIsLoading(false); + }, 250); } // if no search input value, force sort options because it won't be sorted by // `filterSort`. if (isDropdownVisible && !inputValue && selectOptions.length > 1) { - const sortedOptions = isAsync - ? selectOptions.slice().sort(sortComparatorForNoSearch) - : // if not in async mode, revert to the original select options - // (with selected options still sorted to the top) - initialOptionsSorted; + const sortedOptions = selectOptions + .slice() + .sort(sortComparatorForNoSearch); if (!isEqual(sortedOptions, selectOptions)) { setSelectOptions(sortedOptions); } @@ -627,7 +617,7 @@ const AsyncSelect = ( if (isLoading) { return ; } - if (shouldShowSearch && isDropdownVisible) { + if (showSearch && isDropdownVisible) { return ; } return ; @@ -660,7 +650,7 @@ const AsyncSelect = ( ); useEffect(() => { - if (isAsync && loadingEnabled && allowFetch) { + if (loadingEnabled && allowFetch) { // trigger fetch every time inputValue changes if (inputValue) { debouncedFetchPage(inputValue, 0); @@ -668,14 +658,7 @@ const AsyncSelect = ( fetchPage('', 0); } } - }, [ - isAsync, - loadingEnabled, - fetchPage, - allowFetch, - inputValue, - debouncedFetchPage, - ]); + }, [loadingEnabled, fetchPage, allowFetch, inputValue, debouncedFetchPage]); useEffect(() => { if (loading !== undefined && loading !== isLoading) { @@ -706,20 +689,20 @@ const AsyncSelect = ( getPopupContainer={ getPopupContainer || (triggerNode => triggerNode.parentNode) } - labelInValue={isAsync || labelInValue} + labelInValue maxTagCount={MAX_TAG_COUNT} mode={mappedMode} notFoundContent={isLoading ? t('Loading...') : notFoundContent} onDeselect={handleOnDeselect} onDropdownVisibleChange={handleOnDropdownVisibleChange} - onPopupScroll={isAsync ? handlePagination : undefined} - onSearch={shouldShowSearch ? handleOnSearch : undefined} + onPopupScroll={handlePagination} + onSearch={showSearch ? handleOnSearch : undefined} onSelect={handleOnSelect} onClear={handleClear} onChange={onChange} options={hasCustomLabels ? undefined : fullSelectOptions} placeholder={placeholder} - showSearch={shouldShowSearch} + showSearch={showSearch} showArrow tokenSeparators={tokenSeparators || TOKEN_SEPARATORS} value={selectValue} diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx index efcd91c0c38fc..b75e1ff28bd00 100644 --- a/superset-frontend/src/components/Select/Select.stories.tsx +++ b/superset-frontend/src/components/Select/Select.stories.tsx @@ -16,11 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, useState, useCallback, useRef } from 'react'; +import React, { + ReactNode, + useState, + useCallback, + useRef, + useMemo, +} from 'react'; import Button from 'src/components/Button'; import ControlHeader from 'src/explore/components/ControlHeader'; -import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect'; -import Select, { SelectProps, OptionsTypePage, OptionsType } from './Select'; +import AsyncSelect, { + AsyncSelectProps, + AsyncSelectRef, + OptionsTypePage, +} from './AsyncSelect'; + +import Select, { SelectProps, OptionsType } from './Select'; export default { title: 'Select', @@ -452,6 +463,11 @@ export const AsynchronousSelect = ({ reject(new Error('Error while fetching the names from the server')); }); + const initialValue = useMemo( + () => ({ label: 'Valentina', value: 'Valentina' }), + [], + ); + return ( <>
{ expect(screen.getByText(headerText)).toBeInTheDocument(); }); -test('adds a new option if the value is not in the options', async () => { - const { rerender } = render( - ); await open(); expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); + options.forEach((option, i) => + expect(option).toHaveTextContent(OPTIONS[i].label), + ); +}); - rerender( +test('adds a new option if the value is not in the options, when options have values', async () => { + render( , + ); + await open(); + expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); +}); + test('inverts the selection', async () => { render( + + {this.props.datasource?.type === 'query' && ( + + + + + )} + + - - -