Compare commits

..

18 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
06868e8914 feat(table): consolidate Customize Columns into Visual Formatting section
Move the per-column "Customize columns" control into the "Visual formatting"
section so users see that per-column overrides and chart-wide toggles are
related. Rename the chart-wide toggles ("Show cell bars", "Align +/-",
"Add colors to cell bars for +/-") to append "for all columns", clarifying
that they apply globally and not per-column. Applies to both the Table and
Interactive Tables (AG Grid) plugins.
2026-05-04 13:22:19 +03:00
EMMANUELA OPURUM
dc1c0f6ba1 docs: add user-facing Handlebars chart page with full helpers reference (#39591)
Co-authored-by: Emmanuela Opurum <youremail@example.com>
2026-05-02 13:16:39 -04:00
dependabot[bot]
ad73395c89 chore(deps-dev): bump yeoman-test from 11.3.1 to 11.4.2 in /superset-frontend (#39816)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 13:46:47 +07:00
Evan Rusackas
867e173427 chore(deps): drop stale legacy-plugin-chart-map-box lockfile entry (#39825)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 13:35:25 +07:00
dependabot[bot]
c90c8612ad chore(deps): bump @docusaurus/faster from 3.10.0 to 3.10.1 in /docs (#39804)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-05-02 13:32:37 +07:00
Abdul Rehman
b14cca15f6 fix(table): preserve decimals in totals row when Time Comparison is enabled (#39747) 2026-05-02 13:31:54 +07:00
dependabot[bot]
9d4384e49e chore(deps-dev): bump @babel/preset-env from 7.29.2 to 7.29.3 in /superset-frontend (#39822)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 12:38:54 +07:00
jesperct
d8dd2d99b3 fix(time-comparison): use chart row_limit instead of instance config in offset queries (#39490) 2026-05-01 16:24:59 -07:00
dependabot[bot]
dbe26d81ce chore(deps-dev): bump baseline-browser-mapping from 2.10.21 to 2.10.24 in /superset-frontend (#39759)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:24:23 -07:00
Elizabeth Thompson
98eaaaa6d6 fix(mcp): clear stale thread-local DB session in sync tool wrapper (#39798)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:24:48 -07:00
Jay Masiwal
cb74438865 fix(viz): correct table chart drill-to-detail temporal boundaries and null handling (#39668)
Co-authored-by: Samuelinto <samuel.mantilla@mail.utoronto.ca>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:46:18 -04:00
Danylo Korostil
e77fb5e3fc feat(i18n): updated Ukrainian translation (#39720) 2026-05-01 11:12:05 -04:00
dependabot[bot]
1ac113fd44 chore(deps): bump aws-actions/amazon-ecs-render-task-definition from 1.8.4 to 1.8.5 (#39809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 06:31:48 -07:00
dependabot[bot]
6bfdee98cd chore(deps-dev): bump @docusaurus/tsconfig from 3.10.0 to 3.10.1 in /docs (#39811)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:31:29 -04:00
dependabot[bot]
de45f3a928 chore(deps): bump aws-actions/amazon-ecs-deploy-task-definition from 2.6.1 to 2.6.2 (#39806)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:30:49 -04:00
dependabot[bot]
2ec53c0694 chore(deps): bump mapbox-gl from 3.22.0 to 3.23.0 in /superset-frontend (#39769)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:30:21 -04:00
Michael S. Molina
d23b0cad92 chore: Bump core packages to 0.1.0 RC3 (#39823) 2026-05-01 09:54:39 -03:00
Evan Rusackas
e585406fff chore(codeowners): notify @sfirke on translation changes (#39794)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-30 23:07:29 -04:00
37 changed files with 6241 additions and 7701 deletions

4
.github/CODEOWNERS vendored
View File

@@ -36,6 +36,10 @@
**/*.geojson @villebro @rusackas
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
# Notify translation maintainers of changes to translations
/superset/translations/ @sfirke
# Notify PMC members of changes to extension-related files
/docs/developer_portal/extensions/ @michael-s-molina @villebro @rusackas

View File

@@ -265,7 +265,7 @@ jobs:
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1
uses: aws-actions/amazon-ecs-render-task-definition@6853cfae8c3a7d978fbf68b5a55453395541dfbb # v1
with:
task-definition: .github/workflows/ecs-task-definition.json
container-name: superset-ci
@@ -300,7 +300,7 @@ jobs:
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
- name: Deploy Amazon ECS task definition
id: deploy-task
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2
uses: aws-actions/amazon-ecs-deploy-task-definition@a310a830f5c14e583e35d84e4e1ec7dd177c3c9c # v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service

View File

@@ -0,0 +1,143 @@
---
title: Handlebars Chart
hide_title: true
sidebar_position: 10
version: 1
---
## Handlebars Chart
The Handlebars chart lets you render query results using a custom [Handlebars](https://handlebarsjs.com/) template. This gives you full control over how your data is displayed — from simple tables to rich HTML layouts.
### Basic Usage
In the chart editor, write a Handlebars template in the **Template** field. Your query results are available as `data`, an array of row objects.
```handlebars
{{#each data}}
<p>{{this.name}}: {{this.value}}</p>
{{/each}}
```
### Built-in Helpers
Superset registers several custom helpers on top of the standard Handlebars built-ins.
#### `dateFormat`
Formats a date value using [Day.js](https://day.js.org/) format strings.
```handlebars
{{dateFormat my_date format="MMMM YYYY"}}
```
| Option | Default | Description |
|--------|---------|-------------|
| `format` | `YYYY-MM-DD` | A Day.js-compatible format string |
---
#### `stringify`
Converts an object to a JSON string, or any other value to its string representation.
```handlebars
{{stringify myObj}}
```
---
#### `formatNumber`
Formats a number using locale-aware formatting.
```handlebars
{{formatNumber myNumber "en-US"}}
```
| Option | Default | Description |
|--------|---------|-------------|
| `locale` | `en-US` | A BCP 47 language tag |
---
#### `parseJson`
Parses a JSON string into an object that can be used in your template.
```handlebars
{{parseJson myJsonString}}
```
---
#### `groupBy`
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by).
```handlebars
{{#groupBy data "department"}}
<h3>{{value}}</h3>
{{#each items}}
<p>{{this.name}}</p>
{{/each}}
{{/groupBy}}
```
---
### Helpers from just-handlebars-helpers
Superset also registers all helpers from the [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) library. These include a wide range of comparison, math, string, and conditional helpers. Commonly used ones include:
#### Comparison
| Helper | Description | Example |
|--------|-------------|---------|
| `eq` | Strict equality | `{{#if (eq status "active")}}` |
| `eqw` | Weak equality | `{{#if (eqw count "5")}}` |
| `neq` | Strict inequality | `{{#if (neq role "admin")}}` |
| `lt` | Less than | `{{#if (lt score 50)}}` |
| `lte` | Less than or equal | `{{#if (lte score 100)}}` |
| `gt` | Greater than | `{{#if (gt price 0)}}` |
| `gte` | Greater than or equal | `{{#if (gte age 18)}}` |
#### Logical
| Helper | Description | Example |
|--------|-------------|---------|
| `and` | Logical AND | `{{#if (and isActive isVerified)}}` |
| `or` | Logical OR | `{{#if (or isAdmin isMod)}}` |
| `not` | Logical NOT | `{{#if (not isDisabled)}}` |
| `ifx` | Inline conditional | `{{ifx isActive "Yes" "No"}}` |
| `coalesce` | Returns first non-falsy value | `{{coalesce nickname name "Anonymous"}}` |
#### String
| Helper | Description | Example |
|--------|-------------|---------|
| `capitalize` | Capitalizes first letter | `{{capitalize name}}` |
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
| `truncate` | Truncates a string | `{{truncate description 100}}` |
| `contains` | Checks if string contains substring | `{{#if (contains tag "urgent")}}` |
#### Math
| Helper | Description | Example |
|--------|-------------|---------|
| `add` | Addition | `{{add a b}}` |
| `subtract` | Subtraction | `{{subtract total discount}}` |
| `multiply` | Multiplication | `{{multiply price quantity}}` |
| `divide` | Division | `{{divide total count}}` |
| `ceil` | Ceiling | `{{ceil value}}` |
| `floor` | Floor | `{{floor value}}` |
| `round` | Round | `{{round value}}` |
For the full list of available helpers, see the [just-handlebars-helpers documentation](https://github.com/leapfrogtechnology/just-handlebars-helpers).
### Tips
- Use raw blocks to escape Handlebars syntax if you need to display double curly braces literally.
- Comparison helpers like `eq` must be wrapped in a subexpression when used with `#if`: `{{#if (eq myVal "foo")}}`.
- HTML output is sanitized by default based on your Superset configuration (`HTML_SANITIZATION`).

View File

@@ -41,12 +41,12 @@
},
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@docusaurus/core": "^3.10.0",
"@docusaurus/faster": "^3.10.0",
"@docusaurus/plugin-client-redirects": "^3.10.0",
"@docusaurus/preset-classic": "3.10.0",
"@docusaurus/theme-live-codeblock": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
"@docusaurus/core": "^3.10.1",
"@docusaurus/faster": "^3.10.1",
"@docusaurus/plugin-client-redirects": "^3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@docusaurus/theme-live-codeblock": "^3.10.1",
"@docusaurus/theme-mermaid": "^3.10.1",
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
@@ -92,8 +92,8 @@
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/module-type-aliases": "^3.10.1",
"@docusaurus/tsconfig": "^3.10.1",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
@@ -124,8 +124,7 @@
"resolutions": {
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"webpackbar": "^7.0.0"
"baseline-browser-mapping": "^2.9.19"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -1570,10 +1570,10 @@
"@docsearch/core" "4.6.2"
"@docsearch/css" "4.6.2"
"@docusaurus/babel@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.10.0.tgz#819819f107233dfcf50b59cd51158f23fb04878a"
integrity sha512-mqCJhCZNZUDg0zgDEaPTM4DnRsisa24HdqTy/qn/MQlbwhTb4WVaZg6ZyX6yIVKqTz8fS1hBMgM+98z+BeJJDg==
"@docusaurus/babel@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.10.1.tgz#2f714f682117658ba43d308e9b35b6a73a105227"
integrity sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg==
dependencies:
"@babel/core" "^7.25.9"
"@babel/generator" "^7.25.9"
@@ -1584,23 +1584,23 @@
"@babel/preset-typescript" "^7.25.9"
"@babel/runtime" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
babel-plugin-dynamic-import-node "^2.3.3"
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/bundler@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.10.0.tgz#878c4c46bfa3434671ea37a43da184238a6aae26"
integrity sha512-iONUGZGgp+lAkw/cJZH6irONcF4p8+278IsdRlq8lYhxGjkoNUs0w7F4gVXBYSNChq5KG5/JleTSsdJySShxow==
"@docusaurus/bundler@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.10.1.tgz#82fa5079f3787a67502e25f82d37d05ec5de0cc3"
integrity sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw==
dependencies:
"@babel/core" "^7.25.9"
"@docusaurus/babel" "3.10.0"
"@docusaurus/cssnano-preset" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/babel" "3.10.1"
"@docusaurus/cssnano-preset" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
babel-loader "^9.2.1"
clean-css "^5.3.3"
copy-webpack-plugin "^11.0.0"
@@ -1618,20 +1618,20 @@
tslib "^2.6.0"
url-loader "^4.1.1"
webpack "^5.95.0"
webpackbar "^6.0.1"
webpackbar "^7.0.0"
"@docusaurus/core@3.10.0", "@docusaurus/core@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.10.0.tgz#642e71a0209d62c3f5ef275ed9d74a881f40df39"
integrity sha512-mgLdQsO8xppnQZc3LPi+Mf+PkPeyxJeIx11AXAq/14fsaMefInQiMEZUUmrc7J+956G/f7MwE7tn8KZgi3iRcA==
"@docusaurus/core@3.10.1", "@docusaurus/core@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.10.1.tgz#3f8bdb97451b4df14f2a3b39ab0186366fbf8fbe"
integrity sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==
dependencies:
"@docusaurus/babel" "3.10.0"
"@docusaurus/bundler" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/babel" "3.10.1"
"@docusaurus/bundler" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
boxen "^6.2.1"
chalk "^4.1.2"
chokidar "^3.5.3"
@@ -1668,22 +1668,22 @@
webpack-dev-server "^5.2.2"
webpack-merge "^6.0.1"
"@docusaurus/cssnano-preset@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.0.tgz#be1b435c33df09d743473d3fadda67b4568dfae3"
integrity sha512-qzSshTO1DB3TYW+dPUal5KHM7XPc5YQfzF3Kdb2NDACJUyGbNcFtw3tGkCJlYwhNCRKbZcmwraKUS1i5dcHdGg==
"@docusaurus/cssnano-preset@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.1.tgz#4b6bafeca8bb9423364d2fd6683c28e2f85a4665"
integrity sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ==
dependencies:
cssnano-preset-advanced "^6.1.2"
postcss "^8.5.4"
postcss-sort-media-queries "^5.2.0"
tslib "^2.6.0"
"@docusaurus/faster@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.10.0.tgz#0758a93196f685537aa7700bde62faf926e6c817"
integrity sha512-GNPtVH14ISjHfSwnHu3KiFGf86ICmJSQDeSv/QaanpBgiZGOtgZaslnC5q8WiguxM1EVkwcGxPuD8BXF4eggKw==
"@docusaurus/faster@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.10.1.tgz#a63d89ae980c98e1eeab3ff15ee083f7c20ed353"
integrity sha512-XTZhE5C1gZ/DaYYMlSk02dwP5vhpQON5QHVz1s3892mSESAywgWanURpXEDAvt4GvGuq7s+XP8rTWHZvfaJmdQ==
dependencies:
"@docusaurus/types" "3.10.0"
"@docusaurus/types" "3.10.1"
"@rspack/core" "^1.7.10"
"@swc/core" "^1.7.39"
"@swc/html" "^1.13.5"
@@ -1694,22 +1694,22 @@
tslib "^2.6.0"
webpack "^5.95.0"
"@docusaurus/logger@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.10.0.tgz#2bacbd004dd78e3da926dbe8f6fa9a930856575d"
integrity sha512-9jrZzFuBH1LDRlZ7cznAhCLmAZ3HSDqgwdrSSZdGHq9SPUOQgXXu8mnxe2ZRB9NS1PCpMTIOVUqDtZPIhMafZg==
"@docusaurus/logger@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.10.1.tgz#34c964e32e18f120e30f80171a38cfefe72cfb4b"
integrity sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw==
dependencies:
chalk "^4.1.2"
tslib "^2.6.0"
"@docusaurus/mdx-loader@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.10.0.tgz#1d4b050d751389ecf38dee48bcb61e53df8ffb82"
integrity sha512-mQQV97080AH4PYNs087l202NMDqRopZA4mg5W76ZZyTFrmWhJ3mHg+8A+drJVENxw5/Q+wHMHLgsx+9z1nEs0A==
"@docusaurus/mdx-loader@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.10.1.tgz#050ae9bc614158a4ec07a628aa75fa9ae90d7e82"
integrity sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ==
dependencies:
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@mdx-js/mdx" "^3.0.0"
"@slorber/remark-comment" "^1.0.0"
escape-html "^1.0.3"
@@ -1732,12 +1732,12 @@
vfile "^6.0.1"
webpack "^5.88.1"
"@docusaurus/module-type-aliases@3.10.0", "@docusaurus/module-type-aliases@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.0.tgz#749928f104d563f11f046bf0c9ab6489a470c7c8"
integrity sha512-/1O0Zg8w3DFrYX/I6Fbss7OJrtZw1QoyjDhegiFNHVi9A9Y0gQ3jUAytVxF6ywpAWpLyLxch8nN8H/V3XfzdJQ==
"@docusaurus/module-type-aliases@3.10.1", "@docusaurus/module-type-aliases@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.1.tgz#22d39177c296786eb6e0d940699cd590cc93ca77"
integrity sha512-YoOZKUdGlp8xSYhuAkGdSo5Ydkbq4V4eK3sD8v0a2hloxCWdQbNBhkc+Ko9QyjpESc0BYcIGM5iHVAy5hdFV6w==
dependencies:
"@docusaurus/types" "3.10.0"
"@docusaurus/types" "3.10.1"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
@@ -1745,34 +1745,34 @@
react-helmet-async "npm:@slorber/react-helmet-async@1.3.0"
react-loadable "npm:@docusaurus/react-loadable@6.0.0"
"@docusaurus/plugin-client-redirects@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.0.tgz#4dd4619817fd69462d1e6d986580343aeb911111"
integrity sha512-P+VLoLoZTc74so8+IbsaPZ33/mkf2BWL1CYXQpPRkl0v1QVCN2CgfsZY/8QtbYjQnx2upXUnv45abDhNcSggNw==
"@docusaurus/plugin-client-redirects@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.1.tgz#e22ed20e5837b7c3a28258e3d1816c4239c82b36"
integrity sha512-LHgd+YDvkhfOHMAE6XtUng3DQNzVM765RqVRrMJgHtzAvfopQhY6ieprqjxDVBdv21cLma6I0jHr+YCZH8fL9A==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
eta "^2.2.0"
fs-extra "^11.1.1"
lodash "^4.17.21"
tslib "^2.6.0"
"@docusaurus/plugin-content-blog@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.0.tgz#10095291b637440847854ecb2c8afcd8746debd7"
integrity sha512-RuTz68DhB7CL96QO5UsFbciD7GPYq6QV+YMfF9V0+N4ZgLhJIBgpVAr8GobrKF6NRe5cyWWETU5z5T834piG9g==
"@docusaurus/plugin-content-blog@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.1.tgz#0bd8de700ccbd8e95d920df2613304ef59abe72b"
integrity sha512-mmkgE6Q2+K74tnkou7tXlpDLvoCU/qkSa2GSQ3XUiHWvcebCoDQzS670RR3tO8PmaWlIyWWISYWzZLuMfxunRA==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
cheerio "1.0.0-rc.12"
combine-promises "^1.1.0"
feed "^4.2.2"
@@ -1785,20 +1785,20 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@docusaurus/plugin-content-docs@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.0.tgz#9c4ea1d5a405340f28c281d2e4586c695a7c65a5"
integrity sha512-9BjHhf15ct8Z7TThTC0xRndKDVvMKmVsAGAN7W9FpNRzfMdScOGcXtLmcCWtJGvAezjOJIm6CxOYCy3Io5+RnQ==
"@docusaurus/plugin-content-docs@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz#261e0e982e4a937c05b462e3c5729374f433b752"
integrity sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@types/react-router-config" "^5.0.7"
combine-promises "^1.1.0"
fs-extra "^11.1.1"
@@ -1809,142 +1809,142 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@docusaurus/plugin-content-pages@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.0.tgz#7670cbb3c849f434949f542bfdfded1580a13165"
integrity sha512-5amX8kEJI+nIGtuLVjYk59Y5utEJ3CHETFOPEE4cooIRLA4xM4iBsA6zFgu4ljcopeYwvBzFEWf5g2I6Yb9SkA==
"@docusaurus/plugin-content-pages@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.1.tgz#8c6ffc2079ed0262548ecc4df1dea6add6aa9673"
integrity sha512-huJpaRPMl42nsFwuCXvV8bVDj2MazuwRJIUylI/RSlmZeJssVoZXeCjVf1y+1Drtpa9SKcdGn8yoJ76IRJijtw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
fs-extra "^11.1.1"
tslib "^2.6.0"
webpack "^5.88.1"
"@docusaurus/plugin-css-cascade-layers@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.0.tgz#71e318d842be95f92be6c3dca00ceea4971d0edb"
integrity sha512-6q1vtt5FJcg5osgkHeM1euErECNqEZ5Z1j69yiNx2luEBIso+nxCkS9nqj8w+MK5X7rvKEToGhFfOFWncs51pQ==
"@docusaurus/plugin-css-cascade-layers@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.1.tgz#440578d95cbe1a6120936fa83df868d2626cd1d8"
integrity sha512-r//fn+MNHkE1wCof8T29VAQezt1enGCpsFxoziBbvLgBM4JfXN2P3rxrBaavHmvLvm7lYkpJeitcDthwnmWCTw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
tslib "^2.6.0"
"@docusaurus/plugin-debug@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.10.0.tgz#e77f924604e1e09d5d90fe0bdf23a3be8ea3307e"
integrity sha512-XcljKN+G+nmmK69uQA1d9BlYU3ZftG3T3zpK8/7Hf/wrOlV7TA4Ampdrdwkg0jElKdKAoSnPhCO0/U3bQGsVQQ==
"@docusaurus/plugin-debug@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.10.1.tgz#b8b7b24d9a7d185fd8a56a030f90145d3bfd8239"
integrity sha512-9KqOpKNfAyqGZykRb9LhIT/vyRF6sm/ykhjj/39JvaJahDS+jZJE0Z1Wfz9q3DUNDTMNN0Q7u/kk4rKKU+IJuA==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
fs-extra "^11.1.1"
react-json-view-lite "^2.3.0"
tslib "^2.6.0"
"@docusaurus/plugin-google-analytics@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.0.tgz#22c7e976fe4d970c7cd1c73c9723d9a5786c6e37"
integrity sha512-hTEoodatpBZnUat5nFExbuTGA1lhWGy7vZGuTew5Q3QDtGKFpSJLYmZJhdTjvCFwv1+qQ67hgAVlKdJOB8TXow==
"@docusaurus/plugin-google-analytics@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.1.tgz#ac15afc77386e0352edb8a1698d993aa5de36ffc"
integrity sha512-8o0P1KtmgdYQHH+oInitPpRWI0Of5XednAX4+DMhQNSmGSRNrsEEHg1ebv35m9AgRClfAytCJ5jA9KvcASTyuA==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
tslib "^2.6.0"
"@docusaurus/plugin-google-gtag@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.0.tgz#c38a2ba638257851cc845b934506b80c08d47f96"
integrity sha512-iB/Zzjv/eelJRbdULZqzWCbgMgJ7ht4ONVjXtN3+BI/muil6S87gQ1OJyPwlXD+ELdKkitC7bWv5eJdYOZLhrQ==
"@docusaurus/plugin-google-gtag@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.1.tgz#0482b83b9bc411aa99a432be2b39d2e53a00e2e0"
integrity sha512-pu3xIUo5o/zCMLfUY9BO5KOwSH0zIsAGyFRPvXHayFSA5XIhCU/SFuB0g0ZNjFn9niZLCaNvoeAuOGFJZq0fdw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@types/gtag.js" "^0.0.20"
tslib "^2.6.0"
"@docusaurus/plugin-google-tag-manager@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.0.tgz#5469c923cc1ad4608399d0b17e5fcacd8e030d56"
integrity sha512-FEjZxqKgLHa+Wez/EgKxRwvArNCWIScfyEQD95rot7jkxp6nonjI5XIbGfO/iYhM5Qinwe8aIEQHP2KZtpqVuA==
"@docusaurus/plugin-google-tag-manager@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.1.tgz#eaf5765d6f82b4fb661d92a793d1883f9d1ec106"
integrity sha512-f6fyGHiCm7kJHBtAisGQS5oNBnpnMTYQZxDXeVrnw/3zWU+LMA22pr6UHGYkBKDbN+qPC5QHG3NuOfzQLq3+Lw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
tslib "^2.6.0"
"@docusaurus/plugin-sitemap@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.0.tgz#35d59d46803f279f22aa64fc1bd18c048f12662b"
integrity sha512-DVTSLjB97hIjmayGnGcBfognCeI7ZuUKgEnU7Oz81JYqXtVg94mVTthDjq3QHTylYNeCUbkaW8VF0FDLcc8pPw==
"@docusaurus/plugin-sitemap@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.1.tgz#66a6974bb2fd1b9d8f5cb0f3c5ecd2201c118565"
integrity sha512-C26MbmmqgdjkDq1htaZ3aD7LzEDKFWXfpyQpt0EOUThuq5nV77zDaedV20yHcVo9p+3ey9aZ4pbHA0D3QcZTzg==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
fs-extra "^11.1.1"
sitemap "^7.1.1"
tslib "^2.6.0"
"@docusaurus/plugin-svgr@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.0.tgz#8ada2e6dd8318d20206a9b044fc091a5794ba3f0"
integrity sha512-lNljBESaETZqVBMPqkrGchr+UPT1eZzEPLmJhz8I76BxbjqgsUnRvrq6lQJ9sYjgmgX52KB7kkgczqd2yzoswQ==
"@docusaurus/plugin-svgr@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.1.tgz#c217c24d6d23fd2bc6f54d44c040635b49d6b36e"
integrity sha512-6SFxsmjWFkVLDmBUvFK6i72QjUwqyQFe4Ovz+SUJophJjOyVG3ZZG5IQpBC/kX/Gfv1yWeU9nWauH6F6Q7QX/Q==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@svgr/core" "8.1.0"
"@svgr/webpack" "^8.1.0"
tslib "^2.6.0"
webpack "^5.88.1"
"@docusaurus/preset-classic@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.10.0.tgz#74b6facdaf568bcd41ec90cae9aebb7ca0ac8619"
integrity sha512-kw/Ye02Hc6xP1OdTswy8yxQEHg0fdPpyWAQRxr5b2x3h7LlG2Zgbb5BDFROnXDDMpUxB7YejlocJIE5HIEfpNA==
"@docusaurus/preset-classic@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.10.1.tgz#faf330d96aedc9083a59bec09d966ae4dfc8b2fb"
integrity sha512-YO/FL8v1zmbxoTso6mjMz/RDjhaTJxb1UpFFTDdY5847LLDCeyYiYlrhyTbgN1RIN3xnkLKZ9Lj1x8hUzI4JOg==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/plugin-content-blog" "3.10.0"
"@docusaurus/plugin-content-docs" "3.10.0"
"@docusaurus/plugin-content-pages" "3.10.0"
"@docusaurus/plugin-css-cascade-layers" "3.10.0"
"@docusaurus/plugin-debug" "3.10.0"
"@docusaurus/plugin-google-analytics" "3.10.0"
"@docusaurus/plugin-google-gtag" "3.10.0"
"@docusaurus/plugin-google-tag-manager" "3.10.0"
"@docusaurus/plugin-sitemap" "3.10.0"
"@docusaurus/plugin-svgr" "3.10.0"
"@docusaurus/theme-classic" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-search-algolia" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/plugin-content-blog" "3.10.1"
"@docusaurus/plugin-content-docs" "3.10.1"
"@docusaurus/plugin-content-pages" "3.10.1"
"@docusaurus/plugin-css-cascade-layers" "3.10.1"
"@docusaurus/plugin-debug" "3.10.1"
"@docusaurus/plugin-google-analytics" "3.10.1"
"@docusaurus/plugin-google-gtag" "3.10.1"
"@docusaurus/plugin-google-tag-manager" "3.10.1"
"@docusaurus/plugin-sitemap" "3.10.1"
"@docusaurus/plugin-svgr" "3.10.1"
"@docusaurus/theme-classic" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-search-algolia" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/theme-classic@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.10.0.tgz#d937915c691189f27ced649c822994d839ea565b"
integrity sha512-9msCAsRdN+UG+RwPwCFb0uKy4tGoPh5YfBozXeGUtIeAgsMdn6f3G/oY861luZ3t8S2ET8S9Y/1GnpJAGWytww==
"@docusaurus/theme-classic@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.10.1.tgz#deed8cf73cc0f56113e53775cbb3b168c3c61566"
integrity sha512-VU1RK0qb2pab0si4r7HFK37cYco8VzqLj3u1PspVipSr/z/GPVKHO4/HXbnePqHoWDk8urjyGSeatH0NIMBM1A==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/plugin-content-blog" "3.10.0"
"@docusaurus/plugin-content-docs" "3.10.0"
"@docusaurus/plugin-content-pages" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-translations" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/plugin-content-blog" "3.10.1"
"@docusaurus/plugin-content-docs" "3.10.1"
"@docusaurus/plugin-content-pages" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-translations" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@mdx-js/react" "^3.0.0"
clsx "^2.0.0"
copy-text-to-clipboard "^3.2.0"
@@ -1959,15 +1959,15 @@
tslib "^2.6.0"
utility-types "^3.10.0"
"@docusaurus/theme-common@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.10.0.tgz#70b419ccfdf62f092299354a72d1692e81be597d"
integrity sha512-Dkp1YXKn16ByCJAdIjbDIOpVb4Z66MsVD694/ilX1vAAHaVEMrVsf/NPd9VgreyFx08rJ9GqV1MtzsbTcU73Kg==
"@docusaurus/theme-common@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.10.1.tgz#cbfec82b1b107be5c229811ed9caae14a501361c"
integrity sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==
dependencies:
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
@@ -1977,48 +1977,48 @@
tslib "^2.6.0"
utility-types "^3.10.0"
"@docusaurus/theme-live-codeblock@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.10.0.tgz#05a38c6bfac479fd698f18f27ca06ebb126633d9"
integrity sha512-1Ycxu0dBAhEXzXPQ1dQW01aY1MNi7TCTUOBtIF0GcNrQBFj74XxhDqv/T6GxYBsaN+6QnIDs1T+D43iV2/r2hQ==
"@docusaurus/theme-live-codeblock@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.10.1.tgz#29e6ddee467d816205ad611fd7bf10f00db5bdef"
integrity sha512-MKG/0zreelS6YlupQAoKmS5nCw9RRKwDHihJg2FinsU1+rqbrOYNYVq//eQy+m649k9b8XCazEw9VUMTFhpCTg==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-translations" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-translations" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@philpl/buble" "^0.19.7"
clsx "^2.0.0"
fs-extra "^11.1.1"
react-live "^4.1.6"
tslib "^2.6.0"
"@docusaurus/theme-mermaid@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.10.0.tgz#6581ccf16d27e4c02fe8c7cf15488862f27be9c8"
integrity sha512-Y2xrlwhIJ80oOZIO3PXL6A7J869splfcMI87E3NKpYsy3zJxOyV+BP1QMtGi59ajKgU868HPuyyn6J+6BZGOBg==
"@docusaurus/theme-mermaid@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.10.1.tgz#dada9c50c780524d246906234ace8a35446f26fc"
integrity sha512-2gxpmln8Pc4EN1oWzshQEx2HTs67jk14v7MmgqGs8ZU7Nm8oihg+fTouof2u4vN8DtB3Fln4cDJu4UprSX1S3Q==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
mermaid ">=11.6.0"
tslib "^2.6.0"
"@docusaurus/theme-search-algolia@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.0.tgz#0ff57fe58db6abde8f5ad2877e459cd2fa6e7464"
integrity sha512-f5FPKI08e3JRG63vR/o4qeuUVHUHzFzM0nnF+AkB67soAZgNsKJRf2qmUZvlQkGwlV+QFkKe4D0ANMh1jToU3g==
"@docusaurus/theme-search-algolia@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.1.tgz#6f422058711629ce8d7c2f17e1e54efa075c626e"
integrity sha512-OTaARARVZj2GvkJQjB+1jOIxntRaXea+G+fMsNqrZBAU1O1vJKDW22R7kECOHW27oJCLFN9HKaZeRrfAUyviug==
dependencies:
"@algolia/autocomplete-core" "^1.19.2"
"@docsearch/react" "^3.9.0 || ^4.3.2"
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/plugin-content-docs" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-translations" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/plugin-content-docs" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-translations" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
algoliasearch "^5.37.0"
algoliasearch-helper "^3.26.0"
clsx "^2.0.0"
@@ -2028,23 +2028,23 @@
tslib "^2.6.0"
utility-types "^3.10.0"
"@docusaurus/theme-translations@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.10.0.tgz#8fdc23d29bd7f907db49c36cf65e2123d96be300"
integrity sha512-L9IbFLwTc5+XdgH45iQYufLn0SVZd6BUNelDbKIFlH+E4hhjuj/XHWAFMX/w2K59rfy8wak9McOaei7BSUfRPA==
"@docusaurus/theme-translations@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.10.1.tgz#c3119a015652290eea560ca45ac775963d6eb75b"
integrity sha512-cLMyaKivjBVWKMJuWqyFVVgtqe8DPJNPkog0bn8W1MDVAKcPdxRFycBfC1We1RaNp7Rdk513bmtW78RR6OBxBw==
dependencies:
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/tsconfig@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.10.0.tgz#f40a57248828f0503a5f355cf30aa59941c9baaa"
integrity sha512-TXdC3WXuPrdQAexLvjUJfnYf3YKEgEqAs5nK0Q88pRBCW7t7oN4ILvWYb3A5Z1wlSXyXGWW/mCUmLEhdWsjnDQ==
"@docusaurus/tsconfig@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.10.1.tgz#1db31b4a4a5c914bdffa80070a35b6365d34f2e8"
integrity sha512-rYvB7yqkdqWIpAbDzQljGfM4cDBkLTbhmagZBEcsyj6oPUsz47lmW2pYdN1j+7sGFgltbAmQH62xfbrij4Eh6Q==
"@docusaurus/types@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.10.0.tgz#a69232bba74b738fcf4671fd5f0f079366dd3d13"
integrity sha512-F0dOt3FOoO20rRaFK7whGFQZ3ggyrWEdQc/c8/UiRuzhtg4y1w9FspXH5zpCT07uMnJKBPGh+qNazbNlCQqvSw==
"@docusaurus/types@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.10.1.tgz#d42837938ae43ca2be0ca47e63e00476b5eb94be"
integrity sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw==
dependencies:
"@mdx-js/mdx" "^3.0.0"
"@types/history" "^4.7.11"
@@ -2057,36 +2057,36 @@
webpack "^5.95.0"
webpack-merge "^5.9.0"
"@docusaurus/utils-common@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.10.0.tgz#2a6dc76b312664fca7234d33607c085318ff1ae3"
integrity sha512-JyL7sb9QVDgYvudIS81Dv0lsWm7le0vGZSDwsztxWam1SPBqrnkvBy9UYL/amh6pbybkyYTd3CMTkO24oMlCSw==
"@docusaurus/utils-common@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.10.1.tgz#6350b4898691e765de750f90eade0e0fa7902d99"
integrity sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA==
dependencies:
"@docusaurus/types" "3.10.0"
"@docusaurus/types" "3.10.1"
tslib "^2.6.0"
"@docusaurus/utils-validation@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.10.0.tgz#a2418d7f31980d991fd3a1f39c8aad8820b36812"
integrity sha512-c+6n2+ZPOJtWWc8Bb/EYdpSDfjYEScdCu9fB/SNjOmSCf1IdVnGf2T53o0tsz0gDRtCL90tifTL0JE/oMuP1Mw==
"@docusaurus/utils-validation@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz#ddbcce997a5506424cdd16abf6845cc51692acae"
integrity sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==
dependencies:
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
fs-extra "^11.2.0"
joi "^17.9.2"
js-yaml "^4.1.0"
lodash "^4.17.21"
tslib "^2.6.0"
"@docusaurus/utils@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.10.0.tgz#ea7d7b0d325b60f728decc00bb3908d00ef86faf"
integrity sha512-T3B0WTigsIthe0D4LQa2k+7bJY+c3WS+Wq2JhcznOSpn1lSN64yNtHQXboCj3QnUs1EuAZszQG1SHKu5w5ZrlA==
"@docusaurus/utils@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.10.1.tgz#535968caa2c9bff69f997a081b98b95b3c5d3785"
integrity sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==
dependencies:
"@docusaurus/logger" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
escape-string-regexp "^4.0.0"
execa "^5.1.1"
file-loader "^6.2.0"
@@ -13328,7 +13328,7 @@ renderkid@^3.0.0:
repeat-string@^1.5.2:
version "1.6.1"
resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
require-directory@^2.1.1:
@@ -15368,7 +15368,7 @@ webpack@^5.106.2, webpack@^5.88.1, webpack@^5.95.0:
watchpack "^2.5.1"
webpack-sources "^3.3.4"
webpackbar@^6.0.1, webpackbar@^7.0.0:
webpackbar@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-7.0.0.tgz#7228d32881af2392381b6514499ddea73cdf218a"
integrity sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==

View File

@@ -18,7 +18,7 @@
[project]
name = "apache-superset-core"
version = "0.1.0rc2"
version = "0.1.0rc3"
description = "Core Python package for building Apache Superset backend extensions and integrations"
readme = "README.md"
authors = [

View File

@@ -17,7 +17,7 @@
[project]
name = "apache-superset-extensions-cli"
version = "0.1.0rc2"
version = "0.1.0rc3"
description = "Official command-line interface for building, bundling, and managing Apache Superset extensions"
readme = "README.md"
authors = [

View File

@@ -102,7 +102,7 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.22.0",
"mapbox-gl": "^3.23.0",
"markdown-to-jsx": "^9.7.16",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
@@ -163,7 +163,7 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.23.7",
@@ -225,7 +225,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.21",
"baseline-browser-mapping": "^2.10.24",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",
@@ -578,9 +578,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1062,6 +1062,23 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
"integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
@@ -2388,19 +2405,20 @@
}
},
"node_modules/@babel/preset-env": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz",
"integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.3.tgz",
"integrity": "sha512-ySZypNLAIH1ClygLDQzVMoGQRViATnkHkYYV6TcNDz+8+jwZCdsguGvsb3EY5d9wyWyhmF1iSuFM0Yh5XPnqSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.29.0",
"@babel/compat-data": "^7.29.3",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
@@ -18689,9 +18707,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.21",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
"integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==",
"version": "2.10.24",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz",
"integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -35939,9 +35957,9 @@
"license": "MIT"
},
"node_modules/mapbox-gl": {
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.22.0.tgz",
"integrity": "sha512-ZIpF+oAMcQoDlvABmiRkHoydyBR9zI6CyDeVRa2/iyua0/B2+rPuIzoCV/CgN14P5F0HVk53GIZw220WSqJPyA==",
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.23.0.tgz",
"integrity": "sha512-zzjNAaMNvXnAVEUrYpOWmRVEBCIWgDAMLRPvSOoKY3smKvrINFVrRK/1jEpUDbEa7Ppf5Q/nwC6E07tz/i7IKw==",
"license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [
"src/style-spec",
@@ -51021,7 +51039,7 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.1.2",
"yeoman-generator": "^8.2.2",
"yosay": "^3.0.0"
},
"devDependencies": {
@@ -51395,12 +51413,12 @@
},
"packages/superset-core": {
"name": "@apache-superset/core",
"version": "0.1.0-rc2",
"version": "0.1.0-rc3",
"license": "Apache-2.0",
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@emotion/styled": "^11.14.1",
@@ -52666,25 +52684,6 @@
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
"license": "ISC"
},
"plugins/legacy-plugin-chart-map-box": {
"name": "@superset-ui/legacy-plugin-chart-map-box",
"version": "0.20.3",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"prop-types": "^15.8.1",
"react-map-gl": "^6.1.19",
"supercluster": "^8.0.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"mapbox-gl": "*",
"react": "^17.0.2"
}
},
"plugins/legacy-plugin-chart-paired-t-test": {
"name": "@superset-ui/legacy-plugin-chart-paired-t-test",
"version": "0.20.3",
@@ -53062,7 +53061,7 @@
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.22.0",
"mapbox-gl": "^3.23.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.0",
"supercluster": "^8.0.1"
@@ -53473,103 +53472,6 @@
"version": "1.0.0",
"extraneous": true,
"license": "Apache-2.0"
},
"node_modules/mem-fs-editor/node_modules/array-differ": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz",
"integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mem-fs-editor/node_modules/array-union": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz",
"integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mem-fs-editor/node_modules/globby": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz",
"integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@sindresorhus/merge-streams": "^2.1.0",
"fast-glob": "^3.3.2",
"ignore": "^5.2.4",
"path-type": "^5.0.0",
"slash": "^5.1.0",
"unicorn-magic": "^0.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mem-fs-editor/node_modules/multimatch": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-7.0.0.tgz",
"integrity": "sha512-SYU3HBAdF4psHEL/+jXDKHO95/m5P2RvboHT2Y0WtTttvJLP4H/2WS9WlQPFvF6C8d6SpLw8vjCnQOnVIVOSJQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"array-differ": "^4.0.0",
"array-union": "^3.0.1",
"minimatch": "^9.0.3"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mem-fs-editor/node_modules/path-type": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
"integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mem-fs-editor/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}

View File

@@ -183,7 +183,7 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.22.0",
"mapbox-gl": "^3.23.0",
"markdown-to-jsx": "^9.7.16",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
@@ -244,7 +244,7 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.23.7",
@@ -306,7 +306,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.21",
"baseline-browser-mapping": "^2.10.24",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",

View File

@@ -37,7 +37,7 @@
"cross-env": "^10.1.0",
"fs-extra": "^11.3.4",
"jest": "^30.3.0",
"yeoman-test": "^11.3.1"
"yeoman-test": "^11.4.2"
},
"engines": {
"npm": ">= 4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@apache-superset/core",
"version": "0.1.0-rc2",
"version": "0.1.0-rc3",
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
"sideEffects": false,
"main": "lib/index.js",
@@ -75,7 +75,7 @@
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"typescript": "^5.0.0",

View File

@@ -494,6 +494,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'column_config',
@@ -587,18 +593,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'show_cell_bars',
config: {
type: 'CheckboxControl',
label: t('Show cell bars'),
label: t('Show cell bars for all columns'),
renderTrigger: true,
default: true,
description: t(
@@ -612,7 +612,7 @@ const config: ControlPanelConfig = {
name: 'align_pn',
config: {
type: 'CheckboxControl',
label: t('Align +/-'),
label: t('Align +/- for all columns'),
renderTrigger: true,
default: false,
description: t(
@@ -626,7 +626,7 @@ const config: ControlPanelConfig = {
name: 'color_pn',
config: {
type: 'CheckboxControl',
label: t('Add colors to cell bars for +/-'),
label: t('Add colors to cell bars for +/- for all columns'),
renderTrigger: true,
default: true,
description: t(

View File

@@ -1,95 +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.
-->
## @superset-ui/plugin-chart-handlebars
[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-handlebars.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-handlebars)
[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-handlebars?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-handlebars)
This plugin renders the data using a handlebars template.
### Usage
Configure `key`, which can be any `string`, and register the plugin. This `key` will be used to
lookup this chart throughout the app.
```js
import HandlebarsChartPlugin from '@superset-ui/plugin-chart-handlebars';
new HandlebarsChartPlugin().configure({ key: 'handlebars' }).register();
```
Then use it via `SuperChart`. See
[storybook](https://apache-superset.github.io/superset-ui/?selectedKind=plugin-chart-handlebars) for
more details.
```js
<SuperChart
chartType="handlebars"
width={600}
height={600}
formData={...}
queriesData={[{
data: {...},
}]}
/>
```
### File structure generated
```
├── package.json
├── README.md
├── tsconfig.json
├── src
│   ├── Handlebars.tsx
│   ├── images
│   │   └── thumbnail.png
│   ├── index.ts
│   ├── plugin
│   │   ├── buildQuery.ts
│   │   ├── controlPanel.ts
│   │   ├── index.ts
│   │   └── transformProps.ts
│   └── types.ts
├── test
│   └── index.test.ts
└── types
└── external.d.ts
```
### Available Handlebars Helpers in Superset
Below, you will find a list of all currently registered helpers in the Handlebars plugin for Superset. These helpers are registered and managed in the file [`HandlebarsViewer.tsx`](./path/to/HandlebarsViewer.tsx).
#### List of Registered Helpers:
1. **`dateFormat`**: Formats a date using a specified format.
- **Usage**: `{{dateFormat my_date format="MMMM YYYY"}}`
- **Default format**: `YYYY-MM-DD`.
2. **`stringify`**: Converts an object into a JSON string or returns a string representation of non-object values.
- **Usage**: `{{stringify myObj}}`.
3. **`formatNumber`**: Formats a number using locale-specific formatting.
- **Usage**: `{{formatNumber number locale="en-US"}}`.
- **Default locale**: `en-US`.
4. **`parseJson`**: Parses a JSON string into a JavaScript object.
- **Usage**: `{{parseJson jsonString}}`.

View File

@@ -1,4 +1,4 @@
/**
/**
* 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
@@ -24,7 +24,7 @@ import {
import { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/theme';
import { InfoTooltip, SafeMarkdown } from '@superset-ui/core/components';
import { InfoTooltip } from '@superset-ui/core/components';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
import { debounceFunc } from '../../consts';
@@ -37,36 +37,10 @@ const HandlebarsTemplateControl = (
props: CustomControlConfig<HandlebarsCustomControlProps>,
) => {
const theme = useTheme();
const val = String(
props?.value ? props?.value : props?.default ? props?.default : '',
);
const helperDescriptionsHeader = t(
'Available Handlebars Helpers in Superset:',
);
const helperDescriptions = [
{ key: 'dateFormat', descKey: 'Formats a date using a specified format.' },
{ key: 'stringify', descKey: 'Converts an object to a JSON string.' },
{
key: 'formatNumber',
descKey: 'Formats a number using locale-specific formatting.',
},
{
key: 'parseJson',
descKey: 'Parses a JSON string into a JavaScript object.',
},
];
const helpersTooltipContent = `
${helperDescriptionsHeader}
${helperDescriptions
.map(({ key, descKey }) => `- **${key}**: ${t(descKey)}`)
.join('\n')}
`;
return (
<div>
<ControlHeader>
@@ -74,7 +48,7 @@ ${helperDescriptions
{props.label}
<InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={<SafeMarkdown source={helpersTooltipContent} />}
tooltip={<span>{t('See ')} <a href="https://superset.apache.org/docs/using-superset/handlebars-chart" target="_blank" rel="noopener noreferrer">{t('the Handlebars chart documentation')}</a> {t('for a list of available helpers.')}</span>}
/>
</div>
</ControlHeader>
@@ -104,7 +78,6 @@ export const handlebarsTemplateControlSetItem: ControlSetItem = {
isInt: false,
renderTrigger: true,
valueKey: null,
validators: [validateNonEmpty],
mapStateToProps: ({ form_data }) => ({
value: form_data?.handlebarsTemplate ?? form_data?.handlebars_template,

View File

@@ -27,7 +27,7 @@
],
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.22.0",
"mapbox-gl": "^3.23.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.0",
"supercluster": "^8.0.1"

View File

@@ -50,6 +50,7 @@ import {
getTimeFormatterForGranularity,
BinaryQueryObjectFilterClause,
extractTextFromHTML,
TimeGranularity,
} from '@superset-ui/core';
import {
styled,
@@ -309,6 +310,67 @@ function SelectPageSize({
const getNoResultsMessage = (filter: string) =>
filter ? t('No matching records found') : t('No records found');
/**
* Calculates the inclusive/exclusive temporal range for a bucket.
* standard SQL range pattern: [start, end)
*/
function getTimeRangeFromGranularity(
startTime: Date,
granularity: TimeGranularity,
): [Date, Date] {
const time = startTime.getTime();
const date = startTime.getUTCDate();
const month = startTime.getUTCMonth();
const year = startTime.getUTCFullYear();
// Constants
const MS_IN_SECOND = 1000;
const MS_IN_MINUTE = 60 * MS_IN_SECOND;
const MS_IN_HOUR = 60 * MS_IN_MINUTE;
switch (granularity) {
case TimeGranularity.SECOND:
return [startTime, new Date(time + MS_IN_SECOND)];
case TimeGranularity.MINUTE:
return [startTime, new Date(time + MS_IN_MINUTE)];
case TimeGranularity.FIVE_MINUTES:
return [startTime, new Date(time + MS_IN_MINUTE * 5)];
case TimeGranularity.TEN_MINUTES:
return [startTime, new Date(time + MS_IN_MINUTE * 10)];
case TimeGranularity.FIFTEEN_MINUTES:
return [startTime, new Date(time + MS_IN_MINUTE * 15)];
case TimeGranularity.THIRTY_MINUTES:
return [startTime, new Date(time + MS_IN_MINUTE * 30)];
case TimeGranularity.HOUR:
return [startTime, new Date(time + MS_IN_HOUR)];
case TimeGranularity.DAY:
case TimeGranularity.DATE:
return [startTime, new Date(Date.UTC(year, month, date + 1))];
case TimeGranularity.WEEK:
case TimeGranularity.WEEK_STARTING_SUNDAY:
case TimeGranularity.WEEK_STARTING_MONDAY:
return [startTime, new Date(Date.UTC(year, month, date + 7))];
case TimeGranularity.WEEK_ENDING_SATURDAY:
case TimeGranularity.WEEK_ENDING_SUNDAY:
// Week-ending buckets are labeled by the bucket's final day.
return [
new Date(Date.UTC(year, month, date - 6)),
new Date(Date.UTC(year, month, date + 1)),
];
case TimeGranularity.MONTH:
return [startTime, new Date(Date.UTC(year, month + 1, 1))];
case TimeGranularity.QUARTER:
return [
startTime,
new Date(Date.UTC(year, Math.floor(month / 3) * 3 + 3, 1)),
];
case TimeGranularity.YEAR:
return [startTime, new Date(Date.UTC(year + 1, 0, 1))];
default:
return [startTime, new Date(Date.UTC(year, month, date + 1))];
}
}
export default function TableChart<D extends DataRecord = DataRecord>(
props: TableChartTransformedProps<D> & {
sticky?: DataTableProps<D>['sticky'];
@@ -471,7 +533,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// so that cross-filters work on the receiving chart
const resolvedCol = columnLabelToNameMap[col] ?? col;
const val = ensureIsArray(updatedFilters?.[col]);
if (!val.length)
if (!val.length || val[0] === null || (val[0] instanceof DateWithFormatter && val[0].input === null))
return {
col: resolvedCol,
op: 'IS NULL' as const,
@@ -578,15 +640,49 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
filteredColumnsMeta.forEach(col => {
if (!col.isMetric) {
let dataRecordValue = value[col.key];
dataRecordValue = extractTextFromHTML(dataRecordValue);
const dataRecordValue = value[col.key];
drillToDetailFilters.push({
col: col.key,
op: '==',
val: dataRecordValue as string | number | boolean,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
// FIX: Explicitly handle NULL values for temporal and non-temporal columns
// DateWithFormatter objects wrap nulls, so we must check both
if (
dataRecordValue == null ||
(dataRecordValue instanceof DateWithFormatter && dataRecordValue.input == null)
) {
drillToDetailFilters.push({
col: col.key,
op: 'IS NULL' as any,
val: null,
});
} else if (col.dataType === GenericDataType.Temporal && timeGrain) {
const startTime =
dataRecordValue instanceof Date
? dataRecordValue
: new Date(dataRecordValue as string | number);
const [rangeStartTime, rangeEndTime] = getTimeRangeFromGranularity(
startTime,
timeGrain,
);
const timeRangeValue = `${rangeStartTime.toISOString()} : ${rangeEndTime.toISOString()}`;
drillToDetailFilters.push({
col: col.key,
op: 'TEMPORAL_RANGE',
val: timeRangeValue,
grain: timeGrain,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
} else {
// Non-temporal columns use exact match
const sanitizedValue = extractTextFromHTML(dataRecordValue);
drillToDetailFilters.push({
col: col.key,
op: '==',
val: sanitizedValue as string | number | boolean,
formattedVal: formatColumnValue(col, sanitizedValue)[1],
});
}
}
});
onContextMenu(clientX, clientY, {
@@ -600,7 +696,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
filters: [
{
col: cellPoint.key,
op: '==',
op: (cellPoint.value == null || (cellPoint.value instanceof DateWithFormatter && cellPoint.value.input == null) ? 'IS NULL' : '==') as any,
val: extractTextFromHTML(cellPoint.value),
},
],
@@ -615,6 +711,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
isRawRecords,
filteredColumnsMeta,
getCrossFilterDataMask,
timeGrain,
]);
const getHeaderColumns = useCallback(

View File

@@ -552,6 +552,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'column_config',
@@ -648,18 +654,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'show_cell_bars',
config: {
type: 'CheckboxControl',
label: t('Show cell bars'),
label: t('Show cell bars for all columns'),
renderTrigger: true,
default: true,
description: t(
@@ -673,7 +673,7 @@ const config: ControlPanelConfig = {
name: 'align_pn',
config: {
type: 'CheckboxControl',
label: t('Align +/-'),
label: t('Align +/- for all columns'),
renderTrigger: true,
default: false,
description: t(
@@ -687,7 +687,7 @@ const config: ControlPanelConfig = {
name: 'color_pn',
config: {
type: 'CheckboxControl',
label: t('Add colors to cell bars for +/-'),
label: t('Add colors to cell bars for +/- for all columns'),
renderTrigger: true,
default: true,
description: t(

View File

@@ -130,13 +130,12 @@ const processComparisonTotals = (
Object.keys(totalRecord).forEach(key => {
if (totalRecord[key] !== undefined && !key.includes(comparisonSuffix)) {
transformedTotals[`Main ${key}`] =
parseInt(transformedTotals[`Main ${key}`]?.toString() || '0', 10) +
parseInt(totalRecord[key]?.toString() || '0', 10);
parseFloat(transformedTotals[`Main ${key}`]?.toString() || '0') +
parseFloat(totalRecord[key]?.toString() || '0');
transformedTotals[`# ${key}`] =
parseInt(transformedTotals[`# ${key}`]?.toString() || '0', 10) +
parseInt(
parseFloat(transformedTotals[`# ${key}`]?.toString() || '0') +
parseFloat(
totalRecord[`${key}__${comparisonSuffix}`]?.toString() || '0',
10,
);
const { valueDifference, percentDifferenceNum } = calculateDifferences(
transformedTotals[`Main ${key}`] as number,

View File

@@ -2360,3 +2360,76 @@ describe('plugin-chart-table', () => {
});
});
});
/**
* DRILL-TO-DETAIL FIX VERIFICATION (#23847)
*/
describe('Drill-to-Detail Temporal Range Logic', () => {
const renderChartAndOpenContextMenu = (
timeGrain?: TimeGranularity,
timestampValue?: string | number | null,
) => {
const onContextMenu = jest.fn();
const data = cloneDeep(testData.basic);
if (timestampValue !== undefined) {
data.queriesData[0].data[0].__timestamp = timestampValue;
}
const props = transformProps({
...data,
rawFormData: {
...data.rawFormData,
...(timeGrain ? { time_grain_sqla: timeGrain } : {}),
},
hooks: { onAddFilter: jest.fn(), onContextMenu, setDataMask: jest.fn() },
});
render(<TableChart {...props} sticky={false} />);
const tbody = screen.getAllByRole('rowgroup')[1];
fireEvent.contextMenu(tbody.querySelectorAll('td')[0]);
const [, , { drillToDetail }] = onContextMenu.mock.calls[0];
return drillToDetail.find((f: any) => f.col === '__timestamp');
};
test('uses TEMPORAL_RANGE for monthly grain', () => {
const filter = renderChartAndOpenContextMenu(TimeGranularity.MONTH);
expect(filter.op).toBe('TEMPORAL_RANGE');
expect(filter.val).toContain(
'2020-01-01T12:34:56.000Z : 2020-02-01T00:00:00.000Z',
);
});
test('uses the full bucket for week ending sunday grain', () => {
const filter = renderChartAndOpenContextMenu(
TimeGranularity.WEEK_ENDING_SUNDAY,
'2020-01-05T00:00:00',
);
expect(filter.op).toBe('TEMPORAL_RANGE');
expect(filter.val).toBe(
'2019-12-30T00:00:00.000Z : 2020-01-06T00:00:00.000Z',
);
});
test('uses the full bucket for week ending saturday grain', () => {
const filter = renderChartAndOpenContextMenu(
TimeGranularity.WEEK_ENDING_SATURDAY,
'2020-01-04T00:00:00',
);
expect(filter.op).toBe('TEMPORAL_RANGE');
expect(filter.val).toBe(
'2019-12-29T00:00:00.000Z : 2020-01-05T00:00:00.000Z',
);
});
test('correctly handles NULL values by emitting IS NULL instead of 1970 timestamp', () => {
const filter = renderChartAndOpenContextMenu(TimeGranularity.MONTH, null);
expect(filter.op).toBe('IS NULL');
expect(filter.val).toBeNull();
});
});

View File

@@ -17,31 +17,20 @@
* under the License.
*/
import { FC, memo, useCallback, useMemo } from 'react';
import { FC, memo, useMemo } from 'react';
import { t } from '@apache-superset/core/translation';
import { DataMaskStateWithId } from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { Loading } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
import { FilterBarOrientation, RootState } from 'src/dashboard/types';
import { RootState } from 'src/dashboard/types';
import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
import { useSelector } from 'react-redux';
import {
getRisonFilterParam,
parseRisonFilters,
updateUrlWithUnmatchedFilters,
} from 'src/dashboard/util/risonFilters';
import FilterControls from './FilterControls/FilterControls';
import { useChartsVerboseMaps, getFilterBarTestId } from './utils';
import { HorizontalBarProps } from './types';
import FilterBarSettings from './FilterBarSettings';
import crossFiltersSelector from './CrossFilters/selectors';
import {
getUrlFilterIndicators,
UrlFilterIndicator,
} from './UrlFilters/selectors';
import UrlFilterTag from './UrlFilters/UrlFilterTag';
const HorizontalBar = styled.div`
${({ theme }) => `
@@ -76,28 +65,6 @@ const FilterBarEmptyStateContainer = styled.div`
`}
`;
const UrlFiltersContainer = styled.div`
${({ theme }) => `
display: flex;
flex-direction: row;
align-items: center;
gap: ${theme.sizeUnit * 2}px;
padding: 0 ${theme.sizeUnit * 2}px;
margin-right: ${theme.sizeUnit * 2}px;
border-right: 1px solid ${theme.colorBorder};
`}
`;
const UrlFilterTitle = styled.div`
${({ theme }) => `
display: flex;
align-items: center;
gap: ${theme.sizeUnit}px;
font-weight: ${theme.fontWeightStrong};
font-size: ${theme.fontSizeSM}px;
`}
`;
const HorizontalFilterBar: FC<HorizontalBarProps> = ({
actions,
dataMaskSelected,
@@ -127,47 +94,9 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
[chartIds, chartLayoutItems, dataMask, verboseMaps],
);
const activeUrlFilters = useMemo(() => getUrlFilterIndicators(), []);
const handleRemoveUrlFilter = useCallback(
(filterToRemove: UrlFilterIndicator) => {
const risonParam = getRisonFilterParam();
if (!risonParam) return;
const currentFilters = parseRisonFilters(risonParam);
const remaining = currentFilters.filter(
f => f.subject !== filterToRemove.filter.subject,
);
updateUrlWithUnmatchedFilters(remaining);
},
[],
);
const urlFiltersComponent = useMemo(() => {
if (activeUrlFilters.length === 0) return null;
return (
<UrlFiltersContainer>
<UrlFilterTitle>
<Icons.LinkOutlined iconSize="s" />
{t('URL Filters')}
</UrlFilterTitle>
{activeUrlFilters.map(filter => (
<UrlFilterTag
key={filter.subject}
filter={filter}
orientation={FilterBarOrientation.Horizontal}
onRemove={handleRemoveUrlFilter}
/>
))}
</UrlFiltersContainer>
);
}, [activeUrlFilters, handleRemoveUrlFilter]);
const hasFilters =
filterValues.length > 0 ||
selectedCrossFilters.length > 0 ||
activeUrlFilters.length > 0 ||
chartCustomizationValues.length > 0;
return (
@@ -184,19 +113,16 @@ const HorizontalFilterBar: FC<HorizontalBarProps> = ({
</FilterBarEmptyStateContainer>
)}
{hasFilters && (
<>
{urlFiltersComponent}
<FilterControls
dataMaskSelected={dataMaskSelected}
onFilterSelectionChange={onSelectionChange}
onPendingCustomizationDataMaskChange={
onPendingCustomizationDataMaskChange
}
chartCustomizationValues={chartCustomizationValues}
clearAllTriggers={clearAllTriggers}
onClearAllComplete={onClearAllComplete}
/>
</>
<FilterControls
dataMaskSelected={dataMaskSelected}
onFilterSelectionChange={onSelectionChange}
onPendingCustomizationDataMaskChange={
onPendingCustomizationDataMaskChange
}
chartCustomizationValues={chartCustomizationValues}
clearAllTriggers={clearAllTriggers}
onClearAllComplete={onClearAllComplete}
/>
)}
{actions}
</>

View File

@@ -1,85 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useCSSTextTruncation } from '@superset-ui/core';
import { styled, css, useTheme } from '@apache-superset/core/theme';
import { Tag } from 'src/components/Tag';
import { Tooltip } from '@superset-ui/core/components';
import { FilterBarOrientation } from 'src/dashboard/types';
import { ellipsisCss } from '../CrossFilters/styles';
import { UrlFilterIndicator } from './selectors';
const StyledValue = styled.b`
${({ theme }) => `
max-width: ${theme.sizeUnit * 25}px;
`}
${ellipsisCss}
`;
const StyledColumn = styled('span')`
${({ theme }) => `
max-width: ${theme.sizeUnit * 25}px;
padding-right: ${theme.sizeUnit}px;
`}
${ellipsisCss}
`;
const StyledTag = styled(Tag)`
${({ theme }) => `
border: 1px solid ${theme.colorBorder};
border-radius: 2px;
.anticon-close {
vertical-align: middle;
}
`}
`;
const UrlFilterTag = (props: {
filter: UrlFilterIndicator;
orientation: FilterBarOrientation;
onRemove: (filter: UrlFilterIndicator) => void;
}) => {
const { filter, orientation, onRemove } = props;
const theme = useTheme();
const [columnRef, columnIsTruncated] =
useCSSTextTruncation<HTMLSpanElement>();
const [valueRef, valueIsTruncated] = useCSSTextTruncation<HTMLSpanElement>();
return (
<StyledTag
css={css`
${orientation === FilterBarOrientation.Vertical
? `margin-top: ${theme.sizeUnit * 2}px;`
: `margin-left: ${theme.sizeUnit * 2}px;`}
`}
closable
onClose={() => onRemove(filter)}
editable
>
<Tooltip title={columnIsTruncated ? filter.subject : null}>
<StyledColumn ref={columnRef}>{filter.subject}</StyledColumn>
</Tooltip>
<Tooltip title={valueIsTruncated ? filter.value : null}>
<StyledValue ref={valueRef}>{filter.value}</StyledValue>
</Tooltip>
</StyledTag>
);
};
export default UrlFilterTag;

View File

@@ -1,34 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo } from 'react';
import { getUrlFilterIndicators } from './selectors';
import UrlFiltersVerticalCollapse from './VerticalCollapse';
const UrlFiltersVertical = () => {
const urlFilters = useMemo(() => getUrlFilterIndicators(), []);
if (!urlFilters.length) {
return null;
}
return <UrlFiltersVerticalCollapse urlFilters={urlFilters} />;
};
export default UrlFiltersVertical;

View File

@@ -1,173 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo, useState, useCallback } from 'react';
import { t } from '@apache-superset/core/translation';
import { css, useTheme, SupersetTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import { FilterBarOrientation } from 'src/dashboard/types';
import {
updateUrlWithUnmatchedFilters,
getRisonFilterParam,
parseRisonFilters,
} from 'src/dashboard/util/risonFilters';
import UrlFilterTag from './UrlFilterTag';
import { UrlFilterIndicator } from './selectors';
const UrlFiltersVerticalCollapse = (props: {
urlFilters: UrlFilterIndicator[];
}) => {
const { urlFilters: initialFilters } = props;
const theme = useTheme();
const [isOpen, setIsOpen] = useState(true);
const [urlFilters, setUrlFilters] =
useState<UrlFilterIndicator[]>(initialFilters);
const toggleSection = useCallback(() => {
setIsOpen(prev => !prev);
}, []);
const handleRemoveFilter = useCallback(
(filterToRemove: UrlFilterIndicator) => {
const risonParam = getRisonFilterParam();
if (!risonParam) return;
const currentFilters = parseRisonFilters(risonParam);
const remaining = currentFilters.filter(
f => f.subject !== filterToRemove.filter.subject,
);
updateUrlWithUnmatchedFilters(remaining);
setUrlFilters(prev =>
prev.filter(f => f.subject !== filterToRemove.subject),
);
},
[],
);
const sectionContainerStyle = useCallback(
(theme: SupersetTheme) => css`
margin-bottom: ${theme.sizeUnit * 3}px;
padding: 0 ${theme.sizeUnit * 4}px;
`,
[],
);
const sectionHeaderStyle = useCallback(
(theme: SupersetTheme) => css`
display: flex;
align-items: center;
justify-content: space-between;
padding: ${theme.sizeUnit * 2}px 0;
cursor: pointer;
user-select: none;
&:hover {
background: ${theme.colorBgTextHover};
margin: 0 -${theme.sizeUnit * 2}px;
padding: ${theme.sizeUnit * 2}px;
border-radius: ${theme.borderRadius}px;
}
`,
[],
);
const sectionTitleStyle = useCallback(
(theme: SupersetTheme) => css`
margin: 0;
font-size: ${theme.fontSize}px;
font-weight: ${theme.fontWeightStrong};
color: ${theme.colorText};
line-height: 1.3;
display: flex;
align-items: center;
gap: ${theme.sizeUnit}px;
`,
[],
);
const sectionContentStyle = useCallback(
(theme: SupersetTheme) => css`
padding: ${theme.sizeUnit * 2}px 0;
`,
[],
);
const dividerStyle = useCallback(
(theme: SupersetTheme) => css`
height: 1px;
background: ${theme.colorSplit};
margin: ${theme.sizeUnit * 2}px 0;
`,
[],
);
const iconStyle = useCallback(
(open: boolean, theme: SupersetTheme) => css`
transform: ${open ? 'rotate(0deg)' : 'rotate(180deg)'};
transition: transform 0.2s ease;
color: ${theme.colorTextSecondary};
`,
[],
);
const filterIndicators = useMemo(
() =>
urlFilters.map(filter => (
<UrlFilterTag
key={filter.subject}
filter={filter}
orientation={FilterBarOrientation.Vertical}
onRemove={handleRemoveFilter}
/>
)),
[urlFilters, handleRemoveFilter],
);
if (!urlFilters.length) {
return null;
}
return (
<div css={sectionContainerStyle}>
<div
css={sectionHeaderStyle}
onClick={toggleSection}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSection();
}
}}
role="button"
tabIndex={0}
>
<h4 css={sectionTitleStyle}>
<Icons.LinkOutlined iconSize="s" />
{t('URL Filters')}
</h4>
<Icons.UpOutlined iconSize="m" css={iconStyle(isOpen, theme)} />
</div>
{isOpen && <div css={sectionContentStyle}>{filterIndicators}</div>}
{isOpen && <div css={dividerStyle} data-test="url-filters-divider" />}
</div>
);
};
export default UrlFiltersVerticalCollapse;

View File

@@ -1,60 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
getRisonFilterParam,
parseRisonFilters,
RisonFilter,
} from 'src/dashboard/util/risonFilters';
export interface UrlFilterIndicator {
subject: string;
operator: string;
value: string;
filter: RisonFilter;
}
function formatFilterValue(filter: RisonFilter): string {
const { comparator, operator } = filter;
if (operator === 'BETWEEN' && Array.isArray(comparator)) {
return `${comparator[0]} ${comparator[1]}`;
}
if (Array.isArray(comparator)) {
return comparator.join(', ');
}
return String(comparator);
}
export function getUrlFilterIndicators(): UrlFilterIndicator[] {
const risonParam = getRisonFilterParam();
if (!risonParam) {
return [];
}
const filters = parseRisonFilters(risonParam);
return filters.map(filter => ({
subject: filter.subject,
operator: filter.operator,
value: formatFilterValue(filter),
filter,
}));
}

View File

@@ -45,7 +45,6 @@ import Header from './Header';
import FilterControls from './FilterControls/FilterControls';
import CrossFiltersVertical from './CrossFilters/Vertical';
import crossFiltersSelector from './CrossFilters/selectors';
import UrlFiltersVertical from './UrlFilters/Vertical';
enum SectionType {
Filters = 'filters',
@@ -302,7 +301,6 @@ const VerticalFilterBar: FC<VerticalBarProps> = ({
) : (
<div css={tabPaneStyle} onScroll={onScroll}>
<>
<UrlFiltersVertical />
<CrossFiltersVertical hideHeader={hasOnlyOneSectionType} />
{filterControls}
</>

View File

@@ -107,16 +107,9 @@ const publishDataMask = debounce(
const previousParams = new URLSearchParams(search);
const newParams = new URLSearchParams();
let dataMaskKey: string | null;
let risonFilterValue: string | null = null;
previousParams.forEach((value, key) => {
if (!EXCLUDED_URL_PARAMS.includes(key)) {
if (key === 'f') {
// Preserve the original Rison filter value to avoid encoding
risonFilterValue = value;
} else {
newParams.append(key, value);
}
newParams.append(key, value);
}
});
@@ -155,16 +148,9 @@ const publishDataMask = debounce(
if (appRoot !== '/' && replacementPathname.startsWith(appRoot)) {
replacementPathname = replacementPathname.substring(appRoot.length);
}
// Manually reconstruct the search string to preserve Rison filter encoding
let searchString = newParams.toString();
if (risonFilterValue) {
const separator = searchString ? '&' : '';
searchString = `${searchString}${separator}f=${risonFilterValue}`;
}
history.replace({
pathname: replacementPathname,
search: searchString,
search: newParams.toString(),
});
}
},

View File

@@ -64,15 +64,6 @@ import SyncDashboardState, {
getDashboardContextLocalStorage,
} from '../components/SyncDashboardState';
import { AutoRefreshProvider } from '../contexts/AutoRefreshContext';
import { PartialFilters } from '@superset-ui/core';
import {
parseRisonFilters,
risonToAdhocFilters,
getRisonFilterParam,
prettifyRisonFilterUrl,
injectRisonFiltersIntelligently,
updateUrlWithUnmatchedFilters,
} from '../util/risonFilters';
export const DashboardPageIdContext = createContext('');
@@ -204,61 +195,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
dataMask = isOldRison;
}
// Parse Rison URL filters with intelligent native filter injection
const risonFilterParam = getRisonFilterParam();
if (risonFilterParam) {
const risonFilters = parseRisonFilters(risonFilterParam);
if (risonFilters.length > 0) {
// Convert native filter config array to keyed object for lookup
const filterConfigArray =
(dashboard?.metadata?.native_filter_configuration as Array<
Record<string, unknown> & { id: string }
>) || [];
const nativeFilters: PartialFilters = {};
filterConfigArray.forEach(filter => {
nativeFilters[filter.id] = filter as PartialFilters[string];
});
const injectionResult = injectRisonFiltersIntelligently(
risonFilters,
nativeFilters,
dataMask,
);
dataMask = injectionResult.updatedDataMask;
// For unmatched filters, fall back to adhoc filter approach
if (injectionResult.unmatchedFilters.length > 0) {
const unmatchedAdhocFilters = risonToAdhocFilters(
injectionResult.unmatchedFilters,
);
const risonDataMask = {
__rison_filters__: {
filterState: { value: unmatchedAdhocFilters },
ownState: {},
},
};
dataMask = { ...dataMask, ...risonDataMask };
}
// Clean up URL: remove matched filters, keep only unmatched ones
const matchedCount =
risonFilters.length - injectionResult.unmatchedFilters.length;
if (matchedCount > 0) {
setTimeout(
() =>
updateUrlWithUnmatchedFilters(injectionResult.unmatchedFilters),
100,
);
}
if (injectionResult.unmatchedFilters.length > 0) {
setTimeout(() => prettifyRisonFilterUrl(), 150);
}
}
}
if (readyToRender) {
if (!isDashboardHydrated.current) {
isDashboardHydrated.current = true;

View File

@@ -1,325 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PartialFilters, DataMaskStateWithId } from '@superset-ui/core';
import {
injectRisonFiltersIntelligently,
RisonFilter,
parseRisonFilters,
risonFiltersToString,
risonToAdhocFilters,
} from './risonFilters';
const mockNativeFilters: PartialFilters = {
filter_1: {
id: 'filter_1',
targets: [
{
column: { name: 'country' },
datasetId: 1,
},
],
filterType: 'filter_select',
},
filter_2: {
id: 'filter_2',
targets: [
{
column: { name: 'year' },
datasetId: 1,
},
],
filterType: 'filter_range',
},
filter_3: {
id: 'filter_3',
targets: [
{
column: { name: 'Country Code' },
datasetId: 1,
},
],
filterType: 'filter_select',
},
};
const mockDataMask: DataMaskStateWithId = {
filter_1: {
id: 'filter_1',
filterState: { value: undefined },
ownState: {},
},
};
test('should parse simple Rison filters', () => {
const risonString = '(country:USA,year:2024)';
const result = parseRisonFilters(risonString);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
subject: 'country',
operator: '==',
comparator: 'USA',
});
expect(result[1]).toEqual({
subject: 'year',
operator: '==',
comparator: 2024,
});
});
test('should parse IN operator with array syntax', () => {
const result = parseRisonFilters('(country:!(USA,Canada))');
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
subject: 'country',
operator: 'IN',
comparator: ['USA', 'Canada'],
});
});
test('should parse BETWEEN operator', () => {
const result = parseRisonFilters('(msrp:(between:!(35,200)))');
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
subject: 'msrp',
operator: 'BETWEEN',
comparator: [35, 200],
});
});
test('should parse NOT operator', () => {
const result = parseRisonFilters('(NOT:(country:USA))');
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('!=');
expect(result[0].comparator).toBe('USA');
});
test('should parse comparison operators', () => {
expect(parseRisonFilters('(sales:(gt:100000))')[0].operator).toBe('>');
expect(parseRisonFilters('(age:(gte:18))')[0].operator).toBe('>=');
expect(parseRisonFilters('(temp:(lt:32))')[0].operator).toBe('<');
expect(parseRisonFilters('(price:(lte:1000))')[0].operator).toBe('<=');
});
test('should return empty array for invalid Rison', () => {
expect(parseRisonFilters('invalid rison')).toEqual([]);
expect(parseRisonFilters('(unclosed')).toEqual([]);
});
test('should match Rison filter to native filter by column name', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country', operator: '==', comparator: 'USA' },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_1.filterState?.value).toEqual(['USA']);
expect(result.unmatchedFilters).toHaveLength(0);
});
test('should match column names with spaces (case-insensitive)', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'Country Code', operator: '==', comparator: 'USA' },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_3.filterState?.value).toEqual(['USA']);
expect(result.unmatchedFilters).toHaveLength(0);
});
test('should match column names case-insensitively', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country code', operator: '==', comparator: 'USA' },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_3.filterState?.value).toEqual(['USA']);
expect(result.unmatchedFilters).toHaveLength(0);
});
test('should handle unmatched filters with fallback', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'region', operator: '==', comparator: 'North America' },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.unmatchedFilters).toHaveLength(1);
expect(result.unmatchedFilters[0].subject).toBe('region');
});
test('should convert values correctly for different filter types', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country', operator: '==', comparator: 'USA' },
{ subject: 'year', operator: 'BETWEEN', comparator: [2020, 2024] },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
// Select filter should be array
expect(result.updatedDataMask.filter_1.filterState?.value).toEqual(['USA']);
// Range filter should be min/max object
expect(result.updatedDataMask.filter_2.filterState?.value).toEqual({
min: 2020,
max: 2024,
});
expect(result.unmatchedFilters).toHaveLength(0);
});
test('should set extraFormData for auto-application on select filters', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country', operator: '==', comparator: 'USA' },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_1.extraFormData).toEqual({
filters: [{ col: 'country', op: 'IN', val: ['USA'] }],
});
});
test('should set extraFormData for auto-application on IN filters', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country', operator: 'IN', comparator: ['USA', 'Canada'] },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_1.filterState?.value).toEqual([
'USA',
'Canada',
]);
expect(result.updatedDataMask.filter_1.extraFormData).toEqual({
filters: [{ col: 'country', op: 'IN', val: ['USA', 'Canada'] }],
});
});
test('should set extraFormData for auto-application on BETWEEN filters', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'year', operator: 'BETWEEN', comparator: [2020, 2024] },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_2.filterState?.value).toEqual({
min: 2020,
max: 2024,
});
expect(result.updatedDataMask.filter_2.extraFormData).toEqual({
filters: [
{ col: 'year', op: '>=', val: 2020 },
{ col: 'year', op: '<=', val: 2024 },
],
});
});
test('should handle mixed matched and unmatched filters', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country', operator: '==', comparator: 'USA' },
{ subject: 'category', operator: '==', comparator: 'Sales' },
];
const result = injectRisonFiltersIntelligently(
risonFilters,
mockNativeFilters,
mockDataMask,
);
expect(result.updatedDataMask.filter_1.filterState?.value).toEqual(['USA']);
expect(result.unmatchedFilters).toHaveLength(1);
expect(result.unmatchedFilters[0].subject).toBe('category');
});
test('should convert filters to adhoc format', () => {
const risonFilters: RisonFilter[] = [
{ subject: 'country', operator: '==', comparator: 'USA' },
];
const adhocFilters = risonToAdhocFilters(risonFilters);
expect(adhocFilters).toHaveLength(1);
expect(adhocFilters[0]).toMatchObject({
expressionType: 'SIMPLE',
clause: 'WHERE',
subject: 'country',
operator: '==',
comparator: 'USA',
});
});
test('should convert filters to Rison string', () => {
const filters: RisonFilter[] = [
{ subject: 'country', operator: '==', comparator: 'USA' },
];
const result = risonFiltersToString(filters);
expect(result).toBe('(country:USA)');
});
test('should convert IN filters to Rison string', () => {
const filters: RisonFilter[] = [
{ subject: 'country', operator: 'IN', comparator: ['USA', 'Canada'] },
];
const result = risonFiltersToString(filters);
expect(result).toBe('(country:!(USA,Canada))');
});
test('should return empty string for empty filters', () => {
expect(risonFiltersToString([])).toBe('');
});

View File

@@ -1,496 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
QueryObjectFilterClause,
PartialFilters,
DataMaskStateWithId,
} from '@superset-ui/core';
import rison from 'rison';
export interface RisonFilter {
subject: string;
operator: string;
comparator: string | number | boolean | (string | number)[];
}
export interface IntelligentRisonInjectionResult {
updatedDataMask: DataMaskStateWithId;
unmatchedFilters: RisonFilter[];
}
/**
* Parse individual filter condition
*/
function parseFilterCondition(key: string, value: unknown): RisonFilter {
// Handle comparison operators: (gt:100), (between:!(1,10))
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const [operator, operatorValue] = Object.entries(
value as Record<string, unknown>,
)[0];
switch (operator) {
case 'gt':
return {
subject: key,
operator: '>',
comparator: operatorValue as string | number,
};
case 'gte':
return {
subject: key,
operator: '>=',
comparator: operatorValue as string | number,
};
case 'lt':
return {
subject: key,
operator: '<',
comparator: operatorValue as string | number,
};
case 'lte':
return {
subject: key,
operator: '<=',
comparator: operatorValue as string | number,
};
case 'between':
return {
subject: key,
operator: 'BETWEEN',
comparator: operatorValue as (string | number)[],
};
case 'like':
return {
subject: key,
operator: 'LIKE',
comparator: operatorValue as string,
};
default:
return {
subject: key,
operator: '==',
comparator: value as string | number,
};
}
}
// Handle IN operator: !(value1,value2)
if (Array.isArray(value)) {
return {
subject: key,
operator: 'IN',
comparator: value as (string | number)[],
};
}
// Handle simple equality
return {
subject: key,
operator: '==',
comparator: value as string | number | boolean,
};
}
/**
* Parse Rison filter syntax from URL parameter.
* Supports formats like: (country:USA,year:2024)
*/
export function parseRisonFilters(risonString: string): RisonFilter[] {
try {
const parsed = rison.decode(risonString);
const filters: RisonFilter[] = [];
if (!parsed || typeof parsed !== 'object') {
return filters;
}
const parsedObj = parsed as Record<string, unknown>;
// Handle OR operator: OR:!(condition1,condition2)
if (parsedObj.OR && Array.isArray(parsedObj.OR)) {
(parsedObj.OR as Record<string, unknown>[]).forEach(condition => {
if (typeof condition === 'object') {
Object.entries(condition).forEach(([key, value]) => {
filters.push(parseFilterCondition(key, value));
});
}
});
return filters;
}
// Handle NOT operator: NOT:(condition)
if (parsedObj.NOT && typeof parsedObj.NOT === 'object') {
Object.entries(parsedObj.NOT as Record<string, unknown>).forEach(
([key, value]) => {
const filter = parseFilterCondition(key, value);
if (filter.operator === '==') {
filter.operator = '!=';
} else if (filter.operator === 'IN') {
filter.operator = 'NOT IN';
}
filters.push(filter);
},
);
return filters;
}
// Handle regular filters
Object.entries(parsedObj).forEach(([key, value]) => {
if (key !== 'OR' && key !== 'NOT') {
filters.push(parseFilterCondition(key, value));
}
});
return filters;
} catch (error) {
console.warn('Failed to parse Rison filters:', error);
return [];
}
}
/**
* Convert Rison filters to Superset adhoc filter format
*/
export function risonToAdhocFilters(
risonFilters: RisonFilter[],
): QueryObjectFilterClause[] {
return risonFilters.map(
filter =>
({
expressionType: 'SIMPLE' as const,
clause: 'WHERE' as const,
subject: filter.subject,
operator: filter.operator,
comparator: filter.comparator,
}) as unknown as QueryObjectFilterClause,
);
}
/**
* Prettify Rison filter URL by replacing encoded characters.
* Uses browser history API to update URL without page reload.
*/
export function prettifyRisonFilterUrl(): void {
try {
const currentUrl = window.location.href;
if (!currentUrl.includes('&f=') && !currentUrl.includes('?f=')) {
return;
}
const urlMatch = currentUrl.match(/([?&])f=([^&]*)/);
if (!urlMatch) {
return;
}
const separator = urlMatch[1];
let risonValue = urlMatch[2];
if (!risonValue.includes('%') && !risonValue.includes('+')) {
return;
}
let previousValue = '';
let decodeAttempts = 0;
while (risonValue !== previousValue && decodeAttempts < 5) {
previousValue = risonValue;
try {
if (risonValue.includes('%')) {
risonValue = decodeURIComponent(risonValue);
}
} catch {
break;
}
decodeAttempts += 1;
}
risonValue = risonValue.replace(/\+/g, ' ');
const matchIndex = urlMatch.index ?? 0;
const beforeRison = currentUrl.substring(0, matchIndex);
const afterRison = currentUrl.substring(matchIndex + urlMatch[0].length);
const prettifiedUrl = `${beforeRison}${separator}f=${risonValue}${afterRison}`;
if (prettifiedUrl !== currentUrl) {
window.history.replaceState(window.history.state, '', prettifiedUrl);
}
} catch (error) {
console.warn('Failed to prettify Rison URL:', error);
}
}
/**
* Get Rison filter parameter from URL
*/
export function getRisonFilterParam(): string | null {
const params = new URLSearchParams(window.location.search);
return params.get('f');
}
/**
* Convert an array of RisonFilter back to Rison string format
*/
export function risonFiltersToString(filters: RisonFilter[]): string {
if (filters.length === 0) {
return '';
}
const risonObject: Record<
string,
string | number | boolean | (string | number)[] | Record<string, unknown>
> = {};
filters.forEach(filter => {
if (filter.operator === 'IN' && Array.isArray(filter.comparator)) {
risonObject[filter.subject] = filter.comparator;
} else if (filter.operator === '==') {
risonObject[filter.subject] = filter.comparator;
} else {
const operatorMap: Record<string, string> = {
'>': 'gt',
'>=': 'gte',
'<': 'lt',
'<=': 'lte',
BETWEEN: 'between',
LIKE: 'like',
};
const risonOp = operatorMap[filter.operator] || filter.operator;
risonObject[filter.subject] = { [risonOp]: filter.comparator };
}
});
try {
return rison.encode(risonObject);
} catch (error) {
console.warn('Failed to encode Rison filters:', error);
return '';
}
}
/**
* Update the URL to remove successfully matched filters, keeping only unmatched ones
*/
export function updateUrlWithUnmatchedFilters(
unmatchedFilters: RisonFilter[],
): void {
try {
const currentUrl = new URL(window.location.href);
if (unmatchedFilters.length === 0) {
currentUrl.searchParams.delete('f');
} else {
const newRisonString = risonFiltersToString(unmatchedFilters);
if (newRisonString) {
currentUrl.searchParams.set('f', newRisonString);
} else {
currentUrl.searchParams.delete('f');
}
}
window.history.replaceState(
window.history.state,
'',
currentUrl.toString(),
);
} catch (error) {
console.warn('Failed to update URL with unmatched filters:', error);
}
}
/**
* Find a native filter that matches a Rison filter by column name.
* Uses case-insensitive, trimmed comparison to handle column names with spaces.
*/
function findMatchingNativeFilter(
risonFilter: RisonFilter,
nativeFilters: PartialFilters,
): string | null {
const normalizedSubject = risonFilter.subject.trim().toLowerCase();
for (const [filterId, nativeFilter] of Object.entries(nativeFilters)) {
if (!nativeFilter?.targets) continue;
const hasMatchingTarget = nativeFilter.targets.some(target => {
if (typeof target === 'object' && target && 'column' in target) {
return target.column?.name?.trim().toLowerCase() === normalizedSubject;
}
return false;
});
if (hasMatchingTarget) {
return filterId;
}
}
return null;
}
/**
* Build extraFormData filters for a given rison filter and column name
*/
function buildExtraFormDataFilters(
risonFilter: RisonFilter,
columnName: string,
): { col: string; op: string; val: unknown }[] {
const { operator, comparator } = risonFilter;
if (operator === 'IN' || (operator === '==' && Array.isArray(comparator))) {
return [
{
col: columnName,
op: 'IN',
val: Array.isArray(comparator) ? comparator : [comparator],
},
];
}
if (operator === '==' && !Array.isArray(comparator)) {
return [{ col: columnName, op: 'IN', val: [comparator] }];
}
if (
operator === 'BETWEEN' &&
Array.isArray(comparator) &&
comparator.length === 2
) {
return [
{ col: columnName, op: '>=', val: comparator[0] },
{ col: columnName, op: '<=', val: comparator[1] },
];
}
return [{ col: columnName, op: operator, val: comparator }];
}
/**
* Convert a Rison filter value to the format expected by a native filter.
* Also returns extraFormData for auto-application.
*/
function convertRisonToNativeValue(
risonFilter: RisonFilter,
nativeFilter: { filterType?: string },
): unknown {
const { comparator, operator } = risonFilter;
const filterType = nativeFilter?.filterType;
switch (filterType) {
case 'filter_select':
if (operator === 'IN' || Array.isArray(comparator)) {
return Array.isArray(comparator) ? comparator : [comparator];
}
return [comparator];
case 'filter_range':
if (
operator === 'BETWEEN' &&
Array.isArray(comparator) &&
comparator.length === 2
) {
return { min: comparator[0], max: comparator[1] };
}
return comparator;
case 'filter_time_range':
case 'filter_timecolumn':
return comparator;
default:
return Array.isArray(comparator) ? comparator : [comparator];
}
}
/**
* Build a complete DataMask entry for a rison filter matched to a native filter.
* Sets both filterState.value AND extraFormData so the filter auto-applies.
*/
function buildDataMaskForFilter(
risonFilter: RisonFilter,
nativeFilter: {
id: string;
filterType?: string;
targets?: { column?: { name?: string } }[];
},
columnName: string,
) {
const convertedValue = convertRisonToNativeValue(risonFilter, nativeFilter);
return {
id: nativeFilter.id,
filterState: {
value: convertedValue,
},
extraFormData: {
filters: buildExtraFormDataFilters(risonFilter, columnName),
},
ownState: {},
};
}
/**
* Intelligently inject Rison filters into native filters where possible,
* falling back to brute-force injection for unmatched filters
*/
export function injectRisonFiltersIntelligently(
risonFilters: RisonFilter[],
nativeFilters: PartialFilters,
currentDataMask: DataMaskStateWithId,
): IntelligentRisonInjectionResult {
const updatedDataMask = { ...currentDataMask };
const unmatchedFilters: RisonFilter[] = [];
risonFilters.forEach(risonFilter => {
const matchingFilterId = findMatchingNativeFilter(
risonFilter,
nativeFilters,
);
if (matchingFilterId) {
const matchedFilter = nativeFilters[matchingFilterId];
if (matchedFilter) {
const columnName =
matchedFilter.targets?.[0]?.column?.name ?? risonFilter.subject;
const dataMaskEntry = buildDataMaskForFilter(
risonFilter,
matchedFilter as {
id: string;
filterType?: string;
targets?: { column?: { name?: string } }[];
},
columnName,
);
updatedDataMask[matchedFilter.id] = {
...updatedDataMask[matchedFilter.id],
...dataMaskEntry,
};
return;
}
}
unmatchedFilters.push(risonFilter);
});
return {
updatedDataMask,
unmatchedFilters,
};
}

View File

@@ -632,6 +632,15 @@ def mcp_auth_hook(tool_func: F) -> F: # noqa: C901
@functools.wraps(tool_func)
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
with _get_app_context_manager():
# Clear any stale thread-local SQLAlchemy session before user lookup.
# Thread pool workers reuse threads across requests; db.session is
# scoped by thread (not ContextVar), so a prior request's session may
# still be bound to a different tenant's DB engine. Removing it here
# ensures the next DB access creates a fresh session bound to the
# correct engine for the current request.
from superset.extensions import db
db.session.remove()
user = _setup_user_context()
# No Flask context - this is a FastMCP internal operation

View File

@@ -1585,11 +1585,17 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
for metric in metric_names
}
# When the original query has limit or offset we wont apply those
# to the subquery so we prevent data inconsistency due to missing records
# in the dataframes when performing the join
# The subquery drops row_offset (the offset period's own row ordering
# differs from the main query's, so applying the same offset would skew
# the join). It must still fetch enough rows to cover the main query's
# window, hence row_limit + row_offset when a chart limit is set.
if query_object.row_limit or query_object.row_offset:
query_object_clone_dct["row_limit"] = app.config["ROW_LIMIT"]
if query_object.row_limit:
query_object_clone_dct["row_limit"] = (
query_object.row_limit + query_object.row_offset
)
else:
query_object_clone_dct["row_limit"] = app.config["ROW_LIMIT"]
query_object_clone_dct["row_offset"] = 0
# Call the unified query method on the datasource

File diff suppressed because it is too large Load Diff

View File

@@ -1,226 +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.
"""
Parser for Rison URL filters that converts simplified filter syntax
to Superset's adhoc_filters format.
"""
from __future__ import annotations
import logging
from typing import Any, Optional, Union
import prison
from flask import request
logger = logging.getLogger(__name__)
class RisonFilterParser:
"""
Parse Rison filter syntax from URL parameter 'f' and convert to adhoc_filters.
Supports:
- Simple equality: f=(country:USA)
- Lists (IN): f=(country:!(USA,Canada))
- NOT operator: f=(NOT:(country:USA))
- OR operator: f=(OR:!(condition1,condition2))
- Comparison operators: f=(sales:(gt:100000))
- BETWEEN: f=(date:(between:!(2024-01-01,2024-12-31)))
- LIKE: f=(name:(like:'%smith%'))
"""
OPERATORS: dict[str, str] = {
"gt": ">",
"gte": ">=",
"lt": "<",
"lte": "<=",
"between": "BETWEEN",
"like": "LIKE",
"ilike": "ILIKE",
"ne": "!=",
"eq": "==",
}
def parse(self, filter_string: Optional[str] = None) -> list[dict[str, Any]]:
"""
Parse Rison filter string and convert to adhoc_filters format.
Args:
filter_string: Rison-encoded filter string, or None to get from request
Returns:
List of adhoc_filter dictionaries
"""
if filter_string is None:
filter_string = request.args.get("f")
if not filter_string:
return []
try:
filters_obj = prison.loads(filter_string)
return self._convert_to_adhoc_filters(filters_obj)
except Exception:
logger.warning(
"Failed to parse Rison filters: %s", filter_string, exc_info=True
)
return []
def _convert_to_adhoc_filters(
self, filters_obj: Union[dict[str, Any], list[Any], Any]
) -> list[dict[str, Any]]:
if not isinstance(filters_obj, dict):
return []
adhoc_filters: list[dict[str, Any]] = []
for key, value in filters_obj.items():
if key == "OR":
adhoc_filters.extend(self._handle_or_operator(value))
elif key == "NOT":
adhoc_filters.extend(self._handle_not_operator(value))
else:
filter_dict = self._create_filter(key, value)
if filter_dict:
adhoc_filters.append(filter_dict)
return adhoc_filters
def _create_filter(
self, column: str, value: Any, negate: bool = False
) -> Optional[dict[str, Any]]:
filter_dict: dict[str, Any] = {
"expressionType": "SIMPLE",
"clause": "WHERE",
"subject": column,
}
if isinstance(value, list):
filter_dict["operator"] = "NOT IN" if negate else "IN"
filter_dict["comparator"] = value
elif isinstance(value, dict):
operator_info = self._parse_operator_dict(value)
if operator_info:
operator, comparator = operator_info
if negate and operator == "==":
operator = "!="
elif negate and operator == "IN":
operator = "NOT IN"
filter_dict["operator"] = operator
filter_dict["comparator"] = comparator
else:
return None
else:
filter_dict["operator"] = "!=" if negate else "=="
filter_dict["comparator"] = value
return filter_dict
def _parse_operator_dict(
self, op_dict: dict[str, Any]
) -> Optional[tuple[str, Any]]:
if not op_dict:
return None
for op_key, op_value in op_dict.items():
if op_key in self.OPERATORS:
operator = self.OPERATORS[op_key]
if (
operator == "BETWEEN"
and isinstance(op_value, list)
and len(op_value) == 2
):
return operator, op_value
return operator, op_value
if op_key == "in":
return "IN", op_value if isinstance(op_value, list) else [op_value]
if op_key == "nin":
return "NOT IN", op_value if isinstance(op_value, list) else [op_value]
return None
def _handle_or_operator(self, or_value: Any) -> list[dict[str, Any]]:
if not isinstance(or_value, list):
return []
sql_parts: list[str] = []
for item in or_value:
if isinstance(item, dict):
for col, val in item.items():
if col not in ("OR", "NOT"):
sql_part = self._build_sql_condition(col, val)
if sql_part:
sql_parts.append(sql_part)
if sql_parts:
return [
{
"expressionType": "SQL",
"clause": "WHERE",
"sqlExpression": f"({' OR '.join(sql_parts)})",
}
]
return []
def _build_sql_condition(self, column: str, value: Any) -> Optional[str]:
if isinstance(value, list):
values_str = ", ".join(
[f"'{v}'" if isinstance(v, str) else str(v) for v in value]
)
return f"{column} IN ({values_str})"
if isinstance(value, dict):
operator_info = self._parse_operator_dict(value)
if operator_info:
op, comp = operator_info
if op == "BETWEEN" and isinstance(comp, list):
return f"{column} BETWEEN '{comp[0]}' AND '{comp[1]}'"
if op == "LIKE":
return f"{column} LIKE '{comp}'"
comp_str = f"'{comp}'" if isinstance(comp, str) else str(comp)
return f"{column} {op} {comp_str}"
val_str = f"'{value}'" if isinstance(value, str) else str(value)
return f"{column} = {val_str}"
def _handle_not_operator(self, not_value: Any) -> list[dict[str, Any]]:
if isinstance(not_value, dict):
filters: list[dict[str, Any]] = []
for col, val in not_value.items():
if col not in ("OR", "NOT"):
filter_dict = self._create_filter(col, val, negate=True)
if filter_dict:
filters.append(filter_dict)
return filters
return []
def merge_rison_filters(form_data: dict[str, Any]) -> None:
"""
Merge Rison filters from 'f' parameter into form_data.
Modifies form_data in place.
"""
parser = RisonFilterParser()
if rison_filters := parser.parse():
existing_filters = form_data.get("adhoc_filters", [])
form_data["adhoc_filters"] = existing_filters + rison_filters
logger.info("Added %d filters from Rison parameter", len(rison_filters))

View File

@@ -23,7 +23,6 @@ from unittest.mock import Mock, patch
import numpy as np
import pandas as pd
import pytest
from flask import current_app
from pandas import DateOffset
from superset import db
@@ -43,6 +42,7 @@ from superset.utils.core import (
QueryStatus,
)
from superset.utils.pandas_postprocessing.utils import FLAT_COLUMN_SEPARATOR
from tests.conftest import with_config
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.conftest import (
only_postgresql,
@@ -68,6 +68,130 @@ def get_sql_text(payload: dict[str, Any]) -> str:
return response["query"]
def _time_comparison_offset_queries_payload() -> dict[str, Any]:
"""Birth-names chart payload with time comparison and x-axis suitable for tests."""
payload = get_query_context("birth_names")
payload["queries"][0]["columns"] = [
{
"timeGrain": "P1D",
"columnType": "BASE_AXIS",
"sqlExpression": "ds",
"label": "ds",
"expressionType": "SQL",
}
]
payload["queries"][0]["metrics"] = ["sum__num"]
payload["queries"][0]["groupby"] = ["name"]
payload["queries"][0]["is_timeseries"] = True
payload["queries"][0]["time_range"] = "1990 : 1991"
return payload
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@patch("superset.common.query_context.QueryContext.get_query_result")
def test_time_offset_comparison_queries_use_chart_row_limit(
query_result_mock: Mock,
) -> None:
"""Comparison SQL covers the main query's window (row_limit + row_offset)."""
payload = _time_comparison_offset_queries_payload()
payload["queries"][0]["row_limit"] = 100
payload["queries"][0]["row_offset"] = 10
initial_df = pd.DataFrame(
{
"__timestamp": ["1990-01-01", "1990-01-01"],
"name": ["zban", "ahwb"],
"sum__num": [43571, 27225],
}
)
mock_query_result = Mock()
mock_query_result.df = initial_df
query_result_mock.side_effect = [mock_query_result]
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
df = query_context.get_query_result(query_object).df
payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
def cache_key_fn(qo: QueryObject, time_offset: str, time_grain: Any) -> str | None:
return query_context._processor.query_cache_key(
qo, time_offset=time_offset, time_grain=time_grain
)
def cache_timeout_fn() -> int:
return query_context._processor.get_cache_timeout()
time_offsets_obj = query_context.datasource.processing_time_offsets(
df, query_object, cache_key_fn, cache_timeout_fn, query_context.force
)
sqls = time_offsets_obj["queries"]
assert len(sqls) == 2
assert re.search(r"1989-01-01.+1990-01-01", sqls[0], re.S)
assert re.search(r"LIMIT 110", sqls[0], re.S)
assert not re.search(r"OFFSET 10", sqls[0], re.S)
assert re.search(r"1991-01-01.+1992-01-01", sqls[1], re.S)
assert re.search(r"LIMIT 110", sqls[1], re.S)
assert not re.search(r"OFFSET 10", sqls[1], re.S)
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@with_config({"ROW_LIMIT": 4242})
@patch("superset.common.query_context.QueryContext.get_query_result")
def test_time_offset_comparison_queries_use_config_row_limit_without_chart_limit(
query_result_mock: Mock,
) -> None:
"""Chart with row_offset only: subquery widens the config ROW_LIMIT by row_offset.
The schema fills `row_limit` with `app.config["ROW_LIMIT"]` when the payload
omits it, so the query_object arrives with row_limit=4242. The subquery then
covers the window via row_limit + row_offset = 4252.
"""
payload = _time_comparison_offset_queries_payload()
del payload["queries"][0]["row_limit"]
payload["queries"][0]["row_offset"] = 10
initial_df = pd.DataFrame(
{
"__timestamp": ["1990-01-01", "1990-01-01"],
"name": ["zban", "ahwb"],
"sum__num": [43571, 27225],
}
)
mock_query_result = Mock()
mock_query_result.df = initial_df
query_result_mock.side_effect = [mock_query_result]
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
df = query_context.get_query_result(query_object).df
payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
def cache_key_fn(qo: QueryObject, time_offset: str, time_grain: Any) -> str | None:
return query_context._processor.query_cache_key(
qo, time_offset=time_offset, time_grain=time_grain
)
def cache_timeout_fn() -> int:
return query_context._processor.get_cache_timeout()
time_offsets_obj = query_context.datasource.processing_time_offsets(
df, query_object, cache_key_fn, cache_timeout_fn, query_context.force
)
sqls = time_offsets_obj["queries"]
limit_pattern = re.compile(r"LIMIT\s+4252\b")
assert len(sqls) == 2
assert limit_pattern.search(sqls[0])
assert not re.search(r"OFFSET 10", sqls[0], re.S)
assert limit_pattern.search(sqls[1])
assert not re.search(r"OFFSET 10", sqls[1], re.S)
@pytest.mark.skip(
reason=(
"TODO: Fix test class to work with DuckDB example data format. "
@@ -794,28 +918,17 @@ class TestQueryContext(SupersetTestCase):
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@patch("superset.common.query_context.QueryContext.get_query_result")
def test_time_offsets_in_query_object_no_limit(self, query_result_mock):
def test_time_offsets_in_query_object_uses_chart_row_limit(self, query_result_mock):
"""
Ensure that time_offsets can generate the correct queries and
it doesnt use the row_limit nor row_offset from the original
query object
Subquery honors the chart's row_limit (widened by row_offset so the
LEFT JOIN covers the main query's paginated window) and drops
row_offset. Before this fix, row_limit was replaced with
app.config["ROW_LIMIT"], which caused the main query and offset
subquery to fetch different row counts.
"""
payload = get_query_context("birth_names")
payload["queries"][0]["columns"] = [
{
"timeGrain": "P1D",
"columnType": "BASE_AXIS",
"sqlExpression": "ds",
"label": "ds",
"expressionType": "SQL",
}
]
payload["queries"][0]["metrics"] = ["sum__num"]
payload["queries"][0]["groupby"] = ["name"]
payload["queries"][0]["is_timeseries"] = True
payload = _time_comparison_offset_queries_payload()
payload["queries"][0]["row_limit"] = 100
payload["queries"][0]["row_offset"] = 10
payload["queries"][0]["time_range"] = "1990 : 1991"
initial_data = {
"__timestamp": ["1990-01-01", "1990-01-01"],
@@ -839,33 +952,86 @@ class TestQueryContext(SupersetTestCase):
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
def cache_key_fn(qo, time_offset, time_grain):
def cache_key_fn(
qo: QueryObject, time_offset: str, time_grain: Any
) -> str | None:
return query_context._processor.query_cache_key(
qo, time_offset=time_offset, time_grain=time_grain
)
def cache_timeout_fn():
def cache_timeout_fn() -> int:
return query_context._processor.get_cache_timeout()
time_offsets_obj = query_context.datasource.processing_time_offsets(
df, query_object, cache_key_fn, cache_timeout_fn, query_context.force
)
sqls = time_offsets_obj["queries"]
row_limit_value = current_app.config["ROW_LIMIT"]
row_limit_pattern_with_config_value = r"LIMIT " + re.escape(
str(row_limit_value)
)
assert len(sqls) == 2
# 1 year ago
# 1 year ago — subquery widens row_limit to cover main window (100 + 10)
assert re.search(r"1989-01-01.+1990-01-01", sqls[0], re.S)
assert not re.search(r"LIMIT 100", sqls[0], re.S)
assert re.search(r"LIMIT 110", sqls[0], re.S)
assert not re.search(r"OFFSET 10", sqls[0], re.S)
assert re.search(row_limit_pattern_with_config_value, sqls[0], re.S)
# 1 year later
assert re.search(r"1991-01-01.+1992-01-01", sqls[1], re.S)
assert not re.search(r"LIMIT 100", sqls[1], re.S)
assert re.search(r"LIMIT 110", sqls[1], re.S)
assert not re.search(r"OFFSET 10", sqls[1], re.S)
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@with_config({"ROW_LIMIT": 4242})
@patch("superset.common.query_context.QueryContext.get_query_result")
def test_time_offsets_use_config_row_limit_when_chart_has_offset_only(
self, query_result_mock
):
"""
Chart with row_offset only: subquery widens the config ROW_LIMIT by row_offset.
The schema fills row_limit with app.config["ROW_LIMIT"] (4242) when the
payload omits it, so the subquery covers the window via 4242 + 10 = 4252.
"""
payload = _time_comparison_offset_queries_payload()
del payload["queries"][0]["row_limit"]
payload["queries"][0]["row_offset"] = 10
initial_data = {
"__timestamp": ["1990-01-01", "1990-01-01"],
"name": ["zban", "ahwb"],
"sum__num": [43571, 27225],
}
initial_df = pd.DataFrame(initial_data)
mock_query_result = Mock()
mock_query_result.df = initial_df
query_result_mock.side_effect = [mock_query_result]
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
query_result = query_context.get_query_result(query_object)
df = query_result.df
payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
def cache_key_fn(
qo: QueryObject, time_offset: str, time_grain: Any
) -> str | None:
return query_context._processor.query_cache_key(
qo, time_offset=time_offset, time_grain=time_grain
)
def cache_timeout_fn() -> int:
return query_context._processor.get_cache_timeout()
time_offsets_obj = query_context.datasource.processing_time_offsets(
df, query_object, cache_key_fn, cache_timeout_fn, query_context.force
)
sqls = time_offsets_obj["queries"]
limit_pattern = re.compile(r"LIMIT\s+4252\b")
assert len(sqls) == 2
assert limit_pattern.search(sqls[0])
assert not re.search(r"OFFSET 10", sqls[0], re.S)
assert limit_pattern.search(sqls[1])
assert not re.search(r"OFFSET 10", sqls[1], re.S)
assert re.search(row_limit_pattern_with_config_value, sqls[1], re.S)
def test_get_label_map(app_context, virtual_dataset_comma_in_column_value):

View File

@@ -700,6 +700,233 @@ def test_processing_time_offsets_date_range_enabled(processor):
assert isinstance(result["cache_keys"], list)
def test_processing_time_offsets_uses_chart_row_limit(processor):
"""Offset subquery inherits the chart's row_limit when one is set."""
from superset.common.query_object import QueryObject
from superset.models.helpers import ExploreMixin
processor._qc_datasource.processing_time_offsets = (
ExploreMixin.processing_time_offsets.__get__(processor._qc_datasource)
)
df = pd.DataFrame({"__timestamp": ["1990-01-01"], "sum__num": [100]})
query_object = QueryObject(
datasource=MagicMock(),
granularity="ds",
columns=[],
metrics=["sum__num"],
is_timeseries=True,
row_limit=100,
row_offset=0,
time_offsets=["1 year ago"],
filters=[
{
"col": "ds",
"op": "TEMPORAL_RANGE",
"val": "1990-01-01 : 1991-01-01",
}
],
)
captured: list[dict[str, Any]] = []
def fake_query(dct: dict[str, Any]) -> MagicMock:
captured.append(dct)
result = MagicMock()
result.df = pd.DataFrame()
result.query = "SELECT 1"
return result
processor._qc_datasource.query = fake_query
processor._qc_datasource.normalize_df = MagicMock(return_value=pd.DataFrame())
with (
patch(
"superset.models.helpers.get_since_until_from_query_object",
return_value=(pd.Timestamp("1990-01-01"), pd.Timestamp("1991-01-01")),
),
patch(
"superset.common.utils.query_cache_manager.QueryCacheManager"
) as mock_cache_manager,
patch.object(
processor._qc_datasource,
"get_time_grain",
return_value=None,
),
patch.object(
processor._qc_datasource,
"join_offset_dfs",
return_value=df,
),
):
mock_cache = MagicMock()
mock_cache.is_loaded = False
mock_cache_manager.get.return_value = mock_cache
processor._qc_datasource.processing_time_offsets(
df, query_object, None, None, False
)
assert len(captured) == 1
assert captured[0]["row_limit"] == 100
assert captured[0]["row_offset"] == 0
def test_processing_time_offsets_row_offset_extends_window(processor):
"""Offset subquery limit covers the main query's window (row_limit + row_offset).
When the chart has pagination (row_offset > 0), fetching only row_limit rows
in the offset period would likely miss the dimensions present in the main
query's page, yielding null comparison values. The subquery instead drops
row_offset and widens row_limit to cover the full window.
"""
from superset.common.query_object import QueryObject
from superset.models.helpers import ExploreMixin
processor._qc_datasource.processing_time_offsets = (
ExploreMixin.processing_time_offsets.__get__(processor._qc_datasource)
)
df = pd.DataFrame({"__timestamp": ["1990-01-01"], "sum__num": [100]})
query_object = QueryObject(
datasource=MagicMock(),
granularity="ds",
columns=[],
metrics=["sum__num"],
is_timeseries=True,
row_limit=100,
row_offset=10,
time_offsets=["1 year ago"],
filters=[
{
"col": "ds",
"op": "TEMPORAL_RANGE",
"val": "1990-01-01 : 1991-01-01",
}
],
)
captured: list[dict[str, Any]] = []
def fake_query(dct: dict[str, Any]) -> MagicMock:
captured.append(dct)
result = MagicMock()
result.df = pd.DataFrame()
result.query = "SELECT 1"
return result
processor._qc_datasource.query = fake_query
processor._qc_datasource.normalize_df = MagicMock(return_value=pd.DataFrame())
with (
patch(
"superset.models.helpers.get_since_until_from_query_object",
return_value=(pd.Timestamp("1990-01-01"), pd.Timestamp("1991-01-01")),
),
patch(
"superset.common.utils.query_cache_manager.QueryCacheManager"
) as mock_cache_manager,
patch.object(
processor._qc_datasource,
"get_time_grain",
return_value=None,
),
patch.object(
processor._qc_datasource,
"join_offset_dfs",
return_value=df,
),
):
mock_cache = MagicMock()
mock_cache.is_loaded = False
mock_cache_manager.get.return_value = mock_cache
processor._qc_datasource.processing_time_offsets(
df, query_object, None, None, False
)
assert len(captured) == 1
assert captured[0]["row_limit"] == 110
assert captured[0]["row_offset"] == 0
def test_processing_time_offsets_falls_back_to_config_row_limit(processor):
"""Offset subquery uses app config ROW_LIMIT when chart has offset but no limit."""
from superset.common.query_object import QueryObject
from superset.models.helpers import ExploreMixin
processor._qc_datasource.processing_time_offsets = (
ExploreMixin.processing_time_offsets.__get__(processor._qc_datasource)
)
df = pd.DataFrame({"__timestamp": ["1990-01-01"], "sum__num": [100]})
query_object = QueryObject(
datasource=MagicMock(),
granularity="ds",
columns=[],
metrics=["sum__num"],
is_timeseries=True,
row_limit=None,
row_offset=10,
time_offsets=["1 year ago"],
filters=[
{
"col": "ds",
"op": "TEMPORAL_RANGE",
"val": "1990-01-01 : 1991-01-01",
}
],
)
captured: list[dict[str, Any]] = []
def fake_query(dct: dict[str, Any]) -> MagicMock:
captured.append(dct)
result = MagicMock()
result.df = pd.DataFrame()
result.query = "SELECT 1"
return result
processor._qc_datasource.query = fake_query
processor._qc_datasource.normalize_df = MagicMock(return_value=pd.DataFrame())
with (
patch(
"superset.models.helpers.get_since_until_from_query_object",
return_value=(pd.Timestamp("1990-01-01"), pd.Timestamp("1991-01-01")),
),
patch(
"superset.common.utils.query_cache_manager.QueryCacheManager"
) as mock_cache_manager,
patch.object(
processor._qc_datasource,
"get_time_grain",
return_value=None,
),
patch.object(
processor._qc_datasource,
"join_offset_dfs",
return_value=df,
),
patch("superset.models.helpers.app") as mock_app,
):
mock_app.config = {"ROW_LIMIT": 4242}
mock_cache = MagicMock()
mock_cache.is_loaded = False
mock_cache_manager.get.return_value = mock_cache
processor._qc_datasource.processing_time_offsets(
df, query_object, None, None, False
)
assert len(captured) == 1
assert captured[0]["row_limit"] == 4242
assert captured[0]["row_offset"] == 0
def test_ensure_totals_available_updates_cache_values():
"""
Test that ensure_totals_available() updates the query objects AND

View File

@@ -372,6 +372,43 @@ def test_mcp_auth_hook_preserves_g_user_in_request_context(app) -> None:
assert result == "middleware_user"
def test_mcp_auth_hook_removes_stale_db_session_in_sync_wrapper(app) -> None:
"""sync_wrapper calls db.session.remove() BEFORE get_user_from_request().
Thread pool workers reuse threads across requests; db.session is
thread-local and may be bound to a different tenant's DB engine from a
prior request. Removing it before user lookup ensures a fresh session is
created for the current request.
The ordering is critical: if remove() were called after user lookup,
the stale session binding would already have caused a mismatch error.
"""
fresh_user = _make_mock_user("fresh")
def dummy_tool():
"""Dummy tool."""
return g.user.username
wrapped = mcp_auth_hook(dummy_tool)
with app.test_request_context():
g.user = fresh_user
with patch("superset.extensions.db") as mock_db:
def _assert_remove_already_called() -> MagicMock:
"""Verify remove() was called before user resolution runs."""
mock_db.session.remove.assert_called_once_with()
return fresh_user
with patch(
"superset.mcp_service.auth.get_user_from_request",
side_effect=_assert_remove_already_called,
):
result = wrapped()
assert result == "fresh"
# -- default_user_resolver --

View File

@@ -1,132 +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.
"""Unit tests for Rison filter parser."""
from superset.utils.rison_filters import RisonFilterParser
def test_simple_equality():
parser = RisonFilterParser()
result = parser.parse("(country:USA)")
assert len(result) == 1
assert result[0]["expressionType"] == "SIMPLE"
assert result[0]["clause"] == "WHERE"
assert result[0]["subject"] == "country"
assert result[0]["operator"] == "=="
assert result[0]["comparator"] == "USA"
def test_multiple_filters_and():
parser = RisonFilterParser()
result = parser.parse("(country:USA,year:2024)")
assert len(result) == 2
assert result[0]["subject"] == "country"
assert result[0]["comparator"] == "USA"
assert result[1]["subject"] == "year"
assert result[1]["comparator"] == 2024
def test_list_in_operator():
parser = RisonFilterParser()
result = parser.parse("(country:!(USA,Canada))")
assert len(result) == 1
assert result[0]["subject"] == "country"
assert result[0]["operator"] == "IN"
assert result[0]["comparator"] == ["USA", "Canada"]
def test_not_operator():
parser = RisonFilterParser()
result = parser.parse("(NOT:(country:USA))")
assert len(result) == 1
assert result[0]["subject"] == "country"
assert result[0]["operator"] == "!="
assert result[0]["comparator"] == "USA"
def test_not_in_operator():
parser = RisonFilterParser()
result = parser.parse("(NOT:(country:!(USA,Canada)))")
assert len(result) == 1
assert result[0]["subject"] == "country"
assert result[0]["operator"] == "NOT IN"
assert result[0]["comparator"] == ["USA", "Canada"]
def test_or_operator():
parser = RisonFilterParser()
result = parser.parse("(OR:!((status:active),(priority:high)))")
assert len(result) == 1
assert result[0]["expressionType"] == "SQL"
assert result[0]["clause"] == "WHERE"
assert "status = 'active' OR priority = 'high'" in result[0]["sqlExpression"]
def test_comparison_operators():
parser = RisonFilterParser()
result = parser.parse("(sales:(gt:100000))")
assert result[0]["operator"] == ">"
assert result[0]["comparator"] == 100000
result = parser.parse("(age:(gte:18))")
assert result[0]["operator"] == ">="
assert result[0]["comparator"] == 18
result = parser.parse("(temp:(lt:32))")
assert result[0]["operator"] == "<"
assert result[0]["comparator"] == 32
result = parser.parse("(price:(lte:1000))")
assert result[0]["operator"] == "<="
assert result[0]["comparator"] == 1000
def test_between_operator():
parser = RisonFilterParser()
result = parser.parse("(date:(between:!('2024-01-01','2024-12-31')))")
assert len(result) == 1
assert result[0]["operator"] == "BETWEEN"
assert result[0]["comparator"] == ["2024-01-01", "2024-12-31"]
def test_like_operator():
parser = RisonFilterParser()
result = parser.parse("(name:(like:'%smith%'))")
assert len(result) == 1
assert result[0]["operator"] == "LIKE"
assert result[0]["comparator"] == "%smith%"
def test_empty_filter():
parser = RisonFilterParser()
assert parser.parse("") == []
assert parser.parse("()") == []
def test_invalid_rison():
parser = RisonFilterParser()
assert parser.parse("invalid rison") == []
assert parser.parse("(unclosed") == []