Compare commits

...
Author SHA1 Message Date
Evan Rusackas f6e460fc0e Fix readme link 2024-10-22 12:22:10 -07:00
Kamil Gabryjelski 47c5334502 chore: Enable suppressing default chart context menu (#30613) 2024-10-18 15:12:54 +02:00
K Kovács 950a21a020 docs: Clarification on which command to use on which Ubuntu version. (#30523) 2024-10-17 14:39:34 -04:00
Michael S. Molina bad48d0722 fix: Module is not defined in Partition chart (#30626) 2024-10-16 15:14:11 -03:00
Michael S. Molina c8edd1fb25 fix: First item hovered on stacked bar (#30628) 2024-10-16 13:07:12 -03:00
Sam Firke 53a121d9e1 fix(docs): address two linkinator failures (#30617) 2024-10-15 19:32:24 -04:00
Sam Firke 74b63a4ba4 fix(docs): leading whitespace line is causing page title and header to be malformed (#30616) 2024-10-15 19:31:56 -04:00
Geido 362948324c fix(Filters): Apply native & cross filters on common columns (#30438) 2024-10-15 16:13:05 -07:00
Beto Dealmeida 4cac7feb67 feat: use dialect when tokenizing (#30614) 2024-10-15 14:32:02 -04:00
Yerkebulan d6b2e86155 fix: Set correct amount of steps to avoid confusing logs while loading examples (#30606) 2024-10-15 14:55:12 -03:00
Ville Brofeldt 7132d5a86a chore(number-formatter): upgrade pretty-ms to 9.1.0 (#30599) 2024-10-15 10:37:28 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 55f18a34a9 build(deps): bump cookie, @applitools/eyes-storybook and express in /superset-frontend (#30572)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 10:50:02 -06:00
ObservabilityTeamandMuhammad Musfir 2c3ba95768 fix(filters): Adds a fix for saving time range adhoc_filters (#30581)
Co-authored-by: Muhammad Musfir <muhammad.musfir@de-cix.net>
2024-10-15 11:44:31 -03:00
6ede3271ff fix(SQL Lab): hang when result set size is too big (#30522)
Co-authored-by: aadhikari <aadhikari@apple.com>
Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
2024-10-14 18:03:28 -07:00
Mateusz Kopeć 0e9c0f621a feat(formatting): Add memory units adaptive formatter to format bytes (#30559) 2024-10-11 16:02:03 -07:00
Beto Dealmeida 47c1e09c75 fix: sqlparse fallback for formatting queries (#30578) 2024-10-11 15:45:40 -04:00
Đỗ Trọng Hải 9a2b1a5cf7 chore(fe): uplift FE packages to latest version (#30357)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-10-11 12:46:01 -06:00
Vitor Avila 9c12b1c7da fix(Jinja metric macro): Support Drill By and Excel/CSV download without a dataset ID (#30443) 2024-10-10 21:03:19 -03:00
Beto Dealmeida 0db59b45b8 fix: adhoc metrics (#30202) 2024-10-10 16:46:17 -04:00
Joe Li ef0ede7c13 fix: update html rendering to true from false (#30565) 2024-10-10 14:19:26 -03:00
Geido 62b94d5abf fix(dev-server): Revert "chore(fe): bump webpack-related packages to v5" (#30569) 2024-10-10 12:59:26 +03:00
Geido 318eff7327 fix(Jinja): Extra cache keys to consider vars with set (#30549) 2024-10-09 23:26:32 +03:00
Felix 211564a6da fix(dashboard-export): Fixes datasetId is not replaced with datasetUuid in Dashboard export in 4.1.x (#30425) 2024-10-09 11:36:46 -07:00
Michael S. Molina 7a8e8f890f fix: Incorrect type in config.py (#30564) 2024-10-09 14:29:11 -03:00
Michael S. Molina 7b47e43fd0 fix: Horizon Chart are not working any more (#30563) 2024-10-09 14:22:17 -03:00
Michael S. Molina fc857d987b fix: Unable to parse escaped tables (#30560) 2024-10-09 13:25:44 -03:00
Maxime Beauchemin a849c29288 chore: enable lint PT009 'use regular assert over self.assert.*' (#30521) 2024-10-07 13:17:27 -07:00
1f013055d2 refactor: Migration of Chart to TypeScript (#28370)
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
Co-authored-by: JUST.in DO IT <justin.park@airbnb.com>
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
2024-10-07 20:47:28 +03:00
Đỗ Trọng Hải 2e887598ba chore(fe): bump webpack-related packages to v5 (#30528)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-10-07 14:30:04 -03:00
dkrat7 aa94488065 chore(translations): Slovenian translation update (#30526) 2024-10-07 14:22:39 -03:00
Ville Brofeldt 163b71e019 fix(migration): replace unquote with double percentages (#30532) 2024-10-06 20:05:20 -07:00
Beto Dealmeida a098809294 feat(SQL Lab): better SQL parsing error messages (#30501) 2024-10-04 19:17:56 -04:00
Jack 95325c4673 fix(imports): Error when importing charts / dashboards with missing DB credentials (#30503) 2024-10-04 15:43:29 -05:00
Đỗ Trọng Hải 68c9a8177c fix(frontend/generator): fix failed Viz plugin build due to missing JSDOM config and dep (#30069)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-10-04 11:04:30 -06:00
GeidoandBeto Dealmeida f314685a8e fix(Explore): Apply RLS at column values (#30490)
Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
2024-10-04 18:12:28 +03:00
Beto Dealmeida 0b34197815 fix: don't reformat generated queries (#30350) 2024-10-04 11:09:37 -04:00
JUST.in DO IT b627011463 fix(explore): don't discard controls on deprecated (#30447) 2024-10-03 17:12:23 -07:00
Đỗ Trọng Hải 2aa9348759 feat(be/cfg): replace deprecated imp.load_source with importlib.util (#30390) 2024-10-03 15:58:32 -06:00
Ville Brofeldt 6217cb636f fix(examples): fix examples uri for sqlite (#30277) 2024-10-03 15:48:48 -06:00
Đỗ Trọng Hải 2a458a4802 fix(fe/src/dashboard): optional chaining for possibly nullable parent attribute in LayoutItem type (#30442)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-10-03 15:48:18 -06:00
Đỗ Trọng Hải 989744aba2 fix(plugin/echarts): correct enum values for LABEL_POSITION map (#30509)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-10-03 15:47:59 -06:00
JUST.in DO IT 4dfee727e8 fix(sqllab): Remove redundant scrolling (#30500) 2024-10-03 13:17:23 -07:00
Gonçalo Teixeira b2fd56094f fix(radar-chart): metric options not available & add min option (#30349) 2024-10-03 09:41:46 -07:00
Michael S. Molina 03146b21be fix: Open control with Simple tab selected when there is no column selected (#30502) 2024-10-03 08:45:39 -03:00
Beto Dealmeida e0172a24b8 fix(embedded): sankey charts (#30491) 2024-10-02 13:45:35 -04:00
Joe Li bd42e336c7 chore: add native filters to Covid Vaccines dashboard (#30495) 2024-10-02 10:32:04 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 84d3e3d25f build(deps-dev): bump typescript from 5.5.4 to 5.6.2 in /superset-websocket (#30463)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 10:41:13 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a194393efc build(deps): bump express from 4.20.0 to 4.21.0 in /superset-websocket/utils/client-ws-app (#30472)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 10:40:57 -06:00
Maxime Beauchemin bc8e9513bf docs: fix broken links in CI (#30496) 2024-10-02 09:15:58 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f4094829c2 build(deps-dev): bump typescript from 5.5.4 to 5.6.2 in /docs (#30476)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 16:32:55 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 657377b0ab build(deps): bump @rjsf/core from 5.19.4 to 5.21.1 in /superset-frontend (#30461)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 14:30:54 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8019c8b665 build(deps-dev): bump typescript-eslint from 8.6.0 to 8.8.0 in /superset-websocket (#30465)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 14:29:53 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4c73b040c8 build(deps-dev): bump @types/node from 22.0.2 to 22.7.4 in /superset-websocket (#30466)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 14:29:39 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d3a208eb8a build(deps): bump @types/prop-types from 15.7.5 to 15.7.13 in /superset-frontend (#30467)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 14:29:23 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7d0cb39f2a build(deps): bump @types/react-loadable from 5.5.6 to 5.5.11 in /superset-frontend (#30469)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 14:29:08 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 553932dc72 build(deps): bump debug from 4.3.6 to 4.3.7 in /superset-websocket/utils/client-ws-app (#30471)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 14:28:31 -06:00
Đỗ Trọng Hải da7a74e604 refactor(frontend): migrate 6 Enzyme-based tests to RTL, part 2 (#30281)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-10-01 14:27:47 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 15f3ea8d05 build(deps-dev): bump esbuild-loader from 4.1.0 to 4.2.2 in /superset-frontend (#30487)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 13:01:43 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 62350f2e5f build(deps-dev): bump eslint-plugin-file-progress from 1.4.0 to 1.5.0 in /superset-frontend (#30460)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:54:14 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e5ddb3bbd6 build(deps-dev): bump @cypress/react from 5.12.5 to 8.0.2 in /superset-frontend (#30459)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:53:49 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f425c9d315 build(deps-dev): bump @typescript-eslint/eslint-plugin from 8.6.0 to 8.8.0 in /superset-websocket (#30464)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:53:05 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a8500abd8f build(deps): bump re-resizable from 6.9.11 to 6.10.0 in /superset-frontend (#30477)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:52:01 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ee9ece8992 build(deps-dev): bump webpack-manifest-plugin from 4.1.1 to 5.0.0 in /superset-frontend (#30473)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:51:29 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7468904968 build(deps-dev): bump @types/react from 18.3.5 to 18.3.10 in /docs (#30481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:49:20 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8a68a96f77 build(deps): bump @docsearch/react from 3.6.1 to 3.6.2 in /docs (#30483)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:47:51 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fa34e0fae3 build(deps): bump handlebars from 4.7.7 to 4.7.8 in /superset-frontend (#30484)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:45:35 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 54679c5f49 build(deps-dev): bump @types/yargs from 17.0.32 to 17.0.33 in /superset-frontend (#30485)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 11:45:10 -07:00
Ville Brofeldt d1d9011f4d docs(dashboard): add docs for named and index colors (#30445) 2024-10-01 10:50:41 -07:00
Geido 9f476b2ce2 fix(Package.json): Bump dayjs version (#30493) 2024-10-01 19:45:29 +03:00
Diego Lima 70ef40eb33 fix(language): pt_BR translation (#30406) 2024-10-01 10:09:31 -06:00
Elizabeth Thompson 2e5016713a chore: log warnings for database tables api (#30410) 2024-09-30 13:52:17 -07:00
Maxime Beauchemin 9a5e8a4b70 chore: document upper bound for python lib 'holidays' >= 0.26 (#28747) 2024-09-30 13:23:14 -07:00
Maxime Beauchemin daa4b1dc65 fix: battling cypress' dashboard feature (#30441) 2024-09-30 10:50:16 -07:00
Geido c7aeb2160e chore(Dashboard): Unblock Global Styles (#30440) 2024-09-30 18:39:15 +03:00
Michael S. Molina 4834390e6a fix: Histogram chart not able to use decimal datatype column (#30416) 2024-09-30 09:04:55 -03:00
Ross Mabbett bdd50c7553 feat(dashboard): update tab drag and drop reordering with positional placement and indicators for UI (#29395) 2024-09-30 12:15:39 +03:00
Maxime Beauchemin 999dca76c1 fix: cypress on master doesn't work because of --parallel flag (#30430) 2024-09-29 20:32:04 -07:00
Maxime Beauchemin 63e17ca546 fix: CI remove cypress command --headed (#30429) 2024-09-30 00:28:39 +02:00
Maxime BeaucheminandJoe Li a3bfbd0186 chore: alter scripts/cypress_run to run one file per command + retry (#30397)
Co-authored-by: Joe Li <joe@preset.io>
2024-09-28 17:59:04 -06:00
Beto Dealmeida cc9fd88c0d chore: improve DML check (#30417) 2024-09-27 15:26:36 -04:00
Elizabeth Thompson 96b0bcfd78 chore: add logging for dashboards/get warnings (#30365) 2024-09-27 11:08:04 -07:00
Geido 4ac0bb1bed chore(View): Remove unnecessary theme view and defer basic styles (#30128) 2024-09-27 18:12:58 +02:00
Đỗ Trọng Hải 92680b3fe4 fix(plugin/country/map): rectify naming for some Vietnamese provinces (#29444)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-09-27 10:00:06 -06:00
Michael S. Molina 4f5f8fd216 chore: Merge description and reproduction steps in the issue template (#30407) 2024-09-27 08:26:45 -03:00
Michael S. Molina 36f7a3f524 fix: Incorrect hovered items in tooltips (#30405) 2024-09-26 16:08:59 -03:00
Birk Skyum 0fdcd8b27e chore(legacy-plugin-chart-map-box): bump supercluster to v8 (#30305) 2024-09-25 21:18:14 -06:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Evan Rusackas
20b1eabb9b build(deps): update @emotion/react requirement from ^11.4.1 to ^11.13.3 in /superset-frontend/packages/superset-ui-demo (#30086)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2024-09-25 19:49:00 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b7ac697dce build(deps): bump @emotion/react from 11.4.1 to 11.11.4 in /superset-frontend (#27827)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 19:47:50 -06:00
Michael S. Molina abf2943e4d fix: Allows X-Axis Sort By for custom SQL (#30393) 2024-09-25 19:08:28 -03:00
Elizabeth Thompson 43721f1206 chore: split cypress files for less memory (#30354) 2024-09-25 13:50:53 -07:00
Elizabeth Thompson 0e8fa54f81 fix: parse pandas pivot null values (#29898) 2024-09-25 13:50:04 -07:00
Michael S. Molina ad2998598f fix: Pre-query normalization with custom SQL (#30389) 2024-09-25 15:30:53 -03:00
Sam Firke 69d5f766a8 feat(auth): when user is not logged in, failure to access a dashboard should redirect to login screen (#30380) 2024-09-25 14:26:26 -04:00
Antonio Rivero 18c2376b50 feat(datasets): Allow swap dataset after deletion (#30364) 2024-09-25 18:12:45 +02:00
39f1b714a5 refactor: Migration of AnnotationLayerControl to TypeScript (#28346)
Co-authored-by: JUST.in DO IT <justin.park@airbnb.com>
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2024-09-25 17:45:11 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0baac62099 build(deps-dev): bump sinon from 18.0.0 to 18.0.1 in /superset-frontend (#30251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 09:07:07 -06:00
Geido 046c096d86 fix(ECharts): Revert ECharts version bump (#30388) 2024-09-25 15:32:55 +02:00
GeidoandVille Brofeldt de3af85ee1 feat(Digest): Add RLS at digest generation for Charts and Dashboards (#30336)
Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
2024-09-24 15:39:11 +02:00
Tim DörnemannandMichael S. Molina cc1bb69671 docs: Corrected Dremio connection string (#30315)
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
2024-09-23 14:02:31 -06:00
Sam Firke 30ca30370d chore(docs): fix an agreement error in caching docs (#30352) 2024-09-23 15:09:47 -04:00
Beto Dealmeida 710406aa76 feat: allow configuring an engine context manager (#30266) 2024-09-23 12:36:18 -04:00
axuew ee3a56714e docs: add HANA database logo in README.md (#30346) 2024-09-23 10:21:15 -06:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Evan Rusackas
a095b08758 build(deps): update dompurify requirement from ^3.1.0 to ^3.1.2 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (#28290)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2024-09-23 10:18:41 -06:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Evan Rusackas
bfdab40aaf build(deps-dev): bump @storybook/react-webpack5 from 8.1.11 to 8.2.9 in /superset-frontend/packages/superset-ui-demo (#30089)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2024-09-23 10:18:30 -06:00
Đỗ Trọng Hải 55d9d57112 build(websocket): upgrade ESLint to v9 (#30359)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2024-09-22 11:14:42 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f3e4347a8b build(deps): bump deck.gl from 9.0.24 to 9.0.28 in /superset-frontend/plugins/legacy-preset-chart-deckgl (#30084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 13:42:26 -06:00
Evan Rusackas d4351668ba fix(CI): increase node JS heap size (#30340) 2024-09-20 11:49:59 -06:00
Sam Firke ac66ae813f fix(db_engine_specs): add a few missing time grains to Postgres spec (#30325) 2024-09-20 13:34:10 -04:00
Kamil Gabryjelski 038ef32454 fix(explore): Missing markarea component broke annotations in echarts (#30348) 2024-09-20 16:42:47 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 994de1f812 build(deps): bump dompurify from 3.1.0 to 3.1.3 in /superset-frontend (#30300)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 17:25:48 -06:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 36f3cd1f70 build(deps): bump path-to-regexp from 1.8.0 to 1.9.0 in /superset-frontend/cypress-base (#30247)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 17:10:45 -06:00
Michael S. Molina ef9e5e523d fix: KeyError 'sql' when opening a Trino virtual dataset (#30339) 2024-09-19 17:41:08 -03:00
284 changed files with 21971 additions and 12899 deletions
+2 -7
View File
@@ -15,14 +15,9 @@ body:
id: bug-description
attributes:
label: Bug description
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: repro-steps
attributes:
label: How to reproduce the bug
description: A clear description of what the bug is, including reproduction steps and expected behavior.
placeholder: |
The bug is that...
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
+20 -1
View File
@@ -162,7 +162,11 @@ cypress-run-all() {
USE_DASHBOARD_FLAG='--use-dashboard'
fi
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID $USE_DASHBOARD_FLAG
# UNCOMMENT the next few commands to monitor memory usage
# monitor_memory & # Start memory monitoring in the background
# memoryMonitorPid=$!
python ../../scripts/cypress_run.py --parallelism $PARALLELISM --parallelism-id $PARALLEL_ID --group $PARALLEL_ID --retries 5 $USE_DASHBOARD_FLAG
# kill $memoryMonitorPid
# After job is done, print out Flask log for debugging
echo "::group::Flask log for default run"
@@ -178,6 +182,21 @@ eyes-storybook-dependencies() {
say "::endgroup::"
}
monitor_memory() {
# This is a small utility to monitor memory usage. Useful for debugging memory in GHA.
# To use wrap your command as follows
#
# monitor_memory & # Start memory monitoring in the background
# memoryMonitorPid=$!
# YOUR_COMMAND_HERE
# kill $memoryMonitorPid
while true; do
echo "$(date) - Top 5 memory-consuming processes:"
ps -eo pid,comm,%mem --sort=-%mem | head -n 6 # First line is the header, next 5 are top processes
sleep 2
done
}
cypress-run-applitools() {
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
+3 -1
View File
@@ -48,7 +48,8 @@ jobs:
PYTHONPATH: ${{ github.workspace }}
REDIS_PORT: 16379
GITHUB_TOKEN: ${{ github.token }}
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard || (github.ref == 'refs/heads/master' && 'true') || 'false' }}
# use the dashboard feature when running manually OR merging to master
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true'|| (github.ref == 'refs/heads/master' && 'true') || 'false' }}
services:
postgres:
image: postgres:15-alpine
@@ -131,6 +132,7 @@ jobs:
PARALLEL_ID: ${{ matrix.parallel_id }}
PARALLELISM: 6
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
NODE_OPTIONS: "--max-old-space-size=4096"
with:
run: cypress-run-all ${{ env.USE_DASHBOARD }}
- name: Upload Artifacts
+3 -3
View File
@@ -80,9 +80,9 @@ If you believe someone is violating this code of conduct, you may reply to them
Or one of our volunteers:
* [Mark Thomas](http://home.apache.org/~markt/coc.html)
* [Joan Touzet](http://home.apache.org/~wohali/)
* [Sharan Foga](http://home.apache.org/~sharan/coc.html)
* [Mark Thomas](https://www.linkedin.com/in/mark-thomas-b16751158/)
* [Joan Touzet](https://www.apache.org/foundation/conduct-team/wohali.html)
* [Sharan Foga](https://www.linkedin.com/in/sfoga/)
If the violation is in documentation or code, for example inappropriate pronoun usage or word choice within official documentation, we ask that people report these privately to the project in question at <private@project.apache.org>, and, if they have sufficient ability within the project, to resolve or remove the concerning material, being mindful of the perspective of the person originally reporting the issue.
+1
View File
@@ -134,6 +134,7 @@ Here are some of the major database solutions that are supported:
<img src="https://superset.apache.org/img/databases/starrocks.png" alt="starrocks" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/doris.png" alt="doris" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/oceanbase.svg" alt="oceanbase" border="0" width="220" />
<img src="https://superset.apache.org/img/databases/sap-hana.png" alt="oceanbase" border="0" width="220" />
</p>
**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/configuration/databases).
+1 -1
View File
@@ -137,4 +137,4 @@ There is now a [metadata bar](https://github.com/apache/superset/pull/27857) add
## Change to Docker image builds
Starting in 4.1.0, the release's docker image does not ship with drivers needed to operate Superset. Users may need to install a driver for their metadata database (MySQL or Postgres) as well as the driver for their data warehouse. This is a result of changes to the `lean` docker image that official releases come from; see [Docker Build Presets](/docs/installation/docker-builds/#build-presets) for more details.
Starting in 4.1.0, the release's docker image does not ship with drivers needed to operate Superset. Users may need to install a driver for their metadata database (MySQL or Postgres) as well as the driver for their data warehouse. This is a result of changes to the `lean` docker image that official releases come from; see [Docker Build Presets](https://superset.apache.org/docs/installation/docker-builds/#build-presets) for more details.
-1
View File
@@ -80,7 +80,6 @@ Join our growing community!
- [Caizin](https://caizin.com/) [@tejaskatariya]
- [Careem](https://www.careem.com/) [@SamraHanifCareem]
- [Cloudsmith](https://cloudsmith.io) [@alancarson]
- [CnOvit](https://www.cnovit.com/) [@xieshaohu]
- [Cyberhaven](https://www.cyberhaven.com/) [@toliver-ch]
- [Deepomatic](https://deepomatic.com/) [@Zanoellia]
- [Dial Once](https://www.dial-once.com/)
+5 -1
View File
@@ -22,7 +22,11 @@ set -e
#
/app/docker/docker-bootstrap.sh
STEP_CNT=4
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
STEP_CNT=4
else
STEP_CNT=3
fi
echo_step() {
cat <<EOF
+1
View File
@@ -16,6 +16,7 @@ KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
This is the public documentation site for Superset, built using
[Docusaurus 2](https://docusaurus.io/). See
[CONTRIBUTING.md](../CONTRIBUTING.md#documentation) for documentation on
+2 -2
View File
@@ -13,7 +13,7 @@ SimpleCache (in-memory), or the local filesystem.
[Custom cache backends](https://flask-caching.readthedocs.io/en/latest/#custom-cache-backends)
are also supported.
Caching can be configured by providing a dictionaries in
Caching can be configured by providing dictionaries in
`superset_config.py` that comply with [the Flask-Caching config specifications](https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching).
The following cache configurations can be customized in this way:
@@ -22,7 +22,7 @@ The following cache configurations can be customized in this way:
- Metadata cache (optional): `CACHE_CONFIG`
- Charting data queried from datasets (optional): `DATA_CACHE_CONFIG`
For example, to configure the filter state cache using redis:
For example, to configure the filter state cache using Redis:
```python
FILTER_STATE_CACHE_CONFIG = {
+1 -1
View File
@@ -55,7 +55,7 @@ are compatible with Superset.
| [ClickHouse](/docs/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
| [CockroachDB](/docs/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
| [Couchbase](/docs/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
| [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:31010, For ODBC dremio+pyodbc://{username}:{password}@{host}:32010` |
| [Dremio](/docs/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:32010`, often useful: `?UseEncryption=true/false`. For Legacy ODBC: `dremio+pyodbc://{username}:{password}@{host}:31010` |
| [Elasticsearch](/docs/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
| [Exasol](/docs/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
| [Google BigQuery](/docs/configuration/databases#google-bigquery) | `pip install sqlalchemy-bigquery` | `bigquery://{project_id}` |
@@ -1,4 +1,3 @@
---
title: Network and Security Settings
sidebar_position: 7
+6 -3
View File
@@ -174,13 +174,16 @@ You can take a look at this Flask-AppBuilder
## Is there a way to force the dashboard to use specific colors?
It is possible on a per-dashboard basis by providing a mapping of labels to colors in the JSON
Metadata attribute using the `label_colors` key.
Metadata attribute using the `label_colors` key. You can use either the full hex color, a named color,
like `red`, `coral` or `lightblue`, or the index in the current color palette (0 for first color, 1 for
second etc). Example:
```json
{
"label_colors": {
"Girls": "#FF69B4",
"Boys": "#ADD8E6"
"foo": "#FF69B4",
"bar": "lightblue",
"baz": 0
}
}
```
+7 -7
View File
@@ -22,18 +22,18 @@ level dependencies.
**Debian and Ubuntu**
The following command will ensure that the required dependencies are installed:
```bash
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
In Ubuntu 20.04 the following command will ensure that the required dependencies are installed:
In Ubuntu **20.04 and 22.04** the following command will ensure that the required dependencies are installed:
```bash
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev python3-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
In Ubuntu **before 20.04** the following command will ensure that the required dependencies are installed:
```bash
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
**Fedora and RHEL-derivative Linux distributions**
Install the following packages using the `yum` package manager:
+3 -3
View File
@@ -19,7 +19,7 @@
"dependencies": {
"@algolia/client-search": "^4.24.0",
"@ant-design/icons": "^5.4.0",
"@docsearch/react": "^3.6.1",
"@docsearch/react": "^3.6.2",
"@docusaurus/core": "^3.5.2",
"@docusaurus/plugin-client-redirects": "^3.5.2",
"@docusaurus/preset-classic": "^3.5.2",
@@ -48,8 +48,8 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.2",
"@docusaurus/tsconfig": "^3.5.2",
"@types/react": "^18.3.5",
"typescript": "^5.5.4",
"@types/react": "^18.3.10",
"typescript": "^5.6.2",
"webpack": "^5.94.0"
},
"browserslist": {
+8 -1
View File
@@ -116,7 +116,8 @@
"GENERIC_BACKEND_ERROR",
"INVALID_PAYLOAD_FORMAT_ERROR",
"INVALID_PAYLOAD_SCHEMA_ERROR",
"REPORT_NOTIFICATION_ERROR"
"REPORT_NOTIFICATION_ERROR",
"RESULT_TOO_LARGE_ERROR"
],
"type": "string"
},
@@ -3052,6 +3053,12 @@
},
"type": "array"
},
"column_names": {
"items": {
"type": "string"
},
"type": "array"
},
"currency_formats": {
"type": "object"
},
+17 -17
View File
@@ -1335,19 +1335,19 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@docsearch/css@3.6.1":
version "3.6.1"
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.1.tgz#f0a728ecb486c81f2d282650fc1820c914913408"
integrity sha512-VtVb5DS+0hRIprU2CO6ZQjK2Zg4QU5HrDM1+ix6rT0umsYvFvatMAnf97NHZlVWDaaLlx7GRfR/7FikANiM2Fg==
"@docsearch/css@3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.2.tgz#ccd9c83dbfeaf34efe4e3547ee596714ae7e5891"
integrity sha512-vKNZepO2j7MrYBTZIGXvlUOIR+v9KRf70FApRgovWrj3GTs1EITz/Xb0AOlm1xsQBp16clVZj1SY/qaOJbQtZw==
"@docsearch/react@^3.5.2", "@docsearch/react@^3.6.1":
version "3.6.1"
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.1.tgz#0f826df08693293806d64277d6d9c38636211b97"
integrity sha512-qXZkEPvybVhSXj0K7U3bXc233tk5e8PfhoZ6MhPOiik/qUQxYC+Dn9DnoS7CxHQQhHfCvTiN0eY9M12oRghEXw==
"@docsearch/react@^3.5.2", "@docsearch/react@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.2.tgz#32b16dd7d5614f0d39e6bc018549816b68d171b8"
integrity sha512-rtZce46OOkVflCQH71IdbXSFK+S8iJZlUF56XBW5rIgx/eG5qoomC7Ag3anZson1bBac/JFQn7XOBfved/IMRA==
dependencies:
"@algolia/autocomplete-core" "1.9.3"
"@algolia/autocomplete-preset-algolia" "1.9.3"
"@docsearch/css" "3.6.1"
"@docsearch/css" "3.6.2"
algoliasearch "^4.19.1"
"@docusaurus/core@3.5.2", "@docusaurus/core@^3.5.2":
@@ -2939,10 +2939,10 @@
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react@*", "@types/react@^18.3.5":
version "18.3.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f"
integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==
"@types/react@*", "@types/react@^18.3.10":
version "18.3.10"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.10.tgz#6edc26dc22ff8c9c226d3c7bf8357b013c842219"
integrity sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
@@ -9984,10 +9984,10 @@ types-ramda@^0.30.0:
dependencies:
ts-toolbelt "^9.6.0"
typescript@^5.5.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.6.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
undici-types@~5.26.4:
version "5.26.5"
+3 -1
View File
@@ -57,6 +57,7 @@ dependencies = [
"geopy",
"gunicorn>=22.0.0; sys_platform != 'win32'",
"hashids>=1.3.1, <2",
# known issue with holidays 0.26.0 and above related to prophet lib #25017
"holidays>=0.25, <0.26",
"humanize",
"importlib_metadata",
@@ -89,7 +90,7 @@ dependencies = [
"slack_sdk>=3.19.0, <4",
"sqlalchemy>=1.4, <2",
"sqlalchemy-utils>=0.38.3, <0.39",
"sqlglot>=23.0.2,<24",
"sqlglot>=25.24.0,<26",
"sqlparse>=0.5.0",
"tabulate>=0.8.9, <0.9",
"typing-extensions>=4, <5",
@@ -445,6 +446,7 @@ select = [
"E7",
"E9",
"F",
"PT009",
"TRY201",
]
ignore = []
+1 -1
View File
@@ -350,7 +350,7 @@ sqlalchemy-utils==0.38.3
# via
# apache-superset
# flask-appbuilder
sqlglot==23.6.3
sqlglot==25.24.0
# via apache-superset
sqlparse==0.5.0
# via apache-superset
+78 -41
View File
@@ -16,26 +16,18 @@
# under the License.
import argparse
import hashlib
import os
import subprocess
from datetime import datetime
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x24' "
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
GITHUB_EVENT_NAME = os.getenv("GITHUB_REPOSITORY") or "push"
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
def compute_hash(file_path: str) -> str:
return hashlib.md5(file_path.encode()).hexdigest()
def compute_group_index(hash_value: str, num_groups: int) -> int:
return int(hash_value, 16) % num_groups
def generate_build_id() -> str:
"""Generates a build ID based on the current timestamp."""
now = datetime.now()
rounded_minute = now.minute - (now.minute % 20)
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
@@ -44,42 +36,72 @@ def generate_build_id() -> str:
)
def get_cypress_cmd(
spec_list: list[str], _filter: str, group: str, use_dashboard: bool
) -> str:
def run_cypress_for_test_file(
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool, i: int
) -> int:
"""Runs Cypress for a single test file and retries upon failure."""
cypress_cmd = "./node_modules/.bin/cypress run"
os.environ["TERM"] = "xterm"
os.environ["ELECTRON_DISABLE_GPU"] = "true"
build_id = generate_build_id()
browser = os.getenv("CYPRESS_BROWSER", "chrome")
chrome_flags = "--disable-dev-shm-usage"
if use_dashboard:
# Run using cypress.io service
spec: str = "cypress/e2e/*/**/*"
cmd = (
f"{XVFB_PRE_CMD} "
f'{cypress_cmd} --spec "{spec}" --browser {browser} '
f"--record --group {group} --tag {REPO},{GITHUB_EVENT_NAME} "
f"--parallel --ci-build-id {build_id}"
for attempt in range(retries):
# Create Cypress command for a single test file
cmd: str = ""
if use_dashboard:
# If/when we want to use cypress' dashboard feature to record the run
group_id = f"matrix{group}-file{i}-{attempt}"
cmd = (
f"{XVFB_PRE_CMD} "
f'{cypress_cmd} --spec "{test_file}" --browser {browser} '
f"--record --group {group_id} --tag {REPO},{GITHUB_EVENT_NAME} "
f"--ci-build-id {build_id} "
f"-- {chrome_flags}"
)
else:
os.environ.pop("CYPRESS_RECORD_KEY", None)
cmd = (
f"{XVFB_PRE_CMD} "
f"{cypress_cmd} --browser {browser} "
f'--spec "{test_file}" '
f"-- {chrome_flags}"
)
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
if dry_run:
# Print the command instead of executing it
print(f"DRY RUN: {cmd}")
return 0
process = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
else:
# Run local, but split the execution
os.environ.pop("CYPRESS_RECORD_KEY", None)
spec_list_str = ",".join(sorted(spec_list))
if _filter:
spec_list_str = ",".join(sorted([s for s in spec_list if _filter in s]))
cmd = (
f"{XVFB_PRE_CMD} "
f"{cypress_cmd} --browser {browser} "
f'--spec "{spec_list_str}" '
)
return cmd
# Stream stdout in real-time
if process.stdout:
for stdout_line in iter(process.stdout.readline, ""):
print(stdout_line, end="")
process.wait()
if process.returncode == 0:
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
return 0
else:
print(f"Test {test_file} failed on attempt {attempt + 1}")
print(f"Test {test_file} failed after {retries} retries.")
return process.returncode
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate Cypress commands based on test file hash"
description="Run Cypress tests with retries per test file"
)
parser.add_argument(
"--use-dashboard",
@@ -93,9 +115,12 @@ def main() -> None:
"--parallelism-id", type=int, required=True, help="ID of the parallelism group"
)
parser.add_argument(
"--filter", type=str, required=False, default=None, help="filter to test"
"--filter", type=str, required=False, default=None, help="Filter to test"
)
parser.add_argument("--group", type=str, default="Default", help="Group name")
parser.add_argument(
"--retries", type=int, default=3, help="Number of retries per test file"
)
parser.add_argument(
"--dry-run",
action="store_true",
@@ -109,14 +134,17 @@ def main() -> None:
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
test_files = []
file_count = 0
for root, _, files in os.walk(cypress_tests_path):
for file in files:
if file.endswith("test.ts") or file.endswith("test.js"):
file_count += 1
test_files.append(
os.path.join(root, file).replace(cypress_base_full_path, "")
)
print(f"Found {file_count} test files.")
# Initialize groups
# Initialize groups for round-robin distribution
groups: dict[int, list[str]] = {i: [] for i in range(args.parallelism)}
# Sort test files to ensure deterministic distribution
@@ -127,12 +155,21 @@ def main() -> None:
group_index = index % args.parallelism
groups[group_index].append(test_file)
# Only run tests for the group that matches the parallelism ID
group_id = args.parallelism_id
spec_list = groups[group_id]
cmd = get_cypress_cmd(spec_list, args.filter, args.group, args.use_dashboard)
print(f"RUN: {cmd}")
if not args.dry_run:
subprocess.run(cmd, shell=True, check=True, stdout=None, stderr=None)
# Run each test file independently with retry logic or dry-run
processed_file_count: int = 0
for i, test_file in enumerate(spec_list):
result = run_cypress_for_test_file(
test_file, args.retries, args.use_dashboard, args.group, args.dry_run, i
)
if result != 0:
print(f"Exiting due to failure in {test_file}")
exit(result)
processed_file_count += 1
print(f"Ran {processed_file_count} test files successfully.")
if __name__ == "__main__":
@@ -0,0 +1,248 @@
/**
* 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 { nativeFilters } from 'cypress/support/directories';
import {
addCountryNameFilter,
applyNativeFilterValueWithIndex,
enterNativeFilterEditModal,
inputNativeFilterDefaultValue,
saveNativeFilterSettings,
validateFilterNameOnDashboard,
testItems,
interceptFilterState,
} from './utils';
import {
prepareDashboardFilters,
SAMPLE_CHART,
visitDashboard,
} from './shared_dashboard_functions';
function openMoreFilters(waitFilterState = true) {
interceptFilterState();
cy.getBySel('dropdown-container-btn').click();
if (waitFilterState) {
cy.wait('@postFilterState');
}
}
function openVerticalFilterBar() {
cy.getBySel('dashboard-filters-panel').should('exist');
cy.getBySel('filter-bar__expand-button').click();
}
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
cy.getBySel('filterbar-orientation-icon').click();
cy.wait(250);
cy.getBySel('dropdown-selectable-icon-submenu')
.contains('Orientation of filter bar')
.should('exist')
.trigger('mouseover');
if (orientation === 'vertical') {
cy.get('.ant-dropdown-menu-item-selected')
.contains('Horizontal (Top)')
.should('exist');
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
cy.getBySel('dashboard-filters-panel').should('exist');
} else {
cy.get('.ant-dropdown-menu-item-selected')
.contains('Vertical (Left)')
.should('exist');
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
cy.getBySel('loading-indicator').should('exist');
cy.getBySel('filter-bar').should('exist');
cy.getBySel('dashboard-filters-panel').should('not.exist');
}
}
describe('Horizontal FilterBar', () => {
it('should go from vertical to horizontal and the opposite', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
setFilterBarOrientation('vertical');
});
it('should show all default actions in horizontal mode', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
cy.getBySel('horizontal-filterbar-empty')
.contains('No filters are currently added to this dashboard.')
.should('exist');
cy.getBySel('filter-bar__create-filter').should('exist');
cy.getBySel('filterbar-action-buttons').should('exist');
});
it('should stay in horizontal mode when reloading', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
cy.reload();
cy.getBySel('dashboard-filters-panel').should('not.exist');
});
it('should show all filters in available space on load', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.get('.filter-item-wrapper').should('have.length', 3);
});
it('should show "more filters" on window resizing up and down', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.getBySel('form-item-value').should('have.length', 3);
cy.viewport(768, 1024);
cy.getBySel('form-item-value').should('have.length', 0);
openMoreFilters(false);
cy.getBySel('form-item-value').should('have.length', 3);
cy.getBySel('filter-bar').click();
cy.viewport(1000, 1024);
openMoreFilters(false);
cy.getBySel('form-item-value').should('have.length', 3);
cy.getBySel('filter-bar').click();
cy.viewport(1300, 1024);
cy.getBySel('form-item-value').should('have.length', 3);
cy.getBySel('dropdown-container-btn').should('not.exist');
});
it('should show "more filters" and scroll', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
{ name: 'test_4', column: 'year', datasetId: 2 },
{ name: 'test_5', column: 'country_name', datasetId: 2 },
{ name: 'test_6', column: 'country_code', datasetId: 2 },
{ name: 'test_7', column: 'region', datasetId: 2 },
{ name: 'test_8', column: 'year', datasetId: 2 },
{ name: 'test_9', column: 'country_name', datasetId: 2 },
{ name: 'test_10', column: 'country_code', datasetId: 2 },
{ name: 'test_11', column: 'region', datasetId: 2 },
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.get('.filter-item-wrapper').should('have.length', 3);
openMoreFilters();
cy.getBySel('form-item-value').should('have.length', 12);
cy.getBySel('filter-control-name').contains('test_10').should('be.visible');
cy.getBySel('filter-control-name')
.contains('test_12')
.should('not.be.visible');
cy.get('.ant-popover-inner-content').scrollTo('bottom');
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
});
it('should display newly added filter', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
enterNativeFilterEditModal(false);
addCountryNameFilter();
saveNativeFilterSettings([]);
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
});
it('should spot changes in "more filters" and apply their values', () => {
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
{ name: 'test_4', column: 'year', datasetId: 2 },
{ name: 'test_5', column: 'country_name', datasetId: 2 },
{ name: 'test_6', column: 'country_code', datasetId: 2 },
{ name: 'test_7', column: 'region', datasetId: 2 },
{ name: 'test_8', column: 'year', datasetId: 2 },
{ name: 'test_9', column: 'country_name', datasetId: 2 },
{ name: 'test_10', column: 'country_code', datasetId: 2 },
{ name: 'test_11', column: 'region', datasetId: 2 },
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
openMoreFilters();
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.wait('@chart');
cy.get('.antd5-scroll-number.antd5-badge-count').should(
'have.attr',
'title',
'1',
);
});
it('should focus filter and open "more filters" programmatically', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
{ name: 'test_4', column: 'year', datasetId: 2 },
{ name: 'test_5', column: 'country_name', datasetId: 2 },
{ name: 'test_6', column: 'country_code', datasetId: 2 },
{ name: 'test_7', column: 'region', datasetId: 2 },
{ name: 'test_8', column: 'year', datasetId: 2 },
{ name: 'test_9', column: 'country_name', datasetId: 2 },
{ name: 'test_10', column: 'country_code', datasetId: 2 },
{ name: 'test_11', column: 'region', datasetId: 2 },
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
openMoreFilters();
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.getBySel('slice-header').within(() => {
cy.get('.filter-counts').trigger('mouseover');
});
cy.get('.filterStatusPopover').contains('test_9').click();
cy.getBySel('dropdown-content').should('be.visible');
cy.get('.ant-select-focused').should('be.visible');
});
it('should show tag count and one plain tag on focus and only count on blur in select ', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
enterNativeFilterEditModal();
inputNativeFilterDefaultValue('Albania');
cy.get('.ant-select-selection-search-input').clear({ force: true });
inputNativeFilterDefaultValue('Algeria', true);
saveNativeFilterSettings([SAMPLE_CHART]);
cy.getBySel('filter-bar').within(() => {
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
cy.get('.ant-select-selection-search-input').click();
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
});
});
});
@@ -0,0 +1,287 @@
/**
* 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 qs from 'querystring';
import {
dashboardView,
nativeFilters,
exploreView,
dataTestChartName,
} from 'cypress/support/directories';
import {
addCountryNameFilter,
addParentFilterWithValue,
applyAdvancedTimeRangeFilterOnDashboard,
applyNativeFilterValueWithIndex,
cancelNativeFilterSettings,
checkNativeFilterTooltip,
clickOnAddFilterInModal,
collapseFilterOnLeftPanel,
deleteNativeFilter,
enterNativeFilterEditModal,
expandFilterOnLeftPanel,
fillNativeFilterForm,
getNativeFilterPlaceholderWithIndex,
inputNativeFilterDefaultValue,
saveNativeFilterSettings,
nativeFilterTooltips,
undoDeleteNativeFilter,
validateFilterContentOnDashboard,
valueNativeFilterOptions,
validateFilterNameOnDashboard,
testItems,
WORLD_HEALTH_CHARTS,
} from './utils';
import {
prepareDashboardFilters,
SAMPLE_CHART,
visitDashboard,
} from './shared_dashboard_functions';
function selectFilter(index: number) {
cy.get("[data-test='filter-title-container'] [draggable='true']")
.eq(index)
.click();
}
function closeFilterModal() {
cy.get('body').then($body => {
if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
cy.getBySel('native-filter-modal-cancel-button').click();
}
});
}
describe('Native filters', () => {
describe('Nativefilters initial state not required', () => {
it("User can check 'Filter has default value'", () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
});
it('User can add a new native filter', () => {
prepareDashboardFilters([]);
let filterKey: string;
const removeFirstChar = (search: string) =>
search.split('').slice(1, search.length).join('');
cy.location().then(loc => {
cy.url().should('contain', 'native_filters_key');
const queryParams = qs.parse(removeFirstChar(loc.search));
filterKey = queryParams.native_filters_key as string;
expect(typeof filterKey).eq('string');
});
enterNativeFilterEditModal();
addCountryNameFilter();
saveNativeFilterSettings([SAMPLE_CHART]);
cy.location().then(loc => {
cy.url().should('contain', 'native_filters_key');
const queryParams = qs.parse(removeFirstChar(loc.search));
const newfilterKey = queryParams.native_filters_key;
expect(newfilterKey).eq(filterKey);
});
cy.get(nativeFilters.modal.container).should('not.exist');
});
it('User can restore a deleted native filter', () => {
prepareDashboardFilters([
{ name: 'country_code', column: 'country_code', datasetId: 2 },
]);
enterNativeFilterEditModal();
cy.get(nativeFilters.filtersList.removeIcon).first().click();
cy.get('[data-test="restore-filter-button"]')
.should('be.visible')
.click();
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.should(
'have.attr',
'value',
testItems.topTenChart.filterColumnCountryCode,
);
});
it('User can create a time grain filter', () => {
prepareDashboardFilters([]);
enterNativeFilterEditModal();
fillNativeFilterForm(
testItems.filterType.timeGrain,
testItems.filterType.timeGrain,
testItems.datasetForNativeFilter,
);
saveNativeFilterSettings([SAMPLE_CHART]);
applyNativeFilterValueWithIndex(0, testItems.filterTimeGrain);
cy.get(nativeFilters.applyFilter).click();
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
});
validateFilterNameOnDashboard(testItems.filterType.timeGrain);
validateFilterContentOnDashboard(testItems.filterTimeGrain);
});
it.skip('User can create a time range filter', () => {
enterNativeFilterEditModal();
fillNativeFilterForm(
testItems.filterType.timeRange,
testItems.filterType.timeRange,
);
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
cy.get(dashboardView.salesDashboardSpecific.vehicleSalesFilterTimeRange)
.should('be.visible')
.click();
applyAdvancedTimeRangeFilterOnDashboard('2005-12-17', '2006-12-17');
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
});
validateFilterNameOnDashboard(testItems.filterType.timeRange);
cy.get(nativeFilters.filterFromDashboardView.timeRangeFilterContent)
.contains('2005-12-17')
.should('be.visible');
});
it.skip('User can create a time column filter', () => {
enterNativeFilterEditModal();
fillNativeFilterForm(
testItems.filterType.timeColumn,
testItems.filterType.timeColumn,
testItems.datasetForNativeFilter,
);
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
cy.get(nativeFilters.modal.container).should('not.exist');
// assert that native filter is created
validateFilterNameOnDashboard(testItems.filterType.timeColumn);
applyNativeFilterValueWithIndex(
0,
testItems.topTenChart.filterColumnYear,
);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.wait('@chart');
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
});
it('User can create a numerical range filter', () => {
visitDashboard();
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
saveNativeFilterSettings([]);
// assertions
cy.get(nativeFilters.slider.slider).should('be.visible').click('center');
cy.get(nativeFilters.applyFilter).click();
// assert that the url contains 'native_filters' in the url
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
// assert that the start handle has a value
cy.get(nativeFilters.slider.startHandle)
.invoke('attr', 'aria-valuenow')
.should('exist');
// assert that the end handle has a value
cy.get(nativeFilters.slider.endHandle)
.invoke('attr', 'aria-valuenow')
.should('exist');
// assert slider text matches what we should have
cy.get(nativeFilters.slider.sliderText).should('have.text', '49');
});
});
it('User can undo deleting a native filter', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
undoDeleteNativeFilter();
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.should('have.attr', 'value', testItems.topTenChart.filterColumn);
});
it('User can cancel changes in native filter', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
cy.getBySel('filters-config-modal__name-input').type('|EDITED', {
force: true,
});
cancelNativeFilterSettings();
enterNativeFilterEditModal(false);
cy.get(nativeFilters.filtersList.removeIcon).first().click();
cy.contains('You have removed this filter.').should('be.visible');
});
it('User can create a value filter', () => {
visitDashboard();
enterNativeFilterEditModal(false);
addCountryNameFilter();
cy.get(nativeFilters.filtersPanel.filterTypeInput)
.find(nativeFilters.filtersPanel.filterTypeItem)
.should('have.text', testItems.filterType.value);
saveNativeFilterSettings([]);
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
});
it('User can apply value filter with selected values', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
applyNativeFilterValueWithIndex(0, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click();
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
cy.contains(testItems.filterDefaultValue).should('be.visible');
cy.contains(testItems.filterOtherCountry).should('not.exist');
});
});
it('User can stop filtering when filter is removed', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
saveNativeFilterSettings([SAMPLE_CHART]);
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
cy.contains(testItems.filterDefaultValue).should('be.visible');
cy.contains(testItems.filterOtherCountry).should('not.exist');
});
cy.get(nativeFilters.filterItem)
.contains(testItems.filterDefaultValue)
.should('be.visible');
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
enterNativeFilterEditModal(false);
deleteNativeFilter();
saveNativeFilterSettings([SAMPLE_CHART]);
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
cy.contains(testItems.filterDefaultValue).should('be.visible');
cy.contains(testItems.filterOtherCountry).should('be.visible');
});
});
});
});
@@ -23,7 +23,6 @@ import {
exploreView,
dataTestChartName,
} from 'cypress/support/directories';
import { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
import {
addCountryNameFilter,
@@ -48,138 +47,12 @@ import {
validateFilterNameOnDashboard,
testItems,
WORLD_HEALTH_CHARTS,
interceptGet,
interceptCharts,
interceptDatasets,
interceptFilterState,
} from './utils';
const SAMPLE_CHART = { name: 'Most Populated Countries', viz: 'table' };
function visitDashboard(createSample = true) {
interceptCharts();
interceptGet();
interceptDatasets();
if (createSample) {
cy.createSampleDashboards([0]);
}
cy.visit(SAMPLE_DASHBOARD_1);
cy.wait('@get');
cy.wait('@getCharts');
cy.wait('@getDatasets');
cy.url().should('contain', 'native_filters_key');
}
function prepareDashboardFilters(
filters: { name: string; column: string; datasetId: number }[],
) {
cy.createSampleDashboards([0]);
cy.request({
method: 'GET',
url: `api/v1/dashboard/1-sample-dashboard`,
}).then(res => {
const { body } = res;
const dashboardId = body.result.id;
const allFilters: Record<string, unknown>[] = [];
filters.forEach((f, i) => {
allFilters.push({
id: `NATIVE_FILTER-fLH0pxFQ${i}`,
controlValues: {
enableEmptyFilter: false,
defaultToFirstItem: false,
multiSelect: true,
searchAllOptions: false,
inverseSelection: false,
},
name: f.name,
filterType: 'filter_select',
targets: [
{
datasetId: f.datasetId,
column: { name: f.column },
},
],
defaultDataMask: {
extraFormData: {},
filterState: {},
ownState: {},
},
cascadeParentIds: [],
scope: {
rootPath: ['ROOT_ID'],
excluded: [],
},
type: 'NATIVE_FILTER',
description: '',
chartsInScope: [5],
tabsInScope: [],
});
});
if (dashboardId) {
const jsonMetadata = {
native_filter_configuration: allFilters,
timed_refresh_immune_slices: [],
expanded_slices: {},
refresh_frequency: 0,
color_scheme: '',
label_colors: {},
shared_label_colors: {},
color_scheme_domain: [],
cross_filters_enabled: false,
positions: {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
GRID_ID: {
type: 'GRID',
id: 'GRID_ID',
children: ['ROW-0rHnUz4nMA'],
parents: ['ROOT_ID'],
},
HEADER_ID: {
id: 'HEADER_ID',
type: 'HEADER',
meta: { text: '1 - Sample dashboard' },
},
'CHART-DF6EfI55F-': {
type: 'CHART',
id: 'CHART-DF6EfI55F-',
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'ROW-0rHnUz4nMA'],
meta: {
width: 4,
height: 50,
chartId: 5,
sliceName: 'Most Populated Countries',
},
},
'ROW-0rHnUz4nMA': {
type: 'ROW',
id: 'ROW-0rHnUz4nMA',
children: ['CHART-DF6EfI55F-'],
parents: ['ROOT_ID', 'GRID_ID'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
},
default_filters: '{}',
filter_scopes: {},
chart_configuration: {},
};
return cy
.request({
method: 'PUT',
url: `api/v1/dashboard/${dashboardId}`,
body: {
json_metadata: JSON.stringify(jsonMetadata),
},
})
.then(() => visitDashboard(false));
}
return cy;
});
}
import {
prepareDashboardFilters,
SAMPLE_CHART,
visitDashboard,
} from './shared_dashboard_functions';
function selectFilter(index: number) {
cy.get("[data-test='filter-title-container'] [draggable='true']")
@@ -195,219 +68,6 @@ function closeFilterModal() {
});
}
function openVerticalFilterBar() {
cy.getBySel('dashboard-filters-panel').should('exist');
cy.getBySel('filter-bar__expand-button').click();
}
function setFilterBarOrientation(orientation: 'vertical' | 'horizontal') {
cy.getBySel('filterbar-orientation-icon').click();
cy.wait(250);
cy.getBySel('dropdown-selectable-icon-submenu')
.contains('Orientation of filter bar')
.should('exist')
.trigger('mouseover');
if (orientation === 'vertical') {
cy.get('.ant-dropdown-menu-item-selected')
.contains('Horizontal (Top)')
.should('exist');
cy.get('.ant-dropdown-menu-item').contains('Vertical (Left)').click();
cy.getBySel('dashboard-filters-panel').should('exist');
} else {
cy.get('.ant-dropdown-menu-item-selected')
.contains('Vertical (Left)')
.should('exist');
cy.get('.ant-dropdown-menu-item').contains('Horizontal (Top)').click();
cy.getBySel('loading-indicator').should('exist');
cy.getBySel('filter-bar').should('exist');
cy.getBySel('dashboard-filters-panel').should('not.exist');
}
}
function openMoreFilters(waitFilterState = true) {
interceptFilterState();
cy.getBySel('dropdown-container-btn').click();
if (waitFilterState) {
cy.wait('@postFilterState');
}
}
describe('Horizontal FilterBar', () => {
it('should go from vertical to horizontal and the opposite', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
setFilterBarOrientation('vertical');
});
it('should show all default actions in horizontal mode', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
cy.getBySel('horizontal-filterbar-empty')
.contains('No filters are currently added to this dashboard.')
.should('exist');
cy.getBySel('filter-bar__create-filter').should('exist');
cy.getBySel('filterbar-action-buttons').should('exist');
});
it('should stay in horizontal mode when reloading', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
cy.reload();
cy.getBySel('dashboard-filters-panel').should('not.exist');
});
it('should show all filters in available space on load', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.get('.filter-item-wrapper').should('have.length', 3);
});
it('should show "more filters" on window resizing up and down', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.getBySel('form-item-value').should('have.length', 3);
cy.viewport(768, 1024);
cy.getBySel('form-item-value').should('have.length', 0);
openMoreFilters(false);
cy.getBySel('form-item-value').should('have.length', 3);
cy.getBySel('filter-bar').click();
cy.viewport(1000, 1024);
openMoreFilters(false);
cy.getBySel('form-item-value').should('have.length', 3);
cy.getBySel('filter-bar').click();
cy.viewport(1300, 1024);
cy.getBySel('form-item-value').should('have.length', 3);
cy.getBySel('dropdown-container-btn').should('not.exist');
});
it('should show "more filters" and scroll', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
{ name: 'test_4', column: 'year', datasetId: 2 },
{ name: 'test_5', column: 'country_name', datasetId: 2 },
{ name: 'test_6', column: 'country_code', datasetId: 2 },
{ name: 'test_7', column: 'region', datasetId: 2 },
{ name: 'test_8', column: 'year', datasetId: 2 },
{ name: 'test_9', column: 'country_name', datasetId: 2 },
{ name: 'test_10', column: 'country_code', datasetId: 2 },
{ name: 'test_11', column: 'region', datasetId: 2 },
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
cy.get('.filter-item-wrapper').should('have.length', 3);
openMoreFilters();
cy.getBySel('form-item-value').should('have.length', 12);
cy.getBySel('filter-control-name').contains('test_10').should('be.visible');
cy.getBySel('filter-control-name')
.contains('test_12')
.should('not.be.visible');
cy.get('.ant-popover-inner-content').scrollTo('bottom');
cy.getBySel('filter-control-name').contains('test_12').should('be.visible');
});
it('should display newly added filter', () => {
visitDashboard();
openVerticalFilterBar();
setFilterBarOrientation('horizontal');
enterNativeFilterEditModal(false);
addCountryNameFilter();
saveNativeFilterSettings([]);
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
});
it('should spot changes in "more filters" and apply their values', () => {
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
{ name: 'test_4', column: 'year', datasetId: 2 },
{ name: 'test_5', column: 'country_name', datasetId: 2 },
{ name: 'test_6', column: 'country_code', datasetId: 2 },
{ name: 'test_7', column: 'region', datasetId: 2 },
{ name: 'test_8', column: 'year', datasetId: 2 },
{ name: 'test_9', column: 'country_name', datasetId: 2 },
{ name: 'test_10', column: 'country_code', datasetId: 2 },
{ name: 'test_11', column: 'region', datasetId: 2 },
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
openMoreFilters();
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.wait('@chart');
cy.get('.antd5-scroll-number.antd5-badge-count').should(
'have.attr',
'title',
'1',
);
});
it('should focus filter and open "more filters" programmatically', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
{ name: 'test_2', column: 'country_code', datasetId: 2 },
{ name: 'test_3', column: 'region', datasetId: 2 },
{ name: 'test_4', column: 'year', datasetId: 2 },
{ name: 'test_5', column: 'country_name', datasetId: 2 },
{ name: 'test_6', column: 'country_code', datasetId: 2 },
{ name: 'test_7', column: 'region', datasetId: 2 },
{ name: 'test_8', column: 'year', datasetId: 2 },
{ name: 'test_9', column: 'country_name', datasetId: 2 },
{ name: 'test_10', column: 'country_code', datasetId: 2 },
{ name: 'test_11', column: 'region', datasetId: 2 },
{ name: 'test_12', column: 'year', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
openMoreFilters();
applyNativeFilterValueWithIndex(8, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.getBySel('slice-header').within(() => {
cy.get('.filter-counts').trigger('mouseover');
});
cy.get('.filterStatusPopover').contains('test_9').click();
cy.getBySel('dropdown-content').should('be.visible');
cy.get('.ant-select-focused').should('be.visible');
});
it('should show tag count and one plain tag on focus and only count on blur in select ', () => {
prepareDashboardFilters([
{ name: 'test_1', column: 'country_name', datasetId: 2 },
]);
setFilterBarOrientation('horizontal');
enterNativeFilterEditModal();
inputNativeFilterDefaultValue('Albania');
cy.get('.ant-select-selection-search-input').clear({ force: true });
inputNativeFilterDefaultValue('Algeria', true);
saveNativeFilterSettings([SAMPLE_CHART]);
cy.getBySel('filter-bar').within(() => {
cy.get(nativeFilters.filterItem).contains('Albania').should('be.visible');
cy.get(nativeFilters.filterItem).contains('+ 1 ...').should('be.visible');
cy.get('.ant-select-selection-search-input').click();
cy.get(nativeFilters.filterItem).contains('+ 2 ...').should('be.visible');
});
});
});
describe('Native filters', () => {
describe('Nativefilters tests initial state required', () => {
beforeEach(() => {
@@ -650,220 +310,4 @@ describe('Native filters', () => {
cy.contains('Values are dependent on other filters').should('exist');
});
});
describe('Nativefilters initial state not required', () => {
it("User can check 'Filter has default value'", () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
});
it('User can add a new native filter', () => {
prepareDashboardFilters([]);
let filterKey: string;
const removeFirstChar = (search: string) =>
search.split('').slice(1, search.length).join('');
cy.location().then(loc => {
cy.url().should('contain', 'native_filters_key');
const queryParams = qs.parse(removeFirstChar(loc.search));
filterKey = queryParams.native_filters_key as string;
expect(typeof filterKey).eq('string');
});
enterNativeFilterEditModal();
addCountryNameFilter();
saveNativeFilterSettings([SAMPLE_CHART]);
cy.location().then(loc => {
cy.url().should('contain', 'native_filters_key');
const queryParams = qs.parse(removeFirstChar(loc.search));
const newfilterKey = queryParams.native_filters_key;
expect(newfilterKey).eq(filterKey);
});
cy.get(nativeFilters.modal.container).should('not.exist');
});
it('User can restore a deleted native filter', () => {
prepareDashboardFilters([
{ name: 'country_code', column: 'country_code', datasetId: 2 },
]);
enterNativeFilterEditModal();
cy.get(nativeFilters.filtersList.removeIcon).first().click();
cy.get('[data-test="restore-filter-button"]')
.should('be.visible')
.click();
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.should(
'have.attr',
'value',
testItems.topTenChart.filterColumnCountryCode,
);
});
it('User can create a time grain filter', () => {
prepareDashboardFilters([]);
enterNativeFilterEditModal();
fillNativeFilterForm(
testItems.filterType.timeGrain,
testItems.filterType.timeGrain,
testItems.datasetForNativeFilter,
);
saveNativeFilterSettings([SAMPLE_CHART]);
applyNativeFilterValueWithIndex(0, testItems.filterTimeGrain);
cy.get(nativeFilters.applyFilter).click();
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
});
validateFilterNameOnDashboard(testItems.filterType.timeGrain);
validateFilterContentOnDashboard(testItems.filterTimeGrain);
});
it.skip('User can create a time range filter', () => {
enterNativeFilterEditModal();
fillNativeFilterForm(
testItems.filterType.timeRange,
testItems.filterType.timeRange,
);
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
cy.get(dashboardView.salesDashboardSpecific.vehicleSalesFilterTimeRange)
.should('be.visible')
.click();
applyAdvancedTimeRangeFilterOnDashboard('2005-12-17', '2006-12-17');
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
});
validateFilterNameOnDashboard(testItems.filterType.timeRange);
cy.get(nativeFilters.filterFromDashboardView.timeRangeFilterContent)
.contains('2005-12-17')
.should('be.visible');
});
it.skip('User can create a time column filter', () => {
enterNativeFilterEditModal();
fillNativeFilterForm(
testItems.filterType.timeColumn,
testItems.filterType.timeColumn,
testItems.datasetForNativeFilter,
);
saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
cy.get(nativeFilters.modal.container).should('not.exist');
// assert that native filter is created
validateFilterNameOnDashboard(testItems.filterType.timeColumn);
applyNativeFilterValueWithIndex(
0,
testItems.topTenChart.filterColumnYear,
);
cy.get(nativeFilters.applyFilter).click({ force: true });
cy.wait('@chart');
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
});
it('User can create a numerical range filter', () => {
visitDashboard();
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
saveNativeFilterSettings([]);
// assertions
cy.get(nativeFilters.slider.slider).should('be.visible').click('center');
cy.get(nativeFilters.applyFilter).click();
// assert that the url contains 'native_filters' in the url
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
// assert that the start handle has a value
cy.get(nativeFilters.slider.startHandle)
.invoke('attr', 'aria-valuenow')
.should('exist');
// assert that the end handle has a value
cy.get(nativeFilters.slider.endHandle)
.invoke('attr', 'aria-valuenow')
.should('exist');
// assert slider text matches what we should have
cy.get(nativeFilters.slider.sliderText).should('have.text', '49');
});
});
it('User can undo deleting a native filter', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
undoDeleteNativeFilter();
cy.get(nativeFilters.modal.container)
.find(nativeFilters.filtersPanel.filterName)
.should('have.attr', 'value', testItems.topTenChart.filterColumn);
});
it('User can cancel changes in native filter', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
cy.getBySel('filters-config-modal__name-input').type('|EDITED', {
force: true,
});
cancelNativeFilterSettings();
enterNativeFilterEditModal(false);
cy.get(nativeFilters.filtersList.removeIcon).first().click();
cy.contains('You have removed this filter.').should('be.visible');
});
it('User can create a value filter', () => {
visitDashboard();
enterNativeFilterEditModal(false);
addCountryNameFilter();
cy.get(nativeFilters.filtersPanel.filterTypeInput)
.find(nativeFilters.filtersPanel.filterTypeItem)
.should('have.text', testItems.filterType.value);
saveNativeFilterSettings([]);
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
});
it('User can apply value filter with selected values', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
applyNativeFilterValueWithIndex(0, testItems.filterDefaultValue);
cy.get(nativeFilters.applyFilter).click();
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
cy.contains(testItems.filterDefaultValue).should('be.visible');
cy.contains(testItems.filterOtherCountry).should('not.exist');
});
});
it('User can stop filtering when filter is removed', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);
enterNativeFilterEditModal();
inputNativeFilterDefaultValue(testItems.filterDefaultValue);
saveNativeFilterSettings([SAMPLE_CHART]);
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
cy.contains(testItems.filterDefaultValue).should('be.visible');
cy.contains(testItems.filterOtherCountry).should('not.exist');
});
cy.get(nativeFilters.filterItem)
.contains(testItems.filterDefaultValue)
.should('be.visible');
validateFilterNameOnDashboard(testItems.topTenChart.filterColumn);
enterNativeFilterEditModal(false);
deleteNativeFilter();
saveNativeFilterSettings([SAMPLE_CHART]);
cy.get(dataTestChartName(testItems.topTenChart.name)).within(() => {
cy.contains(testItems.filterDefaultValue).should('be.visible');
cy.contains(testItems.filterOtherCountry).should('be.visible');
});
});
});
});
@@ -0,0 +1,148 @@
/**
* 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 { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls';
import { interceptCharts, interceptDatasets, interceptGet } from './utils';
export const SAMPLE_CHART = { name: 'Most Populated Countries', viz: 'table' };
export function visitDashboard(createSample = true) {
interceptCharts();
interceptGet();
interceptDatasets();
if (createSample) {
cy.createSampleDashboards([0]);
}
cy.visit(SAMPLE_DASHBOARD_1);
cy.wait('@get');
cy.wait('@getCharts');
cy.wait('@getDatasets');
cy.url().should('contain', 'native_filters_key');
}
export function prepareDashboardFilters(
filters: { name: string; column: string; datasetId: number }[],
) {
cy.createSampleDashboards([0]);
cy.request({
method: 'GET',
url: `api/v1/dashboard/1-sample-dashboard`,
}).then(res => {
const { body } = res;
const dashboardId = body.result.id;
const allFilters: Record<string, unknown>[] = [];
filters.forEach((f, i) => {
allFilters.push({
id: `NATIVE_FILTER-fLH0pxFQ${i}`,
controlValues: {
enableEmptyFilter: false,
defaultToFirstItem: false,
multiSelect: true,
searchAllOptions: false,
inverseSelection: false,
},
name: f.name,
filterType: 'filter_select',
targets: [
{
datasetId: f.datasetId,
column: { name: f.column },
},
],
defaultDataMask: {
extraFormData: {},
filterState: {},
ownState: {},
},
cascadeParentIds: [],
scope: {
rootPath: ['ROOT_ID'],
excluded: [],
},
type: 'NATIVE_FILTER',
description: '',
chartsInScope: [5],
tabsInScope: [],
});
});
if (dashboardId) {
const jsonMetadata = {
native_filter_configuration: allFilters,
timed_refresh_immune_slices: [],
expanded_slices: {},
refresh_frequency: 0,
color_scheme: '',
label_colors: {},
shared_label_colors: {},
color_scheme_domain: [],
cross_filters_enabled: false,
positions: {
DASHBOARD_VERSION_KEY: 'v2',
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
GRID_ID: {
type: 'GRID',
id: 'GRID_ID',
children: ['ROW-0rHnUz4nMA'],
parents: ['ROOT_ID'],
},
HEADER_ID: {
id: 'HEADER_ID',
type: 'HEADER',
meta: { text: '1 - Sample dashboard' },
},
'CHART-DF6EfI55F-': {
type: 'CHART',
id: 'CHART-DF6EfI55F-',
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'ROW-0rHnUz4nMA'],
meta: {
width: 4,
height: 50,
chartId: 5,
sliceName: 'Most Populated Countries',
},
},
'ROW-0rHnUz4nMA': {
type: 'ROW',
id: 'ROW-0rHnUz4nMA',
children: ['CHART-DF6EfI55F-'],
parents: ['ROOT_ID', 'GRID_ID'],
meta: { background: 'BACKGROUND_TRANSPARENT' },
},
},
default_filters: '{}',
filter_scopes: {},
chart_configuration: {},
};
return cy
.request({
method: 'PUT',
url: `api/v1/dashboard/${dashboardId}`,
body: {
json_metadata: JSON.stringify(jsonMetadata),
},
})
.then(() => visitDashboard(false));
}
return cy;
});
}
@@ -25,7 +25,7 @@ describe('AdhocMetrics', () => {
});
it('Clear metric and set simple adhoc metric', () => {
const metric = 'SUM(num_girls)';
const metric = 'sum(num_girls)';
const metricName = 'Sum Girls';
cy.get('[data-test=metrics]')
.find('[data-test="remove-control-button"]')
@@ -100,7 +100,7 @@ describe('Visualization > Table', () => {
});
cy.verifySliceSuccess({
waitAlias: '@chartData',
querySubstring: /group by\n.*name/i,
querySubstring: /GROUP BY.*name/i,
chartSelector: 'table',
});
});
@@ -247,7 +247,7 @@ describe('Visualization > Table', () => {
cy.visitChartByParams(formData);
cy.verifySliceSuccess({
waitAlias: '@chartData',
querySubstring: /group by\n.*state/i,
querySubstring: /GROUP BY.*state/i,
chartSelector: 'table',
});
cy.get('td').contains(/\d*%/);
+6 -6
View File
@@ -8445,9 +8445,9 @@
"peer": true
},
"node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"dependencies": {
"isarray": "0.0.1"
}
@@ -16793,9 +16793,9 @@
"peer": true
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"requires": {
"isarray": "0.0.1"
}
+1 -1
View File
@@ -56,7 +56,7 @@ module.exports = {
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
snapshotSerializers: ['@emotion/jest/enzyme-serializer'],
transformIgnorePatterns: [
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender)',
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms)',
],
globals: {
__DEV__: true,
+4316 -1460
View File
File diff suppressed because it is too large Load Diff
+17 -16
View File
@@ -44,8 +44,8 @@
"build-storybook": "storybook build",
"build-translation": "scripts/po2json.sh",
"bundle-stats": "cross-env BUNDLE_ANALYZER=true npm run build && npx open-cli ../superset/static/stats/statistics.html",
"core:cover": "cross-env NODE_ENV=test jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\"]' packages",
"cover": "cross-env NODE_ENV=test jest --coverage",
"core:cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\"]' packages",
"cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage",
"dev": "webpack --mode=development --color --watch",
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
"eslint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx",
@@ -67,8 +67,8 @@
"prod": "npm run build",
"prune": "rm -rf ./{packages,plugins}/*/{lib,esm,tsconfig.tsbuildinfo,package-lock.json}",
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
"tdd": "cross-env NODE_ENV=test jest --watch",
"test": "cross-env NODE_ENV=test jest --max-workers=50%",
"tdd": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --watch",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --max-workers=50%",
"type": "tsc --noEmit",
"update-maps": "jupyter nbconvert --to notebook --execute --inplace 'plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb' -Xfrozen_modules=off",
"validate-release": "../RELEASING/validate_this_release.sh"
@@ -82,12 +82,12 @@
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.13.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.3.0",
"@fontsource/fira-code": "^5.0.18",
"@fontsource/inter": "^5.0.20",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.19.3",
"@rjsf/core": "^5.21.1",
"@rjsf/utils": "^5.19.3",
"@rjsf/validator-ajv8": "^5.19.3",
"@scarf/scarf": "^1.3.0",
@@ -135,6 +135,7 @@
"classnames": "^2.2.5",
"core-js": "^3.38.1",
"d3-scale": "^2.1.2",
"dayjs": "^1.11.13",
"dom-to-image-more": "^3.2.0",
"emotion-rgba": "0.0.12",
"fast-glob": "^3.3.2",
@@ -164,7 +165,7 @@
"prop-types": "^15.8.1",
"query-string": "^6.13.7",
"rc-trigger": "^5.3.4",
"re-resizable": "^6.9.11",
"re-resizable": "^6.10.0",
"react": "^16.13.1",
"react-ace": "^10.1.0",
"react-checkbox-tree": "^1.8.0",
@@ -210,7 +211,7 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.50.7",
"@applitools/eyes-storybook": "^3.50.9",
"@babel/cli": "^7.22.6",
"@babel/compat-data": "^7.22.6",
"@babel/core": "^7.23.9",
@@ -226,7 +227,7 @@
"@babel/preset-react": "^7.22.5",
"@babel/register": "^7.23.7",
"@babel/types": "^7.24.9",
"@cypress/react": "^5.10.0",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/jest": "^11.11.0",
"@hot-loader/react-dom": "^16.14.0",
@@ -243,10 +244,10 @@
"@storybook/react-webpack5": "8.1.11",
"@svgr/webpack": "^8.1.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^5.1.3",
"@testing-library/user-event": "^12.7.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3",
"@types/classnames": "^2.2.10",
"@types/dom-to-image": "^2.6.7",
"@types/enzyme": "^3.10.18",
@@ -261,7 +262,7 @@
"@types/react-dom": "^16.9.8",
"@types/react-gravatar": "^2.6.14",
"@types/react-json-tree": "^0.6.11",
"@types/react-loadable": "^5.5.6",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -290,13 +291,13 @@
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.7",
"esbuild": "^0.20.0",
"esbuild-loader": "^4.1.0",
"esbuild-loader": "^4.2.2",
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^7.2.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^3.5.0",
"eslint-plugin-file-progress": "^1.4.0",
"eslint-plugin-file-progress": "^1.5.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^27.8.0",
"eslint-plugin-jest-dom": "^3.6.5",
@@ -353,7 +354,7 @@
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.15.1",
"webpack-manifest-plugin": "^4.1.1",
"webpack-manifest-plugin": "^5.0.0",
"webpack-sources": "^3.2.3",
"webpack-visualizer-plugin2": "^1.1.0",
"xdm": "^3.4.0"
@@ -3,4 +3,5 @@ module.exports = {
moduleNameMapper: {
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/test/__mocks__/mockExportString.js',
},
testEnvironment: 'jsdom',
};
@@ -37,6 +37,7 @@
"@babel/cli": "^7.16.0",
"@types/jest": "^26.0.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^3.0.2",
"typescript": "^4.1.2"
}
@@ -19,7 +19,6 @@
import { t, validateNonEmpty } from '@superset-ui/core';
import {
ControlPanelConfig,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@@ -35,10 +35,10 @@
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^5.0.3",
"@testing-library/user-event": "^12.7.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"antd": "4.10.3",
"brace": "^0.11.1",
@@ -17,7 +17,7 @@
* under the License.
*/
import { useState, ReactNode, useLayoutEffect, RefObject } from 'react';
import { css, styled, SupersetTheme } from '@superset-ui/core';
import { css, SafeMarkdown, styled, SupersetTheme } from '@superset-ui/core';
import { Tooltip } from './Tooltip';
import { ColumnTypeLabel } from './ColumnTypeLabel/ColumnTypeLabel';
import CertifiedIconWithTooltip from './CertifiedIconWithTooltip';
@@ -28,6 +28,7 @@ import {
getColumnTypeTooltipNode,
} from './labelUtils';
import { SQLPopover } from './SQLPopover';
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
export type ColumnOptionProps = {
column: ColumnMeta;
@@ -50,6 +51,8 @@ export function ColumnOption({
}: ColumnOptionProps) {
const { expression, column_name, type_generic } = column;
const hasExpression = expression && expression !== column_name;
const warningMarkdown =
column.warning_markdown || column.warning_text || column.error_text;
const type = hasExpression ? 'expression' : type_generic;
const [tooltipText, setTooltipText] = useState<ReactNode>(column.column_name);
const [columnTypeTooltipText, setcolumnTypeTooltipText] = useState<ReactNode>(
@@ -94,6 +97,19 @@ export function ColumnOption({
details={column.certification_details}
/>
)}
{warningMarkdown && (
<InfoTooltipWithTrigger
className="text-warning"
icon="warning"
tooltip={<SafeMarkdown source={warningMarkdown} />}
label={`warn-${column.column_name}`}
iconsStyle={{ marginLeft: 0 }}
{...(column.error_text && {
className: 'text-danger',
icon: 'exclamation-circle',
})}
/>
)}
</StyleOverrides>
);
}
@@ -81,7 +81,8 @@ export function MetricOption({
</span>
);
const warningMarkdown = metric.warning_markdown || metric.warning_text;
const warningMarkdown =
metric.warning_markdown || metric.warning_text || metric.error_text;
const [tooltipText, setTooltipText] = useState<ReactNode>(metric.metric_name);
@@ -116,6 +117,10 @@ export function MetricOption({
tooltip={<SafeMarkdown source={warningMarkdown} />}
label={`warn-${metric.metric_name}`}
iconsStyle={{ marginLeft: 0 }}
{...(metric.error_text && {
className: 'text-danger',
icon: 'exclamation-circle',
})}
/>
)}
</FlexRowContainer>
@@ -39,6 +39,7 @@ import {
SORT_SERIES_CHOICES,
} from '../constants';
import { checkColumnType } from '../utils/checkColumnType';
import { isSortable } from '../utils/isSortable';
export const contributionModeControl = {
name: 'contributionMode',
@@ -55,27 +56,6 @@ export const contributionModeControl = {
},
};
function isForcedCategorical(controls: ControlStateMapping): boolean {
return (
checkColumnType(
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
controls?.datasource?.datasource,
[GenericDataType.Numeric],
) && !!controls?.xAxisForceCategorical?.value
);
}
function isSortable(controls: ControlStateMapping): boolean {
return (
isForcedCategorical(controls) ||
checkColumnType(
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
controls?.datasource?.datasource,
[GenericDataType.String, GenericDataType.Boolean],
)
);
}
const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) =>
isSortable(controls) &&
ensureIsArray(controls?.groupby?.value).length === 0 &&
@@ -83,6 +83,7 @@ export interface Dataset {
owners?: Owner[];
filter_select?: boolean;
filter_select_enabled?: boolean;
column_names?: string[];
}
export interface ControlPanelState {
@@ -57,6 +57,8 @@ export const D3_FORMAT_OPTIONS: [string, string][] = [
...d3Formatted,
['DURATION', t('Duration in ms (66000 => 1m 6s)')],
['DURATION_SUB', t('Duration in ms (1.40008 => 1ms 400µs 80ns)')],
['MEMORY_DECIMAL', t('Memory in bytes - decimal (1024B => 1.024kB)')],
['MEMORY_BINARY', t('Memory in bytes - binary (1024B => 1KiB)')],
];
export const D3_TIME_FORMAT_DOCS = t(
@@ -0,0 +1,51 @@
/**
* 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 {
GenericDataType,
getColumnLabel,
isPhysicalColumn,
QueryFormColumn,
} from '@superset-ui/core';
import { checkColumnType, ControlStateMapping } from '..';
export function isSortable(controls: ControlStateMapping): boolean {
const isForcedCategorical =
checkColumnType(
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
controls?.datasource?.datasource,
[GenericDataType.Numeric],
) && !!controls?.xAxisForceCategorical?.value;
const xAxisValue = controls?.x_axis?.value as QueryFormColumn;
// Given that we don't know the type of a custom SQL column,
// we treat it as sortable and give the responsibility to the
// user to provide a sortable result.
const isCustomSQL = !isPhysicalColumn(xAxisValue);
return (
isForcedCategorical ||
isCustomSQL ||
checkColumnType(
getColumnLabel(xAxisValue),
controls?.datasource?.datasource,
[GenericDataType.String, GenericDataType.Boolean],
)
);
}
@@ -34,6 +34,9 @@ jest.mock('../../src/components/ColumnTypeLabel/ColumnTypeLabel', () => ({
<div data-test="mock-column-type-label">{type}</div>
),
}));
jest.mock('../../src/components/InfoTooltipWithTrigger', () => () => (
<div data-test="mock-info-tooltip-with-trigger" />
));
const defaultProps: ColumnOptionProps = {
column: {
@@ -111,3 +114,17 @@ test('dttm column has correct column label if showType is true', () => {
String(GenericDataType.Temporal),
);
});
test('doesnt show InfoTooltipWithTrigger when no warning', () => {
const { queryByText } = setup();
expect(queryByText('mock-info-tooltip-with-trigger')).not.toBeInTheDocument();
});
test('shows a warning with InfoTooltipWithTrigger when it contains warning', () => {
const { getByTestId } = setup({
...defaultProps,
column: {
...defaultProps.column,
warning_text: 'This is a warning',
},
});
expect(getByTestId('mock-info-tooltip-with-trigger')).toBeInTheDocument();
});
@@ -0,0 +1,70 @@
/**
* 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 { ControlStateMapping } from '@superset-ui/chart-controls';
import { GenericDataType } from '@superset-ui/core';
import { isSortable } from '../../src/utils/isSortable';
const controls: ControlStateMapping = {
datasource: {
datasource: {
columns: [
{ column_name: 'a', type_generic: GenericDataType.String },
{ column_name: 'b', type_generic: GenericDataType.Numeric },
{ column_name: 'c', type_generic: GenericDataType.Boolean },
],
},
type: 'Select',
},
};
test('should return true if the column is forced to be categorical', () => {
const c: ControlStateMapping = {
...controls,
x_axis: { value: 'b', type: 'Select' },
xAxisForceCategorical: { value: true, type: 'Checkbox' },
};
expect(isSortable(c)).toBe(true);
});
test('should return true if the column is a custom SQL column', () => {
const c: ControlStateMapping = {
...controls,
x_axis: {
value: { label: 'custom_sql', sqlExpression: 'MAX(ID)' },
type: 'Select',
},
};
expect(isSortable(c)).toBe(true);
});
test('should return true if the column type is String or Boolean', () => {
const c: ControlStateMapping = {
...controls,
x_axis: { value: 'c', type: 'Checkbox' },
};
expect(isSortable(c)).toBe(true);
});
test('should return false if none of the conditions are met', () => {
const c: ControlStateMapping = {
...controls,
x_axis: { value: 'b', type: 'Input' },
};
expect(isSortable(c)).toBe(false);
});
@@ -25,21 +25,7 @@
],
"dependencies": {
"@babel/runtime": "^7.25.6",
"@testing-library/react-hooks": "^5.1.3",
"@types/d3-format": "^1.3.0",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^2.1.1",
"@types/d3-time": "^3.0.3",
"@types/d3-time-format": "^4.0.3",
"@types/enzyme": "^3.10.18",
"@types/fetch-mock": "^7.3.8",
"@types/json-bigint": "^1.0.4",
"@types/lodash": "^4.17.7",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.5.4",
"@types/prop-types": "^15.7.2",
"@types/rison": "0.0.9",
"@types/seedrandom": "^3.0.8",
"@vx/responsive": "^0.0.199",
"csstype": "^3.1.3",
"d3-format": "^1.3.2",
@@ -49,9 +35,9 @@
"d3-time-format": "^4.1.0",
"fetch-retry": "^6.0.0",
"jed": "^1.1.1",
"lodash": "^4.17.11",
"lodash": "^4.17.21",
"math-expression-evaluator": "^1.3.8",
"pretty-ms": "^7.0.0",
"pretty-ms": "^9.1.0",
"react-error-boundary": "^1.2.5",
"react-markdown": "^8.0.7",
"rehype-raw": "^7.0.0",
@@ -65,8 +51,21 @@
},
"devDependencies": {
"@emotion/styled": "^11.3.0",
"fetch-mock": "^6.5.2",
"jest-mock-console": "^1.0.0",
"@types/d3-format": "^1.3.0",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^2.1.1",
"@types/d3-time": "^3.0.3",
"@types/d3-time-format": "^4.0.3",
"@types/enzyme": "^3.10.18",
"@types/fetch-mock": "^7.3.8",
"@types/lodash": "^4.17.7",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.5.4",
"@types/prop-types": "^15.7.2",
"@types/rison": "0.0.9",
"@types/seedrandom": "^3.0.8",
"fetch-mock": "^11.1.4",
"jest-mock-console": "^2.0.0",
"resize-observer-polyfill": "1.5.1",
"timezone-mock": "1.3.6"
},
@@ -75,9 +74,10 @@
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^12.7.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
"@types/tinycolor2": "*",
@@ -50,6 +50,9 @@ export interface ChartMetadataConfig {
labelExplanation?: string | null;
queryObjectCount?: number;
parseMethod?: ParseMethod;
// suppressContextMenu: true hides the default context menu for the chart.
// This is useful for viz plugins that define their own context menu.
suppressContextMenu?: boolean;
}
export default class ChartMetadata {
@@ -91,6 +94,8 @@ export default class ChartMetadata {
parseMethod: ParseMethod;
suppressContextMenu?: boolean;
constructor(config: ChartMetadataConfig) {
const {
name,
@@ -111,6 +116,7 @@ export default class ChartMetadata {
labelExplanation = null,
queryObjectCount = 1,
parseMethod = 'json-bigint',
suppressContextMenu = false,
} = config;
this.name = name;
@@ -140,6 +146,7 @@ export default class ChartMetadata {
this.labelExplanation = labelExplanation;
this.queryObjectCount = queryObjectCount;
this.parseMethod = parseMethod;
this.suppressContextMenu = suppressContextMenu;
}
canBeAnnotationType(type: string): boolean {
@@ -17,7 +17,7 @@
* under the License.
*/
import prettyMsFormatter from 'pretty-ms';
import prettyMilliseconds, { Options } from 'pretty-ms';
import NumberFormatter from '../NumberFormatter';
export default function createDurationFormatter(
@@ -26,13 +26,14 @@ export default function createDurationFormatter(
id?: string;
label?: string;
multiplier?: number;
} & prettyMsFormatter.Options = {},
} & Options = {},
) {
const { description, id, label, multiplier = 1, ...prettyMsOptions } = config;
return new NumberFormatter({
description,
formatFunc: value => prettyMsFormatter(value * multiplier, prettyMsOptions),
formatFunc: value =>
prettyMilliseconds(value * multiplier, prettyMsOptions),
id: id ?? 'duration_format',
label: label ?? `Duration formatter`,
});
@@ -0,0 +1,55 @@
/*
* 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 NumberFormatter from '../NumberFormatter';
export default function createMemoryFormatter(
config: {
description?: string;
id?: string;
label?: string;
binary?: boolean;
decimals?: number;
} = {},
) {
const { description, id, label, binary, decimals = 2 } = config;
return new NumberFormatter({
description,
formatFunc: value => {
if (value === 0) return '0B';
const sign = value > 0 ? '' : '-';
const absValue = Math.abs(value);
const suffixes = binary
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'];
const base = binary ? 1024 : 1000;
const i = Math.min(
suffixes.length - 1,
Math.floor(Math.log(absValue) / Math.log(base)),
);
return `${sign}${parseFloat((absValue / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`;
},
id: id ?? 'memory_format',
label: label ?? `Memory formatter`,
});
}
@@ -31,5 +31,6 @@ export {
export { default as NumberFormatterRegistry } from './NumberFormatterRegistry';
export { default as createD3NumberFormatter } from './factories/createD3NumberFormatter';
export { default as createDurationFormatter } from './factories/createDurationFormatter';
export { default as createMemoryFormatter } from './factories/createMemoryFormatter';
export { default as createSiAtMostNDigitFormatter } from './factories/createSiAtMostNDigitFormatter';
export { default as createSmartNumberFormatter } from './factories/createSmartNumberFormatter';
@@ -80,6 +80,24 @@ export type Filter = {
description: string;
};
export type AppliedFilter = {
values: {
filters: Record<string, any>[];
} | null;
};
export type AppliedCrossFilterType = {
filterType: undefined;
targets: number[];
scope: number[];
} & AppliedFilter;
export type AppliedNativeFilterType = {
filterType: 'filter_select';
scope: number[];
targets: Partial<NativeFilterTarget>[];
} & AppliedFilter;
export type FilterWithDataMask = Filter & { dataMask: DataMaskWithId };
export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
@@ -89,6 +107,24 @@ export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
type: typeof NativeFilterType.Divider;
};
export function isAppliedCrossFilterType(
filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
): filterElement is AppliedCrossFilterType {
return (
filterElement.filterType === undefined &&
filterElement.hasOwnProperty('values')
);
}
export function isAppliedNativeFilterType(
filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
): filterElement is AppliedNativeFilterType {
return (
filterElement.filterType === 'filter_select' &&
filterElement.hasOwnProperty('values')
);
}
export function isNativeFilter(
filterElement: Filter | Divider,
): filterElement is Filter {
@@ -71,6 +71,7 @@ export interface Metric {
verbose_name?: string;
warning_markdown?: Maybe<string>;
warning_text?: Maybe<string>;
error_text?: string;
}
export function isSavedMetric(metric: any): metric is SavedMetric {
@@ -227,6 +227,7 @@ export const ErrorTypeEnum = {
ASYNC_WORKERS_ERROR: 'ASYNC_WORKERS_ERROR',
ADHOC_SUBQUERY_NOT_ALLOWED_ERROR: 'ADHOC_SUBQUERY_NOT_ALLOWED_ERROR',
INVALID_SQL_ERROR: 'INVALID_SQL_ERROR',
RESULT_TOO_LARGE_ERROR: 'RESULT_TOO_LARGE_ERROR',
// Generic errors
GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR',
@@ -49,7 +49,7 @@ describe('ChartClient', () => {
chartClient = new ChartClient();
});
afterEach(fetchMock.restore);
afterEach(() => fetchMock.restore());
describe('new ChartClient(config)', () => {
it('creates a client without argument', () => {
@@ -17,9 +17,10 @@
* under the License.
*/
import '@testing-library/jest-dom';
import { ComponentType } from 'react';
import { shallow } from 'enzyme';
import mockConsole, { RestoreConsole } from 'jest-mock-console';
import { render as renderTestComponent, screen } from '@testing-library/react';
import createLoadableRenderer, {
LoadableRenderer as LoadableRendererType,
} from '../../../src/chart/components/createLoadableRenderer';
@@ -67,7 +68,7 @@ describe('createLoadableRenderer', () => {
it('calls onRenderSuccess when succeeds', async () => {
const onRenderSuccess = jest.fn();
const onRenderFailure = jest.fn();
shallow(
renderTestComponent(
<LoadableRenderer
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure}
@@ -95,7 +96,7 @@ describe('createLoadableRenderer', () => {
});
const onRenderSuccess = jest.fn();
const onRenderFailure = jest.fn();
shallow(
renderTestComponent(
<FailedRenderer
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure}
@@ -122,7 +123,7 @@ describe('createLoadableRenderer', () => {
loading,
render,
});
shallow(<FailedRenderer />);
renderTestComponent(<FailedRenderer />);
expect(loadChartFailure).toHaveBeenCalledTimes(1);
setTimeout(() => {
expect(render).not.toHaveBeenCalled();
@@ -132,12 +133,12 @@ describe('createLoadableRenderer', () => {
it('renders the lazy-load components', () =>
new Promise(done => {
const wrapper = shallow(<LoadableRenderer />);
renderTestComponent(<LoadableRenderer />);
// lazy-loaded component not rendered immediately
expect(wrapper.find(TestComponent)).toHaveLength(0);
expect(screen.queryByText('test')).not.toBeInTheDocument();
setTimeout(() => {
// but rendered after the component is loaded.
expect(wrapper.find(TestComponent)).toHaveLength(1);
expect(screen.queryByText('test')).toBeInTheDocument();
done(undefined);
}, 10);
}));
@@ -149,7 +150,7 @@ describe('createLoadableRenderer', () => {
render: () => <div />,
});
expect(() => shallow(<NeverLoadingRenderer />)).not.toThrow();
expect(() => renderTestComponent(<NeverLoadingRenderer />)).not.toThrow();
});
});
});
@@ -22,13 +22,11 @@ import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
describe('SupersetClient', () => {
beforeAll(() => {
fetchMock.get(LOGIN_GLOB, { result: '' });
});
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '' }));
afterAll(fetchMock.restore);
afterAll(() => fetchMock.restore());
afterEach(SupersetClient.reset);
afterEach(() => SupersetClient.reset());
it('exposes reset, configure, init, get, post, postForm, isAuthenticated, and reAuthenticate methods', () => {
expect(typeof SupersetClient.configure).toBe('function');
@@ -21,11 +21,12 @@ import { SupersetClientClass, ClientConfig, CallApi } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
describe('SupersetClientClass', () => {
beforeAll(() => {
beforeEach(() => {
fetchMock.reset();
fetchMock.get(LOGIN_GLOB, { result: '' });
});
afterAll(fetchMock.restore);
afterAll(() => fetchMock.restore());
describe('new SupersetClientClass()', () => {
it('fallback protocol to https when setting only host', () => {
@@ -78,11 +79,10 @@ describe('SupersetClientClass', () => {
});
describe('.init()', () => {
afterEach(() => {
fetchMock.reset();
// reset
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true });
});
beforeEach(() =>
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true }),
);
afterEach(() => fetchMock.reset());
it('calls api/v1/security/csrf_token/ when init() is called if no CSRF token is passed', async () => {
expect.assertions(1);
@@ -165,7 +165,7 @@ describe('SupersetClientClass', () => {
});
describe('.isAuthenticated()', () => {
afterEach(fetchMock.reset);
afterEach(() => fetchMock.reset());
it('returns true if there is a token and false if not', async () => {
expect.assertions(2);
@@ -254,7 +254,8 @@ describe('SupersetClientClass', () => {
});
describe('requests', () => {
afterEach(fetchMock.reset);
afterEach(() => fetchMock.restore());
const protocol = 'https:';
const host = 'host';
const mockGetEndpoint = '/get/url';
@@ -272,13 +273,15 @@ describe('SupersetClientClass', () => {
const mockTextJsonResponse = '{ "value": 9223372036854775807 }';
const mockPayload = { json: () => Promise.resolve('payload') };
fetchMock.get(mockGetUrl, mockPayload);
fetchMock.post(mockPostUrl, mockPayload);
fetchMock.put(mockPutUrl, mockPayload);
fetchMock.delete(mockDeleteUrl, mockPayload);
fetchMock.delete(mockRequestUrl, mockPayload);
fetchMock.get(mockTextUrl, mockTextJsonResponse);
fetchMock.post(mockTextUrl, mockTextJsonResponse);
beforeEach(() => {
fetchMock.get(mockGetUrl, mockPayload);
fetchMock.post(mockPostUrl, mockPayload);
fetchMock.put(mockPutUrl, mockPayload);
fetchMock.delete(mockDeleteUrl, mockPayload);
fetchMock.delete(mockRequestUrl, mockPayload);
fetchMock.get(mockTextUrl, mockTextJsonResponse);
fetchMock.post(mockTextUrl, mockTextJsonResponse);
});
it('checks for authentication before every get and post request', async () => {
expect.assertions(6);
@@ -623,6 +626,8 @@ describe('SupersetClientClass', () => {
let createElement: any;
beforeEach(async () => {
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true });
client = new SupersetClientClass({ protocol, host });
authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');
await client.init();
@@ -29,43 +29,41 @@ const corruptObject = new BadObject();
/* @ts-expect-error */
BadObject.prototype.toString = undefined;
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
const mockPutUrl = '/mock/put/url';
const mockPatchUrl = '/mock/patch/url';
const mockCacheUrl = '/mock/cache/url';
const mockNotFound = '/mock/notfound';
const mockErrorUrl = '/mock/error/url';
const mock503 = '/mock/503';
const mockGetPayload = { get: 'payload' };
const mockPostPayload = { post: 'payload' };
const mockPutPayload = { post: 'payload' };
const mockPatchPayload = { post: 'payload' };
const mockCachePayload = {
status: 200,
body: 'BODY',
headers: { Etag: 'etag' },
};
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
describe('callApi()', () => {
beforeAll(() => {
fetchMock.get(LOGIN_GLOB, { result: '1234' });
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
beforeEach(() => {
fetchMock.get(mockGetUrl, mockGetPayload);
fetchMock.post(mockPostUrl, mockPostPayload);
fetchMock.put(mockPutUrl, mockPutPayload);
fetchMock.patch(mockPatchUrl, mockPatchPayload);
fetchMock.get(mockCacheUrl, mockCachePayload);
fetchMock.get(mockNotFound, { status: 404 });
fetchMock.get(mock503, { status: 503 });
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
});
afterAll(fetchMock.restore);
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
const mockPutUrl = '/mock/put/url';
const mockPatchUrl = '/mock/patch/url';
const mockCacheUrl = '/mock/cache/url';
const mockNotFound = '/mock/notfound';
const mockErrorUrl = '/mock/error/url';
const mock503 = '/mock/503';
const mockGetPayload = { get: 'payload' };
const mockPostPayload = { post: 'payload' };
const mockPutPayload = { post: 'payload' };
const mockPatchPayload = { post: 'payload' };
const mockCachePayload = {
status: 200,
body: 'BODY',
headers: { Etag: 'etag' },
};
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
fetchMock.get(mockGetUrl, mockGetPayload);
fetchMock.post(mockPostUrl, mockPostPayload);
fetchMock.put(mockPutUrl, mockPutPayload);
fetchMock.patch(mockPatchUrl, mockPatchPayload);
fetchMock.get(mockCacheUrl, mockCachePayload);
fetchMock.get(mockNotFound, { status: 404 });
fetchMock.get(mock503, { status: 503 });
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
afterEach(fetchMock.reset);
afterEach(() => fetchMock.reset());
describe('request config', () => {
it('calls the right url with the specified method', async () => {
@@ -401,7 +399,7 @@ describe('callApi()', () => {
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
const calls = fetchMock.calls(mockCacheUrl);
let calls = fetchMock.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const firstBody = await firstResponse.text();
expect(firstBody).toEqual('BODY');
@@ -410,6 +408,7 @@ describe('callApi()', () => {
url: mockCacheUrl,
method: 'GET',
});
calls = fetchMock.calls(mockCacheUrl);
const fetchParams = calls[1][1] as RequestInit;
expect(calls).toHaveLength(2);
// second call should not have If-None-Match header
@@ -425,11 +424,12 @@ describe('callApi()', () => {
expect.assertions(3);
// first call sets the cache
await callApi({ url: mockCacheUrl, method: 'GET' });
const calls = fetchMock.calls(mockCacheUrl);
let calls = fetchMock.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
// second call sends the Etag in the If-None-Match header
await callApi({ url: mockCacheUrl, method: 'GET' });
calls = fetchMock.calls(mockCacheUrl);
const fetchParams = calls[1][1] as RequestInit;
const headers = { 'If-None-Match': 'etag' };
expect(calls).toHaveLength(2);
@@ -442,8 +442,7 @@ describe('callApi()', () => {
expect.assertions(3);
// first call sets the cache
await callApi({ url: mockCacheUrl, method: 'GET' });
const calls = fetchMock.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
expect(fetchMock.calls(mockCacheUrl)).toHaveLength(1);
// second call reuses the cached payload on a 304
const mockCachedPayload = { status: 304 };
fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
@@ -452,7 +451,7 @@ describe('callApi()', () => {
url: mockCacheUrl,
method: 'GET',
});
expect(calls).toHaveLength(2);
expect(fetchMock.calls(mockCacheUrl)).toHaveLength(2);
const secondBody = await secondResponse.text();
expect(secondBody).toEqual('BODY');
});
@@ -641,6 +640,7 @@ describe('callApi()', () => {
it('should ignore "null" postPayload string', async () => {
expect.assertions(1);
fetchMock.post('/post-null-postpayload', {});
fetchMock.post('/post-formdata', {});
await callApi({
url: '/post-formdata',
method: 'POST',
@@ -27,16 +27,15 @@ import * as rejectAfterTimeout from '../../../src/connection/callApi/rejectAfter
import { LOGIN_GLOB } from '../fixtures/constants';
const mockGetUrl = '/mock/get/url';
const mockGetPayload = { get: 'payload' };
describe('callApiAndParseWithTimeout()', () => {
beforeAll(() => {
fetchMock.get(LOGIN_GLOB, { result: '1234' });
});
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
afterAll(fetchMock.restore);
beforeEach(() => fetchMock.get(mockGetUrl, mockGetPayload));
const mockGetUrl = '/mock/get/url';
const mockGetPayload = { get: 'payload' };
fetchMock.get(mockGetUrl, mockGetPayload);
afterAll(() => fetchMock.restore());
afterEach(() => {
fetchMock.reset();
@@ -27,7 +27,7 @@ describe('parseResponse()', () => {
fetchMock.get(LOGIN_GLOB, { result: '1234' });
});
afterAll(fetchMock.restore);
afterAll(() => fetchMock.restore());
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
@@ -38,12 +38,14 @@ describe('parseResponse()', () => {
const mockPostPayload = { post: 'payload' };
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
fetchMock.get(mockGetUrl, mockGetPayload);
fetchMock.post(mockPostUrl, mockPostPayload);
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
fetchMock.get(mockNoParseUrl, new Response('test response'));
beforeEach(() => {
fetchMock.get(mockGetUrl, mockGetPayload);
fetchMock.post(mockPostUrl, mockPostPayload);
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
fetchMock.get(mockNoParseUrl, new Response('test response'));
});
afterEach(fetchMock.reset);
afterEach(() => fetchMock.reset());
it('returns a Promise', () => {
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
@@ -19,40 +19,41 @@
import { NumberFormatter, createDurationFormatter } from '@superset-ui/core';
describe('createDurationFormatter()', () => {
it('creates an instance of NumberFormatter', () => {
const formatter = createDurationFormatter();
expect(formatter).toBeInstanceOf(NumberFormatter);
test('creates an instance of NumberFormatter', () => {
const formatter = createDurationFormatter();
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('format milliseconds in human readable format with default options', () => {
const formatter = createDurationFormatter();
expect(formatter(-1000)).toBe('-1s');
expect(formatter(0)).toBe('0ms');
expect(formatter(1000)).toBe('1s');
expect(formatter(1337)).toBe('1.3s');
expect(formatter(10500)).toBe('10.5s');
expect(formatter(60 * 1000)).toBe('1m');
expect(formatter(90 * 1000)).toBe('1m 30s');
});
test('format seconds in human readable format with default options', () => {
const formatter = createDurationFormatter({ multiplier: 1000 });
expect(formatter(-0.5)).toBe('-500ms');
expect(formatter(0.5)).toBe('500ms');
expect(formatter(1)).toBe('1s');
expect(formatter(30)).toBe('30s');
expect(formatter(60)).toBe('1m');
expect(formatter(90)).toBe('1m 30s');
});
test('format milliseconds in human readable format with additional pretty-ms options', () => {
const colonNotationFormatter = createDurationFormatter({
colonNotation: true,
});
it('format milliseconds in human readable format with default options', () => {
const formatter = createDurationFormatter();
expect(formatter(0)).toBe('0ms');
expect(formatter(1000)).toBe('1s');
expect(formatter(1337)).toBe('1.3s');
expect(formatter(10500)).toBe('10.5s');
expect(formatter(60 * 1000)).toBe('1m');
expect(formatter(90 * 1000)).toBe('1m 30s');
expect(colonNotationFormatter(-10500)).toBe('-0:10.5');
expect(colonNotationFormatter(10500)).toBe('0:10.5');
const zeroDecimalFormatter = createDurationFormatter({
secondsDecimalDigits: 0,
});
it('format seconds in human readable format with default options', () => {
const formatter = createDurationFormatter({ multiplier: 1000 });
expect(formatter(0.5)).toBe('500ms');
expect(formatter(1)).toBe('1s');
expect(formatter(30)).toBe('30s');
expect(formatter(60)).toBe('1m');
expect(formatter(90)).toBe('1m 30s');
expect(zeroDecimalFormatter(10500)).toBe('10s');
const subMillisecondFormatter = createDurationFormatter({
formatSubMilliseconds: true,
});
it('format milliseconds in human readable format with additional pretty-ms options', () => {
const colonNotationFormatter = createDurationFormatter({
colonNotation: true,
});
expect(colonNotationFormatter(10500)).toBe('0:10.5');
const zeroDecimalFormatter = createDurationFormatter({
secondsDecimalDigits: 0,
});
expect(zeroDecimalFormatter(10500)).toBe('10s');
const subMillisecondFormatter = createDurationFormatter({
formatSubMilliseconds: true,
});
expect(subMillisecondFormatter(100.40008)).toBe('100ms 400µs 80ns');
});
expect(subMillisecondFormatter(100.40008)).toBe('100ms 400µs 80ns');
});
@@ -0,0 +1,94 @@
/*
* 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 { NumberFormatter, createMemoryFormatter } from '@superset-ui/core';
test('creates an instance of MemoryFormatter', () => {
const formatter = createMemoryFormatter();
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('formats bytes in human readable format with default options', () => {
const formatter = createMemoryFormatter();
expect(formatter(0)).toBe('0B');
expect(formatter(50)).toBe('50B');
expect(formatter(555)).toBe('555B');
expect(formatter(1000)).toBe('1kB');
expect(formatter(1111)).toBe('1.11kB');
expect(formatter(1024)).toBe('1.02kB');
expect(formatter(1337)).toBe('1.34kB');
expect(formatter(1999)).toBe('2kB');
expect(formatter(10 * 1000)).toBe('10kB');
expect(formatter(100 * 1000)).toBe('100kB');
expect(formatter(Math.pow(1000, 2))).toBe('1MB');
expect(formatter(Math.pow(1000, 3))).toBe('1GB');
expect(formatter(Math.pow(1000, 4))).toBe('1TB');
expect(formatter(Math.pow(1000, 5))).toBe('1PB');
expect(formatter(Math.pow(1000, 6))).toBe('1EB');
expect(formatter(Math.pow(1000, 7))).toBe('1ZB');
expect(formatter(Math.pow(1000, 8))).toBe('1YB');
expect(formatter(Math.pow(1000, 9))).toBe('1RB');
expect(formatter(Math.pow(1000, 10))).toBe('1QB');
expect(formatter(Math.pow(1000, 11))).toBe('1000QB');
expect(formatter(Math.pow(1000, 12))).toBe('1000000QB');
});
test('formats negative bytes in human readable format with default options', () => {
const formatter = createMemoryFormatter();
expect(formatter(-50)).toBe('-50B');
});
test('formats float bytes in human readable format with default options', () => {
const formatter = createMemoryFormatter();
expect(formatter(10.666)).toBe('10.67B');
expect(formatter(1200.666)).toBe('1.2kB');
});
test('formats bytes in human readable format with additional binary option', () => {
const formatter = createMemoryFormatter({ binary: true });
expect(formatter(0)).toBe('0B');
expect(formatter(50)).toBe('50B');
expect(formatter(555)).toBe('555B');
expect(formatter(1000)).toBe('1000B');
expect(formatter(1111)).toBe('1.08KiB');
expect(formatter(1024)).toBe('1KiB');
expect(formatter(1337)).toBe('1.31KiB');
expect(formatter(2047)).toBe('2KiB');
expect(formatter(10 * 1024)).toBe('10KiB');
expect(formatter(100 * 1024)).toBe('100KiB');
expect(formatter(Math.pow(1024, 2))).toBe('1MiB');
expect(formatter(Math.pow(1024, 3))).toBe('1GiB');
expect(formatter(Math.pow(1024, 4))).toBe('1TiB');
expect(formatter(Math.pow(1024, 5))).toBe('1PiB');
expect(formatter(Math.pow(1024, 6))).toBe('1EiB');
expect(formatter(Math.pow(1024, 7))).toBe('1ZiB');
expect(formatter(Math.pow(1024, 8))).toBe('1YiB');
expect(formatter(Math.pow(1024, 9))).toBe('1024YiB');
expect(formatter(Math.pow(1024, 10))).toBe('1048576YiB');
});
test('formats bytes in human readable format with additional decimals option', () => {
const formatter0decimals = createMemoryFormatter({ decimals: 0 });
expect(formatter0decimals(0)).toBe('0B');
expect(formatter0decimals(1111)).toBe('1kB');
const formatter3decimals = createMemoryFormatter({ decimals: 3 });
expect(formatter3decimals(0)).toBe('0B');
expect(formatter3decimals(1111)).toBe('1.111kB');
});
@@ -21,6 +21,7 @@ import {
createD3NumberFormatter,
createDurationFormatter,
createSiAtMostNDigitFormatter,
createMemoryFormatter,
formatNumber,
getNumberFormatter,
getNumberFormatterRegistry,
@@ -35,6 +36,7 @@ describe('index', () => {
createD3NumberFormatter,
createDurationFormatter,
createSiAtMostNDigitFormatter,
createMemoryFormatter,
formatNumber,
getNumberFormatter,
getNumberFormatterRegistry,
@@ -22,9 +22,9 @@ import { getDatasourceMetadata } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
describe('getFormData()', () => {
beforeAll(setupClientForTest);
beforeAll(() => setupClientForTest());
afterEach(fetchMock.restore);
afterEach(() => fetchMock.restore());
it('returns datasource metadata for given datasource key', () => {
const mockData = {
@@ -22,9 +22,9 @@ import { getFormData } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
describe('getFormData()', () => {
beforeAll(setupClientForTest);
beforeAll(() => setupClientForTest());
afterEach(fetchMock.restore);
afterEach(() => fetchMock.restore());
const mockData = {
datasource: '1__table',
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line import/no-extraneous-dependencies -- The below fetch-mock import shouldn't be considered as direct dependency
import fetchMock from 'fetch-mock';
import { SupersetClient } from '@superset-ui/core';
@@ -21,8 +21,8 @@ import { buildQueryContext, ApiV1 } from '@superset-ui/core';
import setupClientForTest from '../setupClientForTest';
describe('API v1 > getChartData()', () => {
beforeAll(setupClientForTest);
afterEach(fetchMock.restore);
beforeAll(() => setupClientForTest());
afterEach(() => fetchMock.restore());
it('returns a promise of ChartDataResponse', async () => {
const response = {
@@ -22,8 +22,8 @@ import { makeApi, SupersetApiError } from '../../../../src/query';
import setupClientForTest from '../setupClientForTest';
describe('makeApi()', () => {
beforeAll(setupClientForTest);
afterEach(fetchMock.restore);
beforeAll(() => setupClientForTest());
afterEach(() => fetchMock.restore());
it('should expose method and endpoint', () => {
const api = makeApi({
@@ -24,6 +24,10 @@ import {
FilterWithDataMask,
Divider,
isNativeFilterWithDataMask,
isAppliedCrossFilterType,
isAppliedNativeFilterType,
AppliedCrossFilterType,
AppliedNativeFilterType,
} from '@superset-ui/core';
const filter: Filter = {
@@ -51,6 +55,20 @@ const filterDivider: Divider = {
description: 'Divider description.',
};
const appliedCrossFilter: AppliedCrossFilterType = {
filterType: undefined,
targets: [1, 2],
scope: [1, 2],
values: null,
};
const appliedNativeFilter: AppliedNativeFilterType = {
filterType: 'filter_select',
scope: [1, 2],
targets: [{}],
values: null,
};
test('filter type guard', () => {
expect(isNativeFilter(filter)).toBeTruthy();
expect(isNativeFilter(filterWithDataMask)).toBeTruthy();
@@ -68,3 +86,13 @@ test('filter divider type guard', () => {
expect(isFilterDivider(filterWithDataMask)).toBeFalsy();
expect(isFilterDivider(filterDivider)).toBeTruthy();
});
test('applied cross filter type guard', () => {
expect(isAppliedCrossFilterType(appliedCrossFilter)).toBeTruthy();
expect(isAppliedCrossFilterType(appliedNativeFilter)).toBeFalsy();
});
test('applied native filter type guard', () => {
expect(isAppliedNativeFilterType(appliedNativeFilter)).toBeTruthy();
expect(isAppliedNativeFilterType(appliedCrossFilter)).toBeFalsy();
});
@@ -17,7 +17,6 @@
* under the License.
*/
import { mount } from 'enzyme';
import {
styled,
supersetTheme,
@@ -27,6 +26,7 @@ import {
EmotionCacheProvider,
emotionCache,
} from '@superset-ui/core';
import { render } from '@testing-library/react';
describe('@superset-ui/style package', () => {
it('exports a theme', () => {
@@ -50,8 +50,8 @@ describe('@superset-ui/style package', () => {
expect(useTheme()).toStrictEqual(supersetTheme);
return <div>test</div>;
}
mount(<ThemeUser />, {
wrappingComponent: ({ children }) => (
render(<ThemeUser />, {
wrapper: ({ children }) => (
<EmotionCacheProvider value={emotionCache}>
<ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
</EmotionCacheProvider>
@@ -64,8 +64,8 @@ describe('@superset-ui/style package', () => {
expect(useTheme).toThrow(/could not find a ThemeContext/);
return <div>test</div>;
}
mount(<ThemeUser />, {
wrappingComponent: ({ children }) => <div>{children}</div>,
render(<ThemeUser />, {
wrapper: ({ children }) => <div>{children}</div>,
});
});
});
@@ -25,7 +25,7 @@ import {
formatTimeRangeComparison,
} from '../../src/time-comparison/fetchTimeRange';
afterEach(fetchMock.restore);
afterEach(() => fetchMock.restore());
test('generates proper time range string', () => {
expect(
@@ -56,7 +56,7 @@ test('generates a readable time range', () => {
});
test('returns a formatted time range from response', async () => {
fetchMock.get("glob:*/api/v1/time_range/?q='Last+day'", {
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
result: [
{
since: '2021-04-13T00:00:00',
@@ -73,7 +73,7 @@ test('returns a formatted time range from response', async () => {
});
test('returns a formatted time range from empty response', async () => {
fetchMock.get("glob:*/api/v1/time_range/?q='Last+day'", {
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
result: [],
});
@@ -84,7 +84,7 @@ test('returns a formatted time range from empty response', async () => {
});
test('returns a formatted error message from response', async () => {
fetchMock.getOnce("glob:*/api/v1/time_range/?q='Last+day'", {
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
throws: new Response(JSON.stringify({ message: 'Network error' })),
});
let timeRange = await fetchTimeRange('Last day');
@@ -92,8 +92,8 @@ test('returns a formatted error message from response', async () => {
error: 'Network error',
});
fetchMock.getOnce(
"glob:*/api/v1/time_range/?q='Last+day'",
fetchMock.get(
'glob:*/api/v1/time_range/?q=%27Last+day%27',
{
throws: new Error('Internal Server Error'),
},
@@ -104,8 +104,8 @@ test('returns a formatted error message from response', async () => {
error: 'Internal Server Error',
});
fetchMock.getOnce(
"glob:*/api/v1/time_range/?q='Last+day'",
fetchMock.get(
'glob:*/api/v1/time_range/?q=%27Last+day%27',
{
throws: new Response(JSON.stringify({ statusText: 'Network error' }), {
statusText: 'Network error',
@@ -117,11 +117,11 @@ test('returns a formatted error message from response', async () => {
expect(timeRange).toEqual({
error: 'Network error',
});
});
}, 10000);
test('fetchTimeRange with shift', async () => {
fetchMock.getOnce(
"glob:*/api/v1/time_range/?q=!((timeRange:'Last+day'),(shift%3A'last%20month'%2CtimeRange%3A'Last%20day'))",
'glob:*/api/v1/time_range/?q=!((timeRange:%27Last+day%27),(shift%3A%27last%20month%27%2CtimeRange%3A%27Last%20day%27))',
{
result: [
{
@@ -33,7 +33,7 @@
"dependencies": {
"@data-ui/event-flow": "^0.0.84",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.3.0",
"@react-icons/all-files": "^4.1.0",
"@storybook/addon-actions": "8.1.11",
@@ -41,7 +41,7 @@
"@storybook/addon-links": "8.1.11",
"@storybook/react": "8.1.11",
"@storybook/types": "8.1.11",
"@types/react-loadable": "^5.5.3",
"@types/react-loadable": "^5.5.11",
"antd": "4.10.3",
"core-js": "3.38.1",
"gh-pages": "^5.0.0",
@@ -58,7 +58,7 @@
"@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@storybook/react-webpack5": "8.1.11",
"@storybook/react-webpack5": "8.2.9",
"babel-loader": "^9.1.3",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"ts-loader": "^9.5.1",
@@ -57,6 +57,6 @@ To update the country maps or add a new country, run scripts in the Jupyter note
`scripts/Country Map GeoJSON Generator.ipynb`.
```bash
pip install jupyter notebook
pip install geopandas shapely matplotlib notebook
jupyter notebook
```
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -24,6 +24,12 @@ import {
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Time'),
expanded: true,
description: t('Time related form attributes'),
controlSetRows: [['granularity_sqla'], ['time_range']],
},
{
label: t('Query'),
expanded: true,
@@ -28,7 +28,7 @@
"dependencies": {
"prop-types": "^15.8.1",
"react-map-gl": "^6.1.19",
"supercluster": "^4.1.1",
"supercluster": "^8.0.1",
"viewport-mercator-project": "^6.1.1"
},
"peerDependencies": {
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import supercluster from 'supercluster';
import Supercluster from 'supercluster';
import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapBox';
const NOOP = () => {};
@@ -72,7 +72,7 @@ export default function transformProps(chartProps) {
/* eslint-enable no-param-reassign */
};
}
const clusterer = supercluster(opts);
const clusterer = new Supercluster(opts);
clusterer.load(geoJSON.features);
return {
@@ -30,7 +30,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"react": "^16.13.1",
"react-dom": "^16.13.1"
@@ -31,7 +31,7 @@
"d3-array": "^1.2.4",
"d3-color": "^1.4.1",
"d3-scale": "^3.0.0",
"deck.gl": "9.0.24",
"deck.gl": "9.0.28",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"mousetrap": "^1.6.5",
@@ -32,7 +32,7 @@
"@data-ui/xy-chart": "^0.0.84",
"d3": "^3.5.17",
"d3-tip": "^0.9.1",
"dompurify": "^3.1.0",
"dompurify": "^3.1.3",
"fast-safe-stringify": "^2.1.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
@@ -25,7 +25,7 @@
],
"dependencies": {
"d3-array": "^1.2.0",
"echarts": "^5.5.1",
"echarts": "^5.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1"
},
@@ -25,7 +25,6 @@ export default function buildQuery(formData: HistogramFormData) {
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
extras: { where: `${column} IS NOT NULL` },
columns: [...groupby, column],
post_processing: [histogramOperator(formData, baseQueryObject)],
metrics: undefined,
@@ -593,6 +593,7 @@ export default function transformProps(
extractForecastValuesFromTooltipParams(forecastValue);
const keys = Object.keys(forecastValues);
let focusedRow;
keys.forEach(key => {
const value = forecastValues[key];
// if there are no dimensions, key is a verbose name of a metric,
@@ -627,12 +628,11 @@ export default function transformProps(
: tooltipFormatterSecondary,
});
rows.push(row);
if (key === focusedSeries) {
focusedRow = rows.length - 1;
}
});
return tooltipHtml(
rows,
tooltipFormatter(xValue),
keys.findIndex(key => key === focusedSeries),
);
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
},
},
legend: {
@@ -56,6 +56,22 @@ const radarMetricMaxValue: { name: string; config: ControlFormItemSpec } = {
},
};
const radarMetricMinValue: { name: string; config: ControlFormItemSpec } = {
name: 'radarMetricMinValue',
config: {
controlType: 'InputNumber',
label: t('Min'),
description: t(
'The minimum value of metrics. It is an optional configuration. If not set, it will be the minimum value of the data',
),
defaultValue: '0',
width: 120,
placeholder: t('auto'),
debounceDelay: 400,
validators: [validateNumber],
},
};
const config: ControlPanelConfig = {
controlPanelSections: [
{
@@ -164,7 +180,9 @@ const config: ControlPanelConfig = {
description: t('Further customize how to display each metric'),
renderTrigger: true,
configFormLayout: {
[GenericDataType.Numeric]: [[radarMetricMaxValue]],
[GenericDataType.Numeric]: [
[radarMetricMinValue, radarMetricMaxValue],
],
},
shouldMapStateToProps() {
return true;
@@ -179,11 +197,17 @@ const config: ControlPanelConfig = {
}
return value.label;
});
const { colnames: _colnames, coltypes: _coltypes } =
chart?.queriesResponse?.[0] ?? {};
const colnames: string[] = _colnames || [];
const coltypes: GenericDataType[] = _coltypes || [];
return {
queryResponse: chart?.queriesResponse?.[0] as
| ChartDataResponseResult
| undefined,
appliedColumnNames: metricColumn,
columnsPropsObject: { colnames, coltypes },
};
},
},
@@ -123,6 +123,7 @@ export default function transformProps(
const groupbyLabels = groupby.map(getColumnLabel);
const metricLabelAndMaxValueMap = new Map<string, number>();
const metricLabelAndMinValueMap = new Map<string, number>();
const columnsLabelMap = new Map<string, string[]>();
const transformedData: RadarSeriesDataItemOption[] = [];
data.forEach(datum => {
@@ -155,6 +156,21 @@ export default function transformProps(
} else {
metricLabelAndMaxValueMap.set(metricLabel, value as number);
}
if (metricLabelAndMinValueMap.has(metricLabel)) {
metricLabelAndMinValueMap.set(
metricLabel,
Math.min(
value as number,
ensureIsInt(
metricLabelAndMinValueMap.get(metricLabel),
Number.MAX_SAFE_INTEGER,
),
),
);
} else {
metricLabelAndMinValueMap.set(metricLabel, value as number);
}
}
const isFiltered =
@@ -199,6 +215,8 @@ export default function transformProps(
const indicator = metricLabels.map(metricLabel => {
const maxValueInControl = columnConfig?.[metricLabel]?.radarMetricMaxValue;
const minValueInControl = columnConfig?.[metricLabel]?.radarMetricMinValue;
// Ensure that 0 is at the center of the polar coordinates
const metricValueAsMax =
metricLabelAndMaxValueMap.get(metricLabel) === 0
@@ -206,9 +224,23 @@ export default function transformProps(
: metricLabelAndMaxValueMap.get(metricLabel);
const max =
maxValueInControl === null ? metricValueAsMax : maxValueInControl;
let min: number;
// If the min value doesn't exist, set it to 0 (default),
// if it is null, set it to the min value of the data,
// otherwise, use the value from the control
if (minValueInControl === undefined) {
min = 0;
} else if (minValueInControl === null) {
min = metricLabelAndMinValueMap.get(metricLabel) || 0;
} else {
min = minValueInControl;
}
return {
name: metricLabel,
max,
min,
};
});
@@ -33,7 +33,10 @@ import {
} from '../types';
import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
type RadarColumnConfig = Record<string, { radarMetricMaxValue?: number }>;
type RadarColumnConfig = Record<
string,
{ radarMetricMaxValue?: number; radarMetricMinValue?: number }
>;
export type EchartsRadarFormData = QueryFormData &
LegendFormData & {
@@ -569,6 +569,7 @@ export default function transformProps(
const showTotal = Boolean(isMultiSeries) && richTooltip && !isForecast;
const showPercentage = showTotal && !forcePercentFormatter;
const keys = Object.keys(forecastValues);
let focusedRow;
keys.forEach(key => {
const value = forecastValues[key];
if (value.observation === 0 && stack) {
@@ -583,10 +584,15 @@ export default function transformProps(
row.push(percentFormatter.format(value.observation / (total || 1)));
}
rows.push(row);
if (key === focusedSeries) {
focusedRow = rows.length - 1;
}
});
if (stack) {
keys.reverse();
rows.reverse();
if (focusedRow !== undefined) {
focusedRow = rows.length - focusedRow - 1;
}
}
if (showTotal) {
const totalRow = ['Total', formatter.format(total)];
@@ -595,11 +601,7 @@ export default function transformProps(
}
rows.push(totalRow);
}
return tooltipHtml(
rows,
tooltipFormatter(xValue),
keys.findIndex(key => key === focusedSeries),
);
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
},
},
legend: {
@@ -55,6 +55,8 @@ import {
ToolboxComponent,
GraphicComponent,
AriaComponent,
MarkAreaComponent,
MarkLineComponent,
} from 'echarts/components';
import { LabelLayout } from 'echarts/features';
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
@@ -84,6 +86,8 @@ use([
DataZoomComponent,
GraphicComponent,
GridComponent,
MarkAreaComponent,
MarkLineComponent,
LegendComponent,
ToolboxComponent,
TooltipComponent,
@@ -53,8 +53,8 @@ export const LABEL_POSITION: [LabelPositionEnum, string][] = [
[LabelPositionEnum.Right, 'Right'],
[LabelPositionEnum.Bottom, 'Bottom'],
[LabelPositionEnum.Inside, 'Inside'],
[LabelPositionEnum.InsideBottomLeft, 'Inside left'],
[LabelPositionEnum.InsideBottomRight, 'Inside right'],
[LabelPositionEnum.InsideLeft, 'Inside left'],
[LabelPositionEnum.InsideRight, 'Inside right'],
[LabelPositionEnum.InsideTop, 'Inside top'],
[LabelPositionEnum.InsideBottom, 'Inside bottom'],
[LabelPositionEnum.InsideTopLeft, 'Inside top left'],
@@ -27,7 +27,7 @@
"access": "public"
},
"dependencies": {
"handlebars": "^4.7.7",
"handlebars": "^4.7.8",
"just-handlebars-helpers": "^1.0.19"
},
"peerDependencies": {
@@ -40,10 +40,10 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^5.0.3",
"@testing-library/user-event": "^12.7.0",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
+1
View File
@@ -26,6 +26,7 @@ export default {
{
id: 'DefaultsID',
filterType: 'filter_select',
chartsInScope: [],
targets: [{}],
cascadeParentIds: [],
},
+1
View File
@@ -133,6 +133,7 @@ export const singleNativeFiltersState = {
id: [NATIVE_FILTER_ID],
name: 'eth',
type: 'text',
filterType: 'filter_select',
targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
defaultDataMask: {
filterState: {
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom';
import { ReactNode, ReactElement } from 'react';
import {
render,
@@ -17,7 +17,7 @@
* under the License.
*/
import { isValidElement } from 'react';
import { styledMount as mount } from 'spec/helpers/theming';
import { render } from 'spec/helpers/testing-library';
import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { mockedActions, table } from 'src/SqlLab/fixtures';
@@ -30,19 +30,25 @@ describe('ColumnElement', () => {
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
});
it('renders a proper primary key', () => {
const wrapper = mount(<ColumnElement column={table.columns[0]} />);
expect(wrapper.find('i.fa-key')).toExist();
expect(wrapper.find('.col-name').first().text()).toBe('id');
const { container } = render(<ColumnElement column={table.columns[0]} />);
expect(container.querySelector('i.fa-key')).toBeInTheDocument();
expect(container.querySelector('.col-name')?.firstChild).toHaveTextContent(
'id',
);
});
it('renders a multi-key column', () => {
const wrapper = mount(<ColumnElement column={table.columns[1]} />);
expect(wrapper.find('i.fa-link')).toExist();
expect(wrapper.find('i.fa-bookmark')).toExist();
expect(wrapper.find('.col-name').first().text()).toBe('first_name');
const { container } = render(<ColumnElement column={table.columns[1]} />);
expect(container.querySelector('i.fa-link')).toBeInTheDocument();
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
expect(container.querySelector('.col-name')?.firstChild).toHaveTextContent(
'first_name',
);
});
it('renders a column with no keys', () => {
const wrapper = mount(<ColumnElement column={table.columns[2]} />);
expect(wrapper.find('i')).not.toExist();
expect(wrapper.find('.col-name').first().text()).toBe('last_name');
const { container } = render(<ColumnElement column={table.columns[2]} />);
expect(container.querySelector('i')).not.toBeInTheDocument();
expect(container.querySelector('.col-name')?.firstChild).toHaveTextContent(
'last_name',
);
});
});
@@ -42,6 +42,7 @@ import {
css,
getNumberFormatter,
getExtensionsRegistry,
ErrorLevel,
ErrorTypeEnum,
} from '@superset-ui/core';
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
@@ -540,18 +541,33 @@ const ResultSet = ({
}
if (query.state === QueryState.Failed) {
const errors = [];
if (query.errorMessage) {
errors.push({
error_type: ErrorTypeEnum.GENERIC_DB_ENGINE_ERROR,
extra: {},
level: 'error' as ErrorLevel,
message: query.errorMessage,
});
}
errors.push(...(query.extra?.errors || []), ...(query.errors || []));
return (
<ResultlessStyles>
<ErrorMessageWithStackTrace
title={t('Database error')}
error={query?.extra?.errors?.[0] || query?.errors?.[0]}
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
copyText={query.errorMessage || undefined}
link={query.link}
source="sqllab"
/>
{(query?.extra?.errors?.[0] || query?.errors?.[0])?.error_type ===
ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR ? (
{errors.map((error, index) => (
<ErrorMessageWithStackTrace
key={index}
title={t('Database error')}
error={error}
subtitle={<MonospaceDiv>{error.message}</MonospaceDiv>}
copyText={error.message || undefined}
link={query.link}
source="sqllab"
/>
))}
{errors.some(
error => error?.error_type === ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
) ? (
<Button
className="sql-result-track-job"
buttonSize="small"
@@ -630,7 +646,7 @@ const ResultSet = ({
: [];
const allowHTML = getItem(
LocalStorageKeys.SqllabIsRenderHtmlEnabled,
false,
true,
);
return (
<ResultContainer>
@@ -24,7 +24,7 @@ import * as uiCore from '@superset-ui/core';
import { Provider } from 'react-redux';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import { render, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import * as utils from 'src/utils/common';
import ShareSqlLabQuery from 'src/SqlLab/components/ShareSqlLabQuery';
@@ -18,7 +18,7 @@
*/
import { render } from 'spec/helpers/testing-library';
import SouthPane from 'src/SqlLab/components/SouthPane';
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom';
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { denormalizeTimestamp } from '@superset-ui/core';
@@ -308,7 +308,7 @@ const SqlEditor: FC<Props> = ({
getItem(LocalStorageKeys.SqllabIsAutocompleteEnabled, true),
);
const [renderHTMLEnabled, setRenderHTMLEnabled] = useState(
getItem(LocalStorageKeys.SqllabIsRenderHtmlEnabled, false),
getItem(LocalStorageKeys.SqllabIsRenderHtmlEnabled, true),
);
const [showCreateAsModal, setShowCreateAsModal] = useState(false);
const [createAs, setCreateAs] = useState('');
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import AlteredSliceTag, {
@@ -16,15 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import {
ensureIsArray,
FeatureFlag,
isFeatureEnabled,
logging,
QueryFormData,
styled,
t,
SqlaFormData,
ClientErrorObject,
ChartDataResponse,
} from '@superset-ui/core';
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
import Loading from 'src/components/Loading';
@@ -36,73 +39,98 @@ import { getUrlParam } from 'src/utils/urlUtils';
import { isCurrentUserBot } from 'src/utils/isBot';
import { ChartSource } from 'src/types/ChartSource';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { Dispatch } from 'redux';
import { Annotation } from 'src/explore/components/controls/AnnotationLayerControl';
import ChartRenderer from './ChartRenderer';
import { ChartErrorMessage } from './ChartErrorMessage';
import { getChartRequiredFieldsMissingMessage } from '../../utils/getChartRequiredFieldsMissingMessage';
const propTypes = {
annotationData: PropTypes.object,
actions: PropTypes.object,
chartId: PropTypes.number.isRequired,
datasource: PropTypes.object,
// current chart is included by dashboard
dashboardId: PropTypes.number,
// original selected values for FilterBox viz
// so that FilterBox can pre-populate selected values
// only affect UI control
initialValues: PropTypes.object,
// formData contains chart's own filter parameter
// and merged with extra filter that current dashboard applying
formData: PropTypes.object.isRequired,
labelsColor: PropTypes.object,
labelsColorMap: PropTypes.object,
width: PropTypes.number,
height: PropTypes.number,
setControlValue: PropTypes.func,
timeout: PropTypes.number,
vizType: PropTypes.string.isRequired,
triggerRender: PropTypes.bool,
force: PropTypes.bool,
isFiltersInitialized: PropTypes.bool,
// state
chartAlert: PropTypes.string,
chartStatus: PropTypes.string,
chartStackTrace: PropTypes.string,
queriesResponse: PropTypes.arrayOf(PropTypes.object),
triggerQuery: PropTypes.bool,
chartIsStale: PropTypes.bool,
errorMessage: PropTypes.node,
// dashboard callbacks
addFilter: PropTypes.func,
onQuery: PropTypes.func,
onFilterMenuOpen: PropTypes.func,
onFilterMenuClose: PropTypes.func,
ownState: PropTypes.object,
postTransformProps: PropTypes.func,
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
isInView: PropTypes.bool,
emitCrossFilters: PropTypes.bool,
};
export type ChartErrorType = Partial<ClientErrorObject>;
export interface ChartProps {
annotationData?: Annotation;
actions: Actions;
chartId: string;
datasource?: {
database?: {
name: string;
};
};
dashboardId?: number;
initialValues?: object;
formData: QueryFormData;
labelColors?: string;
sharedLabelColors?: string;
width: number;
height: number;
setControlValue: Function;
timeout?: number;
vizType: string;
triggerRender?: boolean;
force?: boolean;
isFiltersInitialized?: boolean;
chartAlert?: string;
chartStatus?: string;
chartStackTrace?: string;
queriesResponse: ChartDataResponse[];
triggerQuery?: boolean;
chartIsStale?: boolean;
errorMessage?: React.ReactNode;
addFilter?: (type: string) => void;
onQuery?: () => void;
onFilterMenuOpen?: (chartId: string, column: string) => void;
onFilterMenuClose?: (chartId: string, column: string) => void;
ownState: boolean;
postTransformProps?: Function;
datasetsStatus?: 'loading' | 'error' | 'complete';
isInView?: boolean;
emitCrossFilters?: boolean;
}
export type Actions = {
logEvent(
LOG_ACTIONS_RENDER_CHART: string,
arg1: {
slice_id: string;
has_err: boolean;
error_details: string;
start_offset: number;
ts: number;
duration: number;
},
): Dispatch;
chartRenderingFailed(
arg0: string,
chartId: string,
arg2: string | null,
): Dispatch;
postChartFormData(
formData: SqlaFormData,
arg1: boolean,
timeout: number | undefined,
chartId: string,
dashboardId: number | undefined,
ownState: boolean,
): Dispatch;
};
const BLANK = {};
const NONEXISTENT_DATASET = t(
'The dataset associated with this chart no longer exists',
);
const defaultProps = {
const defaultProps: Partial<ChartProps> = {
addFilter: () => BLANK,
onFilterMenuOpen: () => BLANK,
onFilterMenuClose: () => BLANK,
initialValues: BLANK,
setControlValue() {},
setControlValue: () => BLANK,
triggerRender: false,
dashboardId: null,
chartStackTrace: null,
dashboardId: undefined,
chartStackTrace: undefined,
force: false,
isInView: true,
};
const Styles = styled.div`
const Styles = styled.div<{ height: number; width?: number }>`
min-height: ${p => p.height}px;
position: relative;
text-align: center;
@@ -150,9 +178,12 @@ const MonospaceDiv = styled.div`
overflow-x: auto;
white-space: pre-wrap;
`;
class Chart extends PureComponent<ChartProps, {}> {
static defaultProps = defaultProps;
class Chart extends PureComponent {
constructor(props) {
renderStartTime: any;
constructor(props: ChartProps) {
super(props);
this.handleRenderContainerFailure =
this.handleRenderContainerFailure.bind(this);
@@ -182,7 +213,10 @@ class Chart extends PureComponent {
);
}
handleRenderContainerFailure(error, info) {
handleRenderContainerFailure(
error: Error,
info: { componentStack: string } | null,
) {
const { actions, chartId } = this.props;
logging.warn(error);
actions.chartRenderingFailed(
@@ -201,7 +235,7 @@ class Chart extends PureComponent {
});
}
renderErrorMessage(queryResponse) {
renderErrorMessage(queryResponse: ChartErrorType) {
const {
chartId,
chartAlert,
@@ -241,14 +275,14 @@ class Chart extends PureComponent {
error={error}
subtitle={<MonospaceDiv>{message}</MonospaceDiv>}
copyText={message}
link={queryResponse ? queryResponse.link : null}
link={queryResponse ? queryResponse.link : undefined}
source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
stackTrace={chartStackTrace}
/>
);
}
renderSpinner(databaseName) {
renderSpinner(databaseName: string | undefined) {
const message = databaseName
? t('Waiting on %s', databaseName)
: t('Waiting on database...');
@@ -290,12 +324,15 @@ class Chart extends PureComponent {
queriesResponse = [],
width,
} = this.props;
const databaseName = datasource?.database?.name;
const isLoading = chartStatus === 'loading';
this.renderContainerStartTime = Logger.getTimestamp();
if (chartStatus === 'failed') {
return queriesResponse.map(item => this.renderErrorMessage(item));
return queriesResponse.map(item =>
this.renderErrorMessage(item as ChartErrorType),
);
}
if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
@@ -307,7 +344,6 @@ class Chart extends PureComponent {
/>
);
}
if (
!isLoading &&
!chartAlert &&
@@ -354,8 +390,4 @@ class Chart extends PureComponent {
);
}
}
Chart.propTypes = propTypes;
Chart.defaultProps = defaultProps;
export default Chart;

Some files were not shown because too many files have changed in this diff Show More