Compare commits

...
Author SHA1 Message Date
Elizabeth ThompsonandClaude Sonnet 4.6 8d2b655c22 fix(reports): narrow spinner checks to viewport and tighten exception handling (#39895)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 13:35:07 -07:00
Abdul RehmanandĐỗ Trọng Hải 29b94ced71 fix(i18n): correct Czech translation variables for SQL Lab query message (#40166)
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-15 14:06:25 -04:00
Beto Dealmeida 736a51c13f fix: OAuth2 exception should be 403 (#40074) 2026-05-15 14:53:02 -03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 34c28f7b76 chore(deps): bump zod from 4.4.1 to 4.4.3 in /superset-frontend (#40155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:35:59 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 62c86abcd1 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40152)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:35:45 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Đỗ Trọng Hải
caa357e0d2 chore(deps): bump @ant-design/icons from 6.2.2 to 6.2.3 in /superset-frontend (#40112)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-15 10:35:33 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> cc21683118 chore(deps): bump fast-xml-builder from 1.1.5 to 1.2.0 in /superset-frontend (#40103)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:35:21 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe LiEvan Rusackas
114d88468b chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 in /superset-frontend (#39821)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-15 10:35:06 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>ClaudeClaude Sonnet 4.6
48c0bea906 chore(deps): bump d3-cloud from 1.2.8 to 1.2.9 in /superset-frontend (#39699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:34:51 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a46925d431 chore(deps-dev): bump @types/node from 25.7.0 to 25.8.0 in /superset-websocket (#40148)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:33:49 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0df9cc986a chore(deps): bump immer from 11.1.7 to 11.1.8 in /superset-frontend (#40158)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:33:23 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ade901ed04 chore(deps): bump react-arborist from 3.5.0 to 3.6.1 in /superset-frontend (#40159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 10:33:07 -07:00
Richard Fogaca Nienkotter 1e2d0b5f5b fix(mcp): defer chart preview command imports (#40164) 2026-05-15 12:15:33 -03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 59b5f69627 chore(deps): bump antd from 6.3.7 to 6.4.2 in /docs (#40149)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 21:54:22 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c2980c7c42 chore(deps-dev): bump webpack-dev-server from 5.2.3 to 5.2.4 in /superset-frontend (#40161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 21:53:22 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 982881ac1c chore(deps-dev): bump tsx from 4.21.0 to 4.22.0 in /superset-frontend (#40162)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 21:52:50 +07:00
ShaitanandClaude Sonnet 4.6 2e7a2b1f2d fix: escape SQL identifiers in db engine spec prequeries and metadata queries (#39840)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:48:38 -04:00
Michael S. MolinaandClaude Sonnet 4.6 a06e6ea19b fix(extensions): add cache headers and strip Vary: Cookie for extension static assets (#40120)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:23:39 -03:00
ShaitanandClaude Sonnet 4.6 ee9eec25f9 fix(dataset): validate datasource access during import (#39998)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:06:47 +01:00
ShaitanandClaude Sonnet 4.6 ffa32414ef fix(query): restrict query cancellation to the query owner (#39996)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:05:38 +01:00
ShaitanandClaude Sonnet 4.6 407321e394 fix(database): extend shillelagh URI pattern to cover all driver variants (#39995)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:04:34 +01:00
JUST.in DO IT 2b71d964cc fix(sqllab): missing estimate action button (#40101) 2026-05-14 14:43:07 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f02e5b7e83 chore(deps-dev): bump babel-jest from 30.3.0 to 30.4.1 in /superset-frontend (#40090)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 13:52:53 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>sadpandajoeClaude Sonnet 4.6
5fa9657528 chore(deps): update @ant-design/icons requirement from ^6.2.2 to ^6.2.3 in /superset-frontend/packages/superset-ui-core (#40092)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sadpandajoe <jcli38@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:52:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d853930840 chore(deps): bump react-syntax-highlighter from 16.1.0 to 16.1.1 in /superset-frontend (#40107)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 13:52:14 -07:00
4e09889607 test(datasets): regression coverage for #16141 (export with same table name, different schemas) (#40123)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:08:23 -07:00
Evan RusackasandClaude Code 672e9a1477 fix(docs): tighten onBrokenLinks to throw and fix surfaced broken links (#40102)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-14 11:07:18 -07:00
Richard Fogaca Nienkotter 8fa5a75c70 fix(mcp): apply cached adhoc filters to chart retrieval (#40099) 2026-05-14 14:21:54 -03:00
144dae7c43 fix(dashboard): use datasetUuid instead of datasetId in display controls export/import (SC-104655) (#40008)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 10:18:57 -07:00
Arpit Jain 62dc237014 chore(ci): add explicit permissions to additional workflows (#40067) 2026-05-14 23:24:46 +07:00
Sandesh DevarajuandAmin Ghadersohi 823eb905d3 fix(mcp): JSON-serialize order_by_cols and support sort direction (#39952)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-05-14 11:19:37 -04:00
Alexandru Soare 966e97989b chore(mcp): Standardize error response shapes across chart tools (#39905) 2026-05-14 18:07:31 +03:00
Mehmet Salih YavuzandClaude Opus 4.7 8b0e63b58c fix(rls): prevent double-apply when converting physical dataset to virtual (#39725)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:05:48 +03:00
128 changed files with 3775 additions and 1171 deletions
+4
View File
@@ -8,6 +8,10 @@ on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
pull-requests: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+4
View File
@@ -9,6 +9,10 @@ on:
branches:
- "master"
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
@@ -6,6 +6,9 @@ on:
- "master"
- "[0-9].[0-9]*"
permissions:
contents: read
jobs:
config:
runs-on: ubuntu-24.04
+3
View File
@@ -6,6 +6,9 @@ on:
- "superset-embedded-sdk/**"
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -6,6 +6,9 @@ on:
- "master"
- "[0-9].[0-9]*"
permissions:
contents: read
jobs:
config:
runs-on: ubuntu-24.04
@@ -8,6 +8,9 @@ on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
jobs:
validate-all-ghas:
+3
View File
@@ -4,6 +4,9 @@ on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+4
View File
@@ -8,6 +8,10 @@ on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
pull-requests: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -78,6 +78,13 @@ jobs:
- name: yarn install
run: |
yarn install --check-cache
- name: Lint docs links
# Fast source-level check for bare relative internal links
# like `[Foo](../foo)` that Docusaurus's onBrokenLinks
# setting can't catch. Runs in seconds; fails fast before
# the expensive build step.
run: |
yarn lint:docs-links
- name: yarn typecheck
run: |
yarn typecheck
@@ -8,6 +8,10 @@ on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
pull-requests: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+3
View File
@@ -6,6 +6,9 @@ on:
paths:
- "helm/**"
permissions:
contents: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -8,6 +8,10 @@ on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
pull-requests: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+3
View File
@@ -11,6 +11,9 @@ on:
- "superset-websocket/**"
types: [synchronize, opened, reopened, ready_for_review]
permissions:
contents: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -29,10 +29,10 @@ sidebar_position: 1
## Components
- [DropdownContainer](./dropdowncontainer)
- [Flex](./flex)
- [Grid](./grid)
- [Layout](./layout)
- [MetadataBar](./metadatabar)
- [Space](./space)
- [Table](./table)
- [DropdownContainer](./dropdowncontainer.mdx)
- [Flex](./flex.mdx)
- [Grid](./grid.mdx)
- [Layout](./layout.mdx)
- [MetadataBar](./metadatabar.mdx)
- [Space](./space.mdx)
- [Table](./table.mdx)
+1 -1
View File
@@ -62,7 +62,7 @@ This documentation is auto-generated from Storybook stories. To add or update co
4. Run `yarn generate:superset-components` in the `docs/` directory
:::info Work in Progress
This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation.
This component library is actively being documented. See the [Components TODO](./TODO.md) page for a list of components awaiting documentation.
:::
---
+46 -46
View File
@@ -29,49 +29,49 @@ sidebar_position: 1
## Components
- [AutoComplete](./autocomplete)
- [Avatar](./avatar)
- [Badge](./badge)
- [Breadcrumb](./breadcrumb)
- [Button](./button)
- [ButtonGroup](./buttongroup)
- [CachedLabel](./cachedlabel)
- [Card](./card)
- [Checkbox](./checkbox)
- [Collapse](./collapse)
- [DatePicker](./datepicker)
- [Divider](./divider)
- [EditableTitle](./editabletitle)
- [EmptyState](./emptystate)
- [FaveStar](./favestar)
- [IconButton](./iconbutton)
- [Icons](./icons)
- [IconTooltip](./icontooltip)
- [InfoTooltip](./infotooltip)
- [Input](./input)
- [Label](./label)
- [List](./list)
- [ListViewCard](./listviewcard)
- [Loading](./loading)
- [Menu](./menu)
- [Modal](./modal)
- [ModalTrigger](./modaltrigger)
- [Popover](./popover)
- [ProgressBar](./progressbar)
- [Radio](./radio)
- [SafeMarkdown](./safemarkdown)
- [Select](./select)
- [Skeleton](./skeleton)
- [Slider](./slider)
- [Steps](./steps)
- [Switch](./switch)
- [TableCollection](./tablecollection)
- [TableView](./tableview)
- [Tabs](./tabs)
- [Timer](./timer)
- [Tooltip](./tooltip)
- [Tree](./tree)
- [TreeSelect](./treeselect)
- [Typography](./typography)
- [UnsavedChangesModal](./unsavedchangesmodal)
- [Upload](./upload)
- [AutoComplete](./autocomplete.mdx)
- [Avatar](./avatar.mdx)
- [Badge](./badge.mdx)
- [Breadcrumb](./breadcrumb.mdx)
- [Button](./button.mdx)
- [ButtonGroup](./buttongroup.mdx)
- [CachedLabel](./cachedlabel.mdx)
- [Card](./card.mdx)
- [Checkbox](./checkbox.mdx)
- [Collapse](./collapse.mdx)
- [DatePicker](./datepicker.mdx)
- [Divider](./divider.mdx)
- [EditableTitle](./editabletitle.mdx)
- [EmptyState](./emptystate.mdx)
- [FaveStar](./favestar.mdx)
- [IconButton](./iconbutton.mdx)
- [Icons](./icons.mdx)
- [IconTooltip](./icontooltip.mdx)
- [InfoTooltip](./infotooltip.mdx)
- [Input](./input.mdx)
- [Label](./label.mdx)
- [List](./list.mdx)
- [ListViewCard](./listviewcard.mdx)
- [Loading](./loading.mdx)
- [Menu](./menu.mdx)
- [Modal](./modal.mdx)
- [ModalTrigger](./modaltrigger.mdx)
- [Popover](./popover.mdx)
- [ProgressBar](./progressbar.mdx)
- [Radio](./radio.mdx)
- [SafeMarkdown](./safemarkdown.mdx)
- [Select](./select.mdx)
- [Skeleton](./skeleton.mdx)
- [Slider](./slider.mdx)
- [Steps](./steps.mdx)
- [Switch](./switch.mdx)
- [TableCollection](./tablecollection.mdx)
- [TableView](./tableview.mdx)
- [Tabs](./tabs.mdx)
- [Timer](./timer.mdx)
- [Tooltip](./tooltip.mdx)
- [Tree](./tree.mdx)
- [TreeSelect](./treeselect.mdx)
- [Typography](./typography.mdx)
- [UnsavedChangesModal](./unsavedchangesmodal.mdx)
- [Upload](./upload.mdx)
@@ -327,13 +327,13 @@ stats.sort_stats('cumulative').print_stats(10)
## Resources
### Internal
- [Coding Guidelines](../guidelines/design-guidelines)
- [Testing Guide](../testing/overview)
- [Extension Architecture](../extensions/architecture)
- [Coding Guidelines](../guidelines/design-guidelines.md)
- [Testing Guide](../testing/overview.md)
- [Extension Architecture](../extensions/architecture.md)
### External
- [Google's Code Review Guide](https://google.github.io/eng-practices/review/)
- [Best Practices for Code Review](https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/)
- [The Art of Readable Code](https://www.oreilly.com/library/view/the-art-of/9781449318482/)
Next: [Reporting issues effectively](./issue-reporting)
Next: [Reporting issues effectively](./issue-reporting.md)
@@ -668,7 +668,7 @@ A series of checks will now run when you make a git commit.
## Linting
See [how tos](./howtos#linting)
See [how tos](./howtos.md#linting)
## GitHub Actions and `act`
@@ -77,7 +77,7 @@ Finally, never submit a PR that will put master branch in broken state. If the P
in `requirements.txt` pinned to a specific version which ensures that the application
build is deterministic.
- For TypeScript/JavaScript, include new libraries in `package.json`
- **Tests:** The pull request should include tests, either as doctests, unit tests, or both. Make sure to resolve all errors and test failures. See [Testing](./howtos#testing) for how to run tests.
- **Tests:** The pull request should include tests, either as doctests, unit tests, or both. Make sure to resolve all errors and test failures. See [Testing](./howtos.md#testing) for how to run tests.
- **Documentation:** If the pull request adds functionality, the docs should be updated as part of the same PR.
- **CI:** Reviewers will not review the code until all CI tests are passed. Sometimes there can be flaky tests. You can close and open PR to re-run CI test. Please report if the issue persists. After the CI fix has been deployed to `master`, please rebase your PR.
- **Code coverage:** Please ensure that code coverage does not decrease.
+1 -1
View File
@@ -282,7 +282,7 @@ You can now launch your VSCode debugger with the same config as above. VSCode wi
### Storybook
See the dedicated [Storybook documentation](../testing/storybook) for information on running Storybook locally and adding new stories.
See the dedicated [Storybook documentation](../testing/storybook.md) for information on running Storybook locally and adding new stories.
## Contributing Translations
@@ -413,6 +413,6 @@ Consider:
- **Feature Request**: Use feature request template
- **Question**: Use GitHub Discussions
- **Configuration Help**: Ask in Slack
- **Development Help**: See [Contributing Guide](./overview)
- **Development Help**: See [Contributing Guide](./overview.md)
Next: [Understanding the release process](./release-process)
Next: [Understanding the release process](./release-process.md)
+5 -5
View File
@@ -94,7 +94,7 @@ Look through the GitHub issues. Issues tagged with
Superset could always use better documentation,
whether as part of the official Superset docs,
in docstrings, `docs/*.rst` or even on the web as blog posts or
articles. See [Documentation](./howtos#contributing-to-documentation) for more details.
articles. See [Documentation](./howtos.md#contributing-to-documentation) for more details.
### Add Translations
@@ -103,7 +103,7 @@ text strings from Superset's UI. You can jump into the existing
language dictionaries at
`superset/translations/<language_code>/LC_MESSAGES/messages.po`, or
even create a dictionary for a new language altogether.
See [Translating](./howtos#contributing-translations) for more details.
See [Translating](./howtos.md#contributing-translations) for more details.
### Ask Questions
@@ -158,9 +158,9 @@ Security team members should also follow these general expectations:
Ready to contribute? Here's how to get started:
1. **[Set up your environment](./development-setup)** - Get Superset running locally
1. **[Set up your environment](./development-setup.md)** - Get Superset running locally
2. **[Find something to work on](#types-of-contributions)** - Pick an issue or feature
3. **[Submit your contribution](./submitting-pr)** - Create a pull request
4. **[Follow guidelines](./guidelines)** - Ensure code quality
3. **[Submit your contribution](./submitting-pr.md)** - Create a pull request
4. **[Follow guidelines](./guidelines.md)** - Ensure code quality
Welcome to the Apache Superset community! 🚀
@@ -466,4 +466,4 @@ Credit:
- [Release Scripts](https://github.com/apache/superset/tree/master/scripts/release)
- [Superset Repository Scripts](https://github.com/apache/superset/tree/master/scripts)
Next: Return to [Contributing Overview](./overview)
Next: Return to [Contributing Overview](./overview.md)
@@ -31,11 +31,11 @@ Learn how to create and submit high-quality pull requests to Apache Superset.
### Prerequisites
- [ ] Development environment is set up
- [ ] You've forked and cloned the repository
- [ ] You've read the [contributing overview](./overview)
- [ ] You've read the [contributing overview](./overview.md)
- [ ] You've found or created an issue to work on
### PR Readiness Checklist
- [ ] Code follows [coding guidelines](../guidelines/design-guidelines)
- [ ] Code follows [coding guidelines](../guidelines/design-guidelines.md)
- [ ] Tests are passing locally
- [ ] Linting passes (`pre-commit run --all-files`)
- [ ] Documentation is updated if needed
@@ -318,4 +318,4 @@ git push origin master
- **GitHub**: Tag @apache/superset-committers for attention
- **Mailing List**: dev@superset.apache.org
Next: [Understanding code review process](./code-review)
Next: [Understanding code review process](./code-review.md)
@@ -233,7 +233,7 @@ This architecture provides several key benefits:
Now that you understand the architecture, explore:
- **[Dependencies](./dependencies)** - Managing dependencies and understanding API stability
- **[Quick Start](./quick-start)** - Build your first extension
- **[Contribution Types](./contribution-types)** - What kinds of extensions you can build
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Dependencies](./dependencies.md)** - Managing dependencies and understanding API stability
- **[Quick Start](./quick-start.md)** - Build your first extension
- **[Contribution Types](./contribution-types.md)** - What kinds of extensions you can build
- **[Development](./development.md)** - Project structure, APIs, and development workflow
@@ -29,7 +29,7 @@ These UI components are available to Superset extension developers through the `
## Available Components
- [Alert](./alert)
- [Alert](./alert.mdx)
## Usage
@@ -90,4 +90,4 @@ InteractiveMyComponent.argTypes = {
## Interactive Documentation
For interactive examples with controls, visit the [Storybook](/storybook/?path=/docs/extension-components--docs).
For interactive examples with controls, run Storybook locally — see the [Storybook documentation](/developer-docs/testing/storybook).
@@ -110,7 +110,7 @@ editors.registerEditor(
);
```
See [Editors Extension Point](./extension-points/editors) for implementation details.
See [Editors Extension Point](./extension-points/editors.md) for implementation details.
## Backend
@@ -146,7 +146,7 @@ class MyExtensionAPI(RestApi):
from .api import MyExtensionAPI
```
**Note**: The [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects context and generates appropriate paths:
**Note**: The [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects context and generates appropriate paths:
- **Extension context**: `/extensions/{publisher}/{name}/` with ID prefixed as `extensions.{publisher}.{name}.{id}`
- **Host context**: `/api/v1/` with original ID
@@ -193,7 +193,7 @@ def get_summary() -> dict:
return {"status": "success", "result": {"queries_today": 42}}
```
See [MCP Integration](./mcp) for implementation details.
See [MCP Integration](./mcp.md) for implementation details.
### MCP Prompts
@@ -223,7 +223,7 @@ async def analysis_guide(ctx: Context) -> str:
"""
```
See [MCP Integration](./mcp) for implementation details.
See [MCP Integration](./mcp.md) for implementation details.
### Semantic Layers
@@ -161,6 +161,6 @@ Until then, monitor the Superset release notes and test your extensions with eac
## Next Steps
- **[Architecture](./architecture)** - Understand the extension system design
- **[Development](./development)** - Learn about APIs and development workflow
- **[Quick Start](./quick-start)** - Build your first extension
- **[Architecture](./architecture.md)** - Understand the extension system design
- **[Development](./development.md)** - Learn about APIs and development workflow
- **[Quick Start](./quick-start.md)** - Build your first extension
@@ -252,7 +252,7 @@ class DatasetReferencesAPI(RestApi):
### Automatic Context Detection
The [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects whether it's being used in host or extension code:
The [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects whether it's being used in host or extension code:
- **Extension APIs**: Registered under `/extensions/{publisher}/{name}/` with IDs prefixed as `extensions.{publisher}.{name}.{id}`
- **Host APIs**: Registered under `/api/v1/` with original IDs
@@ -217,6 +217,6 @@ const disposable = handle.registerCompletionProvider(provider);
## Next Steps
- **[SQL Lab Extension Points](./sqllab)** - Learn about other SQL Lab customizations
- **[Contribution Types](../contribution-types)** - Explore other contribution types
- **[Development](../development)** - Set up your development environment
- **[SQL Lab Extension Points](./sqllab.md)** - Learn about other SQL Lab customizations
- **[Contribution Types](../contribution-types.md)** - Explore other contribution types
- **[Development](../development.md)** - Set up your development environment
@@ -51,7 +51,7 @@ SQL Lab provides 4 extension points where extensions can contribute custom UI co
| **Right Sidebar** | `sqllab.rightSidebar` | ✓ | — | Custom panels (AI assistants, query analysis) |
| **Panels** | `sqllab.panels` | ✓ | ✓ | Custom tabs + toolbar actions (data profiling) |
\*Editor views are contributed via [Editor Contributions](./editors), not standard view contributions.
\*Editor views are contributed via [Editor Contributions](./editors.md), not standard view contributions.
## Customization Types
@@ -78,7 +78,7 @@ Extensions can add toolbar actions to **Left Sidebar**, **Editor**, and **Panels
### Custom Editors
Extensions can replace the default SQL editor with custom implementations (Monaco, CodeMirror, etc.). See [Editor Contributions](./editors) for details.
Extensions can replace the default SQL editor with custom implementations (Monaco, CodeMirror, etc.). See [Editor Contributions](./editors.md) for details.
## Examples
@@ -157,6 +157,6 @@ menus.registerMenuItem(
## Next Steps
- **[Contribution Types](../contribution-types)** - Learn about other contribution types (commands, menus)
- **[Development](../development)** - Set up your development environment
- **[Quick Start](../quick-start)** - Build a complete extension
- **[Contribution Types](../contribution-types.md)** - Learn about other contribution types (commands, menus)
- **[Development](../development.md)** - Set up your development environment
- **[Quick Start](../quick-start.md)** - Build a complete extension
+2 -2
View File
@@ -455,5 +455,5 @@ async def metrics_guide(ctx: Context) -> str:
## Next Steps
- **[Development](./development)** - Project structure, APIs, and dev workflow
- **[Security](./security)** - Security best practices for extensions
- **[Development](./development.md)** - Project structure, APIs, and dev workflow
- **[Security](./security.md)** - Security best practices for extensions
+10 -10
View File
@@ -47,13 +47,13 @@ Extension developers have access to pre-built UI components via `@apache-superse
## Next Steps
- **[Quick Start](./quick-start)** - Build your first extension with a complete walkthrough
- **[Architecture](./architecture)** - Design principles and system overview
- **[Dependencies](./dependencies)** - Managing dependencies and understanding API stability
- **[Contribution Types](./contribution-types)** - Available extension points
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Deployment](./deployment)** - Packaging and deploying extensions
- **[MCP Integration](./mcp)** - Adding AI agent capabilities using extensions
- **[Security](./security)** - Security considerations and best practices
- **[Tasks](./tasks)** - Framework for creating and managing long running tasks
- **[Community Extensions](./registry)** - Browse extensions shared by the community
- **[Quick Start](./quick-start.md)** - Build your first extension with a complete walkthrough
- **[Architecture](./architecture.md)** - Design principles and system overview
- **[Dependencies](./dependencies.md)** - Managing dependencies and understanding API stability
- **[Contribution Types](./contribution-types.md)** - Available extension points
- **[Development](./development.md)** - Project structure, APIs, and development workflow
- **[Deployment](./deployment.md)** - Packaging and deploying extensions
- **[MCP Integration](./mcp.md)** - Adding AI agent capabilities using extensions
- **[Security](./security.md)** - Security considerations and best practices
- **[Tasks](./tasks.md)** - Framework for creating and managing long running tasks
- **[Community Extensions](./registry.md)** - Browse extensions shared by the community
@@ -168,7 +168,7 @@ class HelloWorldAPI(RestApi):
**Key points:**
- Uses [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator with automatic context detection
- Uses [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator with automatic context detection
- Extends `RestApi` from `superset_core.rest_api.api`
- Uses Flask-AppBuilder decorators (`@expose`, `@protect`, `@safe`)
- Returns responses using `self.response(status_code, result=data)`
@@ -184,7 +184,7 @@ Replace the generated print statement with API import to trigger registration:
from .api import HelloWorldAPI # noqa: F401
```
The [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects extension context and registers your API with proper namespacing.
The [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects extension context and registers your API with proper namespacing.
## Step 5: Create Frontend Component
@@ -225,7 +225,7 @@ The `@apache-superset/core` package must be listed in both `peerDependencies` (t
The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting.
**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture#module-federation) for a full explanation.
**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture.md#module-federation) for a full explanation.
```javascript
const path = require('path');
@@ -496,7 +496,7 @@ Superset will extract and validate the extension metadata, load the assets, regi
Here's what happens when your extension loads:
1. **Superset starts**: Reads `manifest.json` from the `.supx` bundle and loads the backend entrypoint
2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator to register it automatically
2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator to register it automatically
3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file
4. **Module Federation**: Webpack loads your extension module and resolves `@apache-superset/core` to `window.superset`
5. **Registration**: The module executes at load time, calling `views.registerView` to register your panel
@@ -509,9 +509,9 @@ Here's what happens when your extension loads:
Now that you have a working extension, explore:
- **[Development](./development)** - Project structure, APIs, and development workflow
- **[Contribution Types](./contribution-types)** - Other contribution points beyond panels
- **[Deployment](./deployment)** - Packaging and deploying your extension
- **[Security](./security)** - Security best practices for extensions
- **[Development](./development.md)** - Project structure, APIs, and development workflow
- **[Contribution Types](./contribution-types.md)** - Other contribution points beyond panels
- **[Deployment](./deployment.md)** - Packaging and deploying your extension
- **[Security](./security.md)** - Security best practices for extensions
For a complete real-world example, examine the query insights extension in the Superset codebase.
+1 -1
View File
@@ -28,7 +28,7 @@ By default, extensions are disabled and must be explicitly enabled by setting th
For external extensions, administrators are responsible for evaluating and verifying the security of any extensions they choose to install, just as they would when installing third-party NPM or PyPI packages. At this stage, all extensions run in the same context as the host application, without additional sandboxing. This means that external extensions can impact the security and performance of a Superset environment in the same way as any other installed dependency.
We plan to introduce an optional sandboxed execution model for extensions in the future (as part of an additional SIP). Until then, administrators should exercise caution and follow best practices when selecting and deploying third-party extensions. A directory of community extensions is available in the [Community Extensions](./registry) page. Note that these extensions are not vetted by the Apache Superset project—administrators must evaluate each extension before installation.
We plan to introduce an optional sandboxed execution model for extensions in the future (as part of an additional SIP). Until then, administrators should exercise caution and follow best practices when selecting and deploying third-party extensions. A directory of community extensions is available in the [Community Extensions](./registry.md) page. Note that these extensions are not vetted by the Apache Superset project—administrators must evaluate each extension before installation.
**Any performance or security vulnerabilities introduced by external extensions should be reported directly to the extension author, not as Superset vulnerabilities.**
@@ -114,7 +114,7 @@ class CreateDashboardCommand(BaseCommand):
### Data Access Objects (DAOs)
See: [DAO Style Guidelines and Best Practices](./backend/dao-style-guidelines)
See: [DAO Style Guidelines and Best Practices](./backend/dao-style-guidelines.md)
## Testing
@@ -29,16 +29,16 @@ This is a list of statements that describe how we do frontend development in Sup
- We develop using TypeScript.
- See: [SIP-36](https://github.com/apache/superset/issues/9101)
- We use React for building components, and Redux to manage app/global state.
- See: [Component Style Guidelines and Best Practices](./frontend/component-style-guidelines)
- See: [Component Style Guidelines and Best Practices](./frontend/component-style-guidelines.md)
- We prefer functional components to class components and use hooks for local component state.
- We use [Ant Design](https://ant.design/) components from our component library whenever possible, only building our own custom components when it's required.
- See: [SIP-48](https://github.com/apache/superset/issues/11283)
- We use [@emotion](https://emotion.sh/docs/introduction) to provide styling for our components, co-locating styling within component files.
- See: [SIP-37](https://github.com/apache/superset/issues/9145)
- See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines)
- See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines.md)
- We use Jest for unit tests, React Testing Library for component tests, and Cypress for end-to-end tests.
- See: [SIP-56](https://github.com/apache/superset/issues/11830)
- See: [Testing Guidelines and Best Practices](../testing/testing-guidelines)
- See: [Testing Guidelines and Best Practices](../testing/testing-guidelines.md)
- We add tests for every new component or file added to the frontend.
- We organize our repo so similar files live near each other, and tests are co-located with the files they test.
- See: [SIP-61](https://github.com/apache/superset/issues/12098)
@@ -46,6 +46,6 @@ This is a list of statements that describe how we do frontend development in Sup
- We use OXC (oxlint) and Prettier to automatically fix lint errors and format the code.
- We do not debate code formatting style in PRs, instead relying on automated tooling to enforce it.
- If there's not a linting rule, we don't have a rule!
- See: [Linting How-Tos](../contributing/howtos#typescript--javascript)
- See: [Linting How-Tos](../contributing/howtos.md#typescript--javascript)
- We use [React Storybook](https://storybook.js.org/) to help preview/test and stabilize our components
- A public Storybook with components from the `master` branch is available [here](https://apache-superset.github.io/superset-ui/?path=/story/*)
@@ -31,7 +31,7 @@ This guide is intended primarily for reusable components. Whenever possible, all
## General Guidelines
- We use [Ant Design](https://ant.design/) as our component library. Do not build a new component if Ant Design provides one but rather instead extend or customize what the library provides
- Always style your component using Emotion and always prefer the theme variables whenever applicable. See: [Emotion Styling Guidelines and Best Practices](./emotion-styling-guidelines)
- Always style your component using Emotion and always prefer the theme variables whenever applicable. See: [Emotion Styling Guidelines and Best Practices](./emotion-styling-guidelines.md)
- All components should be made to be reusable whenever possible
- All components should follow the structure and best practices as detailed below
@@ -53,7 +53,7 @@ superset-frontend/src/components
**Storybook:** Components should come with a storybook file whenever applicable, with the following naming convention `\{ComponentName\}.stories.tsx`. More details about Storybook below
**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `\{ComponentName\}.test.tsx`. Read the [Testing Guidelines and Best Practices](../../testing/testing-guidelines) for more details
**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `\{ComponentName\}.test.tsx`. Read the [Testing Guidelines and Best Practices](../../testing/testing-guidelines.md) for more details
**Reference naming:** Use `PascalCase` for React components and `camelCase` for component instances
+4 -4
View File
@@ -37,16 +37,16 @@ Superset embraces a testing pyramid approach:
## Testing Documentation
### Frontend Testing
- **[Frontend Testing](./frontend-testing)** - Jest, React Testing Library, and component testing strategies
- **[Frontend Testing](./frontend-testing.md)** - Jest, React Testing Library, and component testing strategies
### Backend Testing
- **[Backend Testing](./backend-testing)** - pytest, database testing, and API testing patterns
- **[Backend Testing](./backend-testing.md)** - pytest, database testing, and API testing patterns
### End-to-End Testing
- **[E2E Testing](./e2e-testing)** - Playwright testing for complete user workflows
- **[E2E Testing](./e2e-testing.md)** - Playwright testing for complete user workflows
### CI/CD Integration
- **[CI/CD](./ci-cd)** - Continuous integration, automated testing, and deployment pipelines
- **[CI/CD](./ci-cd.md)** - Continuous integration, automated testing, and deployment pipelines
## Testing Tools & Frameworks
+1 -1
View File
@@ -254,7 +254,7 @@ const config: Config = {
'Apache Superset is a modern data exploration and visualization platform',
url: 'https://superset.apache.org',
baseUrl: '/',
onBrokenLinks: 'warn',
onBrokenLinks: 'throw',
markdown: {
mermaid: true,
hooks: {
+2 -1
View File
@@ -30,6 +30,7 @@
"lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md",
"update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme",
"eslint": "eslint .",
"lint:docs-links": "node scripts/lint-docs-links.mjs",
"version:add": "node scripts/manage-versions.mjs add",
"version:remove": "node scripts/manage-versions.mjs remove",
"version:add:docs": "node scripts/manage-versions.mjs add docs",
@@ -70,7 +71,7 @@
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.33",
"antd": "^6.3.7",
"antd": "^6.4.2",
"baseline-browser-mapping": "^2.10.29",
"caniuse-lite": "^1.0.30001792",
"docusaurus-plugin-openapi-docs": "^5.0.2",
+10 -2
View File
@@ -1260,7 +1260,15 @@ function generateCategoryIndex(category, components) {
};
const componentList = components
.sort((a, b) => a.componentName.localeCompare(b.componentName))
.map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()})`)
// `.mdx` suffix matches the actual component page files emitted
// by this generator (see the MDX wrappers below). The extension
// is required: Docusaurus only validates and rewrites *file-based*
// references (.md/.mdx). Bare relative paths bypass the file
// resolver and get emitted as raw HTML hrefs that the browser
// resolves against the current URL — which gives the wrong
// directory for trailing-slash routes and breaks SPA navigation.
// See docs/scripts/lint-docs-links.mjs.
.map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()}.mdx)`)
.join('\n');
return `---
@@ -1366,7 +1374,7 @@ This documentation is auto-generated from Storybook stories. To add or update co
4. Run \`yarn generate:superset-components\` in the \`docs/\` directory
:::info Work in Progress
This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation.
This component library is actively being documented. See the [Components TODO](./TODO.md) page for a list of components awaiting documentation.
:::
---
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env node
/**
* 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.
*/
/**
* lint-docs-links source-level checks for internal markdown links.
*
* Catches three failure modes that combine to break SPA navigation in
* a Docusaurus build:
*
* 1. BARE `[X](../foo)` with no extension. Skips
* Docusaurus's file resolver entirely. Emitted
* as a raw href and resolved by the browser
* against the current page URL usually the
* wrong directory for trailing-slash routes.
* `onBrokenLinks: 'throw'` cannot catch this.
*
* 2. MISSING_TARGET `[X](./gone.md)` with an extension, but no
* file at that path. The Docusaurus build
* catches this too (via
* `onBrokenMarkdownLinks: 'throw'`) but only
* after a multi-minute build. This script
* flags it in ~1s.
*
* 3. WRONG_EXTENSION `[X](./foo.md)` where the file is actually
* `foo.mdx` (or vice versa). Same end result
* as MISSING_TARGET, but the fix is one
* character so we report it as its own
* category with the actual extension on disk.
*
* Skips: fenced code blocks, asset-style targets (.png/.json/etc.),
* external URLs, in-page anchors, and the `versioned_docs/`
* snapshots (those are frozen historical content).
*
* Run from `docs/`:
* node scripts/lint-docs-links.mjs
*
* Exits 0 on clean, 1 on any finding.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const docsRoot = path.join(__dirname, '..');
const ROOTS = ['docs', 'admin_docs', 'developer_docs', 'components'];
const NON_DOC_EXTENSIONS = new Set([
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico',
'.json', '.yaml', '.yml', '.txt', '.csv',
'.zip', '.tar', '.gz',
'.pdf',
'.mp4', '.webm', '.mov',
]);
const LINK_RE = /\[[^\]\n]+?\]\((?<url>\.{1,2}\/[^)\s]+?)\)/g;
/**
* Classify a single markdown link from a source file.
* Returns one of: ok / bare / asset / missing-target / wrong-extension.
*/
function classifyLink(sourceFile, url) {
const stripped = url.split('#', 1)[0].split('?', 1)[0];
const ext = path.extname(stripped).toLowerCase();
// Non-doc assets — legit bare extensions, leave alone.
if (ext && NON_DOC_EXTENSIONS.has(ext)) {
return { kind: 'asset' };
}
// Anything that doesn't end in .md/.mdx is a bare relative URL.
if (ext !== '.md' && ext !== '.mdx') {
return { kind: 'bare' };
}
// Has a .md/.mdx extension — make sure the target exists.
const target = path.normalize(path.join(path.dirname(sourceFile), stripped));
if (fs.existsSync(target)) {
return { kind: 'ok' };
}
// Target doesn't exist — check if the OTHER extension does.
const otherExt = ext === '.md' ? '.mdx' : '.md';
const otherTarget = target.slice(0, -ext.length) + otherExt;
if (fs.existsSync(otherTarget)) {
return { kind: 'wrong-extension', actualExt: otherExt };
}
return { kind: 'missing-target' };
}
function* walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (
entry.name.startsWith('.') ||
entry.name === 'node_modules' ||
entry.name.endsWith('_versioned_docs') ||
entry.name === 'versioned_docs'
) {
continue;
}
yield* walk(full);
} else if (entry.isFile()) {
if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
yield full;
}
}
}
}
function lintFile(file) {
const src = fs.readFileSync(file, 'utf8');
const findings = [];
let inFence = false;
const lines = src.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trimStart().startsWith('```')) {
inFence = !inFence;
continue;
}
if (inFence) continue;
for (const m of line.matchAll(LINK_RE)) {
const url = m.groups.url;
const result = classifyLink(file, url);
if (result.kind !== 'ok' && result.kind !== 'asset') {
findings.push({ line: i + 1, url, ...result });
}
}
}
return findings;
}
const findings = [];
for (const root of ROOTS) {
const abs = path.join(docsRoot, root);
if (!fs.existsSync(abs)) continue;
for (const file of walk(abs)) {
for (const f of lintFile(file)) {
findings.push({ file: path.relative(docsRoot, file), ...f });
}
}
}
if (findings.length === 0) {
console.log('✓ lint-docs-links: no broken internal links found');
process.exit(0);
}
// Group by kind for readable output.
const groups = {
bare: [],
'wrong-extension': [],
'missing-target': [],
};
for (const f of findings) {
groups[f.kind].push(f);
}
console.error(
`✗ lint-docs-links: found ${findings.length} broken internal link(s)`
);
console.error('');
if (groups.bare.length) {
console.error(
` ${groups.bare.length} bare relative link(s) (no .md/.mdx extension)`
);
console.error(
" Docusaurus's file resolver skips these; the browser resolves them"
);
console.error(
' against the current page URL — wrong directory for trailing-slash routes.'
);
console.error(' Add the extension so the file resolver picks them up.');
console.error('');
for (const f of groups.bare) {
console.error(` ${f.file}:${f.line} ${f.url}`);
}
console.error('');
}
if (groups['wrong-extension'].length) {
console.error(
` ${groups['wrong-extension'].length} wrong-extension link(s) (.md vs .mdx mismatch)`
);
console.error(' The target file exists with the other extension on disk.');
console.error('');
for (const f of groups['wrong-extension']) {
console.error(
` ${f.file}:${f.line} ${f.url} → use ${f.actualExt}`
);
}
console.error('');
}
if (groups['missing-target'].length) {
console.error(
` ${groups['missing-target'].length} missing-target link(s) (file doesn't exist)`
);
console.error('');
for (const f of groups['missing-target']) {
console.error(` ${f.file}:${f.line} ${f.url}`);
}
console.error('');
}
process.exit(1);
@@ -20,12 +20,12 @@ Alerts and reports are disabled by default. To turn them on, you need to do some
#### In your `superset_config.py` or `superset_config_docker.py`
- `"ALERT_REPORTS"` [feature flag](/docs/6.0.0/configuration/configuring-superset#feature-flags) must be turned to True.
- `"ALERT_REPORTS"` [feature flag](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) must be turned to True.
- `beat_schedule` in CeleryConfig must contain schedule for `reports.scheduler`.
- At least one of those must be configured, depending on what you want to use:
- emails: `SMTP_*` settings
- Slack messages: `SLACK_API_TOKEN`
- Users can customize the email subject by including date code placeholders, which will automatically be replaced with the corresponding UTC date when the email is sent. To enable this functionality, activate the `"DATE_FORMAT_IN_EMAIL_SUBJECT"` [feature flag](/docs/6.0.0/configuration/configuring-superset#feature-flags). This enables date formatting in email subjects, preventing all reporting emails from being grouped into the same thread (optional for the reporting feature).
- Users can customize the email subject by including date code placeholders, which will automatically be replaced with the corresponding UTC date when the email is sent. To enable this functionality, activate the `"DATE_FORMAT_IN_EMAIL_SUBJECT"` [feature flag](/user-docs/6.0.0/configuration/configuring-superset#feature-flags). This enables date formatting in email subjects, preventing all reporting emails from being grouped into the same thread (optional for the reporting feature).
- Use date codes from [strftime.org](https://strftime.org/) to create the email subject.
- If no date code is provided, the original string will be used as the email subject.
@@ -38,7 +38,7 @@ Screenshots will be taken but no messages actually sent as long as `ALERT_REPORT
- You must install a headless browser, for taking screenshots of the charts and dashboards. Only Firefox and Chrome are currently supported.
> If you choose Chrome, you must also change the value of `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
Note: All the components required (Firefox headless browser, Redis, Postgres db, celery worker and celery beat) are present in the *dev* docker image if you are following [Installing Superset Locally](/docs/6.0.0/installation/docker-compose/).
Note: All the components required (Firefox headless browser, Redis, Postgres db, celery worker and celery beat) are present in the *dev* docker image if you are following [Installing Superset Locally](/user-docs/6.0.0/installation/docker-compose/).
All you need to do is add the required config variables described in this guide (See `Detailed Config`).
If you are running a non-dev docker image, e.g., a stable release like `apache/superset:3.1.0`, that image does not include a headless browser. Only the `superset_worker` container needs this headless browser to browse to the target chart or dashboard.
@@ -70,7 +70,7 @@ Note: when you configure an alert or a report, the Slack channel list takes chan
### Kubernetes-specific
- You must have a `celery beat` pod running. If you're using the chart included in the GitHub repository under [helm/superset](https://github.com/apache/superset/tree/master/helm/superset), you need to put `supersetCeleryBeat.enabled = true` in your values override.
- You can see the dedicated docs about [Kubernetes installation](/docs/6.0.0/installation/kubernetes) for more details.
- You can see the dedicated docs about [Kubernetes installation](/user-docs/6.0.0/installation/kubernetes) for more details.
### Docker Compose specific
@@ -78,11 +78,11 @@ Caching for SQL Lab query results is used when async queries are enabled and is
Note that this configuration does not use a flask-caching dictionary for its configuration, but
instead requires a cachelib object.
See [Async Queries via Celery](/docs/6.0.0/configuration/async-queries-celery) for details.
See [Async Queries via Celery](/user-docs/6.0.0/configuration/async-queries-celery) for details.
## Caching Thumbnails
This is an optional feature that can be turned on by activating its [feature flag](/docs/6.0.0/configuration/configuring-superset#feature-flags) on config:
This is an optional feature that can be turned on by activating its [feature flag](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) on config:
```
FEATURE_FLAGS = {
@@ -37,7 +37,7 @@ ENV SUPERSET_CONFIG_PATH /app/superset_config.py
```
Docker compose deployments handle application configuration differently using specific conventions.
Refer to the [docker compose tips & configuration](/docs/6.0.0/installation/docker-compose#docker-compose-tips--configuration)
Refer to the [docker compose tips & configuration](/user-docs/6.0.0/installation/docker-compose#docker-compose-tips--configuration)
for details.
The following is an example of just a few of the parameters you can set in your `superset_config.py` file:
@@ -254,7 +254,7 @@ flask --app "superset.app:create_app(superset_app_root='/analytics')"
### Docker builds
The [docker compose](/docs/6.0.0/installation/docker-compose#configuring-further) developer
The [docker compose](/user-docs/6.0.0/installation/docker-compose#configuring-further) developer
configuration includes an additional environmental variable,
[`SUPERSET_APP_ROOT`](https://github.com/apache/superset/blob/master/docker/.env),
to simplify the process of setting up a non-default root path across the services.
@@ -449,4 +449,4 @@ FEATURE_FLAGS = {
}
```
A current list of feature flags can be found in the [Feature Flags](/docs/6.0.0/configuration/feature-flags) documentation.
A current list of feature flags can be found in the [Feature Flags](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) documentation.
@@ -14,7 +14,7 @@ in your environment.
Youll need to install the required packages for the database you want to use as your metadata database
as well as the packages needed to connect to the databases you want to access through Superset.
For information about setting up Superset's metadata database, please refer to
installation documentations ([Docker Compose](/docs/6.0.0/installation/docker-compose), [Kubernetes](/docs/6.0.0/installation/kubernetes))
installation documentations ([Docker Compose](/user-docs/6.0.0/installation/docker-compose), [Kubernetes](/user-docs/6.0.0/installation/kubernetes))
:::
This documentation tries to keep pointer to the different drivers for commonly used database
@@ -26,7 +26,7 @@ Superset requires a Python [DB-API database driver](https://peps.python.org/pep-
and a [SQLAlchemy dialect](https://docs.sqlalchemy.org/en/20/dialects/) to be installed for
each database engine you want to connect to.
You can read more [here](/docs/6.0.0/configuration/databases#installing-drivers-in-docker-images) about how to
You can read more [here](/user-docs/6.0.0/configuration/databases#installing-drivers-in-docker-images) about how to
install new database drivers into your Superset configuration.
### Supported Databases and Dependencies
@@ -37,53 +37,53 @@ are compatible with Superset.
| <div style={{width: '150px'}}>Database</div> | PyPI package | Connection String |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [AWS Athena](/docs/6.0.0/configuration/databases#aws-athena) | `pip install pyathena[pandas]` , `pip install PyAthenaJDBC` | `awsathena+rest://{access_key_id}:{access_key}@athena.{region}.amazonaws.com/{schema}?s3_staging_dir={s3_staging_dir}&...` |
| [AWS DynamoDB](/docs/6.0.0/configuration/databases#aws-dynamodb) | `pip install pydynamodb` | `dynamodb://{access_key_id}:{secret_access_key}@dynamodb.{region_name}.amazonaws.com?connector=superset` |
| [AWS Redshift](/docs/6.0.0/configuration/databases#aws-redshift) | `pip install sqlalchemy-redshift` | `redshift+psycopg2://<userName>:<DBPassword>@<AWS End Point>:5439/<Database Name>` |
| [Apache Doris](/docs/6.0.0/configuration/databases#apache-doris) | `pip install pydoris` | `doris://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
| [Apache Drill](/docs/6.0.0/configuration/databases#apache-drill) | `pip install sqlalchemy-drill` | `drill+sadrill://<username>:<password>@<host>:<port>/<storage_plugin>`, often useful: `?use_ssl=True/False` |
| [Apache Druid](/docs/6.0.0/configuration/databases#apache-druid) | `pip install pydruid` | `druid://<User>:<password>@<Host>:<Port-default-9088>/druid/v2/sql` |
| [Apache Hive](/docs/6.0.0/configuration/databases#hive) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
| [Apache Impala](/docs/6.0.0/configuration/databases#apache-impala) | `pip install impyla` | `impala://{hostname}:{port}/{database}` |
| [Apache Kylin](/docs/6.0.0/configuration/databases#apache-kylin) | `pip install kylinpy` | `kylin://<username>:<password>@<hostname>:<port>/<project>?<param1>=<value1>&<param2>=<value2>` |
| [Apache Pinot](/docs/6.0.0/configuration/databases#apache-pinot) | `pip install pinotdb` | `pinot://BROKER:5436/query?server=http://CONTROLLER:5983/` |
| [Apache Solr](/docs/6.0.0/configuration/databases#apache-solr) | `pip install sqlalchemy-solr` | `solr://{username}:{password}@{hostname}:{port}/{server_path}/{collection}` |
| [Apache Spark SQL](/docs/6.0.0/configuration/databases#apache-spark-sql) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
| [Ascend.io](/docs/6.0.0/configuration/databases#ascendio) | `pip install impyla` | `ascend://{username}:{password}@{hostname}:{port}/{database}?auth_mechanism=PLAIN;use_ssl=true` |
| [Azure MS SQL](/docs/6.0.0/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema` |
| [ClickHouse](/docs/6.0.0/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
| [CockroachDB](/docs/6.0.0/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
| [Couchbase](/docs/6.0.0/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
| [CrateDB](/docs/6.0.0/configuration/databases#cratedb) | `pip install sqlalchemy-cratedb` | `crate://{username}:{password}@{hostname}:{port}`, often useful: `?ssl=true/false` or `?schema=testdrive`. |
| [Denodo](/docs/6.0.0/configuration/databases#denodo) | `pip install denodo-sqlalchemy` | `denodo://{username}:{password}@{hostname}:{port}/{database}` |
| [Dremio](/docs/6.0.0/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:32010`, often useful: `?UseEncryption=true/false`. For Legacy ODBC: `dremio+pyodbc://{username}:{password}@{host}:31010` |
| [Elasticsearch](/docs/6.0.0/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
| [Exasol](/docs/6.0.0/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
| [Google BigQuery](/docs/6.0.0/configuration/databases#google-bigquery) | `pip install sqlalchemy-bigquery` | `bigquery://{project_id}` |
| [Google Sheets](/docs/6.0.0/configuration/databases#google-sheets) | `pip install shillelagh[gsheetsapi]` | `gsheets://` |
| [Firebolt](/docs/6.0.0/configuration/databases#firebolt) | `pip install firebolt-sqlalchemy` | `firebolt://{client_id}:{client_secret}@{database}/{engine_name}?account_name={name}` |
| [Hologres](/docs/6.0.0/configuration/databases#hologres) | `pip install psycopg2` | `postgresql+psycopg2://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [IBM Db2](/docs/6.0.0/configuration/databases#ibm-db2) | `pip install ibm_db_sa` | `db2+ibm_db://` |
| [IBM Netezza Performance Server](/docs/6.0.0/configuration/databases#ibm-netezza-performance-server) | `pip install nzalchemy` | `netezza+nzpy://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [MySQL](/docs/6.0.0/configuration/databases#mysql) | `pip install mysqlclient` | `mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [OceanBase](/docs/6.0.0/configuration/databases#oceanbase) | `pip install oceanbase_py` | `oceanbase://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [Oracle](/docs/6.0.0/configuration/databases#oracle) | `pip install cx_Oracle` | `oracle://<username>:<password>@<hostname>:<port>` |
| [Parseable](/docs/6.0.0/configuration/databases#parseable) | `pip install sqlalchemy-parseable` | `parseable://<UserName>:<DBPassword>@<Database Host>/<Stream Name>` |
| [PostgreSQL](/docs/6.0.0/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [Presto](/docs/6.0.0/configuration/databases#presto) | `pip install pyhive` | `presto://{username}:{password}@{hostname}:{port}/{database}` |
| [SAP Hana](/docs/6.0.0/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache_superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
| [SingleStore](/docs/6.0.0/configuration/databases#singlestore) | `pip install sqlalchemy-singlestoredb` | `singlestoredb://{username}:{password}@{host}:{port}/{database}` |
| [StarRocks](/docs/6.0.0/configuration/databases#starrocks) | `pip install starrocks` | `starrocks://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
| [Snowflake](/docs/6.0.0/configuration/databases#snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
| [AWS Athena](/user-docs/6.0.0/configuration/databases#aws-athena) | `pip install pyathena[pandas]` , `pip install PyAthenaJDBC` | `awsathena+rest://{access_key_id}:{access_key}@athena.{region}.amazonaws.com/{schema}?s3_staging_dir={s3_staging_dir}&...` |
| [AWS DynamoDB](/user-docs/6.0.0/configuration/databases#aws-dynamodb) | `pip install pydynamodb` | `dynamodb://{access_key_id}:{secret_access_key}@dynamodb.{region_name}.amazonaws.com?connector=superset` |
| [AWS Redshift](/user-docs/6.0.0/configuration/databases#aws-redshift) | `pip install sqlalchemy-redshift` | `redshift+psycopg2://<userName>:<DBPassword>@<AWS End Point>:5439/<Database Name>` |
| [Apache Doris](/user-docs/6.0.0/configuration/databases#apache-doris) | `pip install pydoris` | `doris://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
| [Apache Drill](/user-docs/6.0.0/configuration/databases#apache-drill) | `pip install sqlalchemy-drill` | `drill+sadrill://<username>:<password>@<host>:<port>/<storage_plugin>`, often useful: `?use_ssl=True/False` |
| [Apache Druid](/user-docs/6.0.0/configuration/databases#apache-druid) | `pip install pydruid` | `druid://<User>:<password>@<Host>:<Port-default-9088>/druid/v2/sql` |
| [Apache Hive](/user-docs/6.0.0/configuration/databases#hive) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
| [Apache Impala](/user-docs/6.0.0/configuration/databases#apache-impala) | `pip install impyla` | `impala://{hostname}:{port}/{database}` |
| [Apache Kylin](/user-docs/6.0.0/configuration/databases#apache-kylin) | `pip install kylinpy` | `kylin://<username>:<password>@<hostname>:<port>/<project>?<param1>=<value1>&<param2>=<value2>` |
| [Apache Pinot](/user-docs/6.0.0/configuration/databases#apache-pinot) | `pip install pinotdb` | `pinot://BROKER:5436/query?server=http://CONTROLLER:5983/` |
| [Apache Solr](/user-docs/6.0.0/configuration/databases#apache-solr) | `pip install sqlalchemy-solr` | `solr://{username}:{password}@{hostname}:{port}/{server_path}/{collection}` |
| [Apache Spark SQL](/user-docs/6.0.0/configuration/databases#apache-spark-sql) | `pip install pyhive` | `hive://hive@{hostname}:{port}/{database}` |
| [Ascend.io](/user-docs/6.0.0/configuration/databases#ascendio) | `pip install impyla` | `ascend://{username}:{password}@{hostname}:{port}/{database}?auth_mechanism=PLAIN;use_ssl=true` |
| [Azure MS SQL](/user-docs/6.0.0/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://UserName@presetSQL:TestPassword@presetSQL.database.windows.net:1433/TestSchema` |
| [ClickHouse](/user-docs/6.0.0/configuration/databases#clickhouse) | `pip install clickhouse-connect` | `clickhousedb://{username}:{password}@{hostname}:{port}/{database}` |
| [CockroachDB](/user-docs/6.0.0/configuration/databases#cockroachdb) | `pip install cockroachdb` | `cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable` |
| [Couchbase](/user-docs/6.0.0/configuration/databases#couchbase) | `pip install couchbase-sqlalchemy` | `couchbase://{username}:{password}@{hostname}:{port}?truststorepath={ssl certificate path}` |
| [CrateDB](/user-docs/6.0.0/configuration/databases#cratedb) | `pip install sqlalchemy-cratedb` | `crate://{username}:{password}@{hostname}:{port}`, often useful: `?ssl=true/false` or `?schema=testdrive`. |
| [Denodo](/user-docs/6.0.0/configuration/databases#denodo) | `pip install denodo-sqlalchemy` | `denodo://{username}:{password}@{hostname}:{port}/{database}` |
| [Dremio](/user-docs/6.0.0/configuration/databases#dremio) | `pip install sqlalchemy_dremio` |`dremio+flight://{username}:{password}@{host}:32010`, often useful: `?UseEncryption=true/false`. For Legacy ODBC: `dremio+pyodbc://{username}:{password}@{host}:31010` |
| [Elasticsearch](/user-docs/6.0.0/configuration/databases#elasticsearch) | `pip install elasticsearch-dbapi` | `elasticsearch+http://{user}:{password}@{host}:9200/` |
| [Exasol](/user-docs/6.0.0/configuration/databases#exasol) | `pip install sqlalchemy-exasol` | `exa+pyodbc://{username}:{password}@{hostname}:{port}/my_schema?CONNECTIONLCALL=en_US.UTF-8&driver=EXAODBC` |
| [Google BigQuery](/user-docs/6.0.0/configuration/databases#google-bigquery) | `pip install sqlalchemy-bigquery` | `bigquery://{project_id}` |
| [Google Sheets](/user-docs/6.0.0/configuration/databases#google-sheets) | `pip install shillelagh[gsheetsapi]` | `gsheets://` |
| [Firebolt](/user-docs/6.0.0/configuration/databases#firebolt) | `pip install firebolt-sqlalchemy` | `firebolt://{client_id}:{client_secret}@{database}/{engine_name}?account_name={name}` |
| [Hologres](/user-docs/6.0.0/configuration/databases#hologres) | `pip install psycopg2` | `postgresql+psycopg2://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [IBM Db2](/user-docs/6.0.0/configuration/databases#ibm-db2) | `pip install ibm_db_sa` | `db2+ibm_db://` |
| [IBM Netezza Performance Server](/user-docs/6.0.0/configuration/databases#ibm-netezza-performance-server) | `pip install nzalchemy` | `netezza+nzpy://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [MySQL](/user-docs/6.0.0/configuration/databases#mysql) | `pip install mysqlclient` | `mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [OceanBase](/user-docs/6.0.0/configuration/databases#oceanbase) | `pip install oceanbase_py` | `oceanbase://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [Oracle](/user-docs/6.0.0/configuration/databases#oracle) | `pip install cx_Oracle` | `oracle://<username>:<password>@<hostname>:<port>` |
| [Parseable](/user-docs/6.0.0/configuration/databases#parseable) | `pip install sqlalchemy-parseable` | `parseable://<UserName>:<DBPassword>@<Database Host>/<Stream Name>` |
| [PostgreSQL](/user-docs/6.0.0/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [Presto](/user-docs/6.0.0/configuration/databases#presto) | `pip install pyhive` | `presto://{username}:{password}@{hostname}:{port}/{database}` |
| [SAP Hana](/user-docs/6.0.0/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache_superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
| [SingleStore](/user-docs/6.0.0/configuration/databases#singlestore) | `pip install sqlalchemy-singlestoredb` | `singlestoredb://{username}:{password}@{host}:{port}/{database}` |
| [StarRocks](/user-docs/6.0.0/configuration/databases#starrocks) | `pip install starrocks` | `starrocks://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
| [Snowflake](/user-docs/6.0.0/configuration/databases#snowflake) | `pip install snowflake-sqlalchemy` | `snowflake://{user}:{password}@{account}.{region}/{database}?role={role}&warehouse={warehouse}` |
| SQLite | No additional library needed | `sqlite://path/to/file.db?check_same_thread=false` |
| [SQL Server](/docs/6.0.0/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://<Username>:<Password>@<Host>:<Port-default:1433>/<Database Name>` |
| [TDengine](/docs/6.0.0/configuration/databases#tdengine) | `pip install taospy` `pip install taos-ws-py` | `taosws://<user>:<password>@<host>:<port>` |
| [Teradata](/docs/6.0.0/configuration/databases#teradata) | `pip install teradatasqlalchemy` | `teradatasql://{user}:{password}@{host}` |
| [TimescaleDB](/docs/6.0.0/configuration/databases#timescaledb) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>:<Port>/<Database Name>` |
| [Trino](/docs/6.0.0/configuration/databases#trino) | `pip install trino` | `trino://{username}:{password}@{hostname}:{port}/{catalog}` |
| [Vertica](/docs/6.0.0/configuration/databases#vertica) | `pip install sqlalchemy-vertica-python` | `vertica+vertica_python://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [YDB](/docs/6.0.0/configuration/databases#ydb) | `pip install ydb-sqlalchemy` | `ydb://{host}:{port}/{database_name}` |
| [YugabyteDB](/docs/6.0.0/configuration/databases#yugabytedb) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [SQL Server](/user-docs/6.0.0/configuration/databases#sql-server) | `pip install pymssql` | `mssql+pymssql://<Username>:<Password>@<Host>:<Port-default:1433>/<Database Name>` |
| [TDengine](/user-docs/6.0.0/configuration/databases#tdengine) | `pip install taospy` `pip install taos-ws-py` | `taosws://<user>:<password>@<host>:<port>` |
| [Teradata](/user-docs/6.0.0/configuration/databases#teradata) | `pip install teradatasqlalchemy` | `teradatasql://{user}:{password}@{host}` |
| [TimescaleDB](/user-docs/6.0.0/configuration/databases#timescaledb) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>:<Port>/<Database Name>` |
| [Trino](/user-docs/6.0.0/configuration/databases#trino) | `pip install trino` | `trino://{username}:{password}@{hostname}:{port}/{catalog}` |
| [Vertica](/user-docs/6.0.0/configuration/databases#vertica) | `pip install sqlalchemy-vertica-python` | `vertica+vertica_python://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [YDB](/user-docs/6.0.0/configuration/databases#ydb) | `pip install ydb-sqlalchemy` | `ydb://{host}:{port}/{database_name}` |
| [YugabyteDB](/user-docs/6.0.0/configuration/databases#yugabytedb) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
---
@@ -109,7 +109,7 @@ The connector library installation process is the same for all additional librar
#### 1. Determine the driver you need
Consult the [list of database drivers](/docs/6.0.0/configuration/databases)
Consult the [list of database drivers](/user-docs/6.0.0/configuration/databases)
and find the PyPI package needed to connect to your database. In this example, we're connecting
to a MySQL database, so we'll need the `mysqlclient` connector library.
@@ -165,11 +165,11 @@ to your database via the Superset web UI.
As an admin user, go to Settings -> Data: Database Connections and click the +DATABASE button.
From there, follow the steps on the
[Using Database Connection UI page](/docs/6.0.0/configuration/databases#connecting-through-the-ui).
[Using Database Connection UI page](/user-docs/6.0.0/configuration/databases#connecting-through-the-ui).
Consult the page for your specific database type in the Superset documentation to determine
the connection string and any other parameters you need to input. For instance,
on the [MySQL page](/docs/6.0.0/configuration/databases#mysql), we see that the connection string
on the [MySQL page](/user-docs/6.0.0/configuration/databases#mysql), we see that the connection string
to a local MySQL database differs depending on whether the setup is running on Linux or Mac.
Click the “Test Connection” button, which should result in a popup message saying,
@@ -407,7 +407,7 @@ this:
crate://<username>:<password>@<clustername>.cratedb.net:4200/?ssl=true
```
Follow the steps [here](/docs/6.0.0/configuration/databases#installing-database-drivers)
Follow the steps [here](/user-docs/6.0.0/configuration/databases#installing-database-drivers)
to install the CrateDB connector package when setting up Superset locally using
Docker Compose.
@@ -782,7 +782,7 @@ The recommended connector library for BigQuery is
##### Install BigQuery Driver
Follow the steps [here](/docs/6.0.0/configuration/databases#installing-drivers-in-docker-images) about how to
Follow the steps [here](/user-docs/6.0.0/configuration/databases#installing-drivers-in-docker-images) about how to
install new database drivers when setting up Superset locally via docker compose.
```bash
@@ -1177,7 +1177,7 @@ risingwave://root@{hostname}:{port}/{database}?sslmode=disable
##### Install Snowflake Driver
Follow the steps [here](/docs/6.0.0/configuration/databases#installing-database-drivers) about how to
Follow the steps [here](/user-docs/6.0.0/configuration/databases#installing-database-drivers) about how to
install new database drivers when setting up Superset locally via docker compose.
```bash
@@ -51,7 +51,7 @@ Restart Superset for this configuration change to take effect.
#### Making a Dashboard Public
1. Add the `'DASHBOARD_RBAC': True` [Feature Flag](/docs/6.0.0/configuration/feature-flags) to `superset_config.py`
1. Add the `'DASHBOARD_RBAC': True` [Feature Flag](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) to `superset_config.py`
2. Add the `Public` role to your dashboard as described [here](https://superset.apache.org/docs/using-superset/creating-your-first-dashboard/#manage-access-to-dashboards)
#### Embedding a Public Dashboard
@@ -10,7 +10,7 @@ version: 1
## Jinja Templates
SQL Lab and Explore supports [Jinja templating](https://jinja.palletsprojects.com/en/2.11.x/) in queries.
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/docs/6.0.0/configuration/configuring-superset#feature-flags) needs to be enabled in
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) needs to be enabled in
`superset_config.py`. When templating is enabled, python code can be embedded in virtual datasets and
in Custom SQL in the filter and metric controls in Explore. By default, the following variables are
made available in the Jinja context:
@@ -20,7 +20,7 @@ To help make the problem somewhat tractable—given that Apache Superset has no
To strive for data consistency (regardless of the timezone of the client) the Apache Superset backend tries to ensure that any timestamp sent to the client has an explicit (or semi-explicit as in the case with [Epoch time](https://en.wikipedia.org/wiki/Unix_time) which is always in reference to UTC) timezone encoded within.
The challenge however lies with the slew of [database engines](/docs/6.0.0/configuration/databases#installing-drivers-in-docker-images) which Apache Superset supports and various inconsistencies between their [Python Database API (DB-API)](https://www.python.org/dev/peps/pep-0249/) implementations combined with the fact that we use [Pandas](https://pandas.pydata.org/) to read SQL into a DataFrame prior to serializing to JSON. Regrettably Pandas ignores the DB-API [type_code](https://www.python.org/dev/peps/pep-0249/#type-objects) relying by default on the underlying Python type returned by the DB-API. Currently only a subset of the supported database engines work correctly with Pandas, i.e., ensuring timestamps without an explicit timestamp are serializd to JSON with the server timezone, thus guaranteeing the client will display timestamps in a consistent manner irrespective of the client's timezone.
The challenge however lies with the slew of [database engines](/user-docs/6.0.0/configuration/databases#installing-drivers-in-docker-images) which Apache Superset supports and various inconsistencies between their [Python Database API (DB-API)](https://www.python.org/dev/peps/pep-0249/) implementations combined with the fact that we use [Pandas](https://pandas.pydata.org/) to read SQL into a DataFrame prior to serializing to JSON. Regrettably Pandas ignores the DB-API [type_code](https://www.python.org/dev/peps/pep-0249/#type-objects) relying by default on the underlying Python type returned by the DB-API. Currently only a subset of the supported database engines work correctly with Pandas, i.e., ensuring timestamps without an explicit timestamp are serializd to JSON with the server timezone, thus guaranteeing the client will display timestamps in a consistent manner irrespective of the client's timezone.
For example the following is a comparison of MySQL and Presto,
@@ -77,7 +77,7 @@ Look through the GitHub issues. Issues tagged with
Superset could always use better documentation,
whether as part of the official Superset docs,
in docstrings, `docs/*.rst` or even on the web as blog posts or
articles. See [Documentation](/docs/6.0.0/contributing/howtos#contributing-to-documentation) for more details.
articles. See [Documentation](/user-docs/6.0.0/contributing/howtos#contributing-to-documentation) for more details.
### Add Translations
@@ -599,7 +599,7 @@ export enum FeatureFlag {
those specified under FEATURE_FLAGS in `superset_config.py`. For example, `DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False }` in `superset/config.py` and `FEATURE_FLAGS = { 'BAR': True, 'BAZ': True }` in `superset_config.py` will result
in combined feature flags of `{ 'FOO': True, 'BAR': True, 'BAZ': True }`.
The current status of the usability of each flag (stable vs testing, etc) can be found in the [Feature Flags](/docs/6.0.0/configuration/feature-flags) documentation.
The current status of the usability of each flag (stable vs testing, etc) can be found in the [Feature Flags](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) documentation.
## Git Hooks
@@ -614,7 +614,7 @@ A series of checks will now run when you make a git commit.
## Linting
See [how tos](/docs/6.0.0/contributing/howtos#linting)
See [how tos](/user-docs/6.0.0/contributing/howtos#linting)
## GitHub Actions and `act`
@@ -57,7 +57,7 @@ Finally, never submit a PR that will put master branch in broken state. If the P
in `requirements.txt` pinned to a specific version which ensures that the application
build is deterministic.
- For TypeScript/JavaScript, include new libraries in `package.json`
- **Tests:** The pull request should include tests, either as doctests, unit tests, or both. Make sure to resolve all errors and test failures. See [Testing](/docs/6.0.0/contributing/howtos#testing) for how to run tests.
- **Tests:** The pull request should include tests, either as doctests, unit tests, or both. Make sure to resolve all errors and test failures. See [Testing](/user-docs/6.0.0/contributing/howtos#testing) for how to run tests.
- **Documentation:** If the pull request adds functionality, the docs should be updated as part of the same PR.
- **CI:** Reviewers will not review the code until all CI tests are passed. Sometimes there can be flaky tests. You can close and open PR to re-run CI test. Please report if the issue persists. After the CI fix has been deployed to `master`, please rebase your PR.
- **Code coverage:** Please ensure that code coverage does not decrease.
+7 -7
View File
@@ -51,11 +51,11 @@ multiple tables as long as your database account has access to the tables.
## How do I create my own visualization?
We recommend reading the instructions in
[Creating Visualization Plugins](/docs/6.0.0/contributing/howtos#creating-visualization-plugins).
[Creating Visualization Plugins](/user-docs/6.0.0/contributing/howtos#creating-visualization-plugins).
## Can I upload and visualize CSV data?
Absolutely! Read the instructions [here](/docs/using-superset/exploring-data) to learn
Absolutely! Read the instructions [here](/user-docs/using-superset/exploring-data) to learn
how to enable and use CSV upload.
## Why are my queries timing out?
@@ -142,7 +142,7 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:////new/location/superset.db?check_same_thread
```
You can read more about customizing Superset using the configuration file
[here](/docs/6.0.0/configuration/configuring-superset).
[here](/user-docs/6.0.0/configuration/configuring-superset).
## What if the table schema changed?
@@ -157,7 +157,7 @@ table afterwards to configure the Columns tab, check the appropriate boxes and s
To clarify, the database backend is an OLTP database used by Superset to store its internal
information like your list of users and dashboard definitions. While Superset supports a
[variety of databases as data _sources_](/docs/6.0.0/configuration/databases#installing-database-drivers),
[variety of databases as data _sources_](/user-docs/6.0.0/configuration/databases#installing-database-drivers),
only a few database engines are supported for use as the OLTP backend / metadata store.
Superset is tested using MySQL, PostgreSQL, and SQLite backends. Its recommended you install
@@ -190,7 +190,7 @@ second etc). Example:
## Does Superset work with [insert database engine here]?
The [Connecting to Databases section](/docs/6.0.0/configuration/databases) provides the best
The [Connecting to Databases section](/user-docs/6.0.0/configuration/databases) provides the best
overview for supported databases. Database engines not listed on that page may work too. We rely on
the community to contribute to this knowledge base.
@@ -226,7 +226,7 @@ are typical in basic SQL:
## Does Superset offer a public API?
Yes, a public REST API, and the surface of that API formal is expanding steadily. You can read more about this API and
interact with it using Swagger [here](/docs/api).
interact with it using Swagger [here](/developer-docs/api).
Some of the
original vision for the collection of endpoints under **/api/v1** was originally specified in
@@ -266,7 +266,7 @@ Superset uses [Scarf](https://about.scarf.sh/) by default to collect basic telem
We use the [Scarf Gateway](https://docs.scarf.sh/gateway/) to sit in front of container registries, the [scarf-js](https://about.scarf.sh/package-sdks) package to track `npm` installations, and a Scarf pixel to gather anonymous analytics on Superset page views.
Scarf purges PII and provides aggregated statistics. Superset users can easily opt out of analytics in various ways documented [here](https://docs.scarf.sh/gateway/#do-not-track) and [here](https://docs.scarf.sh/package-analytics/#as-a-user-of-a-package-using-scarf-js-how-can-i-opt-out-of-analytics).
Superset maintainers can also opt out of telemetry data collection by setting the `SCARF_ANALYTICS` environment variable to `false` in the Superset container (or anywhere Superset/webpack are run).
Additional opt-out instructions for Docker users are available on the [Docker Installation](/docs/6.0.0/installation/docker-compose) page.
Additional opt-out instructions for Docker users are available on the [Docker Installation](/user-docs/6.0.0/installation/docker-compose) page.
## Does Superset have an archive panel or trash bin from which a user can recover deleted assets?
@@ -24,10 +24,10 @@ A Superset installation is made up of these components:
The optional components above are necessary to enable these features:
- [Alerts and Reports](/docs/6.0.0/configuration/alerts-reports)
- [Caching](/docs/6.0.0/configuration/cache)
- [Async Queries](/docs/6.0.0/configuration/async-queries-celery/)
- [Dashboard Thumbnails](/docs/6.0.0/configuration/cache/#caching-thumbnails)
- [Alerts and Reports](/user-docs/6.0.0/configuration/alerts-reports)
- [Caching](/user-docs/6.0.0/configuration/cache)
- [Async Queries](/user-docs/6.0.0/configuration/async-queries-celery/)
- [Dashboard Thumbnails](/user-docs/6.0.0/configuration/cache/#caching-thumbnails)
If you install with Kubernetes or Docker Compose, all of these components will be created.
@@ -59,7 +59,7 @@ The caching layer serves two main functions:
- Store the results of queries to your data warehouse so that when a chart is loaded twice, it pulls from the cache the second time, speeding up the application and reducing load on your data warehouse.
- Act as a message broker for the worker, enabling the Alerts & Reports, async queries, and thumbnail caching features.
Most people use Redis for their cache, but Superset supports other options too. See the [cache docs](/docs/6.0.0/configuration/cache/) for more.
Most people use Redis for their cache, but Superset supports other options too. See the [cache docs](/user-docs/6.0.0/configuration/cache/) for more.
### Worker and Beat
@@ -67,6 +67,6 @@ This is one or more workers who execute tasks like run async queries or take sna
## Other components
Other components can be incorporated into Superset. The best place to learn about additional configurations is the [Configuration page](/docs/6.0.0/configuration/configuring-superset). For instance, you could set up a load balancer or reverse proxy to implement HTTPS in front of your Superset application, or specify a Mapbox URL to enable geospatial charts, etc.
Other components can be incorporated into Superset. The best place to learn about additional configurations is the [Configuration page](/user-docs/6.0.0/configuration/configuring-superset). For instance, you could set up a load balancer or reverse proxy to implement HTTPS in front of your Superset application, or specify a Mapbox URL to enable geospatial charts, etc.
Superset won't even start without certain configuration settings established, so it's essential to review that page.
@@ -21,7 +21,7 @@ with our [installing on k8s](https://superset.apache.org/docs/installation/runni
documentation.
:::
As mentioned in our [quickstart guide](/docs/quickstart), the fastest way to try
As mentioned in our [quickstart guide](/user-docs/quickstart), the fastest way to try
Superset locally is using Docker Compose on a Linux or Mac OSX
computer. Superset does not have official support for Windows. It's also the easiest
way to launch a fully functioning **development environment** quickly.
@@ -9,11 +9,11 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
# Installation Methods
How should you install Superset? Here's a comparison of the different options. It will help if you've first read the [Architecture](/docs/6.0.0/installation/architecture page to understand Superset's different components.
How should you install Superset? Here's a comparison of the different options. It will help if you've first read the [Architecture](/user-docs/6.0.0/installation/architecture) page to understand Superset's different components.
The fundamental trade-off is between you needing to do more of the detail work yourself vs. using a more complex deployment route that handles those details.
## [Docker Compose](/docs/6.0.0/installation/docker-compose
## [Docker Compose](/user-docs/6.0.0/installation/docker-compose)
**Summary:** This takes advantage of containerization while remaining simpler than Kubernetes. This is the best way to try out Superset; it's also useful for developing & contributing back to Superset.
@@ -27,9 +27,9 @@ You will need to back up your metadata DB. That could mean backing up the servic
You will also need to extend the Superset docker image. The default `lean` images do not contain drivers needed to access your metadata database (Postgres or MySQL), nor to access your data warehouse, nor the headless browser needed for Alerts & Reports. You could run a `-dev` image while demoing Superset, which has some of this, but you'll still need to install the driver for your data warehouse. The `-dev` images run as root, which is not recommended for production.
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs. See [Building your own production Docker image](/docs/6.0.0/installation/docker-builds/#building-your-own-production-docker-image).
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs. See [Building your own production Docker image](/user-docs/6.0.0/installation/docker-builds/#building-your-own-production-docker-image).
## [Kubernetes (K8s)](/docs/6.0.0/installation/kubernetes
## [Kubernetes (K8s)](/user-docs/6.0.0/installation/kubernetes)
**Summary:** This is the best-practice way to deploy a production instance of Superset, but has the steepest skill requirement - someone who knows Kubernetes.
@@ -41,7 +41,7 @@ A K8s deployment can scale up and down based on usage and deploy rolling updates
You will need to build your own Docker image, and back up your metadata DB, both as described in Docker Compose above. You'll also need to customize your Helm chart values and deploy and maintain your Kubernetes cluster.
## [PyPI (Python)](/docs/6.0.0/installation/pypi
## [PyPI (Python)](/user-docs/6.0.0/installation/pypi)
**Summary:** This is the only method that requires no knowledge of containers. It requires the most hands-on work to deploy, connect, and maintain each component.
@@ -149,7 +149,7 @@ For production clusters it's recommended to build own image with this step done
Superset requires a Python DB-API database driver and a SQLAlchemy
dialect to be installed for each datastore you want to connect to.
See [Install Database Drivers](/docs/6.0.0/configuration/databases) for more information.
See [Install Database Drivers](/user-docs/6.0.0/configuration/databases) for more information.
It is recommended that you refer to versions listed in
[pyproject.toml](https://github.com/apache/superset/blob/master/pyproject.toml)
instead of hard-coding them in your bootstrap script, as seen below.
@@ -310,7 +310,7 @@ configOverrides:
### Enable Alerts and Reports
For this, as per the [Alerts and Reports doc](/docs/6.0.0/configuration/alerts-reports), you will need to:
For this, as per the [Alerts and Reports doc](/user-docs/6.0.0/configuration/alerts-reports), you will need to:
#### Install a supported webdriver in the Celery worker
+1 -1
View File
@@ -172,7 +172,7 @@ how to set up a development environment.
## Resources
- [Superset "In the Wild"](https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md) - open a PR to add your org to the list!
- [Feature Flags](/docs/6.0.0/configuration/feature-flags) - the status of Superset's Feature Flags.
- [Feature Flags](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) - the status of Superset's Feature Flags.
- [Standard Roles](https://github.com/apache/superset/blob/master/RESOURCES/STANDARD_ROLES.md) - How RBAC permissions map to roles.
- [Superset Wiki](https://github.com/apache/superset/wiki) - Tons of additional community resources: best practices, community content and other information.
- [Superset SIPs](https://github.com/orgs/apache/projects/170) - The status of Superset's SIPs (Superset Improvement Proposals) for both consensus and implementation status.
@@ -15,7 +15,7 @@ Although we recommend using `Docker Compose` for a quick start in a sandbox-type
environment and for other development-type use cases, **we
do not recommend this setup for production**. For this purpose please
refer to our
[Installing on Kubernetes](/docs/6.0.0/installation/kubernetes/)
[Installing on Kubernetes](/user-docs/6.0.0/installation/kubernetes/)
page.
:::
@@ -73,10 +73,10 @@ processes by running Docker Compose `stop` command. By doing so, you can avoid d
From this point on, you can head on to:
- [Create your first Dashboard](/docs/6.0.0/using-superset/creating-your-first-dashboard)
- [Connect to a Database](/docs/6.0.0/configuration/databases)
- [Using Docker Compose](/docs/6.0.0/installation/docker-compose)
- [Configure Superset](/docs/6.0.0/configuration/configuring-superset/)
- [Installing on Kubernetes](/docs/6.0.0/installation/kubernetes/)
- [Create your first Dashboard](/user-docs/6.0.0/using-superset/creating-your-first-dashboard)
- [Connect to a Database](/user-docs/6.0.0/configuration/databases)
- [Using Docker Compose](/user-docs/6.0.0/installation/docker-compose)
- [Configure Superset](/user-docs/6.0.0/configuration/configuring-superset/)
- [Installing on Kubernetes](/user-docs/6.0.0/installation/kubernetes/)
Or just explore our [Documentation](https://superset.apache.org/docs/intro)!
@@ -31,7 +31,7 @@ your existing SQL-speaking database or data store.
First things first, we need to add the connection credentials to your database to be able
to query and visualize data from it. If you're using Superset locally via
[Docker compose](/docs/6.0.0/installation/docker-compose), you can
[Docker compose](/user-docs/6.0.0/installation/docker-compose), you can
skip this step because a Postgres database, named **examples**, is included and
pre-configured in Superset for you.
@@ -188,7 +188,7 @@ Access to dashboards is managed via owners (users that have edit permissions to
Non-owner users access can be managed in two different ways. The dashboard needs to be published to be visible to other users.
1. Dataset permissions - if you add to the relevant role permissions to datasets it automatically grants implicit access to all dashboards that uses those permitted datasets.
2. Dashboard roles - if you enable [**DASHBOARD_RBAC** feature flag](/docs/6.0.0/configuration/configuring-superset#feature-flags) then you will be able to manage which roles can access the dashboard
2. Dashboard roles - if you enable [**DASHBOARD_RBAC** feature flag](/user-docs/6.0.0/configuration/configuring-superset#feature-flags) then you will be able to manage which roles can access the dashboard
- Granting a role access to a dashboard will bypass dataset level checks. Having dashboard access implicitly grants read access to all the featured charts in the dashboard, and thereby also all the associated datasets.
- If no roles are specified for a dashboard, regular **Dataset permissions** will apply.
+83 -99
View File
@@ -212,7 +212,7 @@
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
"@ant-design/icons@^6.1.1", "@ant-design/icons@^6.2.3":
"@ant-design/icons@^6.2.3":
version "6.2.3"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.2.3.tgz#66e1c7fdea009b9c3fab6964062bedc76f308ad8"
integrity sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==
@@ -1158,10 +1158,10 @@
dependencies:
core-js-pure "^3.43.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4":
version "7.28.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.25.6", "@babel/runtime@^7.25.9", "@babel/runtime@^7.28.4", "@babel/runtime@^7.29.2":
version "7.29.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==
"@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.28.6":
version "7.28.6"
@@ -2924,13 +2924,13 @@
dependencies:
"@babel/runtime" "^7.24.4"
"@rc-component/cascader@~1.14.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.14.0.tgz#74e1fca58cb14f8f75f6e4bf1debd90534aaea7c"
integrity sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==
"@rc-component/cascader@~1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.15.0.tgz#554cba8e01e94a1288547cec96422b2cfc73ff40"
integrity sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==
dependencies:
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/tree" "~1.3.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
@@ -2968,10 +2968,10 @@
dependencies:
"@rc-component/util" "^1.3.0"
"@rc-component/dialog@~1.8.4":
version "1.8.4"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.4.tgz#e1f05f311539852f40a5717bc3874ce0af64c6ff"
integrity sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==
"@rc-component/dialog@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.9.0.tgz#3134f8fa8644d9bc228c862668b90de048c7ea1a"
integrity sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==
dependencies:
"@rc-component/motion" "^1.1.3"
"@rc-component/portal" "^2.1.0"
@@ -3025,30 +3025,30 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/input@~1.1.0", "@rc-component/input@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz"
integrity sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==
"@rc-component/input@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/input/-/input-1.3.0.tgz#a8c113000bbc39089cf75337bec68120115b9e05"
integrity sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==
dependencies:
"@rc-component/resize-observer" "^1.1.1"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/mentions@~1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz"
integrity sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==
"@rc-component/mentions@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.9.0.tgz#1e133d607835854430e264b681b7b32c4b49daa7"
integrity sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==
dependencies:
"@rc-component/input" "~1.1.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/textarea" "~1.1.0"
"@rc-component/input" "~1.3.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/menu@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz"
integrity sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==
"@rc-component/menu@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/menu/-/menu-1.3.0.tgz#fc70d81ca76ae6013b0d7955f20a2393adef04b3"
integrity sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/overflow" "^1.0.0"
@@ -3078,13 +3078,13 @@
dependencies:
"@rc-component/util" "^1.2.0"
"@rc-component/notification@~1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz"
integrity sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==
"@rc-component/notification@~2.0.6":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@rc-component/notification/-/notification-2.0.7.tgz#f2450a482f87e4698285833c4a8efcac169acabb"
integrity sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/util" "^1.2.1"
"@rc-component/util" "^1.11.0"
clsx "^2.1.1"
"@rc-component/overflow@^1.0.0":
@@ -3105,10 +3105,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/picker@~1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.9.1.tgz#7ffcb1e4d4655fe2f3d712773e1d3ab9cd5c2a5c"
integrity sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==
"@rc-component/picker@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/picker/-/picker-1.10.0.tgz#6989f0ae67fca8db00e31f81a8217c8bc370cd34"
integrity sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/resize-observer" "^1.0.0"
@@ -3199,10 +3199,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.9.1":
version "1.9.1"
resolved "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz"
integrity sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==
"@rc-component/table@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.0.tgz#7a98d68176f23f50a762df464f4c9142e7db3942"
integrity sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==
dependencies:
"@rc-component/context" "^2.0.1"
"@rc-component/resize-observer" "^1.0.0"
@@ -3210,28 +3210,18 @@
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/tabs@~1.7.0":
version "1.7.0"
resolved "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz"
integrity sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==
"@rc-component/tabs@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.9.0.tgz#8f3e3755450e5a90d240d1ed3dc140d520b1fbef"
integrity sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==
dependencies:
"@rc-component/dropdown" "~1.0.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/motion" "^1.1.3"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/textarea@~1.1.0", "@rc-component/textarea@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz"
integrity sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==
dependencies:
"@rc-component/input" "~1.1.0"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tooltip@~1.4.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz"
@@ -3241,30 +3231,30 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tour@~2.3.0":
version "2.3.0"
resolved "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz"
integrity sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==
"@rc-component/tour@~2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tour/-/tour-2.4.0.tgz#caf89cf8f2f9fb68f1fb0e0c867610015d01f432"
integrity sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==
dependencies:
"@rc-component/portal" "^2.2.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.8.0.tgz#480e84221befbd1fa93ab2034423e2b064e41981"
integrity sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==
"@rc-component/tree-select@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.9.0.tgz#13ea516478b6cb558e04181abb0a01ae6fbdd31f"
integrity sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==
dependencies:
"@rc-component/select" "~1.6.0"
"@rc-component/tree" "~1.2.0"
"@rc-component/tree" "~1.3.0"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/tree@~1.2.0", "@rc-component/tree@~1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.2.4.tgz#cb4f7d818118b3447763e74d3a82fba6454c7317"
integrity sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w==
"@rc-component/tree@~1.3.0", "@rc-component/tree@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.3.1.tgz#6983ca6bd9d5f6d04dd7258d00cb0fe71cdfe661"
integrity sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/util" "^1.8.1"
@@ -3290,10 +3280,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.10.1.tgz#213c84c77e8b2001095530d3b0dc47c49c34ffe3"
integrity sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==
"@rc-component/util@^1.1.0", "@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.11.0.tgz#965c8b44a3f57fc96dc14e5072afbe32e422fd4d"
integrity sha512-jHG3/BYgUWiP5c7RZHiaUNToyw1L3nlPSKG2RPu+YoiD9b3ajiJwBWhsjO+ZELmCsKFAjNR5DelbKdlF0e2BDA==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
@@ -5501,36 +5491,36 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.7.tgz#620354ec04135356cbc5ce0a666871ddc73e4117"
integrity sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw==
antd@^6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.4.2.tgz#9fc0fee455a5c56e7ec27855495eefadc8df636a"
integrity sha512-PNJz8Vxc/mC3EsOg/h3e2YuaZduJ1RDp4RmySDuDmKPCxVgyp4Da4kB36o87p9hbLbOWdAWCKQlnyopsN8utKQ==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
"@ant-design/cssinjs-utils" "^2.1.2"
"@ant-design/fast-color" "^3.0.1"
"@ant-design/icons" "^6.1.1"
"@ant-design/icons" "^6.2.3"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.28.4"
"@rc-component/cascader" "~1.14.0"
"@babel/runtime" "^7.29.2"
"@rc-component/cascader" "~1.15.0"
"@rc-component/checkbox" "~2.0.0"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.1.1"
"@rc-component/dialog" "~1.8.4"
"@rc-component/dialog" "~1.9.0"
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.8.1"
"@rc-component/image" "~1.9.0"
"@rc-component/input" "~1.1.2"
"@rc-component/input" "~1.3.0"
"@rc-component/input-number" "~1.6.2"
"@rc-component/mentions" "~1.6.0"
"@rc-component/menu" "~1.2.0"
"@rc-component/mentions" "~1.9.0"
"@rc-component/menu" "~1.3.0"
"@rc-component/motion" "^1.3.2"
"@rc-component/mutate-observer" "^2.0.1"
"@rc-component/notification" "~1.2.0"
"@rc-component/notification" "~2.0.6"
"@rc-component/pagination" "~1.2.0"
"@rc-component/picker" "~1.9.1"
"@rc-component/picker" "~1.10.0"
"@rc-component/progress" "~1.0.2"
"@rc-component/qrcode" "~1.1.1"
"@rc-component/rate" "~1.0.1"
@@ -5540,13 +5530,12 @@ antd@^6.3.7:
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.9.1"
"@rc-component/tabs" "~1.7.0"
"@rc-component/textarea" "~1.1.2"
"@rc-component/table" "~1.10.0"
"@rc-component/tabs" "~1.9.0"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/tour" "~2.3.0"
"@rc-component/tree" "~1.2.4"
"@rc-component/tree-select" "~1.8.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.3.1"
"@rc-component/tree-select" "~1.9.0"
"@rc-component/trigger" "^3.9.0"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.10.1"
@@ -7101,12 +7090,7 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
dayjs@^1.11.11:
version "1.11.13"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
dayjs@^1.11.19:
dayjs@^1.11.11, dayjs@^1.11.19:
version "1.11.20"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
+394 -149
View File
@@ -102,7 +102,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.6.2",
"immer": "^11.1.7",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -121,7 +121,7 @@
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^18.2.0",
"react-arborist": "^3.5.0",
"react-arborist": "^3.6.1",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
@@ -224,7 +224,7 @@
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"babel-jest": "^30.0.2",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@@ -286,7 +286,7 @@
"terser-webpack-plugin": "^5.6.0",
"ts-jest": "^29.4.9",
"tscw-config": "^1.1.2",
"tsx": "^4.21.0",
"tsx": "^4.22.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
@@ -294,7 +294,7 @@
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
@@ -3630,9 +3630,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
@@ -3664,9 +3664,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
@@ -3698,9 +3698,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
@@ -17027,16 +17027,16 @@
"license": "Apache-2.0"
},
"node_modules/babel-jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
"integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==",
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz",
"integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/transform": "30.3.0",
"@jest/transform": "30.4.1",
"@types/babel__core": "^7.20.5",
"babel-plugin-istanbul": "^7.0.1",
"babel-preset-jest": "30.3.0",
"babel-preset-jest": "30.4.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"slash": "^3.0.0"
@@ -17048,6 +17048,85 @@
"@babel/core": "^7.11.0 || ^8.0.0-0"
}
},
"node_modules/babel-jest/node_modules/@jest/pattern": {
"version": "30.4.0",
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz",
"integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"jest-regex-util": "30.4.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/@jest/schemas": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz",
"integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/@jest/transform": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz",
"integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.4.1",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.4.1",
"jest-regex-util": "30.4.0",
"jest-util": "30.4.1",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/@jest/types": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz",
"integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.4.0",
"@jest/schemas": "30.4.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/node": "*",
"@types/yargs": "^17.0.33",
"chalk": "^4.1.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/@sinclair/typebox": {
"version": "0.34.49",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz",
"integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==",
"dev": true,
"license": "MIT"
},
"node_modules/babel-jest/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -17065,6 +17144,105 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/babel-jest/node_modules/jest-haste-map": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz",
"integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.4.1",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.4.0",
"jest-util": "30.4.1",
"jest-worker": "30.4.1",
"picomatch": "^4.0.3",
"walker": "^1.0.8"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.3"
}
},
"node_modules/babel-jest/node_modules/jest-regex-util": {
"version": "30.4.0",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz",
"integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/jest-util": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz",
"integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/jest-worker": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz",
"integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.3.0",
"jest-util": "30.4.1",
"merge-stream": "^2.0.0",
"supports-color": "^8.1.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-jest/node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/babel-jest/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/babel-jest/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -17132,9 +17310,9 @@
}
},
"node_modules/babel-plugin-jest-hoist": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz",
"integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==",
"version": "30.4.0",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz",
"integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -17288,13 +17466,13 @@
}
},
"node_modules/babel-preset-jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz",
"integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==",
"version": "30.4.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz",
"integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"babel-plugin-jest-hoist": "30.3.0",
"babel-plugin-jest-hoist": "30.4.0",
"babel-preset-current-node-syntax": "^1.2.0"
},
"engines": {
@@ -23794,9 +23972,9 @@
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-builder": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
"funding": [
{
"type": "github",
@@ -23805,7 +23983,8 @@
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.1.3"
"path-expression-matcher": "^1.5.0",
"xml-naming": "^0.1.0"
}
},
"node_modules/fast-xml-parser": {
@@ -27028,9 +27207,9 @@
"license": "MIT"
},
"node_modules/immer": {
"version": "11.1.7",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.7.tgz",
"integrity": "sha512-LFVFtAROHcDy1er5UI6nodRFnZ2SgdCXhfNSI+DpObO8N7Pur/muBGsjzH5wpnFHCYhYVQxZskCkV4koQ//3/Q==",
"version": "11.1.8",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
"integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -28917,6 +29096,58 @@
"dev": true,
"license": "MIT"
},
"node_modules/jest-config/node_modules/babel-jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
"integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/transform": "30.3.0",
"@types/babel__core": "^7.20.5",
"babel-plugin-istanbul": "^7.0.1",
"babel-preset-jest": "30.3.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"slash": "^3.0.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
"@babel/core": "^7.11.0 || ^8.0.0-0"
}
},
"node_modules/jest-config/node_modules/babel-plugin-jest-hoist": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz",
"integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/babel__core": "^7.20.5"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-config/node_modules/babel-preset-jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz",
"integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"babel-plugin-jest-hoist": "30.3.0",
"babel-preset-current-node-syntax": "^1.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
"@babel/core": "^7.11.0 || ^8.0.0-beta.1"
}
},
"node_modules/jest-config/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
@@ -41113,9 +41344,9 @@
}
},
"node_modules/react-arborist": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.5.0.tgz",
"integrity": "sha512-FdXOICSt7P2h+Pxin1ULN02b4qrXJznNcshgwwWVtuYMLWSJcD245PQ4HOSj/Lr2T1uEegmnEm5Lbns2hUUsqg==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.6.1.tgz",
"integrity": "sha512-h2/sPz6PXL79h7mOWjCA6Y5WNUKmA0kL8Uh6RYZQbYk7UOFBd86Jeoga4RjHMBYpOWpBPYrOJOE3HbIPUETp8w==",
"license": "MIT",
"dependencies": {
"react-dnd": "^14.0.3",
@@ -46262,14 +46493,13 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz",
"integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
"esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
@@ -46282,9 +46512,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
@@ -46299,9 +46529,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
@@ -46316,9 +46546,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
@@ -46333,9 +46563,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
@@ -46350,9 +46580,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
@@ -46367,9 +46597,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
@@ -46384,9 +46614,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
@@ -46401,9 +46631,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
@@ -46418,9 +46648,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
@@ -46435,9 +46665,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
@@ -46452,9 +46682,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
@@ -46469,9 +46699,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
@@ -46486,9 +46716,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
@@ -46503,9 +46733,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
@@ -46520,9 +46750,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
@@ -46537,9 +46767,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
@@ -46554,9 +46784,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
@@ -46571,9 +46801,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
@@ -46588,9 +46818,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
@@ -46605,9 +46835,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
@@ -46622,9 +46852,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
@@ -46639,9 +46869,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
@@ -46656,9 +46886,9 @@
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
@@ -46673,9 +46903,9 @@
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -46686,32 +46916,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/tsyringe": {
@@ -48415,9 +48645,9 @@
}
},
"node_modules/webpack-dev-server": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz",
"integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==",
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz",
"integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -49234,6 +49464,21 @@
"node": ">=18"
}
},
"node_modules/xml-naming": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/xml-utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
@@ -50174,7 +50419,7 @@
"version": "0.20.4",
"license": "Apache-2.0",
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@ant-design/icons": "^6.2.3",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.2",
"@types/json-bigint": "^1.0.4",
@@ -50206,7 +50451,7 @@
"react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.1",
"react-syntax-highlighter": "^16.1.0",
"react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
@@ -50275,9 +50520,9 @@
}
},
"packages/superset-ui-core/node_modules/@ant-design/icons": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz",
"integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz",
"integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.1",
@@ -50782,7 +51027,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -50983,7 +51228,7 @@
"license": "Apache-2.0",
"dependencies": {
"@types/d3-scale": "^4.0.9",
"d3-cloud": "^1.2.8",
"d3-cloud": "^1.2.9",
"d3-scale": "^4.0.2"
},
"devDependencies": {
+5 -5
View File
@@ -183,7 +183,7 @@
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^10.6.2",
"immer": "^11.1.7",
"immer": "^11.1.8",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
"js-levenshtein": "^1.1.6",
@@ -202,7 +202,7 @@
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^18.2.0",
"react-arborist": "^3.5.0",
"react-arborist": "^3.6.1",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
@@ -305,7 +305,7 @@
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"babel-jest": "^30.0.2",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@@ -367,7 +367,7 @@
"terser-webpack-plugin": "^5.6.0",
"ts-jest": "^29.4.9",
"tscw-config": "^1.1.2",
"tsx": "^4.21.0",
"tsx": "^4.22.0",
"typescript": "5.4.5",
"unzipper": "^0.12.3",
"vm-browserify": "^1.1.2",
@@ -375,7 +375,7 @@
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"webpack-dev-server": "^5.2.4",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
@@ -24,7 +24,7 @@
"lib"
],
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@ant-design/icons": "^6.2.3",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.2",
"@types/json-bigint": "^1.0.4",
@@ -29,7 +29,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.3"
"zod": "^4.4.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -29,7 +29,7 @@
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.23.1",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.0",
"react-map-gl": "^8.1.1",
"supercluster": "^8.0.1"
},
"peerDependencies": {
@@ -30,7 +30,7 @@
},
"dependencies": {
"@types/d3-scale": "^4.0.9",
"d3-cloud": "^1.2.8",
"d3-cloud": "^1.2.9",
"d3-scale": "^4.0.2"
},
"peerDependencies": {
@@ -113,7 +113,7 @@ const EstimateQueryCostButton = ({
modalBody={renderModalBody()}
triggerNode={
<Button
color="primary"
color="default"
variant="text"
style={{ height: 32, padding: '4px 15px' }}
onClick={onClickHandler}
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SupersetClient } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import type { common as core } from '@apache-superset/core';
import ExtensionsLoader from './ExtensionsLoader';
@@ -111,3 +112,33 @@ test('handles initialization errors gracefully', async () => {
errorSpy.mockRestore();
appendChildSpy.mockRestore();
});
test('logs success after initializeExtensions completes', async () => {
const loader = ExtensionsLoader.getInstance();
const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
jest.spyOn(SupersetClient, 'get').mockResolvedValue({
json: { result: [] },
} as any);
await loader.initializeExtensions();
expect(infoSpy).toHaveBeenCalledWith('Extensions initialized successfully.');
infoSpy.mockRestore();
});
test('logs error when initializeExtensions fails', async () => {
const loader = ExtensionsLoader.getInstance();
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
const fetchError = new Error('Network error');
jest.spyOn(SupersetClient, 'get').mockRejectedValue(fetchError);
await loader.initializeExtensions();
expect(errorSpy).toHaveBeenCalledWith(
'Error setting up extensions:',
fetchError,
);
errorSpy.mockRestore();
});
@@ -34,6 +34,8 @@ class ExtensionsLoader {
private extensionIndex: Map<string, Extension> = new Map();
private initializationPromise: Promise<void> | null = null;
// eslint-disable-next-line no-useless-constructor
private constructor() {
// Private constructor for singleton pattern
@@ -54,16 +56,27 @@ class ExtensionsLoader {
* Initializes extensions by fetching the list from the API and loading each one.
* @throws Error if initialization fails.
*/
public async initializeExtensions(): Promise<void> {
const response = await SupersetClient.get({
endpoint: '/api/v1/extensions/',
});
const extensions: Extension[] = response.json.result;
await Promise.all(
extensions.map(async extension => {
await this.initializeExtension(extension);
}),
);
public initializeExtensions(): Promise<void> {
if (this.initializationPromise) {
return this.initializationPromise;
}
this.initializationPromise = (async () => {
try {
const response = await SupersetClient.get({
endpoint: '/api/v1/extensions/',
});
const extensions: Extension[] = response.json.result;
await Promise.all(
extensions.map(async extension => {
await this.initializeExtension(extension);
}),
);
logging.info('Extensions initialized successfully.');
} catch (error) {
logging.error('Error setting up extensions:', error);
}
})();
return this.initializationPromise;
}
/**
@@ -18,7 +18,6 @@
*/
import { render, waitFor } from 'spec/helpers/testing-library';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import fetchMock from 'fetch-mock';
import ExtensionsStartup from './ExtensionsStartup';
import ExtensionsLoader from './ExtensionsLoader';
@@ -192,14 +191,12 @@ test('only initializes once even with multiple renders', async () => {
loader.initializeExtensions = originalInitialize;
});
test('initializes ExtensionsLoader and logs success when EnableExtensions feature flag is enabled', async () => {
test('initializes ExtensionsLoader when EnableExtensions feature flag is enabled', async () => {
// Ensure feature flag is enabled
mockIsFeatureEnabled.mockImplementation(
(flag: FeatureFlag) => flag === FeatureFlag.EnableExtensions,
);
const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
// Mock the initializeExtensions method to succeed
const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
ExtensionsLoader.prototype.initializeExtensions = jest
@@ -220,15 +217,10 @@ test('initializes ExtensionsLoader and logs success when EnableExtensions featur
expect(
ExtensionsLoader.prototype.initializeExtensions,
).toHaveBeenCalledTimes(1);
// Verify success message was logged
expect(infoSpy).toHaveBeenCalledWith(
'Extensions initialized successfully.',
);
});
// Restore original method
ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
infoSpy.mockRestore();
});
test('does not initialize ExtensionsLoader when EnableExtensions feature flag is disabled', async () => {
@@ -259,38 +251,36 @@ test('does not initialize ExtensionsLoader when EnableExtensions feature flag is
initializeSpy.mockRestore();
});
test('logs error when ExtensionsLoader initialization fails', async () => {
test('continues rendering children even when ExtensionsLoader initialization fails', async () => {
// Ensure feature flag is enabled
mockIsFeatureEnabled.mockReturnValue(true);
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
// Mock the initializeExtensions method to throw an error
// Mock the initializeExtensions method to reject — ExtensionsLoader handles
// its own error logging internally
const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
ExtensionsLoader.prototype.initializeExtensions = jest
.fn()
.mockImplementation(() => {
throw new Error('Test initialization error');
});
.mockImplementation(() => Promise.resolve());
render(<ExtensionsStartup />, {
useRedux: true,
initialState: mockInitialState,
});
const { container } = render(
<ExtensionsStartup>
<div data-testid="child" />
</ExtensionsStartup>,
{
useRedux: true,
initialState: mockInitialState,
},
);
await waitFor(() => {
// Verify feature flag was checked
expect(mockIsFeatureEnabled).toHaveBeenCalledWith(
FeatureFlag.EnableExtensions,
);
// Verify error was logged
expect(errorSpy).toHaveBeenCalledWith(
'Error setting up extensions:',
expect.any(Error),
);
expect(
container.querySelector('[data-testid="child"]'),
).toBeInTheDocument();
});
// Restore original method
ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
errorSpy.mockRestore();
});
@@ -82,17 +82,7 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
const setup = async () => {
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
try {
await ExtensionsLoader.getInstance().initializeExtensions();
supersetCore.utils.logging.info(
'Extensions initialized successfully.',
);
} catch (error) {
supersetCore.utils.logging.error(
'Error setting up extensions:',
error,
);
}
await ExtensionsLoader.getInstance().initializeExtensions();
}
setInitialized(true);
};
+15 -15
View File
@@ -23,7 +23,7 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.24",
"@types/node": "^25.7.0",
"@types/node": "^25.8.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
@@ -1798,13 +1798,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.21.0"
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/stack-utils": {
@@ -6237,9 +6237,9 @@
}
},
"node_modules/undici-types": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
@@ -7894,12 +7894,12 @@
"dev": true
},
"@types/node": {
"version": "25.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true,
"requires": {
"undici-types": "~7.21.0"
"undici-types": ">=7.24.0 <7.24.7"
}
},
"@types/stack-utils": {
@@ -11063,9 +11063,9 @@
"optional": true
},
"undici-types": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true
},
"unix-dgram": {
+1 -1
View File
@@ -31,7 +31,7 @@
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash": "^4.17.24",
"@types/node": "^25.7.0",
"@types/node": "^25.8.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
+8 -1
View File
@@ -36,6 +36,7 @@ else:
from flask import Flask, Response
from werkzeug.exceptions import NotFound
from superset.extensions.cache_middleware import ExtensionCacheMiddleware
from superset.extensions.local_extensions_watcher import (
start_local_extensions_watcher_thread,
)
@@ -66,7 +67,6 @@ def create_app(
or app.config["APPLICATION_ROOT"],
)
if app_root != "/":
app.wsgi_app = AppRootMiddleware(app.wsgi_app, app_root)
# If not set, manually configure options that depend on the
# value of app_root so things work out of the box
if not app.config["STATIC_ASSETS_PREFIX"]:
@@ -77,6 +77,13 @@ def create_app(
app_initializer = app.config.get("APP_INITIALIZER", SupersetAppInitializer)(app)
app_initializer.init_app()
# Must be applied before AppRootMiddleware so the path prefix
# is stripped before the extension asset path regex runs.
app.wsgi_app = ExtensionCacheMiddleware(app.wsgi_app)
if app_root != "/":
app.wsgi_app = AppRootMiddleware(app.wsgi_app, app_root)
# Set up LOCAL_EXTENSIONS file watcher when in debug mode
if app.debug:
start_local_extensions_watcher_thread(app)
+32
View File
@@ -146,6 +146,27 @@ class ExportDashboardsCommand(ExportModelsCommand):
if dataset:
target["datasetUuid"] = str(dataset.uuid)
# Replace display control dataset references with uuid.
# datasetId is intentionally preserved alongside datasetUuid so that
# bundles remain importable by older versions that do not yet understand
# datasetUuid for display-control targets.
for customization in (
payload.get("metadata", {}).get("chart_customization_config") or []
):
for target in customization.get("targets") or []:
dataset_id = target.get("datasetId")
if dataset_id is not None:
dataset = DatasetDAO.find_by_id(dataset_id)
if dataset:
target["datasetUuid"] = str(dataset.uuid)
else:
logger.warning(
"Dashboard '%s': display control target references "
"missing dataset %s; datasetUuid will not be set",
model.dashboard_title,
dataset_id,
)
# the mapping between dashboard -> charts is inferred from the position
# attribute, so if it's not present we need to add a default config
if not payload.get("position"):
@@ -230,3 +251,14 @@ class ExportDashboardsCommand(ExportModelsCommand):
dataset = DatasetDAO.find_by_id(dataset_id)
if dataset:
yield from ExportDatasetsCommand([dataset_id]).run()
# Export datasets referenced by display controls
for customization in (
payload.get("metadata", {}).get("chart_customization_config") or []
):
for target in customization.get("targets") or []:
dataset_id = target.get("datasetId")
if dataset_id is not None:
dataset = DatasetDAO.find_by_id(dataset_id)
if dataset:
yield from ExportDatasetsCommand([dataset_id]).run()
@@ -42,6 +42,11 @@ def find_native_filter_datasets(metadata: dict[str, Any]) -> set[str]:
dataset_uuid = target.get("datasetUuid")
if dataset_uuid:
uuids.add(dataset_uuid)
for customization in metadata.get("chart_customization_config") or []:
for target in customization.get("targets") or []:
dataset_uuid = target.get("datasetUuid")
if dataset_uuid:
uuids.add(dataset_uuid)
return uuids
@@ -139,6 +144,28 @@ def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
native_filter["scope"]["excluded"] = [
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
# fix display control dataset references
for customization in (
fixed.get("metadata", {}).get("chart_customization_config") or []
):
for target in customization.get("targets") or []:
dataset_uuid = target.pop("datasetUuid", None)
if dataset_uuid:
info = dataset_info.get(dataset_uuid)
if info:
target["datasetId"] = info["datasource_id"]
else:
# UUID present but unresolvable — remove stale integer ID
# so the control fails visibly rather than binding to
# whatever dataset happens to own that ID in this environment
target.pop("datasetId", None)
logger.warning(
"Display control target references unknown dataset UUID %s; "
"datasetId will not be restored",
dataset_uuid,
)
fixed = update_cross_filter_scoping(fixed, id_map)
return fixed
@@ -27,9 +27,13 @@ from sqlalchemy.exc import MultipleResultsFound
from sqlalchemy.sql.visitors import VisitableType
from superset import db, security_manager
from superset.commands.dataset.exceptions import DatasetForbiddenDataURI
from superset.commands.dataset.exceptions import (
DatasetAccessDeniedError,
DatasetForbiddenDataURI,
)
from superset.commands.exceptions import ImportFailedError
from superset.connectors.sqla.models import SqlaTable
from superset.exceptions import SupersetSecurityException
from superset.models.core import Database
from superset.sql.parse import Table
from superset.utils import json
@@ -172,6 +176,12 @@ def import_dataset( # noqa: C901
if dataset.id is None:
db.session.flush()
if not ignore_permissions:
try:
security_manager.raise_for_access(datasource=dataset)
except SupersetSecurityException as ex:
raise DatasetAccessDeniedError() from ex
try:
table_exists = dataset.database.has_table(
Table(dataset.table_name, dataset.schema, dataset.catalog),
+1
View File
@@ -2072,6 +2072,7 @@ class SqlaTable(
self.database,
self.catalog,
self.schema or default_schema or "",
exclude_dataset_id=self.id,
)
# Add each predicate as a separate cache key component
extra_cache_keys.extend(rls_predicates)
+5 -1
View File
@@ -58,7 +58,11 @@ class QueryDAO(BaseDAO[Query]):
@staticmethod
def stop_query(client_id: str) -> None:
query = db.session.query(Query).filter_by(client_id=client_id).one_or_none()
query = (
db.session.query(Query)
.filter(Query.client_id == client_id, Query.user_id == get_user_id())
.one_or_none()
)
if not query:
raise QueryNotFoundException(f"Query with client_id {client_id} not found")
+11 -16
View File
@@ -883,6 +883,14 @@ class BigQueryEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-met
# We will return the original exception
return exception
@staticmethod
def _information_schema_ref(schema: str, catalog: str | None) -> str:
escaped_schema = schema.replace("`", "``")
if catalog:
escaped_catalog = catalog.replace("`", "``")
return f"`{escaped_catalog}.{escaped_schema}.INFORMATION_SCHEMA.TABLES`"
return f"`{escaped_schema}.INFORMATION_SCHEMA.TABLES`"
@classmethod
def get_materialized_view_names(
cls,
@@ -899,14 +907,8 @@ class BigQueryEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-met
if not schema:
return set()
# Construct the query to get materialized views from INFORMATION_SCHEMA
if catalog := database.get_default_catalog():
information_schema = f"`{catalog}.{schema}.INFORMATION_SCHEMA.TABLES`"
else:
information_schema = f"`{schema}.INFORMATION_SCHEMA.TABLES`"
# Use string formatting for the table name since it's not user input
# The catalog and schema are from trusted sources (database configuration)
catalog = database.get_default_catalog()
information_schema = cls._information_schema_ref(schema, catalog)
query = f"""
SELECT table_name
FROM {information_schema}
@@ -945,15 +947,8 @@ class BigQueryEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-met
if not schema:
return set()
# Construct the query to get regular views from INFORMATION_SCHEMA
catalog = database.get_default_catalog()
if catalog:
information_schema = f"`{catalog}.{schema}.INFORMATION_SCHEMA.TABLES`"
else:
information_schema = f"`{schema}.INFORMATION_SCHEMA.TABLES`"
# Use string formatting for the table name since it's not user input
# The catalog and schema are from trusted sources (database configuration)
information_schema = cls._information_schema_ref(schema, catalog)
query = f"""
SELECT table_name
FROM {information_schema}
+4 -4
View File
@@ -569,11 +569,11 @@ class DatabricksNativeEngineSpec(DatabricksDynamicBaseEngineSpec):
) -> list[str]:
prequeries = []
if catalog:
catalog = f"`{catalog}`" if not catalog.startswith("`") else catalog
prequeries.append(f"USE CATALOG {catalog}")
escaped_catalog = catalog.replace("`", "``")
prequeries.append(f"USE CATALOG `{escaped_catalog}`")
if schema:
schema = f"`{schema}`" if not schema.startswith("`") else schema
prequeries.append(f"USE SCHEMA {schema}")
escaped_schema = schema.replace("`", "``")
prequeries.append(f"USE SCHEMA `{escaped_schema}`")
return prequeries
@classmethod
+4 -1
View File
@@ -162,4 +162,7 @@ class Db2EngineSpec(BaseEngineSpec):
be anything, and we would have to block users from running any queries
referencing tables without an explicit schema.
"""
return [f'set current_schema "{schema}"'] if schema else []
if not schema:
return []
escaped = schema.replace('"', '""')
return [f'set current_schema "{escaped}"']
+2 -1
View File
@@ -268,7 +268,8 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
schema=table.schema,
) as conn:
cursor = conn.cursor()
cursor.execute(f'SELECT GET_METADATA("{table.table}")')
escaped_table = table.table.replace('"', '""')
cursor.execute(f'SELECT GET_METADATA("{escaped_table}")')
results = cursor.fetchone()[0]
try:
metadata = json.loads(results)
+21 -6
View File
@@ -206,13 +206,24 @@ class HiveEngineSpec(PrestoEngineSpec):
if to_sql_kwargs["if_exists"] == "fail":
# Ensure table doesn't already exist.
escaped_table = (
table.table.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("%", "\\%")
.replace("_", "\\_")
)
# Hive LIKE uses backslash as the escape character. Python needs \\\\
# to produce the two-character SQL literal \\ (a single backslash).
escape_clause = " ESCAPE '\\\\'"
if table.schema:
escaped_schema = table.schema.replace("`", "``")
table_exists = not database.get_df(
f"SHOW TABLES IN {table.schema} LIKE '{table.table}'"
f"SHOW TABLES IN `{escaped_schema}`"
f" LIKE '{escaped_table}'{escape_clause}"
).empty
else:
table_exists = not database.get_df(
f"SHOW TABLES LIKE '{table.table}'"
f"SHOW TABLES LIKE '{escaped_table}'{escape_clause}"
).empty
if table_exists:
@@ -498,9 +509,12 @@ class HiveEngineSpec(PrestoEngineSpec):
order_by: list[tuple[str, bool]] | None = None,
filters: dict[Any, Any] | None = None,
) -> str:
full_table_name = (
f"{table.schema}.{table.table}" if table.schema else table.table
)
escaped_table = table.table.replace("`", "``")
if table.schema:
escaped_schema = table.schema.replace("`", "``")
full_table_name = f"`{escaped_schema}`.`{escaped_table}`"
else:
full_table_name = f"`{escaped_table}`"
return f"SHOW PARTITIONS {full_table_name}"
@classmethod
@@ -628,7 +642,8 @@ class HiveEngineSpec(PrestoEngineSpec):
sql = "SHOW VIEWS"
if schema:
sql += f" IN `{schema}`"
escaped_schema = schema.replace("`", "``")
sql += f" IN `{escaped_schema}`"
with database.get_raw_connection(schema=schema) as conn:
cursor = conn.cursor()
+4 -1
View File
@@ -694,7 +694,10 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
be anything, and we would have to block users from running any queries
referencing tables without an explicit schema.
"""
return [f'set search_path = "{schema}"'] if schema else []
if not schema:
return []
escaped = schema.replace('"', '""')
return [f'set search_path = "{escaped}"']
@classmethod
def get_allow_cost_estimate(cls, extra: dict[str, Any]) -> bool:
+2 -1
View File
@@ -413,6 +413,7 @@ class StarRocksEngineSpec(MySQLEngineSpec):
username = database.get_effective_user(database.url_object)
if username:
return [f'EXECUTE AS "{username}" WITH NO REVERT;']
escaped = username.replace('"', '""')
return [f'EXECUTE AS "{escaped}" WITH NO REVERT;']
return []
+2 -2
View File
@@ -353,10 +353,10 @@ class OAuth2RedirectError(SupersetErrorException):
See the `OAuth2RedirectMessage.tsx` component for more details of how this
information is handled.
TODO (betodealmeida): change status to 403.
"""
status = 403
def __init__(self, url: str, tab_id: str, redirect_uri: str):
super().__init__(
SupersetError(
+6 -1
View File
@@ -225,4 +225,9 @@ class ExtensionsRestApi(BaseApi):
if not mimetype:
mimetype = "application/octet-stream"
return send_file(BytesIO(chunk), mimetype=mimetype)
response = send_file(BytesIO(chunk), mimetype=mimetype)
# Chunk filenames include a content hash, so they are immutable.
response.cache_control.max_age = 31536000
response.cache_control.public = True
response.cache_control.immutable = True
return response
+73
View File
@@ -0,0 +1,73 @@
# 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.
from __future__ import annotations
import re
from types import TracebackType
from typing import Callable, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
# Matches only the static asset endpoint: /api/v1/extensions/<publisher>/<name>/<file>
# Does not match the list (/), get (/<publisher>/<name>), or info (/_info) endpoints.
_ASSET_PATH_RE = re.compile(r"^/api/v1/extensions/[^/]+/[^/]+/[^/]+$")
class ExtensionCacheMiddleware:
"""Strip 'Cookie' from the Vary header on extension asset responses.
Flask's session interface appends Vary: Cookie unconditionally after every
after_request hook runs, so it cannot be removed at the view layer. This
middleware intercepts the WSGI response at the lowest level, after all
Flask processing is complete.
"""
def __init__(self, wsgi_app: WSGIApplication) -> None:
self.wsgi_app = wsgi_app
def __call__(
self, environ: WSGIEnvironment, start_response: StartResponse
) -> Iterable[bytes]:
path = environ.get("PATH_INFO", "")
if not _ASSET_PATH_RE.match(path):
return self.wsgi_app(environ, start_response)
def patched_start_response(
status: str,
response_headers: list[tuple[str, str]],
exc_info: (
tuple[type[BaseException], BaseException, TracebackType]
| tuple[None, None, None]
| None
) = None,
) -> Callable[[bytes], object]:
new_headers = []
for name, value in response_headers:
if name.lower() == "vary":
parts = [
v.strip()
for v in value.split(",")
if v.strip().lower() != "cookie"
]
if parts:
new_headers.append((name, ", ".join(parts)))
else:
new_headers.append((name, value))
return start_response(status, new_headers, exc_info)
return self.wsgi_app(environ, patched_start_response)
+450 -1
View File
@@ -26,14 +26,23 @@ URL parameter extraction. Config mapping logic lives in chart_utils.py.
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from typing import Any, TYPE_CHECKING
from urllib.parse import parse_qs, urlparse
from superset.constants import EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS
if TYPE_CHECKING:
from superset.models.slice import Slice
logger = logging.getLogger(__name__)
QUERY_CONTEXT_EXTRA_FORM_DATA_OVERRIDE_KEYS = {
"granularity",
"time_grain",
"time_grain_sqla",
"time_range",
}
def find_chart_by_identifier(identifier: int | str) -> Slice | None:
"""Find a chart by numeric ID or UUID string.
@@ -69,6 +78,446 @@ def get_cached_form_data(form_data_key: str) -> str | None:
return None
def resolve_datasource_engine(datasource_id: Any, datasource_type: str) -> str:
"""Return the datasource engine name, or ``"base"`` if it cannot be resolved."""
if not isinstance(datasource_id, (int, str)):
return "base"
try:
# avoid circular import
from superset.daos.datasource import DatasourceDAO
from superset.utils.core import DatasourceType
datasource = DatasourceDAO.get_datasource(
datasource_type=DatasourceType(datasource_type),
database_id_or_uuid=datasource_id,
)
return datasource.database.db_engine_spec.engine
except Exception: # noqa: BLE001
# Engine lookup is best-effort; fall back to generic filter normalization.
logger.debug("Could not resolve engine for datasource %s", datasource_id)
return "base"
def prepare_form_data_for_query(
form_data: dict[str, Any],
datasource_id: Any,
datasource_type: str,
extra_form_data: dict[str, Any] | None = None,
datasource_engine: str | None = None,
) -> None:
"""Normalize form_data filters before building a QueryObject payload.
Explore and legacy viz query construction merge dashboard/native filter payloads
and split adhoc filters into the concrete ``filters``/``where``/``having``
fields consumed by QueryObject. MCP tools that build query payloads directly
must perform the same normalization before calling QueryContextFactory.
Mutates ``form_data`` in place.
"""
# avoid circular import
from superset.utils.core import (
convert_legacy_filters_into_adhoc,
form_data_to_adhoc,
merge_extra_filters,
simple_filter_to_adhoc,
split_adhoc_filters_into_base_filters,
)
if isinstance(form_data.get("adhoc_filters"), list):
adhoc_filters = [
*(
form_data_to_adhoc(form_data, clause)
for clause in ("having", "where")
if form_data.get(clause)
),
*(
simple_filter_to_adhoc(filter_, "where")
for filter_ in form_data.get("filters") or []
if filter_ is not None
),
*form_data["adhoc_filters"],
]
form_data["adhoc_filters"] = adhoc_filters
if extra_form_data:
form_data["extra_form_data"] = merge_extra_form_data(
form_data.get("extra_form_data"),
extra_form_data,
)
convert_legacy_filters_into_adhoc(form_data)
merge_extra_filters(form_data)
split_adhoc_filters_into_base_filters(
form_data,
datasource_engine or resolve_datasource_engine(datasource_id, datasource_type),
)
def merge_extra_form_data(
existing: Any,
incoming: dict[str, Any],
) -> dict[str, Any]:
"""Merge cached and request-level extra_form_data payloads."""
merged: dict[str, Any] = dict(existing) if isinstance(existing, dict) else {}
for key, value in incoming.items():
current = merged.get(key)
if isinstance(current, list) and isinstance(value, list):
merged[key] = [*current, *value]
elif isinstance(current, dict) and isinstance(value, dict):
merged[key] = {**current, **value}
else:
merged[key] = value
return merged
def apply_form_data_filters_to_query(
query: dict[str, Any],
form_data: dict[str, Any],
) -> None:
"""Copy normalized form_data filter fields into a fresh query payload."""
if filters := form_data.get("filters"):
query["filters"] = filters
else:
query.setdefault("filters", [])
if time_range := form_data.get("time_range"):
query["time_range"] = time_range
if where := form_data.get("where"):
query["where"] = where
if having := form_data.get("having"):
query["having"] = having
def _join_sql_clause(existing_clause: str, additional_clause: str) -> str:
"""AND two SQL filter clauses while preserving their original grouping."""
return f"({existing_clause}) AND ({additional_clause})"
def _is_temporal_override_filter(
filter_: dict[str, Any],
form_data: dict[str, Any],
) -> bool:
return (
filter_.get("op") == "TEMPORAL_RANGE"
and form_data.get("time_range") is not None
and filter_.get("val") == form_data.get("time_range")
and (
form_data.get("granularity") is None
or filter_.get("col") == form_data.get("granularity")
)
)
def merge_form_data_filters_into_query(
query: dict[str, Any],
form_data: dict[str, Any],
) -> None:
"""Merge normalized form_data filters into an existing query payload.
Saved query contexts can contain query-specific filter, where, or having
fields. This helper adds normalized predicates while applying request-level
extra_form_data overrides for temporal query fields.
"""
if filters := [
filter_
for filter_ in form_data.get("filters") or []
if not _is_temporal_override_filter(filter_, form_data)
]:
query["filters"] = [
*(query.get("filters") or []),
*filters,
]
for key in EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS.values():
if (
key in QUERY_CONTEXT_EXTRA_FORM_DATA_OVERRIDE_KEYS
and key in form_data
and form_data[key] is not None
):
query[key] = form_data[key]
for clause in ("where", "having"):
if additional_clause := form_data.get(clause):
if existing_clause := query.get(clause):
query[clause] = _join_sql_clause(existing_clause, additional_clause)
else:
query[clause] = additional_clause
def merge_extra_form_data_filters_into_query(
query: dict[str, Any],
extra_form_data: dict[str, Any],
datasource_id: Any,
datasource_type: str,
) -> None:
"""Merge request extra_form_data predicates into an existing query payload."""
extra_query_form_data: dict[str, Any] = {"adhoc_filters": []}
prepare_form_data_for_query(
extra_query_form_data,
datasource_id,
datasource_type,
extra_form_data,
)
merge_form_data_filters_into_query(query, extra_query_form_data)
def resolve_metrics(form_data: dict[str, Any], viz_type: str) -> list[Any]:
"""Extract metrics from form_data, handling chart-type-specific fields."""
if viz_type == "bubble":
return [m for field in ("x", "y", "size") if (m := form_data.get(field))]
metrics = form_data.get("metrics", [])
if not metrics and (metric := form_data.get("metric")):
metrics = [metric]
return metrics
def resolve_groupby(form_data: dict[str, Any]) -> list[Any]:
"""Extract groupby columns from form_data with fallback aliases."""
raw_columns = form_data.get("all_columns")
if form_data.get("query_mode") == "raw" and isinstance(raw_columns, list):
return list(raw_columns)
raw_groupby = form_data.get("groupby") or []
if isinstance(raw_groupby, str):
groupby: list[Any] = [raw_groupby]
else:
groupby = list(raw_groupby)
if groupby:
return groupby
for field in ("entity", "series"):
value = form_data.get(field)
if isinstance(value, str) and value not in groupby:
groupby.append(value)
form_columns = form_data.get("columns")
if isinstance(form_columns, list):
for col in form_columns:
if isinstance(col, str) and col not in groupby:
groupby.append(col)
if not groupby and isinstance(raw_columns, list):
groupby.extend(raw_columns)
return groupby
def resolve_metrics_and_groupby(
form_data: dict[str, Any],
chart: Any | None = None,
) -> tuple[list[Any], list[Any]]:
"""Resolve metrics and groupby columns from form_data."""
viz_type = form_data.get(
"viz_type", getattr(chart, "viz_type", "") if chart else ""
)
singular_metric_no_groupby = (
"big_number",
"big_number_total",
"pop_kpi",
)
if viz_type in singular_metric_no_groupby:
metrics: list[Any] = [metric] if (metric := form_data.get("metric")) else []
return metrics, []
return resolve_metrics(form_data, viz_type), resolve_groupby(form_data)
def extract_x_axis_col(form_data: dict[str, Any]) -> str | None:
"""Return the x_axis column name from form_data, or None if not set."""
x_axis = form_data.get("x_axis")
if isinstance(x_axis, str) and x_axis:
return x_axis
if isinstance(x_axis, dict):
col_name = x_axis.get("column_name")
return col_name if isinstance(col_name, str) and col_name else None
return None
def _build_single_query_dict(
form_data: dict[str, Any],
columns: list[Any],
metrics: list[Any],
row_limit: int | None = None,
order_desc: bool | None = None,
) -> dict[str, Any]:
"""Build one query entry for QueryContextFactory from form_data fields."""
qd: dict[str, Any] = {"columns": columns, "metrics": metrics}
effective_row_limit = row_limit
if effective_row_limit is None:
effective_row_limit = form_data.get("row_limit")
if effective_row_limit is not None:
qd["row_limit"] = effective_row_limit
if order_desc is not None:
qd["order_desc"] = order_desc
apply_form_data_filters_to_query(qd, form_data)
return qd
def _build_mixed_timeseries_secondary(
form_data: dict[str, Any],
x_axis_col: str | None,
engine: str,
row_limit: int | None = None,
order_desc: bool | None = None,
) -> dict[str, Any]:
"""Build the secondary query dict for the ``mixed_timeseries`` viz type."""
# avoid circular import
from superset.utils.core import split_adhoc_filters_into_base_filters
metrics_b: list[Any] = list(form_data.get("metrics_b") or [])
raw_b = form_data.get("groupby_b") or []
groupby_b: list[Any] = [raw_b] if isinstance(raw_b, str) else list(raw_b)
if x_axis_col and x_axis_col not in groupby_b:
groupby_b = [x_axis_col] + groupby_b
qd = _build_single_query_dict(
form_data,
groupby_b,
metrics_b,
row_limit=row_limit,
order_desc=order_desc,
)
if time_range_b := form_data.get("time_range_b"):
qd["time_range"] = time_range_b
if row_limit is None and (row_limit_b := form_data.get("row_limit_b")) is not None:
qd["row_limit"] = row_limit_b
if adhoc_filters_b := form_data.get("adhoc_filters_b"):
secondary_fd: dict[str, Any] = {"adhoc_filters": adhoc_filters_b}
split_adhoc_filters_into_base_filters(secondary_fd, engine)
if secondary_filters := secondary_fd.get("filters"):
qd["filters"] = secondary_filters
else:
qd.pop("filters", None)
for clause in ("where", "having"):
if secondary_clause := secondary_fd.get(clause):
qd[clause] = secondary_clause
else:
qd.pop(clause, None)
return qd
def build_query_dicts_from_form_data(
form_data: dict[str, Any],
datasource_id: Any,
datasource_type: str,
chart: Any | None = None,
extra_form_data: dict[str, Any] | None = None,
row_limit: int | None = None,
order_desc: bool | None = None,
) -> list[dict[str, Any]]:
"""Build chart-type-aware query dicts from Explore form_data."""
engine = resolve_datasource_engine(datasource_id, datasource_type)
prepare_form_data_for_query(
form_data,
datasource_id,
datasource_type,
extra_form_data,
datasource_engine=engine,
)
metrics, groupby = resolve_metrics_and_groupby(form_data, chart)
viz_type: str = (
form_data.get("viz_type")
or (getattr(chart, "viz_type", "") if chart else "")
or ""
)
is_timeseries = (
viz_type.startswith("echarts_timeseries") or viz_type == "mixed_timeseries"
)
x_axis_col: str | None = None
if is_timeseries:
x_axis_col = extract_x_axis_col(form_data)
if x_axis_col and x_axis_col not in groupby:
groupby = [x_axis_col] + groupby
queries = [
_build_single_query_dict(
form_data,
groupby,
metrics,
row_limit=row_limit,
order_desc=order_desc,
)
]
if viz_type == "mixed_timeseries":
queries.append(
_build_mixed_timeseries_secondary(
form_data,
x_axis_col,
engine,
row_limit=row_limit,
order_desc=order_desc,
)
)
return queries
def resolve_form_data_datasource(
form_data: dict[str, Any],
chart: Any | None = None,
) -> tuple[int | str | None, str]:
"""Resolve datasource id/type from form_data with chart fallbacks."""
datasource_id = form_data.get("datasource_id")
datasource_type = form_data.get("datasource_type")
if not datasource_id and (combined := form_data.get("datasource")):
if isinstance(combined, str) and "__" in combined:
parts = combined.split("__", 1)
datasource_id = int(parts[0]) if parts[0].isdigit() else parts[0]
datasource_type = parts[1] if len(parts) > 1 else None
if not datasource_id and chart:
datasource_id = getattr(chart, "datasource_id", None)
if not datasource_type and chart:
datasource_type = getattr(chart, "datasource_type", None)
return datasource_id, datasource_type if isinstance(
datasource_type, str
) else "table"
def build_query_context_from_form_data(
form_data: dict[str, Any],
chart: Any | None = None,
extra_form_data: dict[str, Any] | None = None,
row_limit: int | None = None,
order_desc: bool | None = None,
result_type: Any = None,
force: bool = False,
) -> Any:
"""Build a QueryContext from chart-type-aware Explore form_data."""
# avoid circular import
from superset.common.query_context_factory import QueryContextFactory
datasource_id, datasource_type = resolve_form_data_datasource(form_data, chart)
if not isinstance(datasource_id, (int, str)):
raise ValueError(
"Cannot determine datasource ID from form_data. "
"Provide a chart identifier or ensure form_data contains "
"'datasource_id' or 'datasource'."
)
queries = build_query_dicts_from_form_data(
form_data,
datasource_id,
datasource_type,
chart=chart,
extra_form_data=extra_form_data,
row_limit=row_limit,
order_desc=order_desc,
)
return QueryContextFactory().create(
datasource={"id": datasource_id, "type": datasource_type},
queries=queries,
form_data=form_data,
result_type=result_type,
force=force,
)
def extract_form_data_key_from_url(url: str | None) -> str | None:
"""Extract the form_data_key query parameter from an explore URL.
+9 -1
View File
@@ -37,6 +37,7 @@ from superset.mcp_service.chart.schemas import (
MixedTimeseriesChartConfig,
PieChartConfig,
PivotTableChartConfig,
SortByConfig,
TableChartConfig,
XYChartConfig,
)
@@ -466,7 +467,14 @@ def map_table_config(config: TableChartConfig) -> Dict[str, Any]:
_add_adhoc_filters(form_data, config.filters)
if config.sort_by:
form_data["order_by_cols"] = config.sort_by
form_data["order_by_cols"] = [
json.dumps(
[entry.column, entry.ascending]
if isinstance(entry, SortByConfig)
else [entry, False]
)
for entry in config.sort_by
]
form_data["row_limit"] = config.row_limit
+1 -1
View File
@@ -26,7 +26,6 @@ import logging
import math
from typing import Any, Dict, List
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.mcp_service.chart.schemas import (
ASCIIPreview,
ChartError,
@@ -78,6 +77,7 @@ def generate_preview_from_form_data(
"""
try:
# Execute query to get data
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.connectors.sqla.models import SqlaTable
from superset.extensions import db
+33 -13
View File
@@ -22,7 +22,7 @@ Pydantic schemas for chart-related responses
from __future__ import annotations
import difflib
from datetime import datetime, timezone
from datetime import datetime
from typing import Annotated, Any, Dict, List, Literal, Protocol
import humanize
@@ -50,7 +50,7 @@ from superset.mcp_service.common.cache_schemas import (
OwnedByMeMixin,
QueryCacheControl,
)
from superset.mcp_service.common.error_schemas import ChartGenerationError
from superset.mcp_service.common.error_schemas import ChartGenerationError, MCPBaseError
from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
from superset.mcp_service.privacy import filter_user_directory_fields
from superset.mcp_service.system.schemas import (
@@ -183,16 +183,8 @@ class ChartInfo(BaseModel):
return data
class ChartError(BaseModel):
error: str = Field(..., description="Error message")
error_type: str = Field(..., description="Type of error")
timestamp: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
description="Error timestamp",
)
model_config = ConfigDict(ser_json_timedelta="iso8601")
@field_validator("error")
class ChartError(MCPBaseError):
@field_validator("message")
@classmethod
def sanitize_error_for_llm_context(cls, value: str) -> str:
"""Wrap error text before it is exposed to LLM context."""
@@ -806,6 +798,30 @@ class FilterConfig(BaseModel):
return self
class SortByConfig(BaseModel):
"""Sort specification with explicit direction.
Accepts either this object or a bare column-name string in `sort_by`
lists. Bare strings default to descending, which matches the
sort-by-metric "top N" pattern most commonly used for tables.
"""
model_config = ConfigDict(populate_by_name=True)
column: str = Field(
...,
min_length=1,
max_length=255,
validation_alias=AliasChoices("column", "col"),
description="Column to sort by",
)
ascending: bool = Field(
False,
description="Sort ascending. Defaults to False (descending) to match "
"the typical sort-by-metric top-N use case.",
)
# Actual chart types
class PieChartConfig(UnknownFieldCheckMixin):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
@@ -1192,8 +1208,12 @@ class TableChartConfig(UnknownFieldCheckMixin):
description="Structured filters (column/op/value). "
"Do NOT use adhoc_filters or raw SQL expressions.",
)
sort_by: List[str] | None = Field(
sort_by: List[str | SortByConfig] | None = Field(
None,
description="Columns to sort by. Each entry is either a column-name "
"string (defaults to descending) or a SortByConfig object with "
"explicit `ascending`. Descending matches the sort-by-metric "
"top-N pattern most common for tables.",
validation_alias=AliasChoices("sort_by", "order_by_cols", "order_by"),
)
row_limit: int = Field(1000, description="Max rows returned", ge=1, le=50000)
+37 -199
View File
@@ -35,8 +35,11 @@ from superset.commands.exceptions import CommandException
from superset.exceptions import OAuth2Error, OAuth2RedirectError, SupersetException
from superset.extensions import event_logger
from superset.mcp_service.chart.chart_helpers import (
build_query_context_from_form_data,
build_query_dicts_from_form_data,
find_chart_by_identifier,
get_cached_form_data,
merge_extra_form_data_filters_into_query,
)
from superset.mcp_service.chart.chart_utils import validate_chart_dataset
from superset.mcp_service.chart.schemas import (
@@ -55,7 +58,6 @@ from superset.mcp_service.utils.oauth2_utils import (
build_oauth2_redirect_message,
OAUTH2_CONFIG_ERROR_MESSAGE,
)
from superset.utils.core import merge_extra_filters
logger = logging.getLogger(__name__)
@@ -94,16 +96,6 @@ def _sanitize_chart_data_for_llm_context(chart_data: ChartData) -> ChartData:
return ChartData.model_validate(payload)
def _apply_extra_form_data(
form_data: dict[str, Any], extra_form_data: dict[str, Any] | None
) -> None:
"""Merge dashboard native filters into chart form_data in-place."""
if not extra_form_data:
return
form_data["extra_form_data"] = extra_form_data
merge_extra_filters(form_data)
@tool(
tags=["data"],
class_permission_name="Chart",
@@ -293,65 +285,18 @@ async def get_chart_data( # noqa: C901
# If using cached form_data, we need to build query_context from it
if using_unsaved_state and cached_form_data_dict is not None:
# Build query context from cached form_data (unsaved state)
from superset.common.query_context_factory import QueryContextFactory
factory = QueryContextFactory()
row_limit = (
request.limit
or cached_form_data_dict.get("row_limit")
or current_app.config["ROW_LIMIT"]
)
# Get datasource info from cached form_data or fall back to chart
datasource_id = cached_form_data_dict.get(
"datasource_id", chart.datasource_id
)
datasource_type = cached_form_data_dict.get(
"datasource_type", chart.datasource_type
)
# Handle different chart types that have different form_data
# structures. Some charts use "metric" (singular), not "metrics"
# (plural): big_number, big_number_total, pop_kpi.
# These charts also don't have groupby columns.
cached_viz_type = cached_form_data_dict.get(
"viz_type", chart.viz_type or ""
)
if cached_viz_type in ("big_number", "big_number_total", "pop_kpi"):
metric = cached_form_data_dict.get("metric")
cached_metrics = [metric] if metric else []
cached_groupby: list[str] = []
else:
cached_metrics = cached_form_data_dict.get("metrics", [])
raw_groupby = cached_form_data_dict.get("groupby", [])
# Guard against string groupby (e.g. heatmap_v2 migrated
# from legacy heatmap where all_columns_y was a string)
if isinstance(raw_groupby, str):
cached_groupby = [raw_groupby]
else:
cached_groupby = list(raw_groupby)
_apply_extra_form_data(cached_form_data_dict, request.extra_form_data)
cached_query: dict[str, Any] = {
"filters": cached_form_data_dict.get("filters", []),
"columns": cached_groupby,
"metrics": cached_metrics,
"row_limit": row_limit,
"order_desc": cached_form_data_dict.get("order_desc", True),
}
# Include adhoc_filters so dashboard native filters are applied
cached_adhoc = cached_form_data_dict.get("adhoc_filters")
if cached_adhoc:
cached_query["adhoc_filters"] = cached_adhoc
query_context = factory.create(
datasource={
"id": datasource_id,
"type": datasource_type,
},
queries=[cached_query],
form_data=cached_form_data_dict,
query_context = build_query_context_from_form_data(
cached_form_data_dict,
chart=chart,
extra_form_data=request.extra_form_data,
row_limit=row_limit,
order_desc=cached_form_data_dict.get("order_desc", True),
force=request.force_refresh,
)
await ctx.debug(
@@ -420,102 +365,23 @@ async def get_chart_data( # noqa: C901
error_type="MissingQueryContext",
)
singular_metric_no_groupby = (
"big_number",
"big_number_total",
"pop_kpi",
fallback_queries = build_query_dicts_from_form_data(
form_data,
chart.datasource_id,
chart.datasource_type,
chart=chart,
extra_form_data=request.extra_form_data,
row_limit=row_limit,
order_desc=True,
)
singular_metric_types = (
*singular_metric_no_groupby,
"world_map",
"treemap_v2",
"sunburst_v2",
"gauge_chart",
)
if viz_type == "bubble":
# Bubble charts store metrics in x, y, size fields
bubble_metrics = []
for field in ("x", "y", "size"):
m = form_data.get(field)
if m:
bubble_metrics.append(m)
metrics = bubble_metrics
groupby_columns: list[str] = list(
form_data.get("entity", None) and [form_data["entity"]] or []
)
series_field = form_data.get("series")
if series_field and series_field not in groupby_columns:
groupby_columns.append(series_field)
elif viz_type in singular_metric_types:
# These chart types use "metric" (singular)
metric = form_data.get("metric")
metrics = [metric] if metric else []
if viz_type in singular_metric_no_groupby:
groupby_columns = []
else:
# Some singular-metric charts use groupby, entity,
# series, or columns for dimensional breakdown
groupby_columns = list(form_data.get("groupby") or [])
entity = form_data.get("entity")
if entity and entity not in groupby_columns:
groupby_columns.append(entity)
series = form_data.get("series")
if series and series not in groupby_columns:
groupby_columns.append(series)
form_columns = form_data.get("columns")
if form_columns and isinstance(form_columns, list):
for col in form_columns:
if isinstance(col, str) and col not in groupby_columns:
groupby_columns.append(col)
else:
# Standard charts use "metrics" (plural) and "groupby"
metrics = form_data.get("metrics", [])
raw_groupby = form_data.get("groupby") or []
# Guard against string groupby (e.g. heatmap_v2 migrated
# from legacy heatmap where all_columns_y was a string)
if isinstance(raw_groupby, str):
groupby_columns = [raw_groupby]
else:
groupby_columns = list(raw_groupby)
# Some chart types use "columns" instead of "groupby"
if not groupby_columns:
form_columns = form_data.get("columns")
if form_columns and isinstance(form_columns, list):
for col in form_columns:
if isinstance(col, str):
groupby_columns.append(col)
# Fallback: if metrics is still empty, try singular "metric"
if not metrics:
fallback_metric = form_data.get("metric")
if fallback_metric:
metrics = [fallback_metric]
# Fallback: try entity/series if groupby is still empty
if not groupby_columns:
entity = form_data.get("entity")
if entity:
groupby_columns.append(entity)
series = form_data.get("series")
if series and series not in groupby_columns:
groupby_columns.append(series)
# Build query columns list: include both x_axis and groupby
x_axis_config = form_data.get("x_axis")
query_columns = groupby_columns.copy()
if x_axis_config and isinstance(x_axis_config, str):
if x_axis_config not in query_columns:
query_columns.insert(0, x_axis_config)
elif x_axis_config and isinstance(x_axis_config, dict):
col_name = x_axis_config.get("column_name")
if col_name and col_name not in query_columns:
query_columns.insert(0, col_name)
# Safety net: if we could not extract any metrics or
# columns, return a clear error instead of the cryptic
# "Empty query?" that comes from deeper in the stack.
if not metrics and not query_columns:
if all(
not query.get("metrics") and not query.get("columns")
for query in fallback_queries
):
await ctx.warning(
"Cannot construct fallback query for chart %s "
"(viz_type=%s): no metrics, columns, or groupby "
@@ -534,26 +400,12 @@ async def get_chart_data( # noqa: C901
error_type="MissingQueryContext",
)
_apply_extra_form_data(form_data, request.extra_form_data)
fallback_query: dict[str, Any] = {
"filters": form_data.get("filters", []),
"columns": query_columns,
"metrics": metrics,
"row_limit": row_limit,
"order_desc": True,
}
# Include adhoc_filters so dashboard native filters are applied
fallback_adhoc = form_data.get("adhoc_filters")
if fallback_adhoc:
fallback_query["adhoc_filters"] = fallback_adhoc
query_context = factory.create(
datasource={
"id": chart.datasource_id,
"type": chart.datasource_type,
},
queries=[fallback_query],
queries=fallback_queries,
form_data=form_data,
force=request.force_refresh,
)
@@ -566,9 +418,14 @@ async def get_chart_data( # noqa: C901
for query in query_context_json.get("queries", []):
query["row_limit"] = request.limit
# Merge dashboard native filters into query_context's form_data
qc_form_data = query_context_json.setdefault("form_data", {})
_apply_extra_form_data(qc_form_data, request.extra_form_data)
if request.extra_form_data:
for query in query_context_json.get("queries", []):
merge_extra_form_data_filters_into_query(
query,
request.extra_form_data,
query_context_json["datasource"]["id"],
query_context_json["datasource"]["type"],
)
# Create QueryContext from the saved context using the schema
# This is exactly how the API does it
@@ -871,16 +728,14 @@ async def _query_from_form_data(
Used for unsaved charts where we only have form_data_key.
"""
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.common.query_context_factory import QueryContextFactory
datasource_id = form_data.get("datasource_id")
datasource_type: str = form_data.get("datasource_type") or "table"
# Handle combined datasource field (e.g., "1__table")
if not datasource_id and form_data.get("datasource"):
parts = str(form_data["datasource"]).split("__")
if len(parts) == 2:
datasource_id, datasource_type = parts[0], parts[1]
datasource_id = parts[0]
if not datasource_id:
return ChartError(
@@ -888,34 +743,17 @@ async def _query_from_form_data(
error_type="InvalidFormData",
)
viz_type = form_data.get("viz_type", "unknown")
row_limit = (
request.limit or form_data.get("row_limit") or current_app.config["ROW_LIMIT"]
)
# Extract metrics and groupby based on chart type
if viz_type in ("big_number", "big_number_total", "pop_kpi"):
metric = form_data.get("metric")
metrics = [metric] if metric else []
groupby: list[str] = []
else:
metrics = form_data.get("metrics", [])
groupby = list(form_data.get("groupby") or [])
viz_type = form_data.get("viz_type", "unknown")
try:
factory = QueryContextFactory()
query_context = factory.create(
datasource={"id": datasource_id, "type": datasource_type},
queries=[
{
"filters": form_data.get("filters", []),
"columns": groupby,
"metrics": metrics,
"row_limit": row_limit,
"order_desc": form_data.get("order_desc", True),
}
],
form_data=form_data,
query_context = build_query_context_from_form_data(
form_data,
extra_form_data=request.extra_form_data,
row_limit=row_limit,
order_desc=form_data.get("order_desc", True),
force=request.force_refresh,
)
@@ -33,7 +33,10 @@ from superset.mcp_service.chart.ascii_charts import (
generate_ascii_chart,
generate_ascii_table,
)
from superset.mcp_service.chart.chart_helpers import find_chart_by_identifier
from superset.mcp_service.chart.chart_helpers import (
build_query_context_from_form_data,
find_chart_by_identifier,
)
from superset.mcp_service.chart.chart_utils import validate_chart_dataset
from superset.mcp_service.chart.schemas import (
AccessibilityMetadata,
@@ -197,7 +200,6 @@ class ASCIIPreviewStrategy(PreviewFormatStrategy):
def generate(self) -> ASCIIPreview | ChartError:
try:
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.common.query_context_factory import QueryContextFactory
from superset.utils import json as utils_json
form_data = utils_json.loads(self.chart.params) if self.chart.params else {}
@@ -214,50 +216,11 @@ class ASCIIPreviewStrategy(PreviewFormatStrategy):
error_type="InvalidChart",
)
# Build query for chart data
x_axis_config = form_data.get("x_axis")
groupby_columns = form_data.get("groupby", [])
metrics = form_data.get("metrics", [])
# Table charts in raw mode use all_columns or columns
all_columns = form_data.get("all_columns", [])
raw_columns = form_data.get("columns", [])
if form_data.get("query_mode") == "raw" and (all_columns or raw_columns):
columns = list(all_columns or raw_columns)
else:
columns = groupby_columns.copy()
if x_axis_config and isinstance(x_axis_config, str):
columns.append(x_axis_config)
elif x_axis_config and isinstance(x_axis_config, dict):
if "column_name" in x_axis_config:
columns.append(x_axis_config["column_name"])
if not columns and not metrics:
return ChartError(
error=(
"Cannot generate ASCII preview: chart has no columns or "
"metrics in its configuration. This chart type may not "
"support ASCII preview."
),
error_type="UnsupportedChart",
)
factory = QueryContextFactory()
query_context = factory.create(
datasource={
"id": self.chart.datasource_id,
"type": self.chart.datasource_type,
},
queries=[
{
"filters": form_data.get("filters", []),
"columns": columns,
"metrics": metrics,
"row_limit": 50,
"order_desc": True,
}
],
form_data=form_data,
query_context = build_query_context_from_form_data(
form_data,
chart=self.chart,
row_limit=50,
order_desc=True,
force=False,
)
@@ -303,7 +266,6 @@ class TablePreviewStrategy(PreviewFormatStrategy):
def generate(self) -> TablePreview | ChartError:
try:
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.common.query_context_factory import QueryContextFactory
from superset.utils import json as utils_json
form_data = utils_json.loads(self.chart.params) if self.chart.params else {}
@@ -315,24 +277,11 @@ class TablePreviewStrategy(PreviewFormatStrategy):
error_type="InvalidChart",
)
columns = _build_query_columns(form_data)
factory = QueryContextFactory()
query_context = factory.create(
datasource={
"id": self.chart.datasource_id,
"type": self.chart.datasource_type,
},
queries=[
{
"filters": form_data.get("filters", []),
"columns": columns,
"metrics": form_data.get("metrics", []),
"row_limit": 20,
"order_desc": True,
}
],
form_data=form_data,
query_context = build_query_context_from_form_data(
form_data,
chart=self.chart,
row_limit=20,
order_desc=True,
force=False,
)
@@ -386,7 +335,6 @@ class VegaLitePreviewStrategy(PreviewFormatStrategy):
# Get chart data directly using the same logic as get_chart_data tool
# but without calling the MCP tool wrapper
from superset.commands.chart.data.get_data_command import ChartDataCommand
from superset.common.query_context_factory import QueryContextFactory
from superset.daos.chart import ChartDAO
from superset.utils import json as utils_json
@@ -419,26 +367,11 @@ class VegaLitePreviewStrategy(PreviewFormatStrategy):
utils_json.loads(self.chart.params) if self.chart.params else {}
)
# Build columns list: include both x_axis and groupby
columns = _build_query_columns(form_data)
# Create query context for data retrieval
factory = QueryContextFactory()
query_context = factory.create(
datasource={
"id": self.chart.datasource_id,
"type": self.chart.datasource_type,
},
queries=[
{
"filters": form_data.get("filters", []),
"columns": columns,
"metrics": form_data.get("metrics", []),
"row_limit": 1000, # More data for visualization
"order_desc": True,
}
],
form_data=form_data,
query_context = build_query_context_from_form_data(
form_data,
chart=self.chart,
row_limit=1000,
order_desc=True,
force=self.request.force_refresh,
)
+19 -222
View File
@@ -32,6 +32,13 @@ from superset.commands.exceptions import CommandException
from superset.commands.explore.form_data.parameters import CommandParameters
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.extensions import event_logger
from superset.mcp_service.chart.chart_helpers import (
build_query_context_from_form_data,
extract_x_axis_col,
resolve_groupby,
resolve_metrics,
resolve_metrics_and_groupby,
)
from superset.mcp_service.chart.chart_utils import validate_chart_dataset
from superset.mcp_service.chart.schemas import (
ChartError,
@@ -73,160 +80,25 @@ def _get_cached_form_data(form_data_key: str) -> str | None:
def _resolve_metrics(form_data: dict[str, Any], viz_type: str) -> list[Any]:
"""Extract metrics from form_data, handling chart-type-specific fields."""
# Bubble charts store measures in x, y, size fields
if viz_type == "bubble":
return [m for field in ("x", "y", "size") if (m := form_data.get(field))]
metrics = form_data.get("metrics", [])
# Fallback: some chart types store the measure as singular "metric"
if not metrics and (metric := form_data.get("metric")):
metrics = [metric]
return metrics
return resolve_metrics(form_data, viz_type)
def _resolve_groupby(form_data: dict[str, Any]) -> list[str]:
"""Extract groupby columns from form_data with fallback aliases.
Normalises scalar strings (e.g. heatmap_v2 migrated from legacy
``all_columns_y``) so that ``list("country")`` does not split into
individual characters.
"""
raw_groupby = form_data.get("groupby") or []
if isinstance(raw_groupby, str):
groupby: list[str] = [raw_groupby]
else:
groupby = list(raw_groupby)
if groupby:
return groupby
# Fallback: some chart types store dimensions in entity/series/columns
for field in ("entity", "series"):
value = form_data.get(field)
if isinstance(value, str) and value not in groupby:
groupby.append(value)
form_columns = form_data.get("columns")
if isinstance(form_columns, list):
for col in form_columns:
if isinstance(col, str) and col not in groupby:
groupby.append(col)
return groupby
def _resolve_groupby(form_data: dict[str, Any]) -> list[Any]:
"""Extract groupby columns from form_data with fallback aliases."""
return resolve_groupby(form_data)
def _resolve_metrics_and_groupby(
form_data: dict[str, Any],
chart: "Slice | None",
) -> tuple[list[Any], list[str]]:
"""Resolve metrics and groupby columns from form_data.
Handles chart-type-specific field names: singular ``metric`` for
big-number variants, bubble ``x``/``y``/``size``, and fallback
fields ``entity``, ``series``, and ``columns`` for dimensions.
"""
viz_type = form_data.get(
"viz_type", getattr(chart, "viz_type", "") if chart else ""
)
singular_metric_no_groupby = (
"big_number",
"big_number_total",
"pop_kpi",
)
if viz_type in singular_metric_no_groupby:
metrics: list[Any] = [metric] if (metric := form_data.get("metric")) else []
return metrics, []
return _resolve_metrics(form_data, viz_type), _resolve_groupby(form_data)
) -> tuple[list[Any], list[Any]]:
"""Resolve metrics and groupby columns from form_data."""
return resolve_metrics_and_groupby(form_data, chart)
def _extract_x_axis_col(form_data: dict[str, Any]) -> str | None:
"""Return the x_axis column name from form_data, or None if not set.
``x_axis`` may be stored as a plain column-name string or as an adhoc
column dict (``{"column_name": "...", ...}``).
"""
x_axis = form_data.get("x_axis")
if isinstance(x_axis, str) and x_axis:
return x_axis
if isinstance(x_axis, dict):
col_name = x_axis.get("column_name")
return col_name if isinstance(col_name, str) and col_name else None
return None
def _resolve_engine(
datasource_id: Any,
datasource_type: str,
) -> str:
"""Return the DB engine name for *datasource_id*, or ``"base"`` on any error."""
if not isinstance(datasource_id, (int, str)):
return "base"
try:
from superset.daos.datasource import DatasourceDAO
from superset.utils.core import DatasourceType
ds = DatasourceDAO.get_datasource(
datasource_type=DatasourceType(datasource_type),
database_id_or_uuid=datasource_id,
)
return ds.database.db_engine_spec.engine
except Exception: # noqa: BLE001
logger.debug("Could not resolve engine for datasource %s", datasource_id)
return "base"
def _build_single_query_dict(
form_data: dict[str, Any],
columns: list[Any],
metrics: list[Any],
) -> dict[str, Any]:
"""Build one query entry for QueryContextFactory from form_data fields."""
qd: dict[str, Any] = {"columns": columns, "metrics": metrics}
if time_range := form_data.get("time_range"):
qd["time_range"] = time_range
if filters := form_data.get("filters"):
qd["filters"] = filters
if (row_limit := form_data.get("row_limit")) is not None:
qd["row_limit"] = row_limit
return qd
def _build_mixed_timeseries_secondary(
form_data: dict[str, Any],
x_axis_col: str | None,
engine: str = "base",
) -> dict[str, Any]:
"""Build the secondary query dict for the ``mixed_timeseries`` viz type.
``mixed_timeseries`` has two independent series layers; the secondary
layer uses ``metrics_b`` / ``groupby_b`` instead of the primary fields.
Secondary-specific overrides (``time_range_b``, ``row_limit_b``,
``adhoc_filters_b``) replace the corresponding primary values so the
generated SQL accurately reflects each series' independent configuration.
"""
metrics_b: list[Any] = list(form_data.get("metrics_b") or [])
raw_b = form_data.get("groupby_b") or []
groupby_b: list[Any] = [raw_b] if isinstance(raw_b, str) else list(raw_b)
if x_axis_col and x_axis_col not in groupby_b:
groupby_b = [x_axis_col] + groupby_b
qd = _build_single_query_dict(form_data, groupby_b, metrics_b)
if time_range_b := form_data.get("time_range_b"):
qd["time_range"] = time_range_b
if (row_limit_b := form_data.get("row_limit_b")) is not None:
qd["row_limit"] = row_limit_b
# Process adhoc_filters_b into concrete filter clauses for the secondary
# query, mirroring how split_adhoc_filters_into_base_filters handles the
# primary adhoc_filters in _build_query_context_from_form_data.
if adhoc_filters_b := form_data.get("adhoc_filters_b"):
from superset.utils.core import split_adhoc_filters_into_base_filters
secondary_fd: dict[str, Any] = {"adhoc_filters": adhoc_filters_b}
split_adhoc_filters_into_base_filters(secondary_fd, engine)
if secondary_filters := secondary_fd.get("filters"):
qd["filters"] = secondary_filters
return qd
"""Return the x_axis column name from form_data, or None if not set."""
return extract_x_axis_col(form_data)
def _build_query_context_from_form_data(
@@ -239,85 +111,10 @@ def _build_query_context_from_form_data(
instead of executing the query.
"""
from superset.common.chart_data import ChartDataResultType
from superset.common.query_context_factory import QueryContextFactory
factory = QueryContextFactory()
datasource_id = form_data.get("datasource_id")
datasource_type = form_data.get("datasource_type")
# Unsaved Explore state often stores datasource as a combined field
# like "123__table" instead of separate datasource_id/datasource_type.
if not datasource_id and (combined := form_data.get("datasource")):
if isinstance(combined, str) and "__" in combined:
parts = combined.split("__", 1)
datasource_id = int(parts[0]) if parts[0].isdigit() else parts[0]
datasource_type = parts[1] if len(parts) > 1 else None
if not datasource_id and chart:
datasource_id = getattr(chart, "datasource_id", None)
if not datasource_type and chart:
datasource_type = getattr(chart, "datasource_type", None)
metrics, groupby = _resolve_metrics_and_groupby(form_data, chart)
# Preprocess adhoc_filters into where/having/filters on form_data so
# that the QueryObject receives concrete filter clauses. This mirrors
# the view-layer call in viz.py:process_query_filters.
from superset.utils.core import (
merge_extra_filters,
split_adhoc_filters_into_base_filters,
)
resolved_type_str: str = (
datasource_type if isinstance(datasource_type, str) else "table"
)
engine = _resolve_engine(datasource_id, resolved_type_str)
merge_extra_filters(form_data)
split_adhoc_filters_into_base_filters(form_data, engine)
viz_type: str = (
form_data.get("viz_type")
or (getattr(chart, "viz_type", "") if chart else "")
or ""
)
is_timeseries = (
viz_type.startswith("echarts_timeseries") or viz_type == "mixed_timeseries"
)
# For echarts_timeseries_* and mixed_timeseries charts the temporal
# column is stored in x_axis rather than groupby. Prepend it so the
# generated SQL includes the time axis.
x_axis_col: str | None = None
if is_timeseries:
x_axis_col = _extract_x_axis_col(form_data)
if x_axis_col and x_axis_col not in groupby:
groupby = [x_axis_col] + groupby
queries: list[dict[str, Any]] = [
_build_single_query_dict(form_data, groupby, metrics)
]
# mixed_timeseries exposes two independent query layers (primary and
# secondary). Build the second query from metrics_b / groupby_b so
# that get_chart_sql returns SQL for both and neither is silently lost.
if viz_type == "mixed_timeseries":
queries.append(_build_mixed_timeseries_secondary(form_data, x_axis_col, engine))
# Ensure datasource fields satisfy DatasourceDict typing requirements.
# datasource_id must be int | str; datasource_type must be str.
if not isinstance(datasource_id, (int, str)):
raise ValueError(
"Cannot determine datasource ID from form_data. "
"Provide a chart identifier or ensure form_data contains "
"'datasource_id' or 'datasource'."
)
resolved_id: int | str = datasource_id
return factory.create(
datasource={"id": resolved_id, "type": resolved_type_str},
queries=queries,
form_data=form_data,
return build_query_context_from_form_data(
form_data,
chart=chart,
result_type=ChartDataResultType.QUERY,
force=False,
)
+46 -12
View File
@@ -19,9 +19,53 @@
Enhanced error schemas for MCP chart generation with contextual information
"""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Dict, List
from pydantic import BaseModel, Field
from pydantic import BaseModel, computed_field, ConfigDict, Field, model_validator
class MCPBaseError(BaseModel):
"""Base error shape for all MCP tool responses.
Provides a consistent set of fields that every error response includes,
allowing LLM clients to handle errors uniformly regardless of which tool
produced them.
"""
error_type: str = Field(
..., description="Type of error (validation, execution, etc.)"
)
message: str = Field(..., description="Human-readable error message")
timestamp: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
description="Error timestamp",
)
details: str | None = Field(None, description="Detailed error explanation")
suggestions: list[str] = Field(
default_factory=list, description="Actionable suggestions to fix the error"
)
error_code: str | None = Field(
None, description="Unique error code for support reference"
)
model_config = ConfigDict(ser_json_timedelta="iso8601")
@model_validator(mode="before")
@classmethod
def _compat_error_to_message(cls, data: Any) -> Any:
"""Allow construction with error= kwarg for backward compatibility."""
if isinstance(data, dict) and "error" in data and "message" not in data:
data["message"] = data.pop("error")
return data
@computed_field # type: ignore[prop-decorator]
@property
def error(self) -> str:
"""Backward-compatible field: mirrors 'message' in serialized output."""
return self.message
class ColumnSuggestion(BaseModel):
@@ -67,13 +111,9 @@ class DatasetContext(BaseModel):
)
class ChartGenerationError(BaseModel):
class ChartGenerationError(MCPBaseError):
"""Enhanced error response for chart generation failures"""
error_type: str = Field(
..., description="Type of error (validation, execution, etc.)"
)
message: str = Field(..., description="High-level error message")
details: str = Field(..., description="Detailed error explanation")
validation_errors: List[ValidationError] = Field(
default_factory=list, description="Specific field validation errors"
@@ -84,15 +124,9 @@ class ChartGenerationError(BaseModel):
query_info: Dict[str, Any] | None = Field(
None, description="Query execution details"
)
suggestions: List[str] = Field(
default_factory=list, description="Actionable suggestions to fix the error"
)
help_url: str | None = Field(
None, description="URL to documentation for this error type"
)
error_code: str | None = Field(
None, description="Unique error code for support reference"
)
class ChartGenerationResponse(BaseModel):
+5
View File
@@ -2060,12 +2060,17 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
default_schema = self.database.get_default_schema(self.catalog)
try:
rls_applied = False
# ``id`` lives on concrete subclasses (e.g. SqlaTable), not on
# ExploreMixin itself. getattr keeps this safe for non-dataset
# subclasses (e.g. SQL Lab Query), which have no RLS to dedupe.
self_id = getattr(self, "id", None)
for statement in parsed_script.statements:
if apply_rls(
self.database,
self.catalog,
self.schema or default_schema or "",
statement,
exclude_dataset_id=self_id,
):
rls_applied = True

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