Compare commits

...

70 Commits

Author SHA1 Message Date
Joe Li
cba539b91d fix(TimeTable): resolve TypeScript OOM by simplifying sparkline series type
Fixes heap out of memory error during TypeScript compilation caused by complex
SeriesProps generic type forcing exponential type resolution.

Changed chartSeriesMap type annotation from explicit SeriesProps<AxisScale, AxisScale, object>
to type inference with precise union constraint. Uses 'as const satisfies' pattern to validate
structure without triggering expensive type resolution.

- Remove SeriesProps and AxisScale imports (no longer needed)
- Add SparklineSeriesComponent union type (typeof LineSeries | BarSeries | AreaSeries)
- Replace explicit type annotation with inferred type + satisfies constraint
- Preserves type safety while avoiding exponential type checking complexity
- Follows "NO any types" modernization guideline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-02 21:06:11 -07:00
amaannawab923
96170e43c0 fix(ag-grid-table): remove enterprise features to use community version (#35453) 2025-10-02 22:45:34 +03:00
dependabot[bot]
8fde970b6b chore(deps): bump @ant-design/icons from 6.0.0 to 6.1.0 in /docs (#35439)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:28:20 -07:00
dependabot[bot]
38cb577fc6 chore(deps): bump swagger-ui-react from 5.27.1 to 5.29.1 in /docs (#35437)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:27:25 -07:00
dependabot[bot]
8ebe045846 chore(deps): bump antd from 5.27.1 to 5.27.4 in /docs (#35432)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:26:13 -07:00
Beto Dealmeida
62dc5c0306 fix(cache): ensure SQL is sanitized before cache key generation (#35419) 2025-10-02 13:25:54 -04:00
dependabot[bot]
6ad8d29fcd chore(deps-dev): bump globals from 16.3.0 to 16.4.0 in /superset-websocket (#35431)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:25:38 -07:00
dependabot[bot]
d83a88d5d9 chore(deps-dev): bump typescript-eslint from 8.19.0 to 8.45.0 in /superset-websocket (#35430)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:24:47 -07:00
SBIN2010
a7cc48dcda fix(test): changed test use unsaved changes prompt (#35447) 2025-10-02 10:20:30 -07:00
dependabot[bot]
e2a7dc9256 chore(deps): bump ioredis and @types/ioredis in /superset-websocket (#35428)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:19:28 -07:00
Kamil Gabryjelski
0364933e8b fix(explore): Remove query autotrigger (#35418) 2025-10-02 19:08:31 +02:00
Beto Dealmeida
3202ff4b3f fix(pinot): more functions (#35451) 2025-10-02 13:01:47 -04:00
Gabriel Torres Ruiz
553204e613 fix(dashboard): exit markdown edit mode when clicking outside of element (#35336) 2025-10-02 19:42:16 +03:00
Rafael Benitez
fe8348c03a fix(dataset): sort by database in Dataset and Saved queries Issue (#35277) 2025-10-02 16:44:19 +02:00
Beto Dealmeida
30021f8ede fix(pinot): SUBSTR function (#35427) 2025-10-02 10:29:18 -04:00
Beto Dealmeida
f3349388d0 fix(pinot): DATE_SUB function (#35426) 2025-10-02 10:12:19 -04:00
Antonio Rivero
449a89c214 fix(slice): Fix using isdigit when id passed as int (#35452) 2025-10-02 16:04:54 +02:00
Beto Dealmeida
5428376662 fix(pinot): DATE_ADD function (#35424) 2025-10-02 09:56:20 -04:00
Evan Rusackas
5493e2c96d chore(dependabot): switch back to daily cadence (a trickle rather than a flood) (#35421) 2025-10-01 14:53:11 -07:00
dependabot[bot]
2f8657f122 chore(deps-dev): bump eslint from 9.34.0 to 9.36.0 in /superset-websocket (#35359)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:26:06 -07:00
Beto Dealmeida
aa97d2fe03 fix(pinot): dialect date truncation (#35420)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
2025-10-01 13:16:46 -04:00
Joe Li
28389de93e fix(ci): fix GHA for docs during PRs (#35186)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2025-10-01 10:13:47 -07:00
dependabot[bot]
7c415c7cd9 chore(deps): bump hot-shots from 11.1.0 to 11.2.0 in /superset-websocket (#35358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:12:30 -07:00
dependabot[bot]
f6b2abee81 chore(deps-dev): bump webpack from 5.101.3 to 5.102.0 in /docs (#35368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:11:42 -07:00
dependabot[bot]
2ef7232959 chore(deps-dev): bump eslint from 9.34.0 to 9.36.0 in /docs (#35377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:10:45 -07:00
dependabot[bot]
2991f60368 chore(deps-dev): bump @types/react-json-tree from 0.6.11 to 0.13.0 in /superset-frontend (#35406)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:10:25 -07:00
dependabot[bot]
85ec3b1901 chore(deps-dev): bump @applitools/eyes-storybook from 3.55.6 to 3.60.0 in /superset-frontend (#35407)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:10:05 -07:00
dependabot[bot]
0c639abe17 chore(deps-dev): bump jsdom from 26.0.0 to 27.0.0 in /superset-frontend (#35409)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:09:38 -07:00
dependabot[bot]
50ed9b9e0e chore(deps-dev): bump @types/jquery from 3.5.32 to 3.5.33 in /superset-frontend (#35410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:08:36 -07:00
Beto Dealmeida
13a164dd63 fix: table quoting in DBs with supports_cross_catalog_queries=True (#35350) 2025-10-01 12:17:46 -04:00
dependabot[bot]
6b0b13e4e7 chore(deps-dev): bump typescript-eslint from 8.40.0 to 8.45.0 in /docs (#35381)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 08:49:51 -07:00
dependabot[bot]
c188952de2 chore(deps-dev): bump webpack from 5.99.9 to 5.102.0 in /superset-frontend (#35402)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 08:48:01 -07:00
dependabot[bot]
a3b91f924d chore(deps): bump @babel/runtime from 7.28.2 to 7.28.4 in /superset-frontend (#35403)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 08:47:11 -07:00
Rafael Benitez
d8688cf8b1 fix(explore): close unsaved changes modal when discarding changes (#35307) 2025-10-01 17:43:02 +03:00
dependabot[bot]
40378afbf8 chore(deps): bump actions/labeler from 5 to 6 (#35386)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 07:02:52 -07:00
dependabot[bot]
84e21434b3 chore(deps): bump aws-actions/configure-aws-credentials from 4 to 5 (#35387)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 07:02:26 -07:00
dependabot[bot]
d27210794c chore(deps): bump actions/setup-python from 5 to 6 (#35390)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 07:01:48 -07:00
dependabot[bot]
5a90a84685 chore(deps): bump actions/github-script from 7 to 8 (#35385)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 07:00:16 -07:00
dependabot[bot]
6fbbc85df7 chore(deps): bump actions/setup-node from 4 to 5 (#35394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 06:59:44 -07:00
Geidō
a66c230058 fix(SqlLab): Hit tableschemaview with a valid queryEditorId (#35341) 2025-10-01 14:39:02 +03:00
Damian Pendrak
19db0353a9 feat(db): custom database error messages (#34674) 2025-10-01 14:29:57 +03:00
Kamil Gabryjelski
88e5581d04 fix: Broken splitter in sql lab and some minor visual fixes (#35416) 2025-10-01 13:11:50 +02:00
Beto Dealmeida
bf88d9bb1c fix(pinot): restrict types in dialect (#35337) 2025-09-30 16:34:53 -04:00
Beto Dealmeida
d51b35f61b fix: adhoc orderby in explore (#35342) 2025-09-30 16:34:44 -04:00
Gabriel Torres Ruiz
220480b627 feat(theming): add base theme config (#35220) 2025-09-30 11:01:31 -07:00
Mehmet Salih Yavuz
ef78d2af06 fix(doris): Don't set supports_cross_catalog_queries to true (#35332)
Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
2025-09-30 14:42:03 +03:00
Evan Rusackas
7deed00def chore(frontend): Consolidate ESLint configurations in superset-frontend (#35318)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-29 23:52:25 -07:00
Beto Dealmeida
4e093a8e2a feat: sqlglot dialect for Pinot (#35333) 2025-09-29 21:56:35 -04:00
Elizabeth Thompson
0b73b4842f fix: Enable DuckDB examples loading in showtime ephemeral environments (#35294)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-29 13:21:33 -07:00
Geidō
90f281f585 fix: AceEditor Autocomplete Highlight (#35316) 2025-09-29 13:37:30 +03:00
Evan Rusackas
d62249d13f test(frontend): Migrate from describe/it to flat test() pattern (#35305)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-28 11:45:33 -07:00
Maxime Beauchemin
ff102aadb3 refactor(llm): rename LLMS.md to AGENTS.md for modern AI tools (#35314)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-27 12:46:16 +03:00
Elizabeth Thompson
82e2bc6181 fix(DatasourceModal): replace imperative modal updates with declarative state (#35256)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 17:54:17 -07:00
Gabriel Torres Ruiz
784ff82847 fix(sqllab): fix blank bottom section in SQL Lab left panel (#35309) 2025-09-26 20:07:20 +03:00
Mehmet Salih Yavuz
027b25e6b8 fix(DateFilterControl): remove modal overlay style to fix z-index issues (#35292) 2025-09-26 15:42:46 +02:00
SBIN2010
b652fab042 fix(table): New ad-hoc columns retain the name of previous columns (#35274) 2025-09-26 10:34:55 -03:00
Nikita Rybalchenko
77a5969dc1 feat(pdf): add configurable PDF compression level support (#34096) 2025-09-25 08:29:54 -07:00
Geidō
fb9032c05c fix: Cosmetic issues (#35122) 2025-09-25 17:24:34 +03:00
Mehmet Salih Yavuz
7a9dbfe879 fix(BuilderComponentPane): navigation tabs padding (#35213) 2025-09-25 16:59:48 +03:00
Giulio Piccolo
0de78d8203 fix(deck.gl): ensure min/max values are included in polygon map legend breakpoints (#35033)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
2025-09-25 14:30:44 +03:00
Maxime Beauchemin
abc2d46fed refactor: remove obsolete Flask flash messaging system (#35237)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-25 00:05:16 -07:00
dependabot[bot]
927cc1cda1 chore(deps): bump tar-fs from 3.1.0 to 3.1.1 in /superset-frontend (#35272)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 22:29:13 -07:00
JUST.in DO IT
7f3840557a chore(react18): Migrate legacy react methods (#34892) 2025-09-24 12:34:22 -07:00
JUST.in DO IT
0defcb604b chore(sqllab): remove unused json param (#35065) 2025-09-24 10:26:55 -07:00
Beto Dealmeida
94686ddfbe fix(SQL Lab): syncTable on new tabs (#35216) 2025-09-24 11:58:54 -04:00
SBIN2010
ec322dfd8d fix(Mixed Chart): Tooltip incorrectly displays numbers with optional Y-axis format and showQueryIdentifiers set to true (#35224) 2025-09-24 17:44:01 +03:00
Mehmet Salih Yavuz
cb88d886c7 fix(PropertiesModal): do not show validation errors while loading (#35215) 2025-09-24 10:52:16 +03:00
Maxime Beauchemin
608e3baf43 feat(build): auto-rebuild/check TypeScript types for packages/plugins in webpack (#35240)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 19:22:59 -07:00
Elizabeth Thompson
b6f6b75348 fix(dashboard): update header border to use colorBorder token (#35199)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 18:08:17 -07:00
Elizabeth Thompson
a5ad1d186c docs: Add instruction to avoid time-specific language in code comments (#35200)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 18:07:59 -07:00
434 changed files with 10831 additions and 5860 deletions

2
.github/CODEOWNERS vendored
View File

@@ -20,7 +20,7 @@
# Notify PMC members of changes to GitHub Actions
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe
# Notify PMC members of changes to required GitHub Actions

View File

@@ -1 +1 @@
../LLMS.md
../AGENTS.md

View File

@@ -5,7 +5,7 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
interval: "daily"
- package-ecosystem: "npm"
ignore:
@@ -18,7 +18,7 @@ updates:
- dependency-name: "jest-environment-jsdom"
directory: "/superset-frontend/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -40,21 +40,21 @@ updates:
- package-ecosystem: "npm"
directory: ".github/actions"
schedule:
interval: "monthly"
interval: "daily"
open-pull-requests-limit: 10
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/docs/"
schedule:
interval: "monthly"
interval: "daily"
open-pull-requests-limit: 10
versioning-strategy: increase
- package-ecosystem: "npm"
directory: "/superset-websocket/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -63,7 +63,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-websocket/utils/client-ws-app/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -75,7 +75,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-calendar/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -85,7 +85,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-histogram/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -95,7 +95,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -105,7 +105,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -115,7 +115,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -125,7 +125,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -135,7 +135,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -145,7 +145,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -155,7 +155,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -165,7 +165,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-table/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -175,7 +175,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -185,7 +185,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -195,7 +195,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -205,7 +205,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -215,7 +215,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -225,7 +225,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-event-flow/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -235,7 +235,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -245,7 +245,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -255,7 +255,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -265,7 +265,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/preset-chart-xy/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -275,7 +275,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-heatmap/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -285,7 +285,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -295,7 +295,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/legacy-plugin-chart-sunburst/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -305,7 +305,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -315,7 +315,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/generator-superset/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -325,7 +325,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -339,7 +339,7 @@ updates:
- dependency-name: "react-markdown"
- dependency-name: "remark-gfm"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -349,7 +349,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-demo/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot
@@ -359,7 +359,7 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-switchboard/"
schedule:
interval: "monthly"
interval: "daily"
labels:
- npm
- dependabot

View File

@@ -41,7 +41,7 @@ jobs:
uses: ./.github/actions/setup-supersetbot/
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.10"

View File

@@ -27,7 +27,7 @@ jobs:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@v5
- name: Check and notify
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |

View File

@@ -44,7 +44,7 @@ jobs:
pull-requests: write
steps:
- name: Comment access denied
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!

View File

@@ -29,7 +29,7 @@ jobs:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version-file: './superset-embedded-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'

View File

@@ -19,7 +19,7 @@ jobs:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version-file: './superset-embedded-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'

View File

@@ -33,7 +33,7 @@ jobs:
pull-requests: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -69,7 +69,7 @@ jobs:
- name: Comment (success)
if: steps.describe-services.outputs.active == 'true'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{github.token}}
script: |

View File

@@ -63,7 +63,7 @@ jobs:
- name: Get event SHA
id: get-sha
if: steps.eval-label.outputs.result == 'up'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -94,7 +94,7 @@ jobs:
core.setOutput("sha", prSha);
- name: Looking for feature flags in PR description
uses: actions/github-script@v7
uses: actions/github-script@v8
id: eval-feature-flags
if: steps.eval-label.outputs.result == 'up'
with:
@@ -116,7 +116,7 @@ jobs:
return results;
- name: Reply with confirmation comment
uses: actions/github-script@v7
uses: actions/github-script@v8
if: steps.eval-label.outputs.result == 'up'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -189,7 +189,7 @@ jobs:
--extra-flags "--build-arg INCLUDE_CHROMIUM=false"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -225,7 +225,7 @@ jobs:
persist-credentials: false
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -248,7 +248,7 @@ jobs:
- name: Fail on missing container image
if: steps.check-image.outcome == 'failure'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |
@@ -318,7 +318,7 @@ jobs:
echo "ip=$(aws ec2 describe-network-interfaces --network-interface-ids ${{ steps.get-eni.outputs.eni }} | jq -r '.NetworkInterfaces | first | .Association.PublicIp')" >> $GITHUB_OUTPUT
- name: Comment (success)
if: ${{ success() }}
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{github.token}}
script: |
@@ -331,7 +331,7 @@ jobs:
});
- name: Comment (failure)
if: ${{ failure() }}
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{github.token}}
script: |

View File

@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'

View File

@@ -9,7 +9,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v6
with:
sync-labels: true

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check for 'hold' label
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |

View File

@@ -39,7 +39,7 @@ jobs:
echo "HOMEBREW_REPOSITORY=$HOMEBREW_REPOSITORY" >>"${GITHUB_ENV}"
brew install norwoodj/tap/helm-docs
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'

View File

@@ -42,7 +42,7 @@ jobs:
- name: Install Node.js
if: env.HAS_TAGS
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'

View File

@@ -37,7 +37,7 @@ jobs:
steps:
- name: Security Check - Authorize Maintainers Only
id: auth
uses: actions/github-script@v7
uses: actions/github-script@v8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -63,7 +63,7 @@ jobs:
with:
run: testdata
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install npm dependencies

View File

@@ -36,7 +36,7 @@ jobs:
submodules: recursive
ref: master
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install eyes-storybook dependencies

View File

@@ -36,7 +36,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './docs/.nvmrc'
- name: Setup Python

View File

@@ -21,12 +21,14 @@ jobs:
- uses: actions/checkout@v5
# Do not bump this linkinator-action version without opening
# an ASF Infra ticket to allow the new version first!
- uses: JustinBeckwith/linkinator-action@v1.11.0
- uses: JustinBeckwith/linkinator-action@3d5ba091319fa7b0ac14703761eebb7d100e6f6d # v1.11.0
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
with:
paths: "**/*.md, **/*.mdx, !superset-frontend/CHANGELOG.md"
paths: "**/*.md, **/*.mdx"
linksToSkip: >-
^https://github.com/apache/(superset|incubator-superset)/(pull|issue)/\d+,
^https://github.com/apache/(superset|incubator-superset)/(pull|issues)/\d+,
^https://github.com/apache/(superset|incubator-superset)/commit/[a-f0-9]+,
superset-frontend/.*CHANGELOG\.md,
http://localhost:8088/,
http://127.0.0.1:3000/,
http://localhost:9001/,
@@ -41,12 +43,12 @@ jobs:
http://theiconic.com.au/,
https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html,
^https://img\.shields\.io/.*,
https://vkusvill.ru/
https://www.linkedin.com/in/mark-thomas-b16751158/
https://theiconic.com.au/
https://wattbewerb.de/
https://timbr.ai/
https://opensource.org/license/apache-2-0
https://vkusvill.ru/,
https://www.linkedin.com/in/mark-thomas-b16751158/,
https://theiconic.com.au/,
https://wattbewerb.de/,
https://timbr.ai/,
https://opensource.org/license/apache-2-0,
https://www.plaidcloud.com/
build-deploy:
name: Build & Deploy
@@ -61,7 +63,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './docs/.nvmrc'
- name: yarn install

View File

@@ -109,7 +109,7 @@ jobs:
run: testdata
- name: Setup Node.js
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install npm dependencies

View File

@@ -101,7 +101,7 @@ jobs:
CR_RELEASE_NAME_TEMPLATE: "superset-helm-chart-{{ .Version }}"
- name: Open Pull Request
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const branchName = '${{ env.branch_name }}';

View File

@@ -99,7 +99,7 @@ jobs:
run: testdata
- name: Setup Node.js
if: steps.check.outputs.python || steps.check.outputs.frontend
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install npm dependencies

View File

@@ -31,7 +31,7 @@ jobs:
- name: Setup Node.js
if: steps.check.outputs.frontend
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'
- name: Install dependencies

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- name: Quickly add thumbs up!
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, '@supersetbot')
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')

View File

@@ -60,7 +60,7 @@ jobs:
build: "true"
- name: Use Node.js 20
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20
@@ -112,7 +112,7 @@ jobs:
fetch-depth: 0
- name: Use Node.js 20
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20

View File

@@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: './superset-frontend/.nvmrc'

View File

@@ -82,6 +82,7 @@ intro_header.txt
# for LLMs
llm-context.md
AGENTS.md
LLMS.md
CLAUDE.md
CURSOR.md

View File

@@ -68,7 +68,11 @@ superset/
### Apache License Headers
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
- **LLM instruction files are excluded** - Files like AGENTS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
### Code Comments
- **Avoid time-specific language** - Don't use words like "now", "currently", "today" in code comments as they become outdated
- **Write timeless comments** - Comments should remain accurate regardless of when they're read
## Documentation Requirements

View File

@@ -1 +1 @@
LLMS.md
AGENTS.md

View File

@@ -26,6 +26,9 @@ ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
# Include translations in the final build
ARG BUILD_TRANSLATIONS="false"
# Build arg to pre-populate examples DuckDB file
ARG LOAD_EXAMPLES_DUCKDB="false"
######################################################################
# superset-node-ci used as a base for building frontend assets and CI
######################################################################
@@ -143,8 +146,8 @@ RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
######################################################################
FROM python-base AS python-common
# Build arg to pre-populate examples DuckDB file
ARG LOAD_EXAMPLES_DUCKDB="false"
# Re-declare build arg to receive it in this stage
ARG LOAD_EXAMPLES_DUCKDB
ENV SUPERSET_HOME="/app/superset_home" \
HOME="/app/superset_home" \

View File

@@ -1 +1 @@
LLMS.md
AGENTS.md

2
GPT.md
View File

@@ -1 +1 @@
LLMS.md
AGENTS.md

View File

@@ -1769,6 +1769,48 @@ You can use the `Extra` field in the **Edit Databases** form to configure SSL:
}
}
```
##### Custom Error Messages
You can use the `CUSTOM_DATABASE_ERRORS` in the `superset/custom_database_errors.py` file or overwrite it in your config file to configure custom error messages for database exceptions.
This feature lets you transform raw database errors into user-friendly messages, optionally including documentation links and hiding default error codes.
Provide an empty string as the first value to keep the original error message. This way, you can add just a link to the documentation
**Example usage:**
```Python
CUSTOM_DATABASE_ERRORS = {
"database_name": {
re.compile('permission denied for view'): (
__(
'Permission denied'
),
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
{
"custom_doc_links": [
{
"url": "https://example.com/docs/1",
"label": "Check documentation"
},
],
"show_issue_info": False,
}
)
},
"examples": {
re.compile(r'message="(?P<message>[^"]*)"'): (
__(
'Unexpected error: "%(message)s"'
),
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
{}
)
}
}
```
**Options:**
- ``custom_doc_links``: List of documentation links to display with the error.
- ``show_issue_info``: Set to ``False`` to hide default error codes.
## Misc

View File

@@ -27,7 +27,7 @@
"version:remove:components": "node scripts/manage-versions.mjs remove components"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/icons": "^6.1.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/plugin-client-redirects": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
@@ -49,7 +49,7 @@
"@storybook/preview-api": "^8.6.11",
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^5.26.7",
"antd": "^5.27.4",
"caniuse-lite": "^1.0.30001739",
"docusaurus-plugin-less": "^2.0.2",
"json-bigint": "^1.0.0",
@@ -63,7 +63,7 @@
"remark-import-partial": "^0.0.2",
"reselect": "^5.1.1",
"storybook": "^8.6.11",
"swagger-ui-react": "^5.27.1",
"swagger-ui-react": "^5.29.1",
"tinycolor2": "^1.4.2",
"ts-loader": "^9.5.4"
},
@@ -74,15 +74,15 @@
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.42.0",
"eslint": "^9.34.0",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2",
"typescript-eslint": "^8.39.0",
"webpack": "^5.101.0"
"typescript-eslint": "^8.45.0",
"webpack": "^5.102.0"
},
"browserslist": {
"production": [

View File

@@ -16,10 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
{
"settings": {
"import/resolver": {
"typescript": {}
}
declare module '@theme/Layout' {
import type { ReactNode } from 'react';
export interface Props {
readonly children?: ReactNode;
readonly noFooter?: boolean;
readonly wrapperClassName?: string;
readonly title?: string;
readonly description?: string;
}
export default function Layout(props: Props): ReactNode;
}

View File

@@ -23,8 +23,8 @@ import type { Plugin } from '@docusaurus/types';
export default function webpackExtendPlugin(): Plugin<void> {
return {
name: 'custom-webpack-plugin',
configureWebpack(config, isServer, utils) {
const { isDev } = utils;
configureWebpack(config) {
const isDev = process.env.NODE_ENV === 'development';
return {
devtool: isDev ? 'eval-source-map' : config.devtool,
...(isDev && {

View File

@@ -2,12 +2,27 @@
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": "."
"baseUrl": ".",
"skipLibCheck": true,
"noImplicitAny": false,
"strict": false,
"types": ["@docusaurus/module-type-aliases"]
},
"jsx": "react-jsx",
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"@superset-ui/core": ["../superset-frontend/packages/superset-ui-core/src"],
"@superset-ui/core/*": ["../superset-frontend/packages/superset-ui-core/src/*"],
"*": ["src/*", "node_modules/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"../superset-frontend/**/*",
"src/webpack.extend.ts"
]
}

View File

@@ -232,15 +232,15 @@
classnames "^2.2.6"
rc-util "^5.31.1"
"@ant-design/icons@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.0.0.tgz#302c935b8b0b429e4444cbc45809247276186d94"
integrity sha512-o0aCCAlHc1o4CQcapAwWzHeaW2x9F49g7P3IDtvtNXgHowtRWYb7kiubt8sQPFvfVIVU/jLw2hzeSlNt0FU+Uw==
"@ant-design/icons@^6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.1.0.tgz#97cc14a3c0528b8e2b37f41f232b019f2ca38c2c"
integrity sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==
dependencies:
"@ant-design/colors" "^8.0.0"
"@ant-design/icons-svg" "^4.4.0"
"@rc-component/util" "^1.2.1"
classnames "^2.2.6"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@ant-design/react-slick@~1.1.2":
version "1.1.2"
@@ -2385,10 +2385,10 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
version "4.9.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3"
integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==
dependencies:
eslint-visitor-keys "^3.4.3"
@@ -2433,10 +2433,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.34.0", "@eslint/js@^9.32.0":
version "9.34.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.34.0.tgz#fc423168b9d10e08dea9088d083788ec6442996b"
integrity sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==
"@eslint/js@9.36.0", "@eslint/js@^9.32.0":
version "9.36.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.36.0.tgz#b1a3893dd6ce2defed5fd49de805ba40368e8fef"
integrity sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
@@ -2746,10 +2746,10 @@
rc-resize-observer "^1.3.1"
rc-util "^5.44.0"
"@rc-component/util@^1.2.1":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.2.2.tgz#f8363b0e1cc78af3ec56e2235cc3438eb8832040"
integrity sha512-p3zQr9Wu8BKncqmuW23olzBoAFsN8PYMS9FaI4JwJLwknH7DvfHAr1fwbfl9aAWw4Jva64ucpenbgG4fznLUSw==
"@rc-component/util@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.3.0.tgz#fc8e1ce1e5292592ef7f45839a3c07366288275c"
integrity sha512-hfXE04CVsxI/slmWKeSh6du7sSKpbvVdVEZCa8A+2QWDlL97EsCYme2c3ZWLn1uC9FR21JoewlrhUPWO4QgO8w==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
@@ -3050,7 +3050,34 @@
ramda-adjunct "^5.0.0"
unraw "^3.0.0"
"@swagger-api/apidom-core@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-core@^1.0.0-beta.46":
"@swagger-api/apidom-ast@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.50.tgz#9e0f4132ea54dd4fca0f31f919a0c5aad12d93c1"
integrity sha512-uUBUm6J6KlyKppyfS7DIW37De6oyMVIpHYmaNV3YAaDMuRMov5KHHWXKbqWlI+l493OljOcXEqDIPeLzm6B5PQ==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
unraw "^3.0.0"
"@swagger-api/apidom-core@>=1.0.0-beta.50 <1.0.0-rc.0", "@swagger-api/apidom-core@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.50.tgz#d9ed6821d1b38b66ed8cd0b8d8bf5de997354178"
integrity sha512-9N7ySdyzx/3kUnprAi63GQNt+Kq8VUvErwDgPcMRAsZX8jUhk9KLJ9N0fup4mWm6+xGs0JH35wxBxnanS6aiqw==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-ast" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
minim "~0.23.8"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
short-unique-id "^5.3.2"
ts-mixer "^6.0.3"
"@swagger-api/apidom-core@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.46.tgz#6c879f6cf9e67fa0cc36955d3a7f0dd185e36b4f"
integrity sha512-CC8SdAkjSA/l31xfg5l4860cixbYABIXamJeNcXJr+O1z9YNKHIIENeI0zIwORk/TYUaSOg1KUjDnV2Pdctufg==
@@ -3065,14 +3092,31 @@
short-unique-id "^5.3.2"
ts-mixer "^6.0.3"
"@swagger-api/apidom-error@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-error@^1.0.0-beta.46":
"@swagger-api/apidom-error@>=1.0.0-beta.50 <1.0.0-rc.0", "@swagger-api/apidom-error@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.50.tgz#4354c4ade0482824ec7b17667390057f87838ddd"
integrity sha512-vdpi2nRVcxXLGc68JPNwTcKrCKl8PnOEPuykZSxeNbDKnZY80APbsoLDX+1gdRgafK/7k5XdsBkpDQscsTkDng==
dependencies:
"@babel/runtime-corejs3" "^7.20.7"
"@swagger-api/apidom-error@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.46.tgz#fe9039c23ac655930e1f06d99b8ea28bc40d905d"
integrity sha512-AN2ZOHp7gtVsCa6hWxH3nk/1PS7bRtqDVCih3C5qA8k5JfHjP2wfks7b14Ns971SUvDD/SubaCmXmu0CRBHLag==
dependencies:
"@babel/runtime-corejs3" "^7.20.7"
"@swagger-api/apidom-json-pointer@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.46":
"@swagger-api/apidom-json-pointer@>=1.0.0-beta.50 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.50.tgz#8d2c4129c0d59cf059df3f0e40703114a5c3b49a"
integrity sha512-2TgFKHlZ/SlnTZzY7EwE8xx5Pr2BYePX52xZJFqWnueSAIcCcsrqZeazWIAaDe/gXd47CDqU95nDChMECERspA==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@swaggerexpert/json-pointer" "^2.10.1"
"@swagger-api/apidom-json-pointer@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.46.tgz#40edcaf5fddc5c748ed4084c9042a2669cbf507c"
integrity sha512-JsbCkYDG7rbf1QmEhtXgbuCGJ4oW+uWDOEnW+Gar49sjk/OKX/2GSGUpywRstnZcopjNarLcEx/H1mHRLoL45A==
@@ -3136,6 +3180,20 @@
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-2019-09@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.0.0-beta.50.tgz#83c0eae65b2c4563c5233acdfbccb66c07065e93"
integrity sha512-QP6DuthV8ZWQnthYbPEVikK5rTN4T5lhnAnmO1v6zOCS9B1heKCFcIYgBhcqCnuZ0Tt8kGOfLyqGMb57lPkCdw==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-json-schema-draft-7" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-2020-12@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.46.tgz#779dbfb80ad10c3d6b2fd182a35231803e9ee387"
@@ -3150,6 +3208,20 @@
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-2020-12@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.50.tgz#0310fc351efd42695227c012b1407728e1400e92"
integrity sha512-ZaqrtZEXUx35x66ND8sc5vf1sIuWPERA15EdRHeca56E09RnjZMUHkiDvdx78165h31QmM67YLi04zEBYhQS0g==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-json-schema-2019-09" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-draft-4@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.46.tgz#f17bb2010dd933fb6875c82d83826eda62f87bb6"
@@ -3163,6 +3235,19 @@
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-draft-4@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.50.tgz#43b0e37d135b805ab3ea7efce0c5fc5678e95abc"
integrity sha512-aqCwW+iuN7RokH10vDp/eEwlrT4LAlHGy1pLzAS9aFVJyUutfm0I4fxLfddOKD2yd04z858zhLwOVSo4BjrLHg==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-ast" "^1.0.0-beta.50"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-draft-6@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.46.tgz#6629cb35e9e10e670e34a4fd50b6d159f5666766"
@@ -3177,6 +3262,20 @@
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-draft-6@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.50.tgz#a311488627a12953a4a331d3b4231c20311aed9c"
integrity sha512-trF1TZZ79WJOjQw3C1Y7wcqNMxxgHMZtJW2/tP5MwII1hqsExGzmGyUuNlVuSC9k9v/9sCj85hQlJ4TW6HFciQ==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-json-schema-draft-4" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-draft-7@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.46.tgz#04fa39cfe0f869e0c7be0f70ceb34ffb6cc830ad"
@@ -3191,6 +3290,20 @@
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-json-schema-draft-7@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.50.tgz#eb99fdecf9746905331f281578e924082659aef0"
integrity sha512-g9VscnMwjPUYCfR6UxUwsLiIKnyXy2W28J+zN0rbijoSEtUdakcrxwdPhqwgJZHPci8NHNE8574zaocqKBiqSg==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-json-schema-draft-6" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.4"
"@swagger-api/apidom-ns-openapi-2@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-2@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-beta.46.tgz#448f5b7bacd29a3dc37124a7a62f154c94cb818d"
@@ -3219,7 +3332,37 @@
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.3"
"@swagger-api/apidom-ns-openapi-3-1@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.46":
"@swagger-api/apidom-ns-openapi-3-0@^1.0.0-beta.50":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-beta.50.tgz#1d9631c0c7504a8985671f2f8e5ca435235b1a54"
integrity sha512-I4GHyNILNxDsYKYeG1+ZA3rnfU1RAYtNp3dA+G8LCX5AB/2N7dT2VPK8HS4cj9m3ZVz7dl1o+X6tpaJIN5kDsA==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-json-schema-draft-4" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.3"
"@swagger-api/apidom-ns-openapi-3-1@>=1.0.0-beta.50 <1.0.0-rc.0":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.50.tgz#8b9a0c97849cc107228e66dd746bed858b78c50d"
integrity sha512-kxwuaFl1kQddk/RBS5Mz3rE/6v5mXggqhzVwDBObGjgkRmDRVF5nUalziBRNg6A3NcpYbsjNMU/OCA1JihFkrg==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-ast" "^1.0.0-beta.50"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-json-pointer" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-json-schema-2020-12" "^1.0.0-beta.50"
"@swagger-api/apidom-ns-openapi-3-0" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
ramda "~0.30.0"
ramda-adjunct "^5.0.0"
ts-mixer "^6.0.3"
"@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.46":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.46.tgz#87056343a5e2ff48992a27af99611fcbdfbc0f22"
integrity sha512-dSs9h4YEty7h8HJDtWSeC0cg0O2XUTwTVbnOylHX4Z7tXYub8TJ2LEbngRic/R4T58xt76Ro5rZqGUcrt5qXDQ==
@@ -3423,16 +3566,16 @@
tree-sitter "=0.22.4"
web-tree-sitter "=0.24.5"
"@swagger-api/apidom-reference@>=1.0.0-beta.41 <1.0.0-rc.0":
version "1.0.0-beta.46"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.46.tgz#c9cb8edfc1099f05e19075c880c4e7c4a87d5015"
integrity sha512-OpJ72k17hyrUJYvj5HgNg93uUsk/28cBWkDAna/cZtCiTdz9zji31JiDUr5GoUd81rpVGuxo4AuUvxXVsH35tA==
"@swagger-api/apidom-reference@>=1.0.0-beta.50 <1.0.0-rc.0":
version "1.0.0-beta.50"
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.50.tgz#d03f5506327e92dff81a4fac6460edcd2bf3989e"
integrity sha512-aD7gTWPgkJb9oYaC4jZPvxb7YbQKG9pWDYZigAkVGqOAbeYxUXeI00XyCLj/cH8l7KwyhTZNX70F7VnfxOkq7w==
dependencies:
"@babel/runtime-corejs3" "^7.26.10"
"@swagger-api/apidom-core" "^1.0.0-beta.46"
"@swagger-api/apidom-error" "^1.0.0-beta.46"
"@swagger-api/apidom-core" "^1.0.0-beta.50"
"@swagger-api/apidom-error" "^1.0.0-beta.50"
"@types/ramda" "~0.30.0"
axios "^1.9.0"
axios "^1.12.2"
minimatch "^7.4.3"
process "^0.11.10"
ramda "~0.30.0"
@@ -4087,117 +4230,79 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.40.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz#19f959f273b32f5082c891903645e6a85328db4e"
integrity sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==
"@typescript-eslint/eslint-plugin@8.45.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz#9f251d4e85ec5089e7cccb09257ce93dbf0d7744"
integrity sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.40.0"
"@typescript-eslint/type-utils" "8.40.0"
"@typescript-eslint/utils" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/scope-manager" "8.45.0"
"@typescript-eslint/type-utils" "8.45.0"
"@typescript-eslint/utils" "8.45.0"
"@typescript-eslint/visitor-keys" "8.45.0"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.40.0.tgz#1bc9f3701ced29540eb76ff2d95ce0d52ddc7e69"
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
"@typescript-eslint/parser@8.45.0", "@typescript-eslint/parser@^8.42.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.45.0.tgz#571660c98824aefb4a6ec3b3766655d1348520a4"
integrity sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==
dependencies:
"@typescript-eslint/scope-manager" "8.40.0"
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/scope-manager" "8.45.0"
"@typescript-eslint/types" "8.45.0"
"@typescript-eslint/typescript-estree" "8.45.0"
"@typescript-eslint/visitor-keys" "8.45.0"
debug "^4.3.4"
"@typescript-eslint/parser@^8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.42.0.tgz#20ea66f4867981fb5bb62cbe1454250fc4a440ab"
integrity sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==
"@typescript-eslint/project-service@8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.45.0.tgz#f83dda1bca31dae2fd6821f9131daf1edebfd46c"
integrity sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==
dependencies:
"@typescript-eslint/scope-manager" "8.42.0"
"@typescript-eslint/types" "8.42.0"
"@typescript-eslint/typescript-estree" "8.42.0"
"@typescript-eslint/visitor-keys" "8.42.0"
"@typescript-eslint/tsconfig-utils" "^8.45.0"
"@typescript-eslint/types" "^8.45.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz#1b7ba6079ff580c3215882fe75a43e5d3ed166b9"
integrity sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==
"@typescript-eslint/scope-manager@8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz#59615ba506a9e3479d1efb0d09d6ab52f2a19142"
integrity sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.40.0"
"@typescript-eslint/types" "^8.40.0"
debug "^4.3.4"
"@typescript-eslint/types" "8.45.0"
"@typescript-eslint/visitor-keys" "8.45.0"
"@typescript-eslint/project-service@8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.42.0.tgz#636eb3418b6c42c98554dce884943708bf41a583"
integrity sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==
"@typescript-eslint/tsconfig-utils@8.45.0", "@typescript-eslint/tsconfig-utils@^8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz#63d38282790a2566c571bad423e7c1cad1f3d64c"
integrity sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==
"@typescript-eslint/type-utils@8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz#04004bdf2598844faa29fb936fb6b0ee10d6d3f3"
integrity sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.42.0"
"@typescript-eslint/types" "^8.42.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz#2fbfcc8643340d8cd692267e61548b946190be8a"
integrity sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==
dependencies:
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/scope-manager@8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz#36016757bc85b46ea42bae47b61f9421eddedde3"
integrity sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==
dependencies:
"@typescript-eslint/types" "8.42.0"
"@typescript-eslint/visitor-keys" "8.42.0"
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz#8e8fdb9b988854aedd04abdde3239c4bdd2d26e4"
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
"@typescript-eslint/tsconfig-utils@8.42.0", "@typescript-eslint/tsconfig-utils@^8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz#21a3e74396fd7443ff930bc41b27789ba7e9236e"
integrity sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==
"@typescript-eslint/type-utils@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz#a7e4a1f0815dd0ba3e4eef945cc87193ca32c422"
integrity sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==
dependencies:
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/utils" "8.40.0"
"@typescript-eslint/types" "8.45.0"
"@typescript-eslint/typescript-estree" "8.45.0"
"@typescript-eslint/utils" "8.45.0"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.40.0.tgz#0b580fdf643737aa5c01285314b5c6e9543846a9"
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
"@typescript-eslint/types@8.45.0", "@typescript-eslint/types@^8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.45.0.tgz#fc01cd2a4690b9713b02f895e82fb43f7d960684"
integrity sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==
"@typescript-eslint/types@8.42.0", "@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@^8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.42.0.tgz#ae15c09cebda20473772902033328e87372db008"
integrity sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==
"@typescript-eslint/typescript-estree@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz#295149440ce7da81c790a4e14e327599a3a1e5c9"
integrity sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==
"@typescript-eslint/typescript-estree@8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz#3498500f109a89b104d2770497c707e56dfe062d"
integrity sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==
dependencies:
"@typescript-eslint/project-service" "8.40.0"
"@typescript-eslint/tsconfig-utils" "8.40.0"
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/project-service" "8.45.0"
"@typescript-eslint/tsconfig-utils" "8.45.0"
"@typescript-eslint/types" "8.45.0"
"@typescript-eslint/visitor-keys" "8.45.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -4205,46 +4310,22 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/typescript-estree@8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz#593c3af87d4462252c0d7239d1720b84a1b56864"
integrity sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==
dependencies:
"@typescript-eslint/project-service" "8.42.0"
"@typescript-eslint/tsconfig-utils" "8.42.0"
"@typescript-eslint/types" "8.42.0"
"@typescript-eslint/visitor-keys" "8.42.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.40.0.tgz#8d0c6430ed2f5dc350784bb0d8be514da1e54054"
integrity sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==
"@typescript-eslint/utils@8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.45.0.tgz#6e68e92d99019fdf56018d0e6664c76a70470c95"
integrity sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.40.0"
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/scope-manager" "8.45.0"
"@typescript-eslint/types" "8.45.0"
"@typescript-eslint/typescript-estree" "8.45.0"
"@typescript-eslint/visitor-keys@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz#c1b45196981311fed7256863be4bfb2d3eda332a"
integrity sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==
"@typescript-eslint/visitor-keys@8.45.0":
version "8.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz#4e3bcc55da64ac61069ebfe62ca240567ac7d784"
integrity sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==
dependencies:
"@typescript-eslint/types" "8.40.0"
eslint-visitor-keys "^4.2.1"
"@typescript-eslint/visitor-keys@8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz#87c6caaa1ac307bc73a87c1fc469f88f0162f27e"
integrity sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==
dependencies:
"@typescript-eslint/types" "8.42.0"
"@typescript-eslint/types" "8.45.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0":
@@ -4544,10 +4625,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^5.26.7:
version "5.27.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.27.1.tgz#5378fc017cb4057ffefe2a670f20e54b924d897d"
integrity sha512-jGMSdBN7hAMvPV27B4RhzZfL6n6yu8yDbo7oXrlJasaOqB7bSDPcjdEy1kXy3JPsny/Qazb1ykzRI4EfcByAPQ==
antd@^5.27.4:
version "5.27.4"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.27.4.tgz#13c97deb12e6aeb43adecd23f3dbe3139a62e579"
integrity sha512-rhArohoAUCxhkPjGI/BXthOrrjaElL4Fb7d4vEHnIR3DpxFXfegd4rN21IgGdiF+Iz4EFuUZu8MdS8NuJHLSVQ==
dependencies:
"@ant-design/colors" "^7.2.1"
"@ant-design/cssinjs" "^1.23.0"
@@ -4585,10 +4666,10 @@ antd@^5.26.7:
rc-resize-observer "^1.4.3"
rc-segmented "~2.7.0"
rc-select "~14.16.8"
rc-slider "~11.1.8"
rc-slider "~11.1.9"
rc-steps "~6.0.1"
rc-switch "~4.1.0"
rc-table "~7.51.1"
rc-table "~7.53.0"
rc-tabs "~15.7.0"
rc-textarea "~1.10.2"
rc-tooltip "~6.4.0"
@@ -4765,10 +4846,10 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
axios@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.0.tgz#11248459be05a5ee493485628fa0e4323d0abfc3"
integrity sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==
axios@^1.12.2:
version "1.12.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
@@ -4862,11 +4943,16 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.5.1:
base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.8.9:
version "2.8.10"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz#32eb5e253d633fa3fa3ffb1685fabf41680d9e8a"
integrity sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==
batch@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
@@ -4980,14 +5066,15 @@ browser-assert@^1.2.1:
resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0, browserslist@^4.25.3:
version "4.25.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5"
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.24.5, browserslist@^4.25.0, browserslist@^4.25.3:
version "4.26.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.26.3.tgz#40fbfe2d1cd420281ce5b1caa8840049c79afb56"
integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==
dependencies:
caniuse-lite "^1.0.30001735"
electron-to-chromium "^1.5.204"
node-releases "^2.0.19"
baseline-browser-mapping "^2.8.9"
caniuse-lite "^1.0.30001746"
electron-to-chromium "^1.5.227"
node-releases "^2.0.21"
update-browserslist-db "^1.1.3"
buffer-from@^1.0.0:
@@ -4995,6 +5082,14 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -5082,11 +5177,16 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001735, caniuse-lite@^1.0.30001739:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001739:
version "1.0.30001739"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4"
integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==
caniuse-lite@^1.0.30001746:
version "1.0.30001746"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz#199d20f04f5369825e00ff7067d45d5dfa03aee7"
integrity sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
@@ -5254,7 +5354,7 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clsx@^2.0.0:
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -6340,14 +6440,7 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dompurify@=3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
dompurify@^3.2.5:
dompurify@=3.2.6, dompurify@^3.2.5:
version "3.2.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad"
integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
@@ -6416,10 +6509,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.204:
version "1.5.207"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz#0fedde3eec615065ee95531c09a10578644c5552"
integrity sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==
electron-to-chromium@^1.5.227:
version "1.5.228"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz#38b849bc8714bd21fb64f5ad56bf8cfd8638e1e9"
integrity sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -6775,18 +6868,18 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^9.34.0:
version "9.34.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.34.0.tgz#0ea1f2c1b5d1671db8f01aa6b8ce722302016f7b"
integrity sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==
eslint@^9.36.0:
version "9.36.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.36.0.tgz#9cc5cbbfb9c01070425d9bfed81b4e79a1c09088"
integrity sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.21.0"
"@eslint/config-helpers" "^0.3.1"
"@eslint/core" "^0.15.2"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.34.0"
"@eslint/js" "9.36.0"
"@eslint/plugin-kit" "^0.3.5"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
@@ -9983,10 +10076,10 @@ node-gyp-build@^4.8.0, node-gyp-build@^4.8.2, node-gyp-build@^4.8.4:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
node-releases@^2.0.21:
version "2.0.21"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.21.tgz#f59b018bc0048044be2d4c4c04e4c8b18160894c"
integrity sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@@ -11455,10 +11548,10 @@ rc-select@~14.16.2, rc-select@~14.16.8:
rc-util "^5.16.1"
rc-virtual-list "^3.5.2"
rc-slider@~11.1.8:
version "11.1.8"
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-11.1.8.tgz#cf3b30dacac8f98d44f7685f733f6f7da146fc06"
integrity sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==
rc-slider@~11.1.9:
version "11.1.9"
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-11.1.9.tgz#d872130fbf4ec51f28543d62e90451091d6f5208"
integrity sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.5"
@@ -11482,10 +11575,10 @@ rc-switch@~4.1.0:
classnames "^2.2.1"
rc-util "^5.30.0"
rc-table@~7.51.1:
version "7.51.1"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.51.1.tgz#cd69ae3262d3b61e4c93c979c12786906e944691"
integrity sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==
rc-table@~7.53.0:
version "7.53.1"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.53.1.tgz#b891aa39e9d1d944711f018692d2c52013afc90f"
integrity sha512-firAd7Z+liqIDS5TubJ1qqcoBd6YcANLKWQDZhFf3rfoOTt/UNPj4n3O+2vhl+z4QMqwPEUVAil661WHA8H8Aw==
dependencies:
"@babel/runtime" "^7.10.1"
"@rc-component/context" "^1.4.0"
@@ -12515,7 +12608,7 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
sha.js@^2.4.11:
sha.js@^2.4.12:
version "2.4.12"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f"
integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==
@@ -12990,18 +13083,18 @@ svgo@^3.0.2, svgo@^3.2.0:
csso "^5.0.5"
picocolors "^1.0.0"
swagger-client@^3.35.5:
version "3.35.6"
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.35.6.tgz#03892a1ec9995db44d7b49feb7aafafc06d9ed51"
integrity sha512-OgwNneIdC45KXwOfwrlkwgWPeAKiV4K75mOnZioTddo1mpp9dTboCDVJas7185Ww1ziBwzShBqXpNGmyha9ZQg==
swagger-client@^3.35.7:
version "3.35.7"
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.35.7.tgz#2f649c1875cb88a747254963ea85dad09605f238"
integrity sha512-AAVk7lBFIw41wI0tsqyh/l4dwJ0/eslHL2Ex4hmsGtuKcD6/wXunetO8AsmE5MptK4YgRvpmUDvKnF1TaGzdiQ==
dependencies:
"@babel/runtime-corejs3" "^7.22.15"
"@scarf/scarf" "=1.4.0"
"@swagger-api/apidom-core" ">=1.0.0-beta.41 <1.0.0-rc.0"
"@swagger-api/apidom-error" ">=1.0.0-beta.41 <1.0.0-rc.0"
"@swagger-api/apidom-json-pointer" ">=1.0.0-beta.41 <1.0.0-rc.0"
"@swagger-api/apidom-ns-openapi-3-1" ">=1.0.0-beta.41 <1.0.0-rc.0"
"@swagger-api/apidom-reference" ">=1.0.0-beta.41 <1.0.0-rc.0"
"@swagger-api/apidom-core" ">=1.0.0-beta.50 <1.0.0-rc.0"
"@swagger-api/apidom-error" ">=1.0.0-beta.50 <1.0.0-rc.0"
"@swagger-api/apidom-json-pointer" ">=1.0.0-beta.50 <1.0.0-rc.0"
"@swagger-api/apidom-ns-openapi-3-1" ">=1.0.0-beta.50 <1.0.0-rc.0"
"@swagger-api/apidom-reference" ">=1.0.0-beta.50 <1.0.0-rc.0"
"@swaggerexpert/cookie" "^2.0.2"
deepmerge "~4.3.0"
fast-json-patch "^3.0.0-1"
@@ -13014,18 +13107,19 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
swagger-ui-react@^5.29.1:
version "5.29.1"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.29.1.tgz#90875ef551f0fc271e369db60e4a8cd369ce70f9"
integrity sha512-simkO3VoHrWIQWLH2vCpOJSPVbocV+RLGvHzzh+jhqs5heWsA1cnadx31BbKJVajfxIO4dC3lclxgMYbLUk3fQ==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
base64-js "^1.5.1"
buffer "^6.0.3"
classnames "^2.5.1"
css.escape "1.5.1"
deep-extend "0.6.0"
dompurify "=3.2.4"
dompurify "=3.2.6"
ieee754 "^1.2.1"
immutable "^3.x.x"
js-file-download "^0.4.12"
@@ -13046,8 +13140,8 @@ swagger-ui-react@^5.27.1:
remarkable "^2.0.1"
reselect "^5.1.1"
serialize-error "^8.1.0"
sha.js "^2.4.11"
swagger-client "^3.35.5"
sha.js "^2.4.12"
swagger-client "^3.35.7"
url-parse "^1.5.10"
xml "=1.0.1"
xml-but-prettier "^1.0.1"
@@ -13060,10 +13154,10 @@ synckit@^0.11.7:
dependencies:
"@pkgr/core" "^0.2.9"
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.2.tgz#ab4984340d30cb9989a490032f086dbb8b56d872"
integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==
tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.3.tgz#4b67b635b2d97578a06a2713d2f04800c237e99b"
integrity sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==
terser-webpack-plugin@^5.3.11, terser-webpack-plugin@^5.3.9:
version "5.3.14"
@@ -13321,15 +13415,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.39.0:
version "8.40.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.40.0.tgz#27541748f3ca889c9698327bdacf815f7dc61804"
integrity sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==
typescript-eslint@^8.45.0:
version "8.45.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b"
integrity sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==
dependencies:
"@typescript-eslint/eslint-plugin" "8.40.0"
"@typescript-eslint/parser" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/utils" "8.40.0"
"@typescript-eslint/eslint-plugin" "8.45.0"
"@typescript-eslint/parser" "8.45.0"
"@typescript-eslint/typescript-estree" "8.45.0"
"@typescript-eslint/utils" "8.45.0"
typescript@~5.9.2:
version "5.9.2"
@@ -13745,7 +13839,7 @@ vscode-uri@~3.0.8:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
watchpack@^2.4.1:
watchpack@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947"
integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
@@ -13868,10 +13962,10 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.101.3"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346"
integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==
webpack@^5.102.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.102.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.102.0.tgz#7a2416e6da356c35f1fb35333d2f5cee0133e953"
integrity sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@@ -13881,7 +13975,7 @@ webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
browserslist "^4.24.0"
browserslist "^4.24.5"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.3"
es-module-lexer "^1.2.1"
@@ -13894,9 +13988,9 @@ webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
mime-types "^2.1.27"
neo-async "^2.6.2"
schema-utils "^4.3.2"
tapable "^2.1.1"
tapable "^2.2.3"
terser-webpack-plugin "^5.3.11"
watchpack "^2.4.1"
watchpack "^2.4.4"
webpack-sources "^3.3.3"
webpackbar@^6.0.1:

View File

@@ -133,7 +133,8 @@ denodo = ["denodo-sqlalchemy~=1.0.6"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.4, <2"]
druid = ["pydruid>=0.6.5,<0.7"]
duckdb = ["duckdb-engine>=0.17.0"]
# DuckDB 1.x has type system incompatibilities with duckdb-engine.
duckdb = ["duckdb>=0.10.2,<0.11", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]

View File

@@ -181,8 +181,10 @@ dnspython==2.7.0
# email-validator
docker==7.0.0
# via apache-superset
duckdb==1.3.2
# via duckdb-engine
duckdb==0.10.3
# via
# apache-superset
# duckdb-engine
duckdb-engine==0.17.0
# via apache-superset
email-validator==2.2.0

View File

@@ -83,7 +83,6 @@ module.exports = {
'plugin:react-hooks/recommended',
'plugin:react-prefer-function-component/recommended',
'plugin:storybook/recommended',
'plugin:react-you-might-not-need-an-effect/legacy-recommended',
],
parser: '@babel/eslint-parser',
parserOptions: {
@@ -273,6 +272,10 @@ module.exports = {
{
files: ['packages/**'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
'no-restricted-imports': [
'error',
{
@@ -324,7 +327,12 @@ module.exports = {
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'playwright/**/*',
'**/test/**/*',
'**/tests/**/*',
'spec/**/*',
'**/fixtures/**/*',
'**/__mocks__/**/*',
'**/spec/**/*',
],
excludedFiles: 'cypress-base/cypress/**/*',
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
@@ -348,7 +356,9 @@ module.exports = {
devDependencies: true,
},
],
'jest/consistent-test-it': 'error',
'no-only-tests/no-only-tests': 'error',
'prefer-promise-reject-errors': 0,
'max-classes-per-file': 0,
// temporary rules to help with migration - please re-enable!
'testing-library/await-async-queries': 0,
@@ -387,6 +397,12 @@ module.exports = {
'*.stories.tsx',
'*.stories.jsx',
'fixtures.*',
'**/test/**/*',
'**/tests/**/*',
'spec/**/*',
'**/fixtures/**/*',
'**/__mocks__/**/*',
'**/spec/**/*',
'cypress-base/cypress/**/*',
'Stories.tsx',
'packages/superset-ui-core/src/theme/index.tsx',
@@ -400,10 +416,27 @@ module.exports = {
},
},
{
files: ['playwright/**/*'],
// Override specifically for packages stories and overview files
// This must come LAST to override other rules
files: [
'packages/**/*.stories.*',
'packages/**/*.overview.*',
'packages/**/fixtures.*',
],
rules: {
'import/no-unresolved': 0, // Playwright is not installed in main build
'import/no-extraneous-dependencies': 0, // Playwright is not installed in main build
'import/no-extraneous-dependencies': 'off',
},
},
{
// Allow @playwright/test imports in Playwright test files
files: ['playwright/**/*.ts', 'playwright/**/*.js'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
},
],
},
},
],
@@ -412,7 +445,6 @@ module.exports = {
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': ['error', true],
'i18n-strings/sentence-case-buttons': 'error',
camelcase: [
'error',
{

View File

@@ -3,3 +3,4 @@ cypress/screenshots
cypress/videos
src/temp
.temp_cache/
.tsbuildinfo

File diff suppressed because it is too large Load Diff

View File

@@ -129,7 +129,7 @@
"@visx/xychart": "^3.5.1",
"ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0",
"antd": "^5.24.6",
"antd": "^5.24.9",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
"content-disposition": "^0.5.4",
@@ -211,7 +211,7 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.55.6",
"@applitools/eyes-storybook": "^3.60.0",
"@babel/cli": "^7.27.2",
"@babel/compat-data": "^7.28.0",
"@babel/core": "^7.28.3",
@@ -225,7 +225,7 @@
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.26.0",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.28.2",
"@babel/runtime": "^7.28.4",
"@babel/runtime-corejs3": "^7.28.2",
"@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
@@ -260,7 +260,7 @@
"@types/node": "^22.12.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-json-tree": "^0.6.11",
"@types/react-json-tree": "^0.13.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
@@ -318,7 +318,7 @@
"jest-environment-jsdom": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"jest-websocket-mock": "^2.5.0",
"jsdom": "^26.0.0",
"jsdom": "^27.0.0",
"lerna": "^8.2.3",
"mini-css-extract-plugin": "^2.9.0",
"open-cli": "^8.0.0",
@@ -341,7 +341,7 @@
"tsx": "^4.20.3",
"typescript": "5.4.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.99.9",
"webpack": "^5.102.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",

View File

@@ -22,7 +22,7 @@
"typescript": "^5.0.0"
},
"peerDependencies": {
"antd": "^5.24.6",
"antd": "^5.24.9",
"react": "^17.0.2"
},
"scripts": {

View File

@@ -40,7 +40,7 @@ describe('aggregationOperator', () => {
granularity: 'month',
};
test('should return undefined for LAST_VALUE aggregation', () => {
it('should return undefined for LAST_VALUE aggregation', () => {
const formDataWithLastValue = {
...formData,
aggregation: 'LAST_VALUE',
@@ -51,7 +51,7 @@ describe('aggregationOperator', () => {
).toBeUndefined();
});
test('should return undefined when metrics is empty', () => {
it('should return undefined when metrics is empty', () => {
const queryObjectWithoutMetrics = {
...queryObject,
metrics: [],
@@ -67,7 +67,7 @@ describe('aggregationOperator', () => {
).toBeUndefined();
});
test('should apply sum aggregation to all metrics', () => {
it('should apply sum aggregation to all metrics', () => {
const formDataWithSum = {
...formData,
aggregation: 'sum',
@@ -91,7 +91,7 @@ describe('aggregationOperator', () => {
});
});
test('should apply mean aggregation to all metrics', () => {
it('should apply mean aggregation to all metrics', () => {
const formDataWithMean = {
...formData,
aggregation: 'mean',
@@ -115,7 +115,7 @@ describe('aggregationOperator', () => {
});
});
test('should use default aggregation when not specified', () => {
it('should use default aggregation when not specified', () => {
expect(aggregationOperator(formData, queryObject)).toBeUndefined();
});
});

View File

@@ -1,68 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
{
"plugins": ["jest", "jest-dom", "no-only-tests", "testing-library"],
"env": {
"jest/globals": true
},
"settings": {
"jest": {
"version": "detect"
}
},
"extends": [
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react"
],
"overrides": [
{
"files": [
"**/*.stories.*",
"**/*.overview.*",
"**/fixtures.*"
],
"rules": {
"import/no-extraneous-dependencies": "off"
}
}
],
"rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"jest/consistent-test-it": "error",
"no-only-tests/no-only-tests": "error",
"prefer-promise-reject-errors": 0,
"testing-library/no-node-access": "off",
"testing-library/prefer-screen-queries": "off",
"testing-library/no-container": "off",
"testing-library/await-async-queries": "off",
"testing-library/await-async-utils": "off",
"testing-library/no-await-sync-events": "off",
"testing-library/no-render-in-lifecycle": "off",
"testing-library/no-unnecessary-act": "off",
"testing-library/no-wait-for-multiple-assertions": "off",
"testing-library/await-async-events": "off",
"testing-library/no-wait-for-side-effects": "off",
"testing-library/prefer-presence-queries": "off",
"testing-library/render-result-naming-convention": "off",
"testing-library/prefer-find-by": "off",
"testing-library/no-manual-cleanup": "off"
}
}

View File

@@ -26,7 +26,7 @@
"dependencies": {
"@apache-superset/core": "*",
"@ant-design/icons": "^5.2.6",
"@babel/runtime": "^7.28.2",
"@babel/runtime": "^7.28.4",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inter": "^5.2.6",
"@types/json-bigint": "^1.0.4",
@@ -78,7 +78,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/react-table": "^7.7.20",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.8",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.20",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.10.3",
@@ -91,7 +91,7 @@
"timezone-mock": "1.3.6"
},
"peerDependencies": {
"antd": "^5.24.6",
"antd": "^5.24.9",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen } from '@superset-ui/core/spec';
import { render, screen, fireEvent } from '@superset-ui/core/spec';
import { renderHook } from '@testing-library/react-hooks';
import { TableInstance, useTable } from 'react-table';
import TableCollection from '.';
@@ -36,19 +36,28 @@ beforeEach(() => {
accessor: 'col2',
id: 'col2',
},
{
Header: 'Nested Field',
accessor: 'parent.child',
id: 'parent.child',
dataIndex: ['parent', 'child'],
},
];
const data = [
{
col1: 'Line 01 - Col 01',
col2: 'Line 01 - Col 02',
parent: { child: 'Nested Value 1' },
},
{
col1: 'Line 02 - Col 01',
col2: 'Line 02 - Col 02',
parent: { child: 'Nested Value 2' },
},
{
col1: 'Line 03 - Col 01',
col2: 'Line 03 - Col 02',
parent: { child: 'Nested Value 3' },
},
];
// @ts-ignore
@@ -206,3 +215,28 @@ test('Bulk selection should work with pagination', () => {
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes.length).toBeGreaterThan(0);
});
test('should call setSortBy when clicking sortable column header', () => {
const setSortBy = jest.fn();
const sortingProps = {
...defaultProps,
setSortBy,
};
render(<TableCollection {...sortingProps} />);
// Target the nested field column (the column that needs the array-to-dot conversion)
const nestedFieldHeader = screen.getByText('Nested Field');
expect(nestedFieldHeader).toBeInTheDocument();
// Click on the nested field column header to trigger sorting
fireEvent.click(nestedFieldHeader);
// Verify setSortBy was called with the correct arguments and dot notation conversion
expect(setSortBy).toHaveBeenCalledWith([
{
id: 'parent.child',
desc: expect.any(Boolean),
},
]);
});

View File

@@ -215,9 +215,14 @@ function TableCollection<T extends object>({
const handleTableChange = useCallback(
(_pagination: any, _filters: any, sorter: SorterResult) => {
if (sorter && sorter.field) {
// Convert array field back to dot notation for nested fields
const fieldId = Array.isArray(sorter.field)
? sorter.field.join('.')
: sorter.field;
setSortBy?.([
{
id: sorter.field,
id: fieldId,
desc: sorter.order === 'descend',
},
] as SortingRule<T>[]);

View File

@@ -50,7 +50,7 @@ describe('Typography Component', () => {
it('renders strong text', () => {
render(<Typography.Text strong>Strong Text</Typography.Text>);
expect(screen.getByText('Strong Text')).toHaveStyle('font-weight: 500');
expect(screen.getByText('Strong Text')).toHaveStyle('font-weight: 600');
});
it('renders underlined text', () => {

View File

@@ -16,6 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
/* eslint-disable import/extensions */
import '@fontsource/inter/200.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/fira-code/400.css';
import '@fontsource/fira-code/500.css';
import '@fontsource/fira-code/600.css';
/* eslint-enable import/extensions */
import { css, useTheme, Global } from '@emotion/react';
export const GlobalStyles = () => {

View File

@@ -49,14 +49,12 @@ describe('Theme', () => {
});
describe('fromConfig', () => {
it('creates a theme with default tokens when no config is provided', () => {
it('creates a theme with Ant Design defaults when no config is provided', () => {
const theme = Theme.fromConfig();
// Verify default primary color is set
expect(theme.theme.colorPrimary).toBe('#2893b3');
// Verify default font family is set
expect(theme.theme.fontFamily).toContain('Inter');
// Verify Ant Design default tokens are set
expect(theme.theme.colorPrimary).toBeDefined();
expect(theme.theme.fontFamily).toBeDefined();
// Verify the theme is initialized with semantic color tokens
expect(theme.theme.colorText).toBeDefined();
@@ -79,8 +77,8 @@ describe('Theme', () => {
// Verify custom font family is set
expect(theme.theme.fontFamily).toBe('CustomFont, sans-serif');
// But default tokens should still be preserved for unspecified values
expect(theme.theme.colorError).toBe('#e04355');
// Unspecified values will use Ant Design defaults
expect(theme.theme.colorError).toBeDefined();
});
it('creates a theme with dark mode when dark algorithm is specified', () => {
@@ -205,4 +203,586 @@ describe('Theme', () => {
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
describe('fromConfig with baseTheme', () => {
it('applies base theme tokens under the main config', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#ff0000',
colorError: '#00ff00',
fontFamily: 'BaseFont',
},
};
const userConfig: AnyThemeConfig = {
token: {
colorPrimary: '#0000ff',
},
};
const theme = Theme.fromConfig(userConfig, baseTheme);
// User config overrides base theme
expect(theme.theme.colorPrimary).toBe('#0000ff');
// Base theme tokens are preserved when not overridden
expect(theme.theme.colorError).toBe('#00ff00');
expect(theme.theme.fontFamily).toBe('BaseFont');
});
it('applies base theme when no user config is provided', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#ff0000',
fontFamily: 'TestFont',
},
algorithm: antdThemeImport.darkAlgorithm,
};
const theme = Theme.fromConfig(undefined, baseTheme);
// Color may be transformed by dark algorithm, check fontFamily instead
expect(theme.theme.fontFamily).toBe('TestFont');
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('handles empty config with base theme', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#ff0000',
},
algorithm: antdThemeImport.defaultAlgorithm,
};
const emptyConfig: AnyThemeConfig = {};
const theme = Theme.fromConfig(emptyConfig, baseTheme);
// Base theme tokens should be applied
expect(theme.theme.colorPrimary).toBe('#ff0000');
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('merges algorithms correctly with base theme', () => {
const baseTheme: AnyThemeConfig = {
algorithm: antdThemeImport.compactAlgorithm,
};
const userConfig: AnyThemeConfig = {
algorithm: antdThemeImport.darkAlgorithm,
};
const theme = Theme.fromConfig(userConfig, baseTheme);
// User algorithm should override base algorithm
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('merges component overrides with base theme', () => {
const baseTheme: AnyThemeConfig = {
components: {
Button: {
colorPrimary: '#basebutton',
},
Input: {
colorBorder: '#baseinput',
},
},
};
const userConfig: AnyThemeConfig = {
components: {
Button: {
colorPrimary: '#userbutton',
},
},
};
const theme = Theme.fromConfig(userConfig, baseTheme);
const serialized = theme.toSerializedConfig();
// User component config overrides base
expect(serialized.components?.Button?.colorPrimary).toBe('#userbutton');
// Base component config preserved when not overridden
expect(serialized.components?.Input?.colorBorder).toBe('#baseinput');
});
it('handles undefined config and undefined base theme', () => {
const theme = Theme.fromConfig(undefined, undefined);
// Should get Ant Design defaults
expect(theme.theme.colorPrimary).toBeDefined();
expect(theme.theme.fontFamily).toBeDefined();
});
it('preserves custom tokens in base theme', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#ff0000',
// Custom superset-specific tokens
brandLogoAlt: 'CustomLogo',
menuHoverBackgroundColor: '#00ff00',
} as Record<string, any>,
};
const theme = Theme.fromConfig({}, baseTheme);
// Standard token
expect(theme.theme.colorPrimary).toBe('#ff0000');
// Custom tokens should be preserved
expect((theme.theme as any).brandLogoAlt).toBe('CustomLogo');
expect((theme.theme as any).menuHoverBackgroundColor).toBe('#00ff00');
});
});
describe('edge cases with base theme and dark mode', () => {
it('correctly applies base theme tokens in dark mode', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#1890ff',
fontFamily: 'TestFont',
},
algorithm: antdThemeImport.defaultAlgorithm,
};
const baseThemeDark: AnyThemeConfig = {
...baseTheme,
algorithm: antdThemeImport.darkAlgorithm,
};
// Simulate light mode with base theme
const lightTheme = Theme.fromConfig({}, baseTheme);
expect(lightTheme.theme.colorPrimary).toBe('#1890ff');
expect(lightTheme.theme.fontFamily).toBe('TestFont');
// Simulate dark mode with base theme dark
const darkTheme = Theme.fromConfig({}, baseThemeDark);
// Dark algorithm transforms colors, but fontFamily should be preserved
expect(darkTheme.theme.fontFamily).toBe('TestFont');
// Verify the algorithm is different
const lightSerialized = lightTheme.toSerializedConfig();
const darkSerialized = darkTheme.toSerializedConfig();
expect(lightSerialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
expect(darkSerialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('handles switching from custom theme back to base theme', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#1890ff',
},
algorithm: antdThemeImport.defaultAlgorithm,
};
// First apply custom theme
const customConfig: AnyThemeConfig = {
token: {
colorPrimary: '#52c41a',
},
};
const themeWithCustom = Theme.fromConfig(customConfig, baseTheme);
expect(themeWithCustom.theme.colorPrimary).toBe('#52c41a');
// Then switch back to empty config (simulating removal of custom theme)
const themeWithEmpty = Theme.fromConfig({}, baseTheme);
expect(themeWithEmpty.theme.colorPrimary).toBe('#1890ff');
// Verify they produce different outputs
expect(themeWithCustom.theme.colorPrimary).not.toBe(
themeWithEmpty.theme.colorPrimary,
);
});
it('handles algorithm-only config with base theme', () => {
const baseTheme: AnyThemeConfig = {
token: {
fontFamily: 'TestFont',
borderRadius: 8,
},
algorithm: antdThemeImport.defaultAlgorithm,
};
// Config that only specifies algorithm (common for THEME_DARK)
const algorithmOnlyConfig: AnyThemeConfig = {
algorithm: antdThemeImport.darkAlgorithm,
};
const theme = Theme.fromConfig(algorithmOnlyConfig, baseTheme);
// Should have base theme tokens
expect(theme.theme.fontFamily).toBe('TestFont');
expect(theme.theme.borderRadius).toBe(8);
// Should have user's algorithm
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
describe('base theme integration tests', () => {
it('merges base theme tokens with empty user theme', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
colorError: '#e04355',
fontFamily: 'Inter, Helvetica',
},
};
const userTheme: AnyThemeConfig = {
algorithm: antdThemeImport.defaultAlgorithm,
};
const theme = Theme.fromConfig(userTheme, baseTheme);
expect(theme.theme.colorPrimary).toBe('#2893B3');
expect(theme.theme.colorError).toBe('#e04355');
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
// Should have user's algorithm
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('allows user theme to override specific base theme tokens', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
colorError: '#e04355',
fontFamily: 'Inter, Helvetica',
borderRadius: 4,
},
};
const userTheme: AnyThemeConfig = {
algorithm: antdThemeImport.defaultAlgorithm,
token: {
colorPrimary: '#123456', // Override primary color
// Leave other tokens from base
},
};
const theme = Theme.fromConfig(userTheme, baseTheme);
// User override should win
expect(theme.theme.colorPrimary).toBe('#123456');
// Base theme tokens should be preserved
expect(theme.theme.colorError).toBe('#e04355');
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
expect(theme.theme.borderRadius).toBe(4);
});
it('handles base theme with dark algorithm correctly', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
fontFamily: 'Inter, Helvetica',
},
};
const baseThemeDark: AnyThemeConfig = {
...baseTheme,
algorithm: antdThemeImport.darkAlgorithm,
};
const userDarkTheme: AnyThemeConfig = {
algorithm: antdThemeImport.darkAlgorithm,
};
const theme = Theme.fromConfig(userDarkTheme, baseThemeDark);
// Should have base tokens
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
// Should be in dark mode
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('works with real-world Superset base theme configuration', () => {
// Simulate actual Superset base theme (THEME_DEFAULT/THEME_DARK from config)
const supersetBaseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
colorError: '#e04355',
colorWarning: '#fcc700',
colorSuccess: '#5ac189',
colorInfo: '#66bcfe',
fontFamily: "'Inter', Helvetica, Arial",
fontFamilyCode: "'Fira Code', 'Courier New', monospace",
},
};
// Simulate THEME_DEFAULT from config
const themeDefault: AnyThemeConfig = {
algorithm: antdThemeImport.defaultAlgorithm,
};
// Simulate THEME_DARK from config
const themeDark: AnyThemeConfig = {
algorithm: antdThemeImport.darkAlgorithm,
};
// Test light mode
const lightTheme = Theme.fromConfig(themeDefault, supersetBaseTheme);
expect(lightTheme.theme.colorPrimary).toBe('#2893B3');
expect(lightTheme.theme.fontFamily).toBe("'Inter', Helvetica, Arial");
// Test dark mode
const darkTheme = Theme.fromConfig(themeDark, {
...supersetBaseTheme,
algorithm: antdThemeImport.darkAlgorithm,
});
expect(darkTheme.theme.fontFamily).toBe("'Inter', Helvetica, Arial");
const darkSerialized = darkTheme.toSerializedConfig();
expect(darkSerialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('handles component overrides in base theme', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
},
components: {
Button: {
primaryColor: '#custom-button',
borderRadius: 8,
},
},
};
const userTheme: AnyThemeConfig = {
algorithm: antdThemeImport.defaultAlgorithm,
};
const theme = Theme.fromConfig(userTheme, baseTheme);
// Should preserve component overrides
const serialized = theme.toSerializedConfig();
expect(serialized.components?.Button?.primaryColor).toBe(
'#custom-button',
);
expect(serialized.components?.Button?.borderRadius).toBe(8);
});
it('properly handles algorithm property override', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
},
algorithm: antdThemeImport.defaultAlgorithm,
};
const userTheme: AnyThemeConfig = {
algorithm: antdThemeImport.darkAlgorithm,
token: {
borderRadius: 8,
},
};
const theme = Theme.fromConfig(userTheme, baseTheme);
const serialized = theme.toSerializedConfig();
// User algorithm should override base algorithm
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
// Both base and user tokens should be merged
expect(serialized.token?.colorPrimary).toBeTruthy();
expect(serialized.token?.borderRadius).toBe(8);
});
it('handles cssVar, hashed and inherit properties correctly', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
},
cssVar: true,
hashed: false,
};
const userTheme: AnyThemeConfig = {
token: {
borderRadius: 8,
},
inherit: true,
};
const theme = Theme.fromConfig(userTheme, baseTheme);
const serialized = theme.toSerializedConfig();
// User properties override/add to base
expect(serialized.inherit).toBe(true);
expect(serialized.cssVar).toBe(true);
expect(serialized.hashed).toBe(false);
// Tokens are still merged
expect(serialized.token?.colorPrimary).toBeTruthy();
expect(serialized.token?.borderRadius).toBe(8);
});
it('merges nested component styles correctly', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
fontFamily: 'BaseFont',
},
components: {
Button: {
colorPrimary: '#basebutton',
fontSize: 14,
},
Input: {
colorBorder: '#baseinput',
},
},
};
const userTheme: AnyThemeConfig = {
token: {
borderRadius: 8,
},
components: {
Button: {
fontSize: 16, // Override Button fontSize
},
Select: {
colorBorder: '#userselect', // Add new component
},
},
};
const theme = Theme.fromConfig(userTheme, baseTheme);
const serialized = theme.toSerializedConfig();
// Tokens should be merged
// Note: components present may affect color transformation
expect(serialized.token?.colorPrimary).toBeTruthy();
expect(serialized.token?.borderRadius).toBe(8);
expect(serialized.token?.fontFamily).toBe('BaseFont');
// Components should be merged (shallow merge per component)
expect(serialized.components?.Button?.colorPrimary).toBe('#basebutton');
expect(serialized.components?.Button?.fontSize).toBe(16); // User override
expect(serialized.components?.Input?.colorBorder).toBe('#baseinput');
expect(serialized.components?.Select?.colorBorder).toBe('#userselect');
});
it('setConfig replaces theme config entirely (does not preserve base theme)', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
fontFamily: 'Inter',
},
};
const theme = Theme.fromConfig({}, baseTheme);
expect(theme.theme.colorPrimary).toBe('#2893B3');
expect(theme.theme.fontFamily).toBe('Inter');
// Update config (simulating theme change)
theme.setConfig({
token: {
colorPrimary: '#654321',
},
algorithm: antdThemeImport.darkAlgorithm,
});
// setConfig replaces the entire config, so base theme is NOT preserved
// This is expected behavior - setConfig is for complete replacement
expect(theme.theme.colorPrimary).toBeTruthy();
// fontFamily reverts to Ant Design default since base theme is not reapplied
expect(theme.theme.fontFamily).not.toBe('Inter');
});
it('minimal theme preserves ALL base theme tokens except overridden ones', () => {
// Simulate a comprehensive base theme with many tokens
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
colorError: '#e04355',
colorWarning: '#fcc700',
colorSuccess: '#5ac189',
colorInfo: '#66bcfe',
fontFamily: 'Inter, Helvetica',
fontSize: 14,
borderRadius: 4,
lineWidth: 1,
controlHeight: 32,
// Custom Superset tokens
brandLogoAlt: 'CustomLogo',
menuHoverBackgroundColor: '#eeeeee',
} as Record<string, any>,
algorithm: antdThemeImport.defaultAlgorithm,
};
// Minimal theme that only overrides primary color and algorithm
const minimalTheme: AnyThemeConfig = {
token: {
colorPrimary: '#ff05dd', // Only override this
},
algorithm: antdThemeImport.darkAlgorithm, // Change to dark
};
const theme = Theme.fromConfig(minimalTheme, baseTheme);
// User's override should apply
expect(theme.theme.colorPrimary).toBe('#ff05dd');
// ALL base theme tokens should be preserved
expect(theme.theme.colorError).toBe('#e04355');
expect(theme.theme.colorWarning).toBe('#fcc700');
expect(theme.theme.colorSuccess).toBe('#5ac189');
expect(theme.theme.colorInfo).toBe('#66bcfe');
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
expect(theme.theme.fontSize).toBe(14);
expect(theme.theme.borderRadius).toBe(4);
expect(theme.theme.lineWidth).toBe(1);
expect(theme.theme.controlHeight).toBe(32);
// Custom tokens should also be preserved
expect((theme.theme as any).brandLogoAlt).toBe('CustomLogo');
expect((theme.theme as any).menuHoverBackgroundColor).toBe('#eeeeee');
// Algorithm should be updated
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('arrays in themes are replaced entirely, not merged by index', () => {
const baseTheme: AnyThemeConfig = {
token: {
colorPrimary: '#2893B3',
},
algorithm: [
antdThemeImport.compactAlgorithm,
antdThemeImport.defaultAlgorithm,
],
};
const userTheme: AnyThemeConfig = {
algorithm: [antdThemeImport.darkAlgorithm], // Replace with single item array
};
const theme = Theme.fromConfig(userTheme, baseTheme);
const serialized = theme.toSerializedConfig();
// User's array should completely replace base array
expect(Array.isArray(serialized.algorithm)).toBe(true);
expect(serialized.algorithm).toHaveLength(1);
expect(serialized.algorithm).toContain(ThemeAlgorithm.DARK);
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.COMPACT);
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DEFAULT);
});
});
});

View File

@@ -20,31 +20,13 @@
// eslint-disable-next-line no-restricted-syntax
import React from 'react';
import { theme as antdThemeImport, ConfigProvider } from 'antd';
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
/* eslint-disable import/extensions */
import '@fontsource/inter/200.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/400.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/500.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/600.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/400.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/500.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/600.css';
import {
ThemeProvider,
CacheProvider as EmotionCacheProvider,
} from '@emotion/react';
import createCache from '@emotion/cache';
import { noop } from 'lodash';
import { noop, mergeWith } from 'lodash';
import { GlobalStyles } from './GlobalStyles';
import {
AntdThemeConfig,
AnyThemeConfig,
@@ -53,63 +35,16 @@ import {
allowedAntdTokens,
SharedAntdTokens,
} from './types';
import { normalizeThemeConfig, serializeThemeConfig } from './utils';
/* eslint-disable theme-colors/no-literal-colors */
export class Theme {
theme: SupersetTheme;
private static readonly defaultTokens = {
// Brand
brandLogoAlt: 'Apache Superset',
brandLogoUrl: '/static/assets/images/superset-logo-horiz.png',
brandLogoMargin: '18px',
brandLogoHref: '/',
brandLogoHeight: '24px',
// Spinner
brandSpinnerUrl: undefined,
brandSpinnerSvg: undefined,
// Default colors
colorPrimary: '#2893B3', // NOTE: previous lighter primary color was #20a7c9
colorLink: '#2893B3',
colorError: '#e04355',
colorWarning: '#fcc700',
colorSuccess: '#5ac189',
colorInfo: '#66bcfe',
// Forcing some default tokens
fontFamily: `'Inter', Helvetica, Arial`,
fontFamilyCode: `'Fira Code', 'Courier New', monospace`,
// Extra tokens
transitionTiming: 0.3,
brandIconMaxWidth: 37,
fontSizeXS: '8',
fontSizeXXL: '28',
fontWeightNormal: '400',
fontWeightLight: '300',
fontWeightStrong: 500,
};
private antdConfig: AntdThemeConfig;
private constructor({ config }: { config?: AnyThemeConfig }) {
this.SupersetThemeProvider = this.SupersetThemeProvider.bind(this);
// Create a new config object with default tokens
const newConfig: AnyThemeConfig = config ? { ...config } : {};
// Ensure token property exists with defaults
newConfig.token = {
...Theme.defaultTokens,
...(config?.token || {}),
};
this.setConfig(newConfig);
this.setConfig(config || {});
}
/**
@@ -118,9 +53,24 @@ export class Theme {
* If simple tokens are provided as { token: {...} }, they will be applied with defaults
* If no config is provided, uses default tokens
* Dark mode can be set via the algorithm property in the config
* @param config - The theme configuration
* @param baseTheme - Optional base theme to apply under the config
*/
static fromConfig(config?: AnyThemeConfig): Theme {
return new Theme({ config });
static fromConfig(
config?: AnyThemeConfig,
baseTheme?: AnyThemeConfig,
): Theme {
let mergedConfig: AnyThemeConfig | undefined = config;
if (baseTheme && config) {
mergedConfig = mergeWith({}, baseTheme, config, (objValue, srcValue) =>
Array.isArray(srcValue) ? srcValue : undefined,
);
} else if (baseTheme && !config) {
mergedConfig = baseTheme;
}
return new Theme({ config: mergedConfig });
}
private static getFilteredAntdTheme(
@@ -148,22 +98,15 @@ export class Theme {
setConfig(config: AnyThemeConfig): void {
const antdConfig = normalizeThemeConfig(config);
// Apply default tokens to token property
antdConfig.token = {
...Theme.defaultTokens,
...(antdConfig.token || {}),
};
// First phase: Let Ant Design compute the tokens
const tokens = Theme.getFilteredAntdTheme(antdConfig);
// Set the base theme properties
this.antdConfig = antdConfig;
this.theme = {
...Theme.defaultTokens,
...antdConfig.token, // Passing through the extra, superset-specific tokens
...tokens,
};
...tokens, // First apply Ant Design computed tokens
...(antdConfig.token || {}), // Then override with our custom tokens
} as SupersetTheme;
// Update the providers with the fully formed theme
this.updateProviders(
@@ -250,5 +193,3 @@ export class Theme {
);
}
}
/* eslint-enable theme-colors/no-literal-colors */

View File

@@ -78,6 +78,7 @@ export type SerializableThemeConfig = {
algorithm?: ThemeAlgorithmOption;
hashed?: boolean;
inherit?: boolean;
cssVar?: boolean | { key?: string; prefix?: string };
};
/**

View File

@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getFontSize, getColorVariants, isThemeDark } from './themeUtils';
import { theme as antdTheme } from 'antd';
import {
getFontSize,
getColorVariants,
isThemeDark,
isThemeConfigDark,
} from './themeUtils';
import { Theme } from '../Theme';
import { ThemeAlgorithm } from '../types';
@@ -71,8 +77,7 @@ describe('themeUtils', () => {
token: { fontSize: '14' },
});
// Ant Design provides fontSizeXS: '8' by default
expect(getFontSize(minimalTheme.theme, 'xs')).toBe('8');
expect(getFontSize(minimalTheme.theme, 'xs')).toBe('14');
expect(getFontSize(minimalTheme.theme, 'm')).toBe('14');
});
});
@@ -131,4 +136,111 @@ describe('themeUtils', () => {
expect(variants.bg).toBeUndefined();
});
});
describe('isThemeConfigDark', () => {
it('returns true for config with dark algorithm', () => {
const config = {
algorithm: antdTheme.darkAlgorithm,
};
expect(isThemeConfigDark(config)).toBe(true);
});
it('returns true for config with dark algorithm in array', () => {
const config = {
algorithm: [antdTheme.darkAlgorithm, antdTheme.compactAlgorithm],
};
expect(isThemeConfigDark(config)).toBe(true);
});
it('returns false for config without dark algorithm', () => {
const config = {
algorithm: antdTheme.defaultAlgorithm,
};
expect(isThemeConfigDark(config)).toBe(false);
});
it('returns false for config with no algorithm', () => {
const config = {
token: {
colorPrimary: '#1890ff',
},
};
expect(isThemeConfigDark(config)).toBe(false);
});
it('detects manually-created dark theme without dark algorithm', () => {
// This is the edge case: dark colors without dark algorithm
const config = {
token: {
colorBgContainer: '#1a1a1a', // Dark background
colorBgBase: '#0a0a0a', // Dark base
colorText: '#ffffff', // Light text
},
};
expect(isThemeConfigDark(config)).toBe(true);
});
it('does not false-positive on light theme with custom colors', () => {
const config = {
token: {
colorBgContainer: '#ffffff', // Light background
colorBgBase: '#f5f5f5', // Light base
colorText: '#000000', // Dark text
},
};
expect(isThemeConfigDark(config)).toBe(false);
});
it('handles partial color tokens gracefully', () => {
// With actual theme computation, a dark colorBgContainer results in a dark theme
const config = {
token: {
colorBgContainer: '#1a1a1a', // Dark background
// Missing other color tokens
},
};
expect(isThemeConfigDark(config)).toBe(true);
});
it('respects colorBgContainer as the primary indicator', () => {
// The computed theme uses colorBgContainer as the main background
const darkConfig = {
token: {
colorBgContainer: '#1a1a1a', // Dark background
colorText: '#000000', // Dark text (unusual but doesn't override)
},
};
expect(isThemeConfigDark(darkConfig)).toBe(true);
const lightConfig = {
token: {
colorBgContainer: '#ffffff', // Light background
colorText: '#ffffff', // Light text (unusual but doesn't override)
},
};
expect(isThemeConfigDark(lightConfig)).toBe(false);
});
it('handles non-string color tokens gracefully', () => {
const config = {
token: {
colorBgContainer: undefined,
colorText: null,
colorBgBase: 123, // Invalid type
},
};
expect(isThemeConfigDark(config)).toBe(false);
});
it('returns false for empty config', () => {
expect(isThemeConfigDark({})).toBe(false);
});
it('returns false for config with empty token object', () => {
const config = {
token: {},
};
expect(isThemeConfigDark(config)).toBe(false);
});
});
});

View File

@@ -18,7 +18,14 @@
*/
import tinycolor from 'tinycolor2';
import { useTheme as useEmotionTheme } from '@emotion/react';
import type { SupersetTheme, FontSizeKey, ColorVariants } from '../types';
import { theme as antdTheme } from 'antd';
import type {
SupersetTheme,
FontSizeKey,
ColorVariants,
AnyThemeConfig,
} from '../types';
import { normalizeThemeConfig } from '../utils';
const fontSizeMap: Record<FontSizeKey, keyof SupersetTheme> = {
xs: 'fontSizeXS',
@@ -113,6 +120,22 @@ export function isThemeDark(theme: SupersetTheme): boolean {
return tinycolor(theme.colorBgContainer).isDark();
}
/**
* Check if a theme configuration results in a dark theme
* @param config - The theme configuration to check
* @returns true if the config results in a dark theme, false otherwise
*/
export function isThemeConfigDark(config: AnyThemeConfig): boolean {
try {
const normalizedConfig = normalizeThemeConfig(config);
const themeConfig = antdTheme.getDesignToken(normalizedConfig);
return tinycolor(themeConfig.colorBgContainer).isDark();
} catch {
return false;
}
}
/**
* Hook to determine if the current theme is dark mode
* @returns true if theme is dark, false if light

View File

@@ -122,7 +122,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
test('should use getBuckets for linear_palette color scheme', () => {
it('should use getBuckets for linear_palette color scheme', () => {
const propsWithLinearPalette = {
...mockProps,
formData: {
@@ -138,7 +138,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('should use getBuckets for fixed_color color scheme', () => {
it('should use getBuckets for fixed_color color scheme', () => {
const propsWithFixedColor = {
...mockProps,
formData: {
@@ -154,7 +154,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => {
it('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => {
const propsWithBreakpoints = {
...mockProps,
formData: {
@@ -187,7 +187,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
expect(mockGetBuckets).not.toHaveBeenCalled();
});
test('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => {
it('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => {
const propsWithUndefinedScheme = {
...mockProps,
formData: {
@@ -203,7 +203,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('should use getBuckets for unsupported color schemes (categorical_palette)', () => {
it('should use getBuckets for unsupported color schemes (categorical_palette)', () => {
const propsWithUnsupportedScheme = {
...mockProps,
formData: {
@@ -230,7 +230,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
test('handles empty features data gracefully', () => {
it('handles empty features data gracefully', () => {
const propsWithEmptyData = {
...mockProps,
payload: {
@@ -249,7 +249,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
});
test('handles missing color_breakpoints for color_breakpoints scheme', () => {
it('handles missing color_breakpoints for color_breakpoints scheme', () => {
const propsWithMissingBreakpoints = {
...mockProps,
formData: {
@@ -266,7 +266,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
expect(mockGetBuckets).not.toHaveBeenCalled();
});
test('handles null legend_position correctly', () => {
it('handles null legend_position correctly', () => {
const propsWithNullLegendPosition = {
...mockProps,
formData: {
@@ -294,7 +294,7 @@ describe('DeckGLPolygon Legend Integration', () => {
const renderWithTheme = (component: React.ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
test('renders legend with non-empty categories when metric and linear_palette are defined', () => {
it('renders legend with non-empty categories when metric and linear_palette are defined', () => {
const { container } = renderWithTheme(<DeckGLPolygon {...mockProps} />);
// Verify the component renders and calls the correct bucket function
@@ -309,7 +309,7 @@ describe('DeckGLPolygon Legend Integration', () => {
expect(Object.keys(categoriesData)).toHaveLength(2);
});
test('does not render legend when metric is null', () => {
it('does not render legend when metric is null', () => {
const propsWithoutMetric = {
...mockProps,
formData: {
@@ -326,7 +326,7 @@ describe('DeckGLPolygon Legend Integration', () => {
});
describe('getPoints utility', () => {
test('extracts points from polygon data', () => {
it('extracts points from polygon data', () => {
const data = [
{
polygon: [

View File

@@ -64,7 +64,7 @@ Object.defineProperty(document, 'getElementById', {
const mockDecode = decode as jest.MockedFunction<typeof decode>;
describe('spatialUtils', () => {
test('getSpatialColumns returns correct columns for latlong type', () => {
it('getSpatialColumns returns correct columns for latlong type', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'latlong',
lonCol: 'longitude',
@@ -75,7 +75,7 @@ describe('spatialUtils', () => {
expect(result).toEqual(['longitude', 'latitude']);
});
test('getSpatialColumns returns correct columns for delimited type', () => {
it('getSpatialColumns returns correct columns for delimited type', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'delimited',
lonlatCol: 'coordinates',
@@ -85,7 +85,7 @@ describe('spatialUtils', () => {
expect(result).toEqual(['coordinates']);
});
test('getSpatialColumns returns correct columns for geohash type', () => {
it('getSpatialColumns returns correct columns for geohash type', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'geohash',
geohashCol: 'geohash_code',
@@ -95,16 +95,16 @@ describe('spatialUtils', () => {
expect(result).toEqual(['geohash_code']);
});
test('getSpatialColumns throws error when spatial is null', () => {
it('getSpatialColumns throws error when spatial is null', () => {
expect(() => getSpatialColumns(null as any)).toThrow('Bad spatial key');
});
test('getSpatialColumns throws error when spatial type is missing', () => {
it('getSpatialColumns throws error when spatial type is missing', () => {
const spatial = {} as SpatialFormData['spatial'];
expect(() => getSpatialColumns(spatial)).toThrow('Bad spatial key');
});
test('getSpatialColumns throws error when latlong columns are missing', () => {
it('getSpatialColumns throws error when latlong columns are missing', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'latlong',
};
@@ -113,7 +113,7 @@ describe('spatialUtils', () => {
);
});
test('getSpatialColumns throws error when delimited column is missing', () => {
it('getSpatialColumns throws error when delimited column is missing', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'delimited',
};
@@ -122,7 +122,7 @@ describe('spatialUtils', () => {
);
});
test('getSpatialColumns throws error when geohash column is missing', () => {
it('getSpatialColumns throws error when geohash column is missing', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'geohash',
};
@@ -131,7 +131,7 @@ describe('spatialUtils', () => {
);
});
test('getSpatialColumns throws error for unknown spatial type', () => {
it('getSpatialColumns throws error for unknown spatial type', () => {
const spatial = {
type: 'unknown',
} as any;
@@ -140,7 +140,7 @@ describe('spatialUtils', () => {
);
});
test('addSpatialNullFilters adds null filters for spatial columns', () => {
it('addSpatialNullFilters adds null filters for spatial columns', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'latlong',
lonCol: 'longitude',
@@ -159,7 +159,7 @@ describe('spatialUtils', () => {
]);
});
test('addSpatialNullFilters returns original filters when spatial is null', () => {
it('addSpatialNullFilters returns original filters when spatial is null', () => {
const existingFilters: QueryObjectFilterClause[] = [
{ col: 'test_col', op: '==', val: 'test' },
];
@@ -168,7 +168,7 @@ describe('spatialUtils', () => {
expect(result).toBe(existingFilters);
});
test('addSpatialNullFilters works with empty filters array', () => {
it('addSpatialNullFilters works with empty filters array', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'delimited',
lonlatCol: 'coordinates',
@@ -181,7 +181,7 @@ describe('spatialUtils', () => {
]);
});
test('buildSpatialQuery throws error when spatial is missing', () => {
it('buildSpatialQuery throws error when spatial is missing', () => {
const formData = {} as SpatialFormData;
expect(() => buildSpatialQuery(formData)).toThrow(
@@ -189,7 +189,7 @@ describe('spatialUtils', () => {
);
});
test('buildSpatialQuery calls buildQueryContext with correct parameters', () => {
it('buildSpatialQuery calls buildQueryContext with correct parameters', () => {
const mockBuildQueryContext =
jest.requireMock('@superset-ui/core').buildQueryContext;
const formData: SpatialFormData = {
@@ -209,7 +209,7 @@ describe('spatialUtils', () => {
});
});
test('processSpatialData processes latlong data correctly', () => {
it('processSpatialData processes latlong data correctly', () => {
const records = [
{ longitude: -122.4, latitude: 37.8, count: 10, extra: 'test1' },
{ longitude: -122.5, latitude: 37.9, count: 20, extra: 'test2' },
@@ -237,7 +237,7 @@ describe('spatialUtils', () => {
});
});
test('processSpatialData processes delimited data correctly', () => {
it('processSpatialData processes delimited data correctly', () => {
const records = [
{ coordinates: '-122.4,37.8', count: 15 },
{ coordinates: '-122.5,37.9', count: 25 },
@@ -257,7 +257,7 @@ describe('spatialUtils', () => {
});
});
test('processSpatialData processes geohash data correctly', () => {
it('processSpatialData processes geohash data correctly', () => {
mockDecode.mockReturnValue({
latitude: 37.8,
longitude: -122.4,
@@ -284,7 +284,7 @@ describe('spatialUtils', () => {
expect(mockDecode).toHaveBeenCalledWith('dr5regw3p');
});
test('processSpatialData reverses coordinates when reverseCheckbox is true', () => {
it('processSpatialData reverses coordinates when reverseCheckbox is true', () => {
const records = [{ longitude: -122.4, latitude: 37.8, count: 10 }];
const spatial: SpatialFormData['spatial'] = {
type: 'latlong',
@@ -298,7 +298,7 @@ describe('spatialUtils', () => {
expect(result[0].position).toEqual([37.8, -122.4]);
});
test('processSpatialData handles invalid coordinates', () => {
it('processSpatialData handles invalid coordinates', () => {
const records = [
{ longitude: 'invalid', latitude: 37.8, count: 10 },
{ longitude: -122.4, latitude: NaN, count: 20 },
@@ -318,7 +318,7 @@ describe('spatialUtils', () => {
expect(result).toHaveLength(0);
});
test('processSpatialData handles missing metric values', () => {
it('processSpatialData handles missing metric values', () => {
const records = [
{ longitude: -122.4, latitude: 37.8, count: null },
{ longitude: -122.5, latitude: 37.9 },
@@ -338,7 +338,7 @@ describe('spatialUtils', () => {
expect(result[2].weight).toBe(1);
});
test('processSpatialData returns empty array for empty records', () => {
it('processSpatialData returns empty array for empty records', () => {
const spatial: SpatialFormData['spatial'] = {
type: 'latlong',
lonCol: 'longitude',
@@ -350,7 +350,7 @@ describe('spatialUtils', () => {
expect(result).toEqual([]);
});
test('processSpatialData returns empty array when spatial is null', () => {
it('processSpatialData returns empty array when spatial is null', () => {
const records = [{ longitude: -122.4, latitude: 37.8 }];
const result = processSpatialData(records, null as any);
@@ -358,7 +358,7 @@ describe('spatialUtils', () => {
expect(result).toEqual([]);
});
test('processSpatialData handles delimited coordinate edge cases', () => {
it('processSpatialData handles delimited coordinate edge cases', () => {
const records = [
{ coordinates: '', count: 10 },
{ coordinates: null, count: 20 },
@@ -382,7 +382,7 @@ describe('spatialUtils', () => {
});
});
test('processSpatialData copies additional properties correctly', () => {
it('processSpatialData copies additional properties correctly', () => {
const records = [
{
longitude: -122.4,
@@ -416,7 +416,7 @@ describe('spatialUtils', () => {
expect(result[0]).not.toHaveProperty('extra_col');
});
test('transformSpatialProps transforms chart props correctly', () => {
it('transformSpatialProps transforms chart props correctly', () => {
const mockGetMetricLabel =
jest.requireMock('@superset-ui/core').getMetricLabel;
mockGetMetricLabel.mockReturnValue('count_label');
@@ -510,7 +510,7 @@ describe('spatialUtils', () => {
expect(result.payload.data.metricLabels).toEqual(['count_label']);
});
test('transformSpatialProps handles missing hooks gracefully', () => {
it('transformSpatialProps handles missing hooks gracefully', () => {
const chartProps: ChartProps = {
datasource: {
id: 1,
@@ -556,7 +556,7 @@ describe('spatialUtils', () => {
expect(typeof result.setTooltip).toBe('function');
});
test('transformSpatialProps handles missing metric', () => {
it('transformSpatialProps handles missing metric', () => {
const mockGetMetricLabel =
jest.requireMock('@superset-ui/core').getMetricLabel;
mockGetMetricLabel.mockReturnValue(undefined);

View File

@@ -505,6 +505,7 @@ export const tooltipTemplate = {
config: {
type: TooltipTemplateControl,
label: t('Customize tooltips template'),
renderTrigger: true,
debounceDelay: 30,
default: '',
description: '',

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getColorBreakpointsBuckets } from './utils';
import { getColorBreakpointsBuckets, getBreakPoints } from './utils';
import { ColorBreakpointType } from './types';
describe('getColorBreakpointsBuckets', () => {
@@ -44,3 +44,447 @@ describe('getColorBreakpointsBuckets', () => {
expect(result).toEqual({});
});
});
describe('getBreakPoints', () => {
const accessor = (d: any) => d.value;
describe('automatic breakpoint generation', () => {
it('generates correct number of breakpoints for given buckets', () => {
const features = [{ value: 0 }, { value: 50 }, { value: 100 }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
expect(breakPoints).toHaveLength(6); // n buckets = n+1 breakpoints
expect(breakPoints.every(bp => typeof bp === 'string')).toBe(true);
});
it('ensures data range is fully covered', () => {
// Test various data ranges to ensure min/max are always included
const testCases = [
{ data: [0, 100], buckets: 5 },
{ data: [0.1, 99.9], buckets: 4 },
{ data: [-50, 50], buckets: 10 },
{ data: [3.2, 38.7], buckets: 5 }, // Original max bug case
{ data: [3.14, 100], buckets: 5 }, // Min rounding bug case (3.14 -> 3)
{ data: [2.345, 10], buckets: 4 }, // Min rounding bug case (2.345 -> 2.35)
{ data: [0.0001, 0.0009], buckets: 3 }, // Very small numbers
{ data: [1000000, 9000000], buckets: 8 }, // Large numbers
];
testCases.forEach(({ data, buckets }) => {
const [min, max] = data;
const features = [{ value: min }, { value: max }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: String(buckets) },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// Critical: min and max must be within the breakpoint range
expect(firstBp).toBeLessThanOrEqual(min);
expect(lastBp).toBeGreaterThanOrEqual(max);
expect(breakPoints).toHaveLength(buckets + 1);
});
});
it('handles uniform distribution correctly', () => {
const features = [
{ value: 0 },
{ value: 25 },
{ value: 50 },
{ value: 75 },
{ value: 100 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '4' },
features,
accessor,
);
// Check that breakpoints are evenly spaced
const numericBreakPoints = breakPoints.map(parseFloat);
const deltas = [];
for (let i = 1; i < numericBreakPoints.length; i += 1) {
deltas.push(numericBreakPoints[i] - numericBreakPoints[i - 1]);
}
// All deltas should be approximately equal
const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
deltas.forEach(delta => {
expect(delta).toBeCloseTo(avgDelta, 1);
});
});
it('handles single value datasets', () => {
const features = [{ value: 42 }, { value: 42 }, { value: 42 }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
expect(firstBp).toBeLessThanOrEqual(42);
expect(lastBp).toBeGreaterThanOrEqual(42);
});
it('preserves appropriate precision for different scales', () => {
const testCases = [
{ data: [0, 1], expectedMaxPrecision: 1 }, // 0.0, 0.2, 0.4...
{ data: [0, 0.1], expectedMaxPrecision: 2 }, // 0.00, 0.02...
{ data: [0, 0.01], expectedMaxPrecision: 3 }, // 0.000, 0.002...
{ data: [0, 1000], expectedMaxPrecision: 0 }, // 0, 200, 400...
];
testCases.forEach(({ data, expectedMaxPrecision }) => {
const [min, max] = data;
const features = [{ value: min }, { value: max }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
breakPoints.forEach(bp => {
const decimalPlaces = (bp.split('.')[1] || '').length;
expect(decimalPlaces).toBeLessThanOrEqual(expectedMaxPrecision);
});
});
});
it('handles negative values correctly', () => {
const features = [
{ value: -100 },
{ value: -50 },
{ value: 0 },
{ value: 50 },
{ value: 100 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const numericBreakPoints = breakPoints.map(parseFloat);
expect(numericBreakPoints[0]).toBeLessThanOrEqual(-100);
expect(
numericBreakPoints[numericBreakPoints.length - 1],
).toBeGreaterThanOrEqual(100);
// Verify ascending order
for (let i = 1; i < numericBreakPoints.length; i += 1) {
expect(numericBreakPoints[i]).toBeGreaterThan(
numericBreakPoints[i - 1],
);
}
});
it('handles mixed integer and decimal values', () => {
const features = [
{ value: 1 },
{ value: 2.5 },
{ value: 3.7 },
{ value: 5 },
{ value: 8.2 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '4' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
expect(firstBp).toBeLessThanOrEqual(1);
expect(lastBp).toBeGreaterThanOrEqual(8.2);
});
it('uses floor/ceil for boundary breakpoints to ensure inclusion', () => {
// Test that Math.floor and Math.ceil are used for boundaries
// This ensures all data points fall within the breakpoint range
const testCases = [
{ minValue: 3.14, maxValue: 100, buckets: 5 },
{ minValue: 2.345, maxValue: 10.678, buckets: 4 },
{ minValue: 1.67, maxValue: 5.33, buckets: 3 },
{ minValue: 0.123, maxValue: 0.987, buckets: 5 },
];
testCases.forEach(({ minValue, maxValue, buckets }) => {
const features = [{ value: minValue }, { value: maxValue }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: String(buckets) },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// First breakpoint should be floored (always <= minValue)
expect(firstBp).toBeLessThanOrEqual(minValue);
// Last breakpoint should be ceiled (always >= maxValue)
expect(lastBp).toBeGreaterThanOrEqual(maxValue);
// All values should be within range
expect(minValue).toBeGreaterThanOrEqual(firstBp);
expect(maxValue).toBeLessThanOrEqual(lastBp);
});
});
it('prevents minimum value exclusion edge case', () => {
// Specific edge case test for minimum value exclusion
// Tests the exact scenario where rounding would exclude the min value
const features = [
{ value: 3.14 }, // This would round to 3 at precision 0
{ value: 50 },
{ value: 100 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
// The first breakpoint must be <= 3.14 (floor behavior)
expect(firstBp).toBeLessThanOrEqual(3.14);
// Verify that 3.14 is not excluded
expect(3.14).toBeGreaterThanOrEqual(firstBp);
// The first breakpoint should be a clean floor value
expect(breakPoints[0]).toMatch(/^3(\.0*)?$/);
});
it('prevents maximum value exclusion edge case', () => {
// Specific edge case test for maximum value exclusion
// Tests the exact scenario where rounding would exclude the max value
const features = [
{ value: 0 },
{ value: 20 },
{ value: 38.7 }, // Original bug case
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// The last breakpoint must be >= 38.7 (ceil behavior)
expect(lastBp).toBeGreaterThanOrEqual(38.7);
// Verify that 38.7 is not excluded
expect(38.7).toBeLessThanOrEqual(lastBp);
// The last breakpoint should be a clean ceil value
expect(breakPoints[breakPoints.length - 1]).toMatch(/^39(\.0*)?$/);
});
});
describe('custom breakpoints', () => {
it('uses custom breakpoints when provided', () => {
const features = [{ value: 5 }, { value: 15 }, { value: 25 }];
const customBreakPoints = ['0', '10', '20', '30', '40'];
const breakPoints = getBreakPoints(
{ break_points: customBreakPoints, num_buckets: '' },
features,
accessor,
);
expect(breakPoints).toEqual(['0', '10', '20', '30', '40']);
});
it('sorts custom breakpoints in ascending order', () => {
const features = [{ value: 5 }];
const customBreakPoints = ['30', '10', '0', '20'];
const breakPoints = getBreakPoints(
{ break_points: customBreakPoints, num_buckets: '' },
features,
accessor,
);
expect(breakPoints).toEqual(['0', '10', '20', '30']);
});
it('ignores num_buckets when custom breakpoints are provided', () => {
const features = [{ value: 5 }];
const customBreakPoints = ['0', '50', '100'];
const breakPoints = getBreakPoints(
{ break_points: customBreakPoints, num_buckets: '10' }, // num_buckets should be ignored
features,
accessor,
);
expect(breakPoints).toEqual(['0', '50', '100']);
expect(breakPoints).toHaveLength(3); // not 11
});
});
describe('edge cases and error handling', () => {
it('returns empty array when features are undefined', () => {
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
undefined as any,
accessor,
);
expect(breakPoints).toEqual([]);
});
it('returns empty array when features is null', () => {
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
null as any,
accessor,
);
expect(breakPoints).toEqual([]);
});
it('returns empty array when all values are undefined', () => {
const features = [
{ value: undefined },
{ value: undefined },
{ value: undefined },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
expect(breakPoints).toEqual([]);
});
it('handles empty features array', () => {
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
[],
accessor,
);
expect(breakPoints).toEqual([]);
});
it('handles string values that can be parsed as numbers', () => {
const features = [
{ value: '10.5' },
{ value: '20.3' },
{ value: '30.7' },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '3' },
features,
(d: any) =>
typeof d.value === 'string' ? parseFloat(d.value) : d.value,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
expect(firstBp).toBeLessThanOrEqual(10.5);
expect(lastBp).toBeGreaterThanOrEqual(30.7);
});
it('uses default number of buckets when not specified', () => {
const features = [{ value: 0 }, { value: 100 }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '' },
features,
accessor,
);
// Should use DEFAULT_NUM_BUCKETS (10)
expect(breakPoints).toHaveLength(11); // 10 buckets = 11 breakpoints
});
it('handles Infinity and -Infinity values', () => {
const features = [
{ value: -Infinity },
{ value: 0 },
{ value: Infinity },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
// Should return empty array when Infinity values are present
expect(breakPoints).toEqual([]);
});
});
describe('breakpoint boundaries validation', () => {
it('ensures no data points fall outside breakpoint range', () => {
// Generate random test data
const generateRandomData = (count: number, min: number, max: number) => {
const data = [];
for (let i = 0; i < count; i += 1) {
data.push({ value: Math.random() * (max - min) + min });
}
return data;
};
// Test with various random datasets
for (let i = 0; i < 10; i += 1) {
const features = generateRandomData(20, -1000, 1000);
const minValue = Math.min(...features.map(f => f.value));
const maxValue = Math.max(...features.map(f => f.value));
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// Every data point should fall within the breakpoint range
features.forEach(feature => {
expect(feature.value).toBeGreaterThanOrEqual(firstBp);
expect(feature.value).toBeLessThanOrEqual(lastBp);
});
// The range should be as tight as possible while including all data
expect(firstBp).toBeLessThanOrEqual(minValue);
expect(lastBp).toBeGreaterThanOrEqual(maxValue);
}
});
});
});

View File

@@ -75,19 +75,35 @@ export function getBreakPoints(
if (minValue === undefined || maxValue === undefined) {
return [];
}
// Handle Infinity values
if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) {
return [];
}
const delta = (maxValue - minValue) / numBuckets;
const precision =
delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta)));
const extraBucket =
maxValue > parseFloat(maxValue.toFixed(precision)) ? 1 : 0;
const startValue =
minValue < parseFloat(minValue.toFixed(precision))
? minValue - 1
: minValue;
return new Array(numBuckets + 1 + extraBucket)
.fill(0)
.map((_, i) => (startValue + i * delta).toFixed(precision));
// Generate breakpoints
const breakPoints = new Array(numBuckets + 1).fill(0).map((_, i) => {
const value = minValue + i * delta;
// For the first breakpoint, floor to ensure minimum is included
if (i === 0) {
const scale = Math.pow(10, precision);
return (Math.floor(minValue * scale) / scale).toFixed(precision);
}
// For the last breakpoint, ceil to ensure maximum is included
if (i === numBuckets) {
const scale = Math.pow(10, precision);
return (Math.ceil(maxValue * scale) / scale).toFixed(precision);
}
// For middle breakpoints, use standard rounding
return value.toFixed(precision);
});
return breakPoints;
}
return formDataBreakPoints.sort(
@@ -146,7 +162,10 @@ export function getBreakPointColorScaler(
scaler = scaleThreshold<number, string>()
.domain(points)
.range(bucketedColors);
maskPoint = value => !!value && (value > points[n] || value < points[0]);
// Only mask values that are strictly outside the min/max bounds
// Include values equal to the max breakpoint
maskPoint = value =>
!!value && (value > points[points.length - 1] || value < points[0]);
} else {
// interpolate colors linearly
const linearScaleDomain = extent(features, accessor);

View File

@@ -131,10 +131,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
const defaultColDef = useMemo<ColDef>(
() => ({
flex: 1,
filter: true,
enableRowGroup: true,
enableValue: true,
sortable: true,
resizable: true,
minWidth: 100,
@@ -251,6 +248,12 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
}
}, [hasServerPageLengthChanged]);
useEffect(() => {
if (gridRef.current?.api) {
gridRef.current.api.sizeColumnsToFit();
}
}, [width]);
const onGridReady = (params: GridReadyEvent) => {
// This will make columns fill the grid width
params.api.sizeColumnsToFit();
@@ -312,7 +315,6 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
onCellClicked={handleCrossFilter}
initialState={gridInitialState}
suppressAggFuncInHeader
rowGroupPanelShow="always"
enableCellTextSelection
quickFilterText={serverPagination ? '' : quickFilterText}
suppressMovableColumns={!allowRearrangeColumns}

View File

@@ -322,12 +322,6 @@ export default function transformProps(
primarySeries.add(seriesOption.id as string);
}
};
rawSeriesA.forEach(seriesOption =>
mapSeriesIdToAxis(seriesOption, yAxisIndex),
);
rawSeriesB.forEach(seriesOption =>
mapSeriesIdToAxis(seriesOption, yAxisIndexB),
);
const showValueIndexesA = extractShowValueIndexes(rawSeriesA, {
stack,
onlyTotal,
@@ -460,7 +454,11 @@ export default function transformProps(
theme,
},
);
if (transformedSeries) series.push(transformedSeries);
if (transformedSeries) {
series.push(transformedSeries);
mapSeriesIdToAxis(transformedSeries, yAxisIndex);
}
});
rawSeriesB.forEach(entry => {
@@ -528,7 +526,11 @@ export default function transformProps(
theme,
},
);
if (transformedSeries) series.push(transformedSeries);
if (transformedSeries) {
series.push(transformedSeries);
mapSeriesIdToAxis(transformedSeries, yAxisIndexB);
}
});
// default to 0-100% range when doing row-level contribution chart

View File

@@ -19,7 +19,7 @@
import { mergeReplaceArrays } from '@superset-ui/core';
describe('Theme Override Deep Merge Behavior', () => {
test('should merge nested objects correctly', () => {
it('should merge nested objects correctly', () => {
const baseOptions = {
grid: {
left: '5%',
@@ -67,7 +67,7 @@ describe('Theme Override Deep Merge Behavior', () => {
});
});
test('should replace arrays instead of merging them', () => {
it('should replace arrays instead of merging them', () => {
const baseOptions = {
series: [
{ name: 'Series 1', type: 'line' },
@@ -86,7 +86,7 @@ describe('Theme Override Deep Merge Behavior', () => {
expect(result.series).toHaveLength(1);
});
test('should handle null overrides correctly', () => {
it('should handle null overrides correctly', () => {
const baseOptions = {
grid: {
left: '5%',
@@ -127,7 +127,7 @@ describe('Theme Override Deep Merge Behavior', () => {
});
});
test('should handle override precedence correctly', () => {
it('should handle override precedence correctly', () => {
const baseTheme = {
textStyle: { color: '#000', fontSize: 12 },
};
@@ -167,7 +167,7 @@ describe('Theme Override Deep Merge Behavior', () => {
});
});
test('should preserve deep nested structures', () => {
it('should preserve deep nested structures', () => {
const baseOptions = {
xAxis: {
axisLabel: {
@@ -215,7 +215,7 @@ describe('Theme Override Deep Merge Behavior', () => {
});
});
test('should handle function values correctly', () => {
it('should handle function values correctly', () => {
const formatFunction = (value: any) => `${value}%`;
const overrideFunction = (value: any) => `$${value}`;
@@ -241,7 +241,7 @@ describe('Theme Override Deep Merge Behavior', () => {
expect(result.yAxis.axisLabel.formatter('100')).toBe('$100');
});
test('should handle empty objects and arrays', () => {
it('should handle empty objects and arrays', () => {
const baseOptions = {
series: [{ name: 'Test', data: [1, 2, 3] }],
grid: { left: '5%' },

View File

@@ -117,7 +117,7 @@ const chartPropsConfig = {
theme: supersetTheme,
};
it('should transform chart props for viz with showQueryIdentifiers=false', () => {
test('should transform chart props for viz with showQueryIdentifiers=false', () => {
const chartPropsConfigWithoutIdentifiers = {
...chartPropsConfig,
formData: {
@@ -158,7 +158,7 @@ it('should transform chart props for viz with showQueryIdentifiers=false', () =>
]);
});
it('should transform chart props for viz with showQueryIdentifiers=true', () => {
test('should transform chart props for viz with showQueryIdentifiers=true', () => {
const chartPropsConfigWithIdentifiers = {
...chartPropsConfig,
formData: {
@@ -259,7 +259,7 @@ describe('legend sorting', () => {
});
});
it('legend margin: top orientation sets grid.top correctly', () => {
test('legend margin: top orientation sets grid.top correctly', () => {
const chartPropsConfigWithoutIdentifiers = {
...chartPropsConfig,
formData: {
@@ -274,7 +274,7 @@ it('legend margin: top orientation sets grid.top correctly', () => {
expect((transformed.echartOptions.grid as any).top).toEqual(270);
});
it('legend margin: bottom orientation sets grid.bottom correctly', () => {
test('legend margin: bottom orientation sets grid.bottom correctly', () => {
const chartPropsConfigWithoutIdentifiers = {
...chartPropsConfig,
formData: {
@@ -290,7 +290,7 @@ it('legend margin: bottom orientation sets grid.bottom correctly', () => {
expect((transformed.echartOptions.grid as any).bottom).toEqual(270);
});
it('legend margin: left orientation sets grid.left correctly', () => {
test('legend margin: left orientation sets grid.left correctly', () => {
const chartPropsConfigWithoutIdentifiers = {
...chartPropsConfig,
formData: {
@@ -306,7 +306,7 @@ it('legend margin: left orientation sets grid.left correctly', () => {
expect((transformed.echartOptions.grid as any).left).toEqual(270);
});
it('legend margin: right orientation sets grid.right correctly', () => {
test('legend margin: right orientation sets grid.right correctly', () => {
const chartPropsConfigWithoutIdentifiers = {
...chartPropsConfig,
formData: {

View File

@@ -32,7 +32,7 @@ const mockColorScale = jest.fn(
describe('transformSeries', () => {
const series = { name: 'test-series' };
test('should use the colorScaleKey if timeShiftColor is enabled', () => {
it('should use the colorScaleKey if timeShiftColor is enabled', () => {
const opts = {
timeShiftColor: true,
colorScaleKey: 'test-key',
@@ -44,7 +44,7 @@ describe('transformSeries', () => {
expect((result as any)?.itemStyle.color).toBe('color-for-test-key-1');
});
test('should use seriesKey if timeShiftColor is not enabled', () => {
it('should use seriesKey if timeShiftColor is not enabled', () => {
const opts = {
timeShiftColor: false,
seriesKey: 'series-key',
@@ -56,7 +56,7 @@ describe('transformSeries', () => {
expect((result as any)?.itemStyle.color).toBe('color-for-series-key-2');
});
test('should apply border styles for bar series with connectNulls', () => {
it('should apply border styles for bar series with connectNulls', () => {
const opts = {
seriesType: EchartsTimeseriesSeriesType.Bar,
connectNulls: true,
@@ -72,7 +72,7 @@ describe('transformSeries', () => {
);
});
test('should not apply border styles for non-bar series', () => {
it('should not apply border styles for non-bar series', () => {
const opts = {
seriesType: EchartsTimeseriesSeriesType.Line,
connectNulls: true,
@@ -88,7 +88,7 @@ describe('transformSeries', () => {
});
describe('transformNegativeLabelsPosition', () => {
test('label position bottom of negative value no Horizontal', () => {
it('label position bottom of negative value no Horizontal', () => {
const isHorizontal = false;
const series: SeriesOption = {
data: [
@@ -112,7 +112,7 @@ describe('transformNegativeLabelsPosition', () => {
expect((result as any)[4].label).toBe(undefined);
});
test('label position left of negative value is Horizontal', () => {
it('label position left of negative value is Horizontal', () => {
const isHorizontal = true;
const series: SeriesOption = {
data: [
@@ -137,7 +137,7 @@ describe('transformNegativeLabelsPosition', () => {
expect((result as any)[4].label.position).toBe('outside');
});
test('label position to line type', () => {
it('label position to line type', () => {
const isHorizontal = false;
const series: SeriesOption = {
data: [
@@ -165,7 +165,7 @@ describe('transformNegativeLabelsPosition', () => {
expect((result as any)[4].label).toBe(undefined);
});
test('label position to bar type and stack', () => {
it('label position to bar type and stack', () => {
const isHorizontal = false;
const series: SeriesOption = {
data: [

View File

@@ -1,40 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
{
"plugins": ["jest", "jest-dom", "no-only-tests", "testing-library"],
"env": {
"jest/globals": true
},
"settings": {
"jest": {
"version": "detect"
}
},
"extends": [
"plugin:jest/recommended",
"plugin:jest-dom/recommended",
"plugin:testing-library/react"
],
"rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"jest/consistent-test-it": "error",
"no-only-tests/no-only-tests": "error",
"prefer-promise-reject-errors": 0
}
}

View File

@@ -49,7 +49,6 @@ export default {
dash_edit_perm: true,
dash_save_perm: true,
common: {
flash_messages: [],
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
},
filterBarOrientation: FilterBarOrientation.Vertical,

View File

@@ -1,30 +0,0 @@
{
"extends": "prettier",
"plugins": ["prettier"],
"rules": {
"prefer-template": 2,
"new-cap": 2,
"no-restricted-syntax": 2,
"guard-for-in": 2,
"prefer-arrow-callback": 2,
"func-names": 2,
"react/jsx-no-bind": 2,
"no-confusing-arrow": 2,
"jsx-a11y/no-static-element-interactions": 2,
"jsx-a11y/anchor-has-content": 2,
"react/require-default-props": 2,
"no-plusplus": 2,
"no-mixed-operators": 0,
"no-continue": 2,
"no-bitwise": 2,
"no-multi-assign": 2,
"no-restricted-properties": 2,
"no-prototype-builtins": 2,
"class-methods-use-this": 2,
"import/no-named-as-default": 2,
"react/no-unescaped-entities": 2,
"react/no-string-refs": 2,
"react/jsx-indent": 0,
"prettier/prettier": "error"
}
}

View File

@@ -0,0 +1,11 @@
{
"overrides": [
{
"files": ["*.test.ts", "*.test.tsx", "*.test.js", "*.test.jsx"],
"rules": {
"jest/consistent-test-it": ["error", {"fn": "test"}],
"no-restricted-globals": ["error", "describe", "it"]
}
}
]
}

View File

@@ -338,7 +338,6 @@ export function runQuery(query, runPreviewOnly) {
const postPayload = {
client_id: query.id,
database_id: query.dbId,
json: true,
runAsync: query.runAsync,
catalog: query.catalog,
schema: query.schema,
@@ -956,7 +955,7 @@ export function addTable(queryEditor, tableName, catalogName, schemaName) {
const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
const table = {
dbId,
queryEditorId: queryEditor.id,
queryEditorId: queryEditor.tabViewId ?? queryEditor.id,
catalog: catalogName,
schema: schemaName,
name: tableName,
@@ -1018,12 +1017,13 @@ export function runTablePreviewQuery(newTable, runPreviewOnly) {
};
}
export function syncTable(table, tableMetadata) {
export function syncTable(table, tableMetadata, finalQueryEditorId) {
return function (dispatch) {
const finalTable = { ...table, queryEditorId: finalQueryEditorId };
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.post({
endpoint: encodeURI('/tableschemaview/'),
postPayload: { table: { ...tableMetadata, ...table } },
postPayload: { table: { ...tableMetadata, ...finalTable } },
})
: Promise.resolve({ json: { id: table.id } });

View File

@@ -49,6 +49,7 @@ jest.mock('@superset-ui/core', () => ({
isFeatureEnabled: jest.fn(),
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getUpToDateQuery', () => {
test('should return the up to date query editor state', () => {
const outOfUpdatedQueryEditor = {
@@ -72,6 +73,7 @@ describe('getUpToDateQuery', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('async actions', () => {
const mockBigNumber = '9223372036854775807';
const queryEditor = {
@@ -100,6 +102,7 @@ describe('async actions', () => {
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveQuery', () => {
const saveQueryEndpoint = 'glob:*/api/v1/saved_query/';
fetchMock.post(saveQueryEndpoint, { results: { json: {} } });
@@ -109,7 +112,7 @@ describe('async actions', () => {
return request(dispatch, () => initialState);
};
it('posts to the correct url', () => {
test('posts to the correct url', () => {
expect.assertions(1);
const store = mockStore(initialState);
@@ -118,7 +121,7 @@ describe('async actions', () => {
});
});
it('posts the correct query object', () => {
test('posts the correct query object', () => {
const store = mockStore(initialState);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
const call = fetchMock.calls(saveQueryEndpoint)[0];
@@ -131,7 +134,7 @@ describe('async actions', () => {
});
});
it('calls 3 dispatch actions', () => {
test('calls 3 dispatch actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -139,7 +142,7 @@ describe('async actions', () => {
});
});
it('calls QUERY_EDITOR_SAVED after making a request', () => {
test('calls QUERY_EDITOR_SAVED after making a request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -147,7 +150,7 @@ describe('async actions', () => {
});
});
it('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
test('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
expect.assertions(1);
const store = mockStore(initialState);
@@ -163,6 +166,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('formatQuery', () => {
const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
const expectedSql = 'SELECT 1';
@@ -179,6 +183,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('fetchQueryResults', () => {
const makeRequest = () => {
const store = mockStore(initialState);
@@ -186,7 +191,7 @@ describe('async actions', () => {
return request(dispatch, store.getState);
};
it('makes the fetch request', () => {
test('makes the fetch request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -194,7 +199,7 @@ describe('async actions', () => {
});
});
it('calls requestQueryResults', () => {
test('calls requestQueryResults', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -202,7 +207,7 @@ describe('async actions', () => {
});
});
it.skip('parses large number result without losing precision', () =>
test.skip('parses large number result without losing precision', () =>
makeRequest().then(() => {
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2);
@@ -211,7 +216,7 @@ describe('async actions', () => {
);
}));
it('calls querySuccess on fetch success', () => {
test('calls querySuccess on fetch success', () => {
expect.assertions(1);
const store = mockStore({});
@@ -226,7 +231,7 @@ describe('async actions', () => {
});
});
it('calls queryFailed on fetch error', () => {
test('calls queryFailed on fetch error', () => {
expect.assertions(1);
fetchMock.get(
@@ -248,13 +253,14 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runQuery without query params', () => {
const makeRequest = () => {
const request = actions.runQuery(query);
return request(dispatch, () => initialState);
};
it('makes the fetch request', () => {
test('makes the fetch request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -262,7 +268,7 @@ describe('async actions', () => {
});
});
it('calls startQuery', () => {
test('calls startQuery', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -270,7 +276,7 @@ describe('async actions', () => {
});
});
it.skip('parses large number result without losing precision', () =>
test.skip('parses large number result without losing precision', () =>
makeRequest().then(() => {
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2);
@@ -279,7 +285,7 @@ describe('async actions', () => {
);
}));
it('calls querySuccess on fetch success', () => {
test('calls querySuccess on fetch success', () => {
expect.assertions(1);
const store = mockStore({});
@@ -293,7 +299,7 @@ describe('async actions', () => {
});
});
it('calls queryFailed on fetch error and logs the error details', () => {
test('calls queryFailed on fetch error and logs the error details', () => {
expect.assertions(2);
fetchMock.post(
@@ -324,6 +330,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runQuery with query params', () => {
const { location } = window;
@@ -342,7 +349,7 @@ describe('async actions', () => {
return request(dispatch, () => initialState);
};
it('makes the fetch request', async () => {
test('makes the fetch request', async () => {
const runQueryEndpointWithParams =
'glob:*/api/v1/sqllab/execute/?foo=bar';
fetchMock.post(
@@ -355,8 +362,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('reRunQuery', () => {
it('creates new query with a new id', () => {
test('creates new query with a new id', () => {
const id = 'id';
const state = {
sqlLab: {
@@ -372,6 +380,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('postStopQuery', () => {
const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
fetchMock.post(stopQueryEndpoint, {});
@@ -385,7 +394,7 @@ describe('async actions', () => {
return request(dispatch);
};
it('makes the fetch request', () => {
test('makes the fetch request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -393,7 +402,7 @@ describe('async actions', () => {
});
});
it('calls stopQuery', () => {
test('calls stopQuery', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -401,7 +410,7 @@ describe('async actions', () => {
});
});
it('sends the correct data', () => {
test('sends the correct data', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -412,8 +421,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('cloneQueryToNewTab', () => {
it('creates new query editor', () => {
test('creates new query editor', () => {
expect.assertions(1);
const id = 'id';
@@ -455,6 +465,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('popSavedQuery', () => {
const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
const store = mockStore({});
@@ -508,7 +519,7 @@ describe('async actions', () => {
supersetClientGetSpy.mockRestore();
});
it('calls API endpint with correct params', async () => {
test('calls API endpint with correct params', async () => {
supersetClientGetSpy.mockResolvedValue({
json: { result: mockSavedQueryApiResponse },
});
@@ -520,7 +531,7 @@ describe('async actions', () => {
});
});
it('dispatches addQueryEditor with correct params on successful API call', async () => {
test('dispatches addQueryEditor with correct params on successful API call', async () => {
supersetClientGetSpy.mockResolvedValue({
json: { result: mockSavedQueryApiResponse },
});
@@ -547,7 +558,7 @@ describe('async actions', () => {
);
});
it('should dispatch addDangerToast on API error', async () => {
test('should dispatch addDangerToast on API error', async () => {
supersetClientGetSpy.mockResolvedValue(new Error());
await makeRequest(1);
@@ -561,8 +572,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => {
it('creates new query editor', () => {
test('creates new query editor', () => {
expect.assertions(1);
const store = mockStore(initialState);
@@ -582,8 +594,9 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addNewQueryEditor', () => {
it('creates new query editor with new tab name', () => {
test('creates new query editor with new tab name', () => {
const store = mockStore({
...initialState,
sqlLab: {
@@ -621,7 +634,7 @@ describe('async actions', () => {
});
});
it('set current query editor', () => {
test('set current query editor', () => {
expect.assertions(1);
const store = mockStore(initialState);
@@ -636,8 +649,9 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('swithQueryEditor', () => {
it('switch to the next tab editor', () => {
test('switch to the next tab editor', () => {
const store = mockStore(initialState);
const expectedActions = [
{
@@ -650,7 +664,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
it('switch to the first tab editor once it reaches the rightmost tab', () => {
test('switch to the first tab editor once it reaches the rightmost tab', () => {
const store = mockStore({
...initialState,
sqlLab: {
@@ -673,7 +687,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
it('switch to the previous tab editor', () => {
test('switch to the previous tab editor', () => {
const store = mockStore({
...initialState,
sqlLab: {
@@ -692,7 +706,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
it('switch to the last tab editor once it reaches the leftmost tab', () => {
test('switch to the last tab editor once it reaches the leftmost tab', () => {
const store = mockStore({
...initialState,
sqlLab: {
@@ -715,6 +729,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('backend sync', () => {
const updateTabStateEndpoint = 'glob:*/tabstateview/*';
fetchMock.put(updateTabStateEndpoint, {});
@@ -745,8 +760,9 @@ describe('async actions', () => {
afterEach(() => fetchMock.resetHistory());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => {
it('creates the tab state in the local storage', () => {
test('creates the tab state in the local storage', () => {
expect.assertions(2);
const store = mockStore({});
@@ -769,8 +785,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('removeQueryEditor', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const store = mockStore({});
@@ -785,8 +802,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetDb', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const dbId = 42;
@@ -803,8 +821,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetCatalog', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const catalog = 'public';
@@ -821,8 +840,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetSchema', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const schema = 'schema';
@@ -839,8 +859,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetAutorun', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const autorun = true;
@@ -857,8 +878,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetTitle', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const name = 'name';
@@ -877,6 +899,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetAndSaveSql', () => {
const sql = 'SELECT * ';
const expectedActions = [
@@ -886,8 +909,9 @@ describe('async actions', () => {
sql,
},
];
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with backend persistence flag on', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(2);
const store = mockStore({
@@ -904,8 +928,9 @@ describe('async actions', () => {
});
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with backend persistence flag off', () => {
it('does not update the tab state in the backend', () => {
test('does not update the tab state in the backend', () => {
isFeatureEnabled.mockImplementation(
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
);
@@ -927,8 +952,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetQueryLimit', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const queryLimit = 10;
@@ -947,8 +973,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetTemplateParams', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const templateParams = '{"foo": "bar"}';
@@ -968,8 +995,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addTable', () => {
it('dispatches table state from unsaved change', () => {
test('dispatches table state from unsaved change', () => {
const tableName = 'table';
const catalogName = null;
const schemaName = 'schema';
@@ -1002,10 +1030,90 @@ describe('async actions', () => {
}),
);
});
test('uses tabViewId when available', () => {
const tableName = 'table';
const catalogName = null;
const schemaName = 'schema';
const expectedDbId = 473892;
const tabViewId = '123';
const queryWithTabViewId = { ...query, tabViewId };
const store = mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: query.id,
dbId: expectedDbId,
},
},
});
const request = actions.addTable(
queryWithTabViewId,
tableName,
catalogName,
schemaName,
);
request(store.dispatch, store.getState);
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
table: expect.objectContaining({
name: tableName,
catalog: catalogName,
schema: schemaName,
dbId: expectedDbId,
queryEditorId: tabViewId, // Should use tabViewId, not id
}),
}),
);
});
test('falls back to id when tabViewId is not available', () => {
const tableName = 'table';
const catalogName = null;
const schemaName = 'schema';
const expectedDbId = 473892;
const queryWithoutTabViewId = { ...query, tabViewId: undefined };
const store = mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: query.id,
dbId: expectedDbId,
},
},
});
const request = actions.addTable(
queryWithoutTabViewId,
tableName,
catalogName,
schemaName,
);
request(store.dispatch, store.getState);
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
table: expect.objectContaining({
name: tableName,
catalog: catalogName,
schema: schemaName,
dbId: expectedDbId,
queryEditorId: query.id, // Should use id when tabViewId is not available
}),
}),
);
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('syncTable', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(4);
const tableName = 'table';
@@ -1028,6 +1136,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runTablePreviewQuery', () => {
const results = {
data: mockBigNumber,
@@ -1058,7 +1167,7 @@ describe('async actions', () => {
fetchMock.resetHistory();
});
it('updates and runs data preview query when configured', () => {
test('updates and runs data preview query when configured', () => {
expect.assertions(3);
const expectedActionTypes = [
@@ -1082,7 +1191,7 @@ describe('async actions', () => {
});
});
it('runs data preview query only', () => {
test('runs data preview query only', () => {
const expectedActionTypes = [
actions.START_QUERY, // runQuery (data preview)
actions.QUERY_SUCCESS, // querySuccess
@@ -1107,8 +1216,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('expandTable', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(2);
const table = { id: 1 };
@@ -1126,8 +1236,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('collapseTable', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(2);
const table = { id: 1 };
@@ -1145,8 +1256,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('removeTables', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(2);
const table = { id: 1, initialized: true };
@@ -1163,7 +1275,7 @@ describe('async actions', () => {
});
});
it('deletes multiple tables and updates the table schema state in the backend', () => {
test('deletes multiple tables and updates the table schema state in the backend', () => {
expect.assertions(2);
const tables = [
@@ -1183,7 +1295,7 @@ describe('async actions', () => {
});
});
it('only updates the initialized table schema state in the backend', () => {
test('only updates the initialized table schema state in the backend', () => {
expect.assertions(2);
const tables = [{ id: 1 }, { id: 2, initialized: true }];
@@ -1201,8 +1313,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('syncQueryEditor', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(3);
const results = {

View File

@@ -68,12 +68,13 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
},
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('AceEditorWrapper', () => {
beforeEach(() => {
(FullSQLEditor as any as jest.Mock).mockClear();
});
it('renders ace editor including sql value', async () => {
test('renders ace editor including sql value', async () => {
const store = createStore(initialState, reducerIndex);
const { getByTestId } = setup(defaultQueryEditor, store);
await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
@@ -83,7 +84,7 @@ describe('AceEditorWrapper', () => {
);
});
it('renders current sql for unrelated unsaved changes', () => {
test('renders current sql for unrelated unsaved changes', () => {
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
const store = createStore(
{
@@ -108,7 +109,7 @@ describe('AceEditorWrapper', () => {
);
});
it('skips rerendering for updating cursor position', () => {
test('skips rerendering for updating cursor position', () => {
const store = createStore(initialState, reducerIndex);
setup(defaultQueryEditor, store);

View File

@@ -65,6 +65,7 @@ const AceEditorWrapper = ({
'catalog',
'schema',
'templateParams',
'tabViewId',
]);
// Prevent a maximum update depth exceeded error
// by skipping access the unsaved query editor state
@@ -172,6 +173,7 @@ const AceEditorWrapper = ({
dbId: queryEditor.dbId,
catalog: queryEditor.catalog,
schema: queryEditor.schema,
tabViewId: queryEditor.tabViewId,
},
!autocomplete,
);
@@ -191,6 +193,11 @@ const AceEditorWrapper = ({
width: ${theme.sizeUnit * 130}px !important;
}
.ace_completion-highlight {
color: ${theme.colorPrimaryText} !important;
background-color: ${theme.colorPrimaryBgHover};
}
.ace_tooltip {
max-width: ${SQL_EDITOR_LEFTBAR_WIDTH}px;
}

View File

@@ -44,6 +44,7 @@ type Params = {
dbId?: string | number;
catalog?: string | null;
schema?: string;
tabViewId?: string;
};
const EMPTY_LIST = [] as typeof sqlKeywords;
@@ -59,7 +60,7 @@ const getHelperText = (value: string) =>
const extensionsRegistry = getExtensionsRegistry();
export function useKeywords(
{ queryEditorId, dbId, catalog, schema }: Params,
{ queryEditorId, dbId, catalog, schema, tabViewId }: Params,
skip = false,
) {
const useCustomKeywords = extensionsRegistry.get(
@@ -147,7 +148,12 @@ export function useKeywords(
const insertMatch = useEffectEvent((editor: Editor, data: any) => {
if (data.meta === 'table') {
dispatch(
addTable({ id: queryEditorId, dbId }, data.value, catalog, schema),
addTable(
{ id: queryEditorId, dbId, tabViewId },
data.value,
catalog,
schema,
),
);
}

View File

@@ -47,6 +47,7 @@ const sqlLabReducer = combineReducers({
});
const mockAction = {} as AnyAction;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlLab App', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -60,23 +61,23 @@ describe('SqlLab App', () => {
jest.useRealTimers();
});
it('is valid', () => {
test('is valid', () => {
expect(isValidElement(<App />)).toBe(true);
});
it('should render', () => {
test('should render', () => {
const { getByTestId } = render(<App />, { useRedux: true, store });
expect(getByTestId('SqlLabApp')).toBeInTheDocument();
expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument();
});
it('reset hotkey events on unmount', () => {
test('reset hotkey events on unmount', () => {
const { unmount } = render(<App />, { useRedux: true, store });
unmount();
expect(Mousetrap.reset).toHaveBeenCalled();
});
it('logs current usage warning', () => {
test('logs current usage warning', () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
const initialState = {
localStorageUsageInKilobytes,
@@ -100,7 +101,7 @@ describe('SqlLab App', () => {
);
});
it('logs current local storage usage', async () => {
test('logs current local storage usage', async () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10;
const storeExceedLocalStorage = mockStore(
sqlLabReducer(

View File

@@ -21,22 +21,23 @@ import { render } from 'spec/helpers/testing-library';
import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { mockedActions, table } from 'src/SqlLab/fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ColumnElement', () => {
const mockedProps = {
actions: mockedActions,
column: table.columns[0],
};
it('is valid with props', () => {
test('is valid with props', () => {
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
});
it('renders a proper primary key', () => {
test('renders a proper primary key', () => {
const { container } = render(<ColumnElement column={table.columns[0]} />);
expect(container.querySelector('i.fa-key')).toBeInTheDocument();
expect(
container.querySelector('[data-test="col-name"]')?.firstChild,
).toHaveTextContent('id');
});
it('renders a multi-key column', () => {
test('renders a multi-key column', () => {
const { container } = render(<ColumnElement column={table.columns[1]} />);
expect(container.querySelector('i.fa-link')).toBeInTheDocument();
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
@@ -44,7 +45,7 @@ describe('ColumnElement', () => {
container.querySelector('[data-test="col-name"]')?.firstChild,
).toHaveTextContent('first_name');
});
it('renders a column with no keys', () => {
test('renders a column with no keys', () => {
const { container } = render(<ColumnElement column={table.columns[2]} />);
expect(container.querySelector('i')).not.toBeInTheDocument();
expect(

View File

@@ -53,14 +53,15 @@ const setup = (props: Partial<EstimateQueryCostButtonProps>, store?: Store) =>
},
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('EstimateQueryCostButton', () => {
it('renders EstimateQueryCostButton', async () => {
test('renders EstimateQueryCostButton', async () => {
const { queryByText } = setup({}, mockStore(initialState));
expect(queryByText('Estimate cost')).toBeInTheDocument();
});
it('renders label for selected query', async () => {
test('renders label for selected query', async () => {
const { queryByText } = setup(
{ queryEditorId: extraQueryEditor1.id },
mockStore(initialState),
@@ -69,7 +70,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
});
it('renders label for selected query from unsaved', async () => {
test('renders label for selected query from unsaved', async () => {
const { queryByText } = setup(
{},
mockStore({
@@ -87,7 +88,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
});
it('renders estimation error result', async () => {
test('renders estimation error result', async () => {
const { queryByText, getByText } = setup(
{},
mockStore({
@@ -109,7 +110,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate error')).toBeInTheDocument();
});
it('renders estimation success result', async () => {
test('renders estimation success result', async () => {
const { queryByText, getByText } = setup(
{},
mockStore({

View File

@@ -47,17 +47,18 @@ const setup = (props: Partial<ExploreCtasResultsButtonProps>, store?: Store) =>
},
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ExploreCtasResultsButton', () => {
const postFormSpy = jest.spyOn(SupersetClientClass.prototype, 'postForm');
postFormSpy.mockImplementation(jest.fn());
it('renders', async () => {
test('renders', async () => {
const { queryByText } = setup({}, mockStore(initialState));
expect(queryByText('Explore')).toBeInTheDocument();
});
it('visualize results', async () => {
test('visualize results', async () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();
@@ -75,7 +76,7 @@ describe('ExploreCtasResultsButton', () => {
});
});
it('visualize results fails', async () => {
test('visualize results fails', async () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();

View File

@@ -31,8 +31,9 @@ const setup = (
useRedux: true,
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ExploreResultsButton', () => {
it('renders', async () => {
test('renders', async () => {
const { queryByText } = setup(jest.fn(), {
database: { allows_subquery: true },
});
@@ -41,7 +42,7 @@ describe('ExploreResultsButton', () => {
expect(screen.getByRole('button', { name: /Create chart/i })).toBeEnabled();
});
it('renders disabled if subquery not allowed', async () => {
test('renders disabled if subquery not allowed', async () => {
const { queryByText } = setup(jest.fn());
expect(queryByText('Create chart')).toBeInTheDocument();
// Updated line to match the actual button name that includes the icon

View File

@@ -43,6 +43,7 @@ const mockState = {
databases: mockDatabases,
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryAutoRefresh', () => {
const runningQueries: QueryDictionary = { [runningQuery.id]: runningQuery };
const successfulQueries: QueryDictionary = {
@@ -62,15 +63,15 @@ describe('QueryAutoRefresh', () => {
jest.useRealTimers();
});
it('isQueryRunning returns true for valid running query', () => {
test('isQueryRunning returns true for valid running query', () => {
expect(isQueryRunning(runningQuery)).toBe(true);
});
it('isQueryRunning returns false for valid not-running query', () => {
test('isQueryRunning returns false for valid not-running query', () => {
expect(isQueryRunning(successfulQuery)).toBe(false);
});
it('isQueryRunning returns false for invalid query', () => {
test('isQueryRunning returns false for invalid query', () => {
// @ts-ignore
expect(isQueryRunning(null)).toBe(false);
// @ts-ignore
@@ -81,15 +82,15 @@ describe('QueryAutoRefresh', () => {
expect(isQueryRunning({ state: { badFormat: true } })).toBe(false);
});
it('shouldCheckForQueries is true for valid running query', () => {
test('shouldCheckForQueries is true for valid running query', () => {
expect(shouldCheckForQueries(runningQueries)).toBe(true);
});
it('shouldCheckForQueries is false for valid completed query', () => {
test('shouldCheckForQueries is false for valid completed query', () => {
expect(shouldCheckForQueries(successfulQueries)).toBe(false);
});
it('shouldCheckForQueries is false for invalid inputs', () => {
test('shouldCheckForQueries is false for invalid inputs', () => {
// @ts-ignore
expect(shouldCheckForQueries(null)).toBe(false);
// @ts-ignore
@@ -109,7 +110,7 @@ describe('QueryAutoRefresh', () => {
).toBe(false);
});
it('Attempts to refresh when given pending query', async () => {
test('Attempts to refresh when given pending query', async () => {
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
@@ -135,7 +136,7 @@ describe('QueryAutoRefresh', () => {
);
});
it('Attempts to clear inactive queries when updated queries are empty', async () => {
test('Attempts to clear inactive queries when updated queries are empty', async () => {
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, { result: [] });
@@ -164,7 +165,7 @@ describe('QueryAutoRefresh', () => {
expect(fetchMock.calls(refreshApi)).toHaveLength(1);
});
it('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
test('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
@@ -191,7 +192,7 @@ describe('QueryAutoRefresh', () => {
);
});
it('Does NOT Attempt to refresh when given only completed queries', async () => {
test('Does NOT Attempt to refresh when given only completed queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
@@ -219,7 +220,7 @@ describe('QueryAutoRefresh', () => {
expect(fetchMock.calls(refreshApi)).toHaveLength(0);
});
it('logs the failed error for async queries', async () => {
test('logs the failed error for async queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {

View File

@@ -58,8 +58,9 @@ const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
},
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryLimitSelect', () => {
it('renders current query limit size', () => {
test('renders current query limit size', () => {
const queryLimit = 10;
const { getByText } = setup(
{
@@ -81,12 +82,12 @@ describe('QueryLimitSelect', () => {
expect(getByText(queryLimit)).toBeInTheDocument();
});
it('renders default query limit for initial queryEditor', () => {
test('renders default query limit for initial queryEditor', () => {
const { getByText } = setup({}, mockStore(initialState));
expect(getByText(defaultQueryLimit)).toBeInTheDocument();
});
it('renders queryLimit from unsavedQueryEditor', () => {
test('renders queryLimit from unsavedQueryEditor', () => {
const queryLimit = 10000;
const { getByText } = setup(
{},
@@ -104,7 +105,7 @@ describe('QueryLimitSelect', () => {
expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
});
it('renders dropdown select', async () => {
test('renders dropdown select', async () => {
const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 50000 },
mockStore(initialState),
@@ -126,7 +127,7 @@ describe('QueryLimitSelect', () => {
expect(actualLabels).toEqual(expectedLabels);
});
it('renders dropdown select correctly when maxRow is less than 10', async () => {
test('renders dropdown select correctly when maxRow is less than 10', async () => {
const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 5 },
mockStore(initialState),
@@ -146,7 +147,7 @@ describe('QueryLimitSelect', () => {
expect(actualLabels).toEqual(expectedLabels);
});
it('renders dropdown select correctly when maxRow is a multiple of 10', async () => {
test('renders dropdown select correctly when maxRow is a multiple of 10', async () => {
const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 10000 },
mockStore(initialState),
@@ -168,7 +169,7 @@ describe('QueryLimitSelect', () => {
expect(actualLabels).toEqual(expectedLabels);
});
it('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
test('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
const store = mockStore(initialState);
const expectedIndex = 1;
const { baseElement, getAllByRole, getByRole } = setup({}, store);

View File

@@ -29,6 +29,7 @@ const mockedProps = {
latestQueryId: 'ryhMUZCGb',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryTable', () => {
test('is valid', () => {
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);

View File

@@ -150,6 +150,7 @@ const setup = (props?: any, store?: Store) =>
...(store && { store }),
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ResultSet', () => {
beforeAll(() => {
setupAGGridModules();

View File

@@ -53,17 +53,17 @@ const setup = (props?: Partial<RunQueryActionButtonProps>, store?: Store) =>
...(store && { store }),
});
it('renders a single Button', () => {
test('renders a single Button', () => {
const { getByRole } = setup({}, mockStore(initialState));
expect(getByRole('button')).toBeInTheDocument();
});
it('renders a label for Run Query', () => {
test('renders a label for Run Query', () => {
const { getByText } = setup({}, mockStore(initialState));
expect(getByText('Run')).toBeInTheDocument();
});
it('renders a label for Selected Query', () => {
test('renders a label for Selected Query', () => {
const { getByText } = setup(
{},
mockStore({
@@ -80,7 +80,7 @@ it('renders a label for Selected Query', () => {
expect(getByText('Run selection')).toBeInTheDocument();
});
it('disable button when sql from unsaved changes is empty', () => {
test('disable button when sql from unsaved changes is empty', () => {
const { getByRole } = setup(
{},
mockStore({
@@ -98,7 +98,7 @@ it('disable button when sql from unsaved changes is empty', () => {
expect(button).toBeDisabled();
});
it('disable button when selectedText only contains blank contents', () => {
test('disable button when selectedText only contains blank contents', () => {
const { getByRole } = setup(
{},
mockStore({
@@ -116,7 +116,7 @@ it('disable button when selectedText only contains blank contents', () => {
expect(button).toBeDisabled();
});
it('enable default button for unrelated unsaved changes', () => {
test('enable default button for unrelated unsaved changes', () => {
const { getByRole } = setup(
{},
mockStore({
@@ -134,7 +134,7 @@ it('enable default button for unrelated unsaved changes', () => {
expect(button).toBeEnabled();
});
it('dispatch runQuery on click', async () => {
test('dispatch runQuery on click', async () => {
const runQuery = jest.fn();
const { getByRole } = setup({ runQuery }, mockStore(initialState));
const button = getByRole('button');
@@ -143,7 +143,7 @@ it('dispatch runQuery on click', async () => {
await waitFor(() => expect(runQuery).toHaveBeenCalledTimes(1));
});
it('dispatch stopQuery on click while running state', async () => {
test('dispatch stopQuery on click while running state', async () => {
const stopQuery = jest.fn();
const { getByRole } = setup(
{ queryState: 'running', stopQuery },

View File

@@ -24,6 +24,7 @@ const overlayMenu = (
<Menu items={[{ label: 'Save dataset', key: 'save-dataset' }]} />
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetActionButton', () => {
test('renders a split save button', async () => {
render(

View File

@@ -62,8 +62,9 @@ jest.mock('src/explore/exploreUtils/formData', () => ({
postFormData: jest.fn(),
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetModal', () => {
it('renders a "Save as new" field', () => {
test('renders a "Save as new" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const saveRadioBtn = screen.getByRole('radio', {
@@ -80,7 +81,7 @@ describe('SaveDatasetModal', () => {
expect(inputFieldText).toBeInTheDocument();
});
it('renders an "Overwrite existing" field', () => {
test('renders an "Overwrite existing" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -96,20 +97,20 @@ describe('SaveDatasetModal', () => {
expect(placeholderText).toBeInTheDocument();
});
it('renders a close button', () => {
test('renders a close button', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});
it('renders a save button when "Save as new" is selected', () => {
test('renders a save button when "Save as new" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// "Save as new" is selected when the modal opens by default
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
it('renders an overwrite button when "Overwrite existing" is selected', () => {
test('renders an overwrite button when "Overwrite existing" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
@@ -123,7 +124,7 @@ describe('SaveDatasetModal', () => {
).toBeInTheDocument();
});
it('renders the overwrite button as disabled until an existing dataset is selected', async () => {
test('renders the overwrite button as disabled until an existing dataset is selected', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -155,7 +156,7 @@ describe('SaveDatasetModal', () => {
expect(overwriteConfirmationBtn).toBeEnabled();
});
it('renders a confirm overwrite screen when overwrite is clicked', async () => {
test('renders a confirm overwrite screen when overwrite is clicked', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -196,7 +197,7 @@ describe('SaveDatasetModal', () => {
).toBeInTheDocument();
});
it('sends the schema when creating the dataset', async () => {
test('sends the schema when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
@@ -221,7 +222,7 @@ describe('SaveDatasetModal', () => {
});
});
it('sends the catalog when creating the dataset', async () => {
test('sends the catalog when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
@@ -252,12 +253,12 @@ describe('SaveDatasetModal', () => {
});
});
it('does not renders a checkbox button when template processing is disabled', () => {
test('does not renders a checkbox button when template processing is disabled', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
it('renders a checkbox button when template processing is enabled', () => {
test('renders a checkbox button when template processing is enabled', () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
@@ -266,7 +267,7 @@ describe('SaveDatasetModal', () => {
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
it('correctly includes template parameters when template processing is enabled', () => {
test('correctly includes template parameters when template processing is enabled', () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
@@ -301,7 +302,7 @@ describe('SaveDatasetModal', () => {
});
});
it('correctly excludes template parameters when template processing is enabled', () => {
test('correctly excludes template parameters when template processing is enabled', () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,

View File

@@ -64,8 +64,9 @@ const splitSaveBtnProps = {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SavedQuery', () => {
it('doesnt render save button when allows_virtual_table_explore is undefined', async () => {
test('doesnt render save button when allows_virtual_table_explore is undefined', async () => {
const noRenderProps = {
...mockedProps,
database: {
@@ -84,7 +85,7 @@ describe('SavedQuery', () => {
);
});
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
test('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
render(<SaveQuery {...mockedProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -95,7 +96,7 @@ describe('SavedQuery', () => {
expect(saveBtn).toBeVisible();
});
it('renders a save query modal when user clicks save button', () => {
test('renders a save query modal when user clicks save button', () => {
render(<SaveQuery {...mockedProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -111,7 +112,7 @@ describe('SavedQuery', () => {
expect(saveQueryModalHeader).toBeInTheDocument();
});
it('renders the save query modal UI', () => {
test('renders the save query modal UI', () => {
render(<SaveQuery {...mockedProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -146,7 +147,7 @@ describe('SavedQuery', () => {
expect(cancelBtn).toBeInTheDocument();
});
it('renders a "save as new" and "update" button if query already exists', () => {
test('renders a "save as new" and "update" button if query already exists', () => {
render(<SaveQuery {...mockedProps} />, {
useRedux: true,
store: mockStore({
@@ -171,7 +172,7 @@ describe('SavedQuery', () => {
expect(updateBtn).toBeInTheDocument();
});
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
test('renders a split save button when allows_virtual_table_explore is enabled', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -186,7 +187,7 @@ describe('SavedQuery', () => {
});
});
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
test('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -205,7 +206,7 @@ describe('SavedQuery', () => {
expect(saveDatasetHeader).toBeInTheDocument();
});
it('renders the save dataset modal UI', async () => {
test('renders the save dataset modal UI', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -246,7 +247,7 @@ describe('SavedQuery', () => {
expect(overwritePlaceholderText).toBeInTheDocument();
});
it('modal stays open while save is in progress and closes after completion', async () => {
test('modal stays open while save is in progress and closes after completion', async () => {
let resolveSave: () => void;
const savePromise = new Promise<void>(resolve => {
resolveSave = resolve;
@@ -290,7 +291,7 @@ describe('SavedQuery', () => {
expect(mockOnSave).toHaveBeenCalledTimes(1);
});
it('handles save with a new tab that has no changes', async () => {
test('handles save with a new tab that has no changes', async () => {
const mockOnSave = jest.fn().mockResolvedValue(undefined);
// Mock state for a new tab with default SQL

View File

@@ -76,6 +76,7 @@ const unsavedQueryEditor = {
templateParams: '{ "my_value": "foo" }',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ShareSqlLabQuery', () => {
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
const storeQueryMockId = 'ci39c3';
@@ -94,6 +95,7 @@ describe('ShareSqlLabQuery', () => {
afterAll(() => fetchMock.reset());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('via permalink api', () => {
beforeAll(() => {
mockedIsFeatureEnabled.mockImplementation(() => true);
@@ -103,7 +105,7 @@ describe('ShareSqlLabQuery', () => {
mockedIsFeatureEnabled.mockReset();
});
it('calls storeQuery() with the query when getCopyUrl() is called', async () => {
test('calls storeQuery() with the query when getCopyUrl() is called', async () => {
await act(async () => {
render(<ShareSqlLabQuery {...defaultProps} />, {
useRedux: true,
@@ -121,7 +123,7 @@ describe('ShareSqlLabQuery', () => {
).toEqual(expected);
});
it('calls storeQuery() with unsaved changes', async () => {
test('calls storeQuery() with unsaved changes', async () => {
await act(async () => {
render(<ShareSqlLabQuery {...defaultProps} />, {
useRedux: true,

View File

@@ -149,6 +149,7 @@ const createStore = (initState: object) =>
getDefaultMiddleware().concat(api.middleware, logAction),
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlEditor', () => {
beforeAll(() => {
jest.setTimeout(30000);
@@ -192,7 +193,7 @@ describe('SqlEditor', () => {
});
});
it('does not render SqlEditor if no db selected', async () => {
test('does not render SqlEditor if no db selected', async () => {
const queryEditor = initialState.sqlLab.queryEditors[2];
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
expect(
@@ -200,7 +201,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
it('renders db unavailable message', async () => {
test('renders db unavailable message', async () => {
const queryEditor = initialState.sqlLab.queryEditors[1];
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
expect(
@@ -210,7 +211,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
it('render a SqlEditorLeftBar', async () => {
test('render a SqlEditorLeftBar', async () => {
const { getByTestId, unmount } = setup(mockedProps, store);
await waitFor(
@@ -222,7 +223,7 @@ describe('SqlEditor', () => {
}, 15000);
// Update other similar tests with timeouts
it('render an AceEditorWrapper', async () => {
test('render an AceEditorWrapper', async () => {
const { findByTestId, unmount } = setup(mockedProps, store);
await waitFor(
@@ -233,7 +234,7 @@ describe('SqlEditor', () => {
unmount();
}, 15000);
it('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
test('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
const { findByTestId, queryByTestId } = setup(
{
...mockedProps,
@@ -245,7 +246,7 @@ describe('SqlEditor', () => {
expect(queryByTestId('react-ace')).not.toBeInTheDocument();
});
it('avoids rerendering EditorLeftBar and ResultSet while typing', async () => {
test('avoids rerendering EditorLeftBar and ResultSet while typing', async () => {
const { findByTestId } = setup(mockedProps, store);
const editor = await findByTestId('react-ace');
const sql = 'select *';
@@ -260,7 +261,7 @@ describe('SqlEditor', () => {
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
});
it('renders sql from unsaved change', async () => {
test('renders sql from unsaved change', async () => {
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
store = createStore({
...initialState,
@@ -293,12 +294,12 @@ describe('SqlEditor', () => {
expect(editor).toHaveValue(expectedSql);
});
it('render a SouthPane', async () => {
test('render a SouthPane', async () => {
const { findByTestId } = setup(mockedProps, store);
expect(await findByTestId('mock-result-set')).toBeInTheDocument();
});
it('runs query action with ctas false', async () => {
test('runs query action with ctas false', async () => {
store = createStore({
...initialState,
sqlLab: {
@@ -338,7 +339,7 @@ describe('SqlEditor', () => {
);
});
it('render a Limit Dropdown', async () => {
test('render a Limit Dropdown', async () => {
const defaultQueryLimit = 101;
const updatedProps = { ...mockedProps, defaultQueryLimit };
const { findByText } = setup(updatedProps, store);
@@ -346,7 +347,7 @@ describe('SqlEditor', () => {
expect(await findByText('10 000')).toBeInTheDocument();
});
it('renders an Extension if provided', async () => {
test('renders an Extension if provided', async () => {
const extensionsRegistry = getExtensionsRegistry();
extensionsRegistry.set('sqleditor.extension.form', () => (
@@ -360,6 +361,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with EstimateQueryCost enabled', () => {
beforeEach(() => {
mockIsFeatureEnabled.mockImplementation(
@@ -370,7 +372,7 @@ describe('SqlEditor', () => {
mockIsFeatureEnabled.mockClear();
});
it('sends the catalog and schema to the endpoint', async () => {
test('sends the catalog and schema to the endpoint', async () => {
const estimateApi = 'http://localhost/api/v1/sqllab/estimate/';
fetchMock.post(estimateApi, {});
@@ -436,6 +438,7 @@ describe('SqlEditor', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with SqllabBackendPersistence enabled', () => {
beforeEach(() => {
mockIsFeatureEnabled.mockImplementation(
@@ -446,7 +449,7 @@ describe('SqlEditor', () => {
mockIsFeatureEnabled.mockClear();
});
it('should render loading state when its Editor is not loaded', async () => {
test('should render loading state when its Editor is not loaded', async () => {
const switchTabApi = `glob:*/tabstateview/${defaultQueryEditor.id}/activate`;
fetchMock.post(switchTabApi, {});
const { getByTestId } = setup(

View File

@@ -168,6 +168,9 @@ const StyledToolbar = styled.div`
const StyledSidebar = styled.div`
padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
height: 100%;
display: flex;
flex-direction: column;
`;
const StyledSqlEditor = styled.div`

View File

@@ -47,7 +47,6 @@ import TableElement from '../TableElement';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
height?: number;
database?: DatabaseObject;
}
@@ -72,7 +71,6 @@ const LeftBarStyles = styled.div`
const SqlEditorLeftBar = ({
database,
queryEditorId,
height = 500,
}: SqlEditorLeftBarProps) => {
const allSelectedTables = useSelector<SqlLabRootState, Table[]>(
({ sqlLab }) =>
@@ -84,6 +82,7 @@ const SqlEditorLeftBar = ({
'dbId',
'catalog',
'schema',
'tabViewId',
]);
const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
@@ -170,7 +169,6 @@ const SqlEditorLeftBar = ({
};
const shouldShowReset = window.location.search === '?reset=1';
const tableMetaDataHeight = height - 130; // 130 is the height of the selects above
const handleCatalogChange = useCallback(
(catalog: string | null) => {
@@ -227,22 +225,16 @@ const SqlEditorLeftBar = ({
/>
<div className="divider" />
<StyledScrollbarContainer>
<div
css={css`
height: ${tableMetaDataHeight}px;
`}
>
{tables.map(table => (
<TableElement
table={table}
key={table.id}
activeKey={tables
.filter(({ expanded }) => expanded)
.map(({ id }) => id)}
onChange={onToggleTable}
/>
))}
</div>
{tables.map(table => (
<TableElement
table={table}
key={table.id}
activeKey={tables
.filter(({ expanded }) => expanded)
.map(({ id }) => id)}
onChange={onToggleTable}
/>
))}
</StyledScrollbarContainer>
{shouldShowReset && (
<Button

View File

@@ -56,15 +56,16 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
...(store && { store }),
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlEditorTabHeader', () => {
it('renders name', () => {
test('renders name', () => {
const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
expect(queryByText(defaultQueryEditor.name)).toBeInTheDocument();
expect(queryByText(extraQueryEditor1.name)).not.toBeInTheDocument();
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
});
it('renders name from unsaved changes', () => {
test('renders name from unsaved changes', () => {
const expectedTitle = 'updated title';
const { queryByText } = setup(
defaultQueryEditor,
@@ -85,7 +86,7 @@ describe('SqlEditorTabHeader', () => {
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
});
it('renders current name for unrelated unsaved changes', () => {
test('renders current name for unrelated unsaved changes', () => {
const unrelatedTitle = 'updated title';
const { queryByText } = setup(
defaultQueryEditor,
@@ -106,6 +107,7 @@ describe('SqlEditorTabHeader', () => {
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with dropdown menus', () => {
let store = mockStore();
beforeEach(async () => {
@@ -116,7 +118,7 @@ describe('SqlEditorTabHeader', () => {
userEvent.click(dropdown);
});
it('should dispatch removeQueryEditor action', async () => {
test('should dispatch removeQueryEditor action', async () => {
await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);
@@ -132,7 +134,7 @@ describe('SqlEditorTabHeader', () => {
);
});
it('should dispatch queryEditorSetTitle action', async () => {
test('should dispatch queryEditorSetTitle action', async () => {
await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);
@@ -155,7 +157,7 @@ describe('SqlEditorTabHeader', () => {
mockPrompt.mockClear();
});
it('should dispatch toggleLeftBar action', async () => {
test('should dispatch toggleLeftBar action', async () => {
await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);
@@ -173,7 +175,7 @@ describe('SqlEditorTabHeader', () => {
);
});
it('should dispatch removeAllOtherQueryEditors action', async () => {
test('should dispatch removeAllOtherQueryEditors action', async () => {
await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);
@@ -194,7 +196,7 @@ describe('SqlEditorTabHeader', () => {
);
});
it('should dispatch cloneQueryToNewTab action', async () => {
test('should dispatch cloneQueryToNewTab action', async () => {
await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);

View File

@@ -56,6 +56,7 @@ afterEach(() => {
pathStub.mockReset();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('componentDidMount', () => {
let uriStub = jest.spyOn(URI.prototype, 'search');
let replaceState = jest.spyOn(window.history, 'replaceState');

View File

@@ -22,6 +22,8 @@ import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import TableElement, { Column } from 'src/SqlLab/components/TableElement';
import { table, initialState } from 'src/SqlLab/fixtures';
import { render, waitFor, fireEvent } from 'spec/helpers/testing-library';
import * as sqlLabActions from 'src/SqlLab/actions/sqlLab';
import { QueryEditor } from 'src/SqlLab/types';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
@@ -79,6 +81,27 @@ const mockedProps = {
activeKey: [table.id],
};
const createStateWithQueryEditor = (queryEditor: Partial<QueryEditor>) => ({
...initialState,
sqlLab: {
...initialState.sqlLab,
queryEditors: [queryEditor],
},
});
const setupSyncTableTest = () => {
const spy = jest.spyOn(sqlLabActions, 'syncTable');
mockedIsFeatureEnabled.mockImplementation(
featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence,
);
fetchMock.post(
updateTableSchemaEndpoint,
{ id: 100 },
{ overwriteRoutes: true },
);
return spy;
};
test('renders', () => {
expect(isValidElement(<TableElement table={table} />)).toBe(true);
});
@@ -207,3 +230,212 @@ test('refreshes table metadata when triggered', async () => {
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1),
);
});
test('calls syncTable with valid backend ID when query editor has tabViewId', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'temp-id-123',
};
const state = createStateWithQueryEditor({
id: 'temp-id-123',
tabViewId: '42',
inLocalStorage: false,
name: 'Test Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(syncTableSpy).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
'42', // finalQueryEditorId
);
});
syncTableSpy.mockRestore();
});
test('does not call syncTable when query editor is in localStorage', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'local-id',
};
const state = createStateWithQueryEditor({
id: 'local-id',
tabViewId: undefined,
inLocalStorage: true,
name: 'Local Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
});
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
syncTableSpy.mockRestore();
});
test('does not call syncTable with non-numeric queryEditorId', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'not-a-number',
};
const state = createStateWithQueryEditor({
id: 'not-a-number',
tabViewId: 'also-not-a-number',
inLocalStorage: false,
name: 'Invalid Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
});
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
syncTableSpy.mockRestore();
});
test('does not call syncTable for already initialized tables', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: true, // Already initialized
queryEditorId: '789',
};
const state = createStateWithQueryEditor({
id: '789',
tabViewId: '789',
inLocalStorage: false,
name: 'Initialized Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
});
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
syncTableSpy.mockRestore();
});
test('calls syncTable after query editor is migrated from localStorage', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'temp-editor-id',
};
// Start with editor in localStorage
const localState = createStateWithQueryEditor({
id: 'temp-editor-id',
tabViewId: undefined,
inLocalStorage: true,
name: 'Temp Editor',
});
const { rerender } = render(
<TableElement table={testTable} activeKey={[testTable.id]} />,
{
useRedux: true,
initialState: localState,
},
);
await new Promise(resolve => setTimeout(resolve, 100));
expect(syncTableSpy).not.toHaveBeenCalled();
const migratedState = createStateWithQueryEditor({
id: 'temp-editor-id',
tabViewId: '999',
inLocalStorage: false,
name: 'Temp Editor',
});
rerender(<TableElement table={testTable} activeKey={[testTable.id]} />);
const { unmount } = render(
<TableElement table={testTable} activeKey={[testTable.id]} />,
{
useRedux: true,
initialState: migratedState,
},
);
await waitFor(() => {
expect(syncTableSpy).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Object),
'999',
);
});
unmount();
syncTableSpy.mockRestore();
});
test('passes numeric queryEditorId validation', async () => {
const syncTableSpy = setupSyncTableTest();
const testTable = {
...table,
initialized: false,
queryEditorId: 'editor-123',
};
const state = createStateWithQueryEditor({
id: 'editor-123',
tabViewId: '456',
inLocalStorage: false,
name: 'Valid Editor',
});
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
useRedux: true,
initialState: state,
});
await waitFor(() => {
expect(syncTableSpy).toHaveBeenCalled();
const [, , finalQueryEditorId] = syncTableSpy.mock.calls[0];
// Verify it's a valid numeric string
expect(Number.isNaN(Number(finalQueryEditorId))).toBe(false);
expect(typeof finalQueryEditorId).toBe('string');
expect(finalQueryEditorId).toMatch(/^\d+$/);
});
syncTableSpy.mockRestore();
});

View File

@@ -17,8 +17,8 @@
* under the License.
*/
import { useState, useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import type { Table } from 'src/SqlLab/types';
import { useDispatch, useSelector } from 'react-redux';
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
import {
ButtonGroup,
Card,
@@ -103,6 +103,19 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
},
{ skip: !expanded },
);
const tableData = {
...tableMetadata,
...tableExtendedMetadata,
};
const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
state => state.sqlLab.queryEditors,
);
const currentTable = { ...tableData, ...table };
const { queryEditorId } = currentTable;
const queryEditor = queryEditors.find(
qe => qe.id === queryEditorId || qe.tabViewId === queryEditorId,
);
const currentQueryEditorId = queryEditor?.tabViewId || queryEditorId;
useEffect(() => {
if (hasMetadataError || hasExtendedMetadataError) {
@@ -112,16 +125,16 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
}
}, [hasMetadataError, hasExtendedMetadataError, dispatch]);
const tableData = {
...tableMetadata,
...tableExtendedMetadata,
};
// TODO: migrate syncTable logic by SIP-93
const syncTableMetadata = useEffectEvent(() => {
const { initialized } = table;
if (!initialized) {
dispatch(syncTable(table, tableData));
// if not a valid number, wait for backend to assign one
const hasFinalQueryEditorId =
currentQueryEditorId &&
!Number.isNaN(Number(currentQueryEditorId)) &&
currentTable.queryEditorId !== currentQueryEditorId;
if (!initialized && hasFinalQueryEditorId) {
dispatch(syncTable(currentTable, tableData, currentQueryEditorId));
}
});
@@ -129,7 +142,12 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
if (isMetadataSuccess && isExtraMetadataSuccess) {
syncTableMetadata();
}
}, [isMetadataSuccess, isExtraMetadataSuccess, syncTableMetadata]);
}, [
isMetadataSuccess,
isExtraMetadataSuccess,
currentQueryEditorId,
syncTableMetadata,
]);
const [sortColumns, setSortColumns] = useState(false);
const [hovered, setHovered] = useState(false);

View File

@@ -135,6 +135,7 @@ test('renders preview', async () => {
);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('table actions', () => {
test('refreshes table metadata when triggered', async () => {
const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, {

View File

@@ -64,13 +64,14 @@ const setup = (
},
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('TemplateParamsEditor', () => {
it('should render with a title', () => {
test('should render with a title', () => {
const { container } = setup();
expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
});
it('should open a modal with the ace editor', async () => {
test('should open a modal with the ace editor', async () => {
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
@@ -78,7 +79,7 @@ describe('TemplateParamsEditor', () => {
});
});
it('renders templateParams', async () => {
test('renders templateParams', async () => {
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
@@ -89,7 +90,7 @@ describe('TemplateParamsEditor', () => {
);
});
it('renders code from unsaved changes', async () => {
test('renders code from unsaved changes', async () => {
const expectedCode = 'custom code value';
const { container, getByTestId } = setup(
{},

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