mirror of
https://github.com/apache/superset.git
synced 2026-05-21 07:45:08 +00:00
Compare commits
27 Commits
fix/dashbo
...
feat/glyph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b7b8cdea | ||
|
|
2908674e1f | ||
|
|
86f2f056d2 | ||
|
|
79eb91da96 | ||
|
|
96be1ccb2d | ||
|
|
f63e369687 | ||
|
|
67231d194e | ||
|
|
816583b602 | ||
|
|
49b9afa591 | ||
|
|
8efda4f704 | ||
|
|
12ebe0f032 | ||
|
|
857ba7f855 | ||
|
|
dd1226f8e5 | ||
|
|
46555d12cb | ||
|
|
5b0d895322 | ||
|
|
1eae7b4ee5 | ||
|
|
6a054f9170 | ||
|
|
865b75c90e | ||
|
|
6fc18b38fa | ||
|
|
3f61764543 | ||
|
|
9dad9cbfc2 | ||
|
|
b2c58a0856 | ||
|
|
2e16b8266a | ||
|
|
4e09889607 | ||
|
|
672e9a1477 | ||
|
|
8fa5a75c70 | ||
|
|
144dae7c43 |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"a0289491-ebb9-4d03-aa1d-023b0219c585","pid":48965,"procStart":"Fri May 1 19:01:07 2026","acquiredAt":1778687635094}
|
||||
7
.github/workflows/superset-docs-verify.yml
vendored
7
.github/workflows/superset-docs-verify.yml
vendored
@@ -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
|
||||
|
||||
@@ -111,6 +111,8 @@ services:
|
||||
superset-init-light:
|
||||
condition: service_completed_successfully
|
||||
volumes: *superset-volumes
|
||||
ports:
|
||||
- "${SUPERSET_PORT:-8088}:8088"
|
||||
environment:
|
||||
DATABASE_HOST: db-light
|
||||
DATABASE_DB: superset_light
|
||||
@@ -162,7 +164,7 @@ services:
|
||||
environment:
|
||||
# set this to false if you have perf issues running the npm i; npm run dev in-docker
|
||||
# if you do so, you have to run this manually on the host, which should perform better!
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER: false
|
||||
NPM_RUN_PRUNE: false
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"
|
||||
|
||||
@@ -34,6 +34,14 @@ x-superset-volumes: &superset-volumes
|
||||
- superset_home:/app/superset_home
|
||||
- ./tests:/app/tests
|
||||
- superset_data:/app/data
|
||||
# Python package metadata for the editable `uv pip install -e .` that
|
||||
# docker-bootstrap.sh runs at container start. Without these bind mounts
|
||||
# the editable install reads stale metadata baked into the image at
|
||||
# build time and may conflict with apache-superset-core's current pins.
|
||||
- ./pyproject.toml:/app/pyproject.toml
|
||||
- ./setup.py:/app/setup.py
|
||||
- ./MANIFEST.in:/app/MANIFEST.in
|
||||
- ./README.md:/app/README.md
|
||||
x-common-build: &common-build
|
||||
context: .
|
||||
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
docs/scripts/lint-docs-links.mjs
Normal file
230
docs/scripts/lint-docs-links.mjs
Normal 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.
|
||||
You’ll 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.
|
||||
|
||||
@@ -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. It’s 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ module.exports = {
|
||||
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
'^spec/(.*)$': '<rootDir>/spec/$1',
|
||||
// mapping glyph-core to local package source
|
||||
'^@superset-ui/glyph-core$':
|
||||
'<rootDir>/packages/superset-ui-glyph-core/src',
|
||||
'^@superset-ui/glyph-core/(.*)$':
|
||||
'<rootDir>/packages/superset-ui-glyph-core/src/$1',
|
||||
// mapping plugins of superset-ui to source code
|
||||
'^@superset-ui/([^/]+)/(.*)$':
|
||||
'<rootDir>/node_modules/@superset-ui/$1/src/$2',
|
||||
|
||||
10959
superset-frontend/package-lock.json
generated
10959
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -109,7 +109,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/fira-code": "^5.2.7",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@googleapis/sheets": "^13.0.1",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
@@ -199,6 +201,7 @@
|
||||
"nanoid": "^5.1.11",
|
||||
"ol": "^10.9.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "9.3.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -441,6 +441,8 @@ export interface ControlPanelConfig {
|
||||
sectionOverrides?: SectionOverrides;
|
||||
onInit?: (state: ControlStateMapping) => void;
|
||||
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
|
||||
/** @internal Raw glyph argument definitions from defineChart() – used for native control panel rendering */
|
||||
_glyphArgs?: unknown;
|
||||
}
|
||||
|
||||
export type ControlOverrides = {
|
||||
|
||||
@@ -33,6 +33,14 @@ export enum Behavior {
|
||||
*/
|
||||
DrillToDetail = 'DRILL_TO_DETAIL',
|
||||
DrillBy = 'DRILL_BY',
|
||||
|
||||
/**
|
||||
* Include `ALLOWS_EMPTY_RESULTS` behavior if the chart handles empty/no data
|
||||
* gracefully (e.g., showing a drop zone for drag-and-drop configuration).
|
||||
* Charts with this behavior will receive empty data instead of seeing
|
||||
* the "No results" message.
|
||||
*/
|
||||
AllowsEmptyResults = 'ALLOWS_EMPTY_RESULTS',
|
||||
}
|
||||
|
||||
export interface ContextMenuFilters {
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
# Glyph Pattern Migration Guide
|
||||
|
||||
This guide documents how to migrate traditional Superset chart plugins to the single-file Glyph pattern.
|
||||
|
||||
## Overview
|
||||
|
||||
The Glyph pattern simplifies chart plugin development by:
|
||||
- **Arguments define BOTH controls AND render props** - No separate files needed
|
||||
- **No `controlPanel.ts`** - Generated from argument definitions
|
||||
- **No `transformProps.ts`** - Arguments are passed directly to render
|
||||
- **No `buildQuery.ts`** - Inferred from Metric/Dimension/Temporal arguments
|
||||
- **Single file** - Everything in one place (~200 lines vs 500+ across multiple files)
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. Analyze the Existing Chart
|
||||
|
||||
Identify from the original chart:
|
||||
- **Metrics/Dimensions**: What data does it query?
|
||||
- **Controls**: What options does the user configure?
|
||||
- **Styling**: What visual customizations exist?
|
||||
- **Rendering**: How is the data displayed?
|
||||
|
||||
### 2. Create the Glyph Chart File
|
||||
|
||||
Create a new file: `src/BigNumber/BigNumberGlyph/index.tsx`
|
||||
|
||||
```typescript
|
||||
import { t } from '@apache-superset/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { Behavior, getNumberFormatter, CurrencyFormatter } from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
defineChart,
|
||||
Metric,
|
||||
Select,
|
||||
Text,
|
||||
Checkbox,
|
||||
NumberFormat,
|
||||
Currency,
|
||||
TimeFormat,
|
||||
ConditionalFormatting,
|
||||
} from '@superset-ui/glyph-core';
|
||||
```
|
||||
|
||||
### 3. Define Arguments (Controls + Props)
|
||||
|
||||
**CRITICAL: Use camelCase for argument names!**
|
||||
|
||||
Superset converts control names to camelCase in `formData`. If you use snake_case (`show_metric_name`), it won't match the camelCase key in formData (`showMetricName`).
|
||||
|
||||
```typescript
|
||||
arguments: {
|
||||
// Data arguments
|
||||
metric: Metric.with({ label: t('Metric') }),
|
||||
|
||||
// Visual arguments - USE CAMELCASE!
|
||||
headerFontSize: Select.with({
|
||||
label: t('Font Size'),
|
||||
options: [
|
||||
{ label: t('Small'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.4 },
|
||||
],
|
||||
default: 0.4,
|
||||
}),
|
||||
|
||||
showMetricName: Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
default: false,
|
||||
}),
|
||||
|
||||
// Declarative visibility (preferred)
|
||||
metricNameFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { showMetricName: true },
|
||||
},
|
||||
|
||||
// Declarative disabled state
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
disabledWhen: { subtitle: '' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Available Argument Types
|
||||
|
||||
| Type | Control Generated | Value Type | Properties |
|
||||
|------|------------------|------------|------------|
|
||||
| `Metric` | MetricControl | `{ value, name, formattedValue }` | `label` |
|
||||
| `Dimension` | GroupByControl | `string[]` | `label` |
|
||||
| `Temporal` | TemporalControl | `string` | `label` |
|
||||
| `Select` | SelectControl | `string \| number` | `label`, `description`, `options`, `default` |
|
||||
| `Text` | TextControl | `string` | `label`, `description`, `default`, `placeholder` |
|
||||
| `Checkbox` | CheckboxControl | `boolean` | `label`, `description`, `default` |
|
||||
| `Int` | SliderControl | `number` | `label`, `description`, `default`, `min`, `max`, `step` |
|
||||
| `Color` | ColorPickerControl | `string` (hex) | `label`, `description`, `default` |
|
||||
| `NumberFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
|
||||
| `Currency` | CurrencyControl | `{ symbol?, symbolPosition? }` | `label`, `description`, `default` |
|
||||
| `TimeFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` |
|
||||
| `ConditionalFormatting` | ConditionalFormattingControl | `Rule[]` | `label`, `description` |
|
||||
|
||||
### 5. Declarative Visibility & Disabled States
|
||||
|
||||
Instead of Redux `mapStateToProps`, use declarative conditions:
|
||||
|
||||
```typescript
|
||||
// Simple equality check - visible when showMetricName is true
|
||||
metricNameFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { showMetricName: true },
|
||||
},
|
||||
|
||||
// Function check - visible when subtitle is not empty
|
||||
subtitleFontSize: {
|
||||
arg: Select.with({ ... }),
|
||||
visibleWhen: { subtitle: (val) => !!val },
|
||||
},
|
||||
|
||||
// Multiple conditions (AND) - visible when both conditions are met
|
||||
advancedOption: {
|
||||
arg: Checkbox.with({ ... }),
|
||||
visibleWhen: {
|
||||
showMetricName: true,
|
||||
subtitle: (val) => !!val,
|
||||
},
|
||||
},
|
||||
|
||||
// Disabled state (control visible but not editable)
|
||||
formatOption: {
|
||||
arg: Select.with({ ... }),
|
||||
disabledWhen: { forceTimestampFormatting: true },
|
||||
},
|
||||
```
|
||||
|
||||
### 6. Number, Currency, and Time Formatting
|
||||
|
||||
Use the built-in format argument types:
|
||||
|
||||
```typescript
|
||||
arguments: {
|
||||
numberFormat: NumberFormat.with({
|
||||
label: t('Number Format'),
|
||||
description: t('D3 format string'),
|
||||
default: 'SMART_NUMBER',
|
||||
}),
|
||||
|
||||
currencyFormat: Currency.with({
|
||||
label: t('Currency Format'),
|
||||
}),
|
||||
|
||||
timeFormat: TimeFormat.with({
|
||||
label: t('Date Format'),
|
||||
default: 'smart_date',
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Then use them directly in the render function:
|
||||
|
||||
```typescript
|
||||
render: ({ numberFormat, currencyFormat, timeFormat, metric }) => {
|
||||
const formatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({
|
||||
currency: { symbol: currencyFormat.symbol, symbolPosition: currencyFormat.symbolPosition ?? 'prefix' },
|
||||
d3Format: numberFormat,
|
||||
})
|
||||
: getNumberFormatter(numberFormat);
|
||||
|
||||
return <div>{formatter(metric.value)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Conditional Formatting (Colors)
|
||||
|
||||
Use `ConditionalFormatting` for color-based rules:
|
||||
|
||||
```typescript
|
||||
import { getColorFormatters } from '@superset-ui/chart-controls';
|
||||
|
||||
arguments: {
|
||||
conditionalFormatting: ConditionalFormatting.with({
|
||||
label: t('Conditional Formatting'),
|
||||
description: t('Apply conditional color formatting to metric'),
|
||||
}),
|
||||
},
|
||||
|
||||
render: ({ conditionalFormatting, metric, data, theme }) => {
|
||||
let numberColor: string | undefined;
|
||||
|
||||
if (conditionalFormatting?.length > 0 && metric.value != null) {
|
||||
const colorFormatters = getColorFormatters(conditionalFormatting, data, theme, false);
|
||||
if (colorFormatters) {
|
||||
for (const formatter of colorFormatters) {
|
||||
const color = formatter.getColorFromValue(metric.value as number);
|
||||
if (color) {
|
||||
numberColor = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <BigNumberText color={numberColor}>{metric.formattedValue}</BigNumberText>;
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Styled Components
|
||||
|
||||
Use Superset's theme properties with template literal syntax:
|
||||
|
||||
```typescript
|
||||
const Container = styled.div<{ height: number }>`
|
||||
${({ theme, height }) => `
|
||||
height: ${height}px;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
font-family: ${theme.fontFamily};
|
||||
color: ${theme.colorText};
|
||||
`}
|
||||
`;
|
||||
```
|
||||
|
||||
**Common theme properties:**
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `theme.sizeUnit` | Base spacing unit (typically 4px) |
|
||||
| `theme.fontFamily` | Default font family |
|
||||
| `theme.fontWeightNormal` | Normal font weight |
|
||||
| `theme.fontWeightLight` | Light font weight |
|
||||
| `theme.fontSizeSM` | Small font size |
|
||||
| `theme.colorText` | Primary text color |
|
||||
| `theme.colorTextTertiary` | Muted/secondary text color |
|
||||
| `theme.borderRadius` | Standard border radius |
|
||||
|
||||
### 9. Render Function
|
||||
|
||||
The render function receives all arguments directly - no formData lookup needed:
|
||||
|
||||
```typescript
|
||||
render: ({
|
||||
metric,
|
||||
headerFontSize,
|
||||
showMetricName,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
conditionalFormatting,
|
||||
height,
|
||||
data,
|
||||
theme,
|
||||
}) => {
|
||||
// All arguments are directly available!
|
||||
const formatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({ currency: currencyFormat, d3Format: numberFormat })
|
||||
: getNumberFormatter(numberFormat);
|
||||
|
||||
const formattedValue = metric.value != null
|
||||
? formatter(metric.value as number)
|
||||
: t('No data');
|
||||
|
||||
return (
|
||||
<Container height={height}>
|
||||
{showMetricName && <MetricName>{metric.name}</MetricName>}
|
||||
<BigNumberText>{formattedValue}</BigNumberText>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
```
|
||||
|
||||
### 10. Register the Plugin
|
||||
|
||||
In `BigNumber/index.ts`:
|
||||
```typescript
|
||||
export { default as BigNumberGlyphChartPlugin } from './BigNumberGlyph';
|
||||
```
|
||||
|
||||
In `plugin-chart-echarts/src/index.ts`:
|
||||
```typescript
|
||||
export { BigNumberGlyphChartPlugin } from './BigNumber';
|
||||
```
|
||||
|
||||
In `MainPreset.js`:
|
||||
```typescript
|
||||
import { BigNumberGlyphChartPlugin } from '@superset-ui/plugin-chart-echarts';
|
||||
|
||||
new BigNumberGlyphChartPlugin().configure({ key: 'big_number_glyph' }),
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Snake Case vs Camel Case
|
||||
- **WRONG**: `show_metric_name` - won't match formData
|
||||
- **RIGHT**: `showMetricName` - matches Superset's camelCase conversion
|
||||
|
||||
### 2. Theme Undefined
|
||||
- **WRONG**: `theme.gridUnit` - crashes if theme is undefined
|
||||
- **RIGHT**: `theme?.gridUnit ?? 4` - safe with fallback
|
||||
|
||||
### 3. Metric Value Extraction
|
||||
The Glyph core automatically extracts metric values from query results. The `metric` argument provides:
|
||||
- `metric.value` - The raw numeric value
|
||||
- `metric.name` - The metric label/name
|
||||
- `metric.formattedValue` - Basic string representation
|
||||
|
||||
### 4. Visibility vs Legacy Functions
|
||||
- **Prefer**: `visibleWhen: { showMetricName: true }` - declarative, clean
|
||||
- **Legacy**: `visibility: ({ controls }) => controls?.showMetricName?.value === true` - still works
|
||||
|
||||
## File Structure Comparison
|
||||
|
||||
### Traditional (5+ files, ~500 lines)
|
||||
```
|
||||
BigNumberTotal/
|
||||
├── index.ts # Plugin registration
|
||||
├── controlPanel.ts # Control definitions (~100 lines)
|
||||
├── transformProps.ts # Data transformation (~150 lines)
|
||||
├── buildQuery.ts # Query building (~50 lines)
|
||||
├── BigNumberViz.tsx # React component (~150 lines)
|
||||
└── types.ts # TypeScript types (~50 lines)
|
||||
```
|
||||
|
||||
### Glyph Pattern (1 file, ~250 lines)
|
||||
```
|
||||
BigNumberGlyph/
|
||||
└── index.tsx # Everything in one file!
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `BigNumber/BigNumberGlyph/index.tsx` for a complete working example with:
|
||||
- Metric display
|
||||
- Number/currency/time formatting
|
||||
- Conditional color formatting
|
||||
- Declarative visibility
|
||||
- Subtitle support
|
||||
- Font size controls
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@superset-ui/glyph-core",
|
||||
"version": "0.20.3",
|
||||
"description": "Glyph Core - A declarative visualization plugin framework for Apache Superset",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/packages/superset-ui-glyph-core"
|
||||
},
|
||||
"keywords": [
|
||||
"superset",
|
||||
"glyph",
|
||||
"visualization",
|
||||
"chart"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ColumnType,
|
||||
SelectOptions,
|
||||
SelectOption,
|
||||
TextOptions,
|
||||
CheckboxOptions,
|
||||
IntOptions,
|
||||
ColorOptions,
|
||||
MetricOptions,
|
||||
DimensionOptions,
|
||||
NumberFormatOptions,
|
||||
CurrencyOptions,
|
||||
CurrencyValue,
|
||||
TimeFormatOptions,
|
||||
ConditionalFormattingOptions,
|
||||
ConditionalFormattingRule,
|
||||
SliderOptions,
|
||||
BoundsOptions,
|
||||
BoundsValue,
|
||||
ColorPickerOptions,
|
||||
RadioButtonOptions,
|
||||
RadioOption,
|
||||
RgbaColor,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Base Argument class - all argument types extend from this.
|
||||
*
|
||||
* Arguments define:
|
||||
* 1. What the chart needs (semantically)
|
||||
* 2. How to render controls in the control panel
|
||||
* 3. Default values and validation
|
||||
*/
|
||||
export class Argument {
|
||||
static label: string | null = null;
|
||||
static description: string | null = null;
|
||||
static columnType: ColumnType = ColumnType.Argument;
|
||||
static controlType: string = 'TextControl';
|
||||
|
||||
value: unknown;
|
||||
|
||||
constructor(value: unknown) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric - represents a numeric aggregation (SUM, COUNT, AVG, etc.)
|
||||
*
|
||||
* Maps to Superset's MetricsControl in the query section.
|
||||
*/
|
||||
export class Metric extends Argument {
|
||||
static override label: string | null = 'Metric';
|
||||
static override description: string | null =
|
||||
'A numeric aggregation (SUM, COUNT, AVG, etc.)';
|
||||
static override columnType = ColumnType.Metric;
|
||||
static override controlType = 'MetricsControl';
|
||||
static multi = false;
|
||||
|
||||
static with(options: MetricOptions): typeof Metric {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimension - represents a categorical column for grouping data
|
||||
*
|
||||
* Maps to Superset's GroupByControl in the query section.
|
||||
*/
|
||||
export class Dimension extends Argument {
|
||||
static override label: string | null = 'Dimension';
|
||||
static override description: string | null =
|
||||
'A categorical column for grouping data';
|
||||
static override columnType = ColumnType.Dimension;
|
||||
static override controlType = 'GroupByControl';
|
||||
static multi = true;
|
||||
|
||||
static with(options: DimensionOptions): typeof Dimension {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override multi = options.multi ?? Base.multi;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporal - represents a time column
|
||||
*
|
||||
* Maps to Superset's temporal controls (x_axis, time_grain_sqla).
|
||||
*/
|
||||
export class Temporal extends Argument {
|
||||
static override label: string | null = 'Time Column';
|
||||
static override description: string | null =
|
||||
'A temporal column for time series data';
|
||||
static override columnType = ColumnType.Temporal;
|
||||
static override controlType = 'TemporalControl';
|
||||
|
||||
static with(options: {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}): typeof Temporal {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select - dropdown selection from predefined options
|
||||
*
|
||||
* Maps to Superset's SelectControl.
|
||||
*/
|
||||
export class Select extends Argument {
|
||||
static override label: string | null = 'Select';
|
||||
static override description: string | null = 'Choose from options';
|
||||
static override controlType = 'SelectControl';
|
||||
static default: string | number = '';
|
||||
static options: SelectOption[] = [];
|
||||
static clearable = false;
|
||||
|
||||
static with(options: SelectOptions): typeof Select {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options ?? Base.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text - free-form text input
|
||||
*
|
||||
* Maps to Superset's TextControl.
|
||||
*/
|
||||
export class Text extends Argument {
|
||||
static override label: string | null = 'Text';
|
||||
static override description: string | null = 'Text input';
|
||||
static override controlType = 'TextControl';
|
||||
static default: string = '';
|
||||
static placeholder: string = '';
|
||||
|
||||
static with(options: TextOptions): typeof Text {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override placeholder = options.placeholder ?? Base.placeholder;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox - boolean toggle
|
||||
*
|
||||
* Maps to Superset's CheckboxControl.
|
||||
*/
|
||||
export class Checkbox extends Argument {
|
||||
static override label: string | null = 'Checkbox';
|
||||
static override description: string | null = 'Toggle option';
|
||||
static override controlType = 'CheckboxControl';
|
||||
static default: boolean = false;
|
||||
|
||||
static with(options: CheckboxOptions): typeof Checkbox {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Int - numeric input with slider
|
||||
*
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Int extends Argument {
|
||||
static override label: string | null = 'Integer';
|
||||
static override description: string | null = 'A numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 100;
|
||||
static step: number = 1;
|
||||
|
||||
static with(options: IntOptions): typeof Int {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color - color picker
|
||||
*
|
||||
* Maps to Superset's ColorPickerControl.
|
||||
*/
|
||||
export class Color extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'A color value';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
static default: string = '#000000';
|
||||
|
||||
static with(options: ColorOptions): typeof Color {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NumberFormat - D3 number format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class NumberFormat extends Argument {
|
||||
static override label: string | null = 'Number Format';
|
||||
static override description: string | null =
|
||||
'D3 format string for number display (e.g., ".2f", ".1%", ",.0f")';
|
||||
static override controlType = 'NumberFormatControl';
|
||||
static default: string = 'SMART_NUMBER';
|
||||
|
||||
// Standard D3 format options
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = [
|
||||
{ label: 'Adaptive formatting', value: 'SMART_NUMBER' },
|
||||
{ label: 'Original value', value: '~g' },
|
||||
{ label: '12,345.432', value: ',.3f' },
|
||||
{ label: '12,345.43', value: ',.2f' },
|
||||
{ label: '12,345.4', value: ',.1f' },
|
||||
{ label: '12,345', value: ',.0f' },
|
||||
{ label: '12345.432', value: '.3f' },
|
||||
{ label: '12345.43', value: '.2f' },
|
||||
{ label: '12345.4', value: '.1f' },
|
||||
{ label: '12345', value: '.0f' },
|
||||
{ label: '12K', value: '.0s' },
|
||||
{ label: '12.3K', value: '.1s' },
|
||||
{ label: '12.35K', value: '.2s' },
|
||||
{ label: '12.346K', value: '.3s' },
|
||||
{ label: '1234543.21%', value: '.2%' },
|
||||
{ label: '1234543%', value: '.0%' },
|
||||
{ label: '12.34%', value: '.2r' },
|
||||
{ label: '+12,345.4', value: '+,.1f' },
|
||||
{ label: '$12,345.43', value: '$,.2f' },
|
||||
{ label: 'Duration (1m 6s)', value: 'DURATION' },
|
||||
{ label: 'Duration (1ms 400µs)', value: 'DURATION_SUB' },
|
||||
];
|
||||
|
||||
static with(options: NumberFormatOptions): typeof NumberFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency - currency format with symbol and position
|
||||
*
|
||||
* Maps to Superset's CurrencyControl.
|
||||
* Value is { symbol: 'USD', symbolPosition: 'prefix' | 'suffix' }
|
||||
*/
|
||||
export class Currency extends Argument {
|
||||
static override label: string | null = 'Currency Format';
|
||||
static override description: string | null =
|
||||
'Currency symbol and position for formatting';
|
||||
static override controlType = 'CurrencyControl';
|
||||
static default: CurrencyValue = {};
|
||||
|
||||
static with(options: CurrencyOptions): typeof Currency {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TimeFormat - D3 time format string selection
|
||||
*
|
||||
* Maps to Superset's SelectControl with D3 time format options.
|
||||
* Allows freeform input for custom formats.
|
||||
*/
|
||||
export class TimeFormat extends Argument {
|
||||
static override label: string | null = 'Time Format';
|
||||
static override description: string | null =
|
||||
'D3 time format string (e.g., "%Y-%m-%d", "%H:%M:%S")';
|
||||
static override controlType = 'TimeFormatControl';
|
||||
static default: string = 'smart_date';
|
||||
|
||||
// Standard D3 time format options
|
||||
static readonly FORMAT_OPTIONS: SelectOption[] = [
|
||||
{ label: 'Adaptive formatting', value: 'smart_date' },
|
||||
{ label: '%d/%m/%Y | 14/01/2019', value: '%d/%m/%Y' },
|
||||
{ label: '%m/%d/%Y | 01/14/2019', value: '%m/%d/%Y' },
|
||||
{ label: '%d.%m.%Y | 14.01.2019', value: '%d.%m.%Y' },
|
||||
{ label: '%Y-%m-%d | 2019-01-14', value: '%Y-%m-%d' },
|
||||
{
|
||||
label: '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10',
|
||||
value: '%Y-%m-%d %H:%M:%S',
|
||||
},
|
||||
{
|
||||
label: '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10',
|
||||
value: '%d-%m-%Y %H:%M:%S',
|
||||
},
|
||||
{ label: '%H:%M:%S | 01:32:10', value: '%H:%M:%S' },
|
||||
];
|
||||
|
||||
static with(options: TimeFormatOptions): typeof TimeFormat {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ConditionalFormatting - apply color rules based on metric values
|
||||
*
|
||||
* This is a special argument type that encapsulates the complex
|
||||
* mapStateToProps logic needed for conditional formatting controls.
|
||||
* The control automatically receives numeric column options from the chart response.
|
||||
*/
|
||||
export class ConditionalFormatting extends Argument {
|
||||
static override label: string | null = 'Conditional Formatting';
|
||||
static override description: string | null =
|
||||
'Apply conditional color formatting to metric values';
|
||||
static override controlType = 'ConditionalFormattingControl';
|
||||
static default: ConditionalFormattingRule[] = [];
|
||||
|
||||
static with(
|
||||
options: ConditionalFormattingOptions,
|
||||
): typeof ConditionalFormatting {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider - continuous floating point values with min/max/step
|
||||
*
|
||||
* Similar to Int but for float values.
|
||||
* Maps to Superset's SliderControl.
|
||||
*/
|
||||
export class Slider extends Argument {
|
||||
static override label: string | null = 'Slider';
|
||||
static override description: string | null = 'A continuous numeric value';
|
||||
static override controlType = 'SliderControl';
|
||||
static default: number = 0;
|
||||
static min: number = 0;
|
||||
static max: number = 1;
|
||||
static step: number = 0.1;
|
||||
|
||||
static with(options: SliderOptions): typeof Slider {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override min = options.min ?? Base.min;
|
||||
static override max = options.max ?? Base.max;
|
||||
static override step = options.step ?? Base.step;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds - min/max value pairs
|
||||
*
|
||||
* Used for axis bounds, value ranges, etc.
|
||||
* Maps to Superset's BoundsControl.
|
||||
*/
|
||||
export class Bounds extends Argument {
|
||||
static override label: string | null = 'Bounds';
|
||||
static override description: string | null = 'Min and max value bounds';
|
||||
static override controlType = 'BoundsControl';
|
||||
static default: BoundsValue = [null, null];
|
||||
|
||||
static with(options: BoundsOptions): typeof Bounds {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ColorPicker - RGBA color selection
|
||||
*
|
||||
* Different from Color (which uses hex strings).
|
||||
* Maps to Superset's ColorPickerControl with RGBA format.
|
||||
*/
|
||||
export class ColorPicker extends Argument {
|
||||
static override label: string | null = 'Color';
|
||||
static override description: string | null = 'Select a color';
|
||||
static override controlType = 'ColorPickerControl';
|
||||
static default: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
|
||||
|
||||
static with(options: ColorPickerOptions): typeof ColorPicker {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RadioButton - mutually exclusive options
|
||||
*
|
||||
* Use for small sets of exclusive choices (2-4 options).
|
||||
* Maps to Superset's RadioButtonControl.
|
||||
*/
|
||||
export class RadioButton extends Argument {
|
||||
static override label: string | null = 'Option';
|
||||
static override description: string | null = 'Select one option';
|
||||
static override controlType = 'RadioButtonControl';
|
||||
static default: string | boolean = '';
|
||||
static options: RadioOption[] = [];
|
||||
|
||||
static with(options: RadioButtonOptions): typeof RadioButton {
|
||||
const Base = this;
|
||||
return class extends Base {
|
||||
static override label = options.label ?? Base.label;
|
||||
static override description = options.description ?? Base.description;
|
||||
static override default = options.default ?? Base.default;
|
||||
static override options = options.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ConditionalFormatting type
|
||||
*/
|
||||
export function isConditionalFormattingArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ConditionalFormatting {
|
||||
return argClass.controlType === 'ConditionalFormattingControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a TimeFormat type
|
||||
*/
|
||||
export function isTimeFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof TimeFormat {
|
||||
return argClass.controlType === 'TimeFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a NumberFormat type
|
||||
*/
|
||||
export function isNumberFormatArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof NumberFormat {
|
||||
return argClass.controlType === 'NumberFormatControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Currency type
|
||||
*/
|
||||
export function isCurrencyArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Currency {
|
||||
return argClass.controlType === 'CurrencyControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Select type
|
||||
*/
|
||||
export function isSelectArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Select {
|
||||
return (
|
||||
'options' in argClass && Array.isArray((argClass as typeof Select).options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Checkbox type
|
||||
*/
|
||||
export function isCheckboxArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Checkbox {
|
||||
return (
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof Checkbox).default === 'boolean'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Text type
|
||||
*/
|
||||
export function isTextArg(argClass: typeof Argument): argClass is typeof Text {
|
||||
return (
|
||||
argClass.controlType === 'TextControl' ||
|
||||
(argClass.prototype instanceof Text &&
|
||||
!isSelectArg(argClass) &&
|
||||
!isCheckboxArg(argClass))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is an Int type
|
||||
*/
|
||||
export function isIntArg(argClass: typeof Argument): argClass is typeof Int {
|
||||
return 'min' in argClass && 'max' in argClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Color type
|
||||
*/
|
||||
export function isColorArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Color {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' ||
|
||||
argClass.prototype instanceof Color
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Metric type
|
||||
*/
|
||||
export function isMetricArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Metric {
|
||||
return argClass.columnType === ColumnType.Metric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Dimension type
|
||||
*/
|
||||
export function isDimensionArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Dimension {
|
||||
return argClass.columnType === ColumnType.Dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Temporal type
|
||||
*/
|
||||
export function isTemporalArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Temporal {
|
||||
return argClass.columnType === ColumnType.Temporal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Slider type
|
||||
*/
|
||||
export function isSliderArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Slider {
|
||||
return (
|
||||
argClass.controlType === 'SliderControl' &&
|
||||
'step' in argClass &&
|
||||
typeof (argClass as typeof Slider).step === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a Bounds type
|
||||
*/
|
||||
export function isBoundsArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof Bounds {
|
||||
return argClass.controlType === 'BoundsControl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a ColorPicker type
|
||||
*/
|
||||
export function isColorPickerArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof ColorPicker {
|
||||
return (
|
||||
argClass.controlType === 'ColorPickerControl' &&
|
||||
'default' in argClass &&
|
||||
typeof (argClass as typeof ColorPicker).default === 'object' &&
|
||||
'r' in ((argClass as typeof ColorPicker).default as object)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an argument class is a RadioButton type
|
||||
*/
|
||||
export function isRadioButtonArg(
|
||||
argClass: typeof Argument,
|
||||
): argClass is typeof RadioButton {
|
||||
return argClass.controlType === 'RadioButtonControl';
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cross-Filter Utilities for Glyph Charts
|
||||
*
|
||||
* This module provides helpers for implementing cross-filtering in Glyph charts.
|
||||
* Cross-filtering allows charts to filter other charts on the dashboard when
|
||||
* users click on data points.
|
||||
*
|
||||
* ## Quick Start
|
||||
*
|
||||
* 1. Add behaviors to metadata:
|
||||
* ```typescript
|
||||
* metadata: {
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 2. Extract cross-filter props in transform:
|
||||
* ```typescript
|
||||
* transform: (chartProps) => {
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap);
|
||||
* return { transformedProps: { ...otherProps, ...crossFilterProps } };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 3. Use event handlers in render:
|
||||
* ```typescript
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChartProps,
|
||||
FilterState,
|
||||
QueryFormColumn,
|
||||
SetDataMaskHook,
|
||||
ContextMenuFilters,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Props needed for cross-filtering in the render component.
|
||||
* These are typically returned from the transform function and passed to Echart.
|
||||
*/
|
||||
export interface CrossFilterRenderProps {
|
||||
/** Groupby columns used for filtering */
|
||||
groupby: QueryFormColumn[];
|
||||
/** Maps series names to their groupby column values */
|
||||
labelMap: Record<string, string[]>;
|
||||
/** Callback to emit cross-filter data mask */
|
||||
setDataMask: SetDataMaskHook;
|
||||
/** Maps series indices to selected value names */
|
||||
selectedValues: Record<number, string>;
|
||||
/** Whether cross-filters are enabled for this chart */
|
||||
emitCrossFilters?: boolean;
|
||||
/** Context menu handler for drill actions */
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
/** Column type mapping for formatting */
|
||||
coltypeMapping?: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a selectedValues map from filterState.
|
||||
*
|
||||
* The selectedValues map is used by the Echart component to track which
|
||||
* data points are currently selected (for highlighting).
|
||||
*
|
||||
* @param filterState - Current filter state from chartProps
|
||||
* @param seriesNames - Array of series/data point names
|
||||
* @returns Map of index -> name for selected values
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const selectedValues = createSelectedValuesMap(
|
||||
* filterState,
|
||||
* transformedData.map(d => d.name),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createSelectedValuesMap(
|
||||
filterState: FilterState | undefined,
|
||||
seriesNames: string[],
|
||||
): Record<number, string> {
|
||||
return (filterState?.selectedValues || []).reduce(
|
||||
(acc: Record<number, string>, selectedValue: string) => {
|
||||
const index = seriesNames.findIndex(name => name === selectedValue);
|
||||
if (index >= 0) {
|
||||
return { ...acc, [index]: selectedValue };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cross-filter related props from ChartProps.
|
||||
*
|
||||
* This is a convenience function that extracts all the props needed for
|
||||
* cross-filtering from the standard ChartProps object.
|
||||
*
|
||||
* @param chartProps - The chart props from Superset
|
||||
* @param groupby - The groupby columns (dimensions) from form data
|
||||
* @param labelMap - A map from series names to their groupby values
|
||||
* @param seriesNames - Array of series/data point names for selectedValues mapping
|
||||
* @param coltypeMapping - Optional column type mapping
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In transform function:
|
||||
* const labelMap = data.reduce((acc, datum) => ({
|
||||
* ...acc,
|
||||
* [extractGroupbyLabel({ datum, groupby })]: groupby.map(col => datum[col]),
|
||||
* }), {});
|
||||
*
|
||||
* const crossFilterProps = extractCrossFilterProps(
|
||||
* chartProps,
|
||||
* groupby,
|
||||
* labelMap,
|
||||
* transformedData.map(d => d.name),
|
||||
* coltypeMapping,
|
||||
* );
|
||||
*
|
||||
* return {
|
||||
* transformedProps: {
|
||||
* echartOptions,
|
||||
* formData,
|
||||
* width,
|
||||
* height,
|
||||
* refs,
|
||||
* ...crossFilterProps,
|
||||
* },
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export function extractCrossFilterProps(
|
||||
chartProps: ChartProps,
|
||||
groupby: QueryFormColumn[],
|
||||
labelMap: Record<string, string[]>,
|
||||
seriesNames: string[],
|
||||
coltypeMapping?: Record<string, number>,
|
||||
): CrossFilterRenderProps {
|
||||
const { hooks, filterState, emitCrossFilters, formData } = chartProps;
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks ?? {};
|
||||
|
||||
const selectedValues = createSelectedValuesMap(filterState, seriesNames);
|
||||
|
||||
return {
|
||||
groupby,
|
||||
labelMap,
|
||||
setDataMask,
|
||||
selectedValues,
|
||||
emitCrossFilters,
|
||||
onContextMenu,
|
||||
coltypeMapping,
|
||||
// Also include formData for context menu formatting
|
||||
formData,
|
||||
} as CrossFilterRenderProps & { formData: unknown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a data point is currently filtered (should be dimmed).
|
||||
*
|
||||
* Use this in the transform function to apply opacity/styling to
|
||||
* data points that are not part of the current filter selection.
|
||||
*
|
||||
* @param filterState - Current filter state from chartProps
|
||||
* @param name - The name/label of the data point to check
|
||||
* @returns true if the data point should be dimmed, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isFiltered = isDataPointFiltered(filterState, datum.name);
|
||||
* const opacity = isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent;
|
||||
* ```
|
||||
*/
|
||||
export function isDataPointFiltered(
|
||||
filterState: FilterState | undefined,
|
||||
name: string,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
filterState?.selectedValues &&
|
||||
filterState.selectedValues.length > 0 &&
|
||||
!filterState.selectedValues.includes(name),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a labelMap from data records.
|
||||
*
|
||||
* The labelMap maps series names (like "USA" or "2024-01") to their
|
||||
* corresponding groupby column values. This is needed for the cross-filter
|
||||
* event handlers to construct proper filter clauses.
|
||||
*
|
||||
* @param data - Array of data records
|
||||
* @param groupbyLabels - Array of groupby column labels
|
||||
* @param extractLabel - Function to extract the series label from a datum
|
||||
* @returns Map of label -> groupby values
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const labelMap = createLabelMap(
|
||||
* data,
|
||||
* groupbyLabels,
|
||||
* datum => extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createLabelMap<T extends Record<string, unknown>>(
|
||||
data: T[],
|
||||
groupbyLabels: string[],
|
||||
extractLabel: (datum: T) => string,
|
||||
): Record<string, string[]> {
|
||||
return data.reduce((acc: Record<string, string[]>, datum: T) => {
|
||||
const label = extractLabel(datum);
|
||||
return {
|
||||
...acc,
|
||||
[label]: groupbyLabels.map(col => datum[col] as string),
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import type {
|
||||
ControlPanelConfig,
|
||||
ControlSetRow,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
Argument,
|
||||
Select,
|
||||
Text,
|
||||
Checkbox,
|
||||
Int,
|
||||
Color,
|
||||
isSelectArg,
|
||||
isCheckboxArg,
|
||||
isIntArg,
|
||||
isColorArg,
|
||||
isMetricArg,
|
||||
isDimensionArg,
|
||||
isTemporalArg,
|
||||
} from './arguments';
|
||||
import type { VisibilityFn, RgbaColor } from './types';
|
||||
|
||||
/**
|
||||
* Configuration for a glyph argument with optional visibility control
|
||||
*/
|
||||
export interface GlyphArgConfig {
|
||||
arg: typeof Argument;
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments map - parameter name to argument class or config
|
||||
*/
|
||||
export type GlyphArguments = Map<string, typeof Argument | GlyphArgConfig>;
|
||||
|
||||
/**
|
||||
* Convert hex color string to RGBA object for Superset's ColorPickerControl
|
||||
*/
|
||||
function hexToRgba(hex: string): RgbaColor {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (result && result[1] && result[2] && result[3]) {
|
||||
return {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
a: 1,
|
||||
};
|
||||
}
|
||||
return { r: 0, g: 0, b: 0, a: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGBA object to hex color string
|
||||
*/
|
||||
function rgbaToHex(rgba: RgbaColor): string {
|
||||
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument class from a config (handles both direct class and config object)
|
||||
*/
|
||||
function getArgClass(
|
||||
argOrConfig: typeof Argument | GlyphArgConfig,
|
||||
): typeof Argument {
|
||||
return 'arg' in argOrConfig ? argOrConfig.arg : argOrConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility config if present
|
||||
*/
|
||||
function getVisibilityConfig(argOrConfig: typeof Argument | GlyphArgConfig): {
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
} {
|
||||
if ('arg' in argOrConfig) {
|
||||
return {
|
||||
visibility: argOrConfig.visibility,
|
||||
resetOnHide: argOrConfig.resetOnHide,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Superset control config from a glyph Argument class
|
||||
*/
|
||||
export function getControlConfig(
|
||||
argClass: typeof Argument,
|
||||
paramName: string,
|
||||
): Record<string, unknown> & { type: string } {
|
||||
const label = argClass.label || paramName;
|
||||
const description = argClass.description || '';
|
||||
|
||||
// Select control
|
||||
if (isSelectArg(argClass)) {
|
||||
return {
|
||||
type: 'SelectControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
options: argClass.options,
|
||||
clearable: argClass.clearable ?? false,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Checkbox control
|
||||
if (isCheckboxArg(argClass)) {
|
||||
return {
|
||||
type: 'CheckboxControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Int/Slider control
|
||||
if (isIntArg(argClass)) {
|
||||
return {
|
||||
type: 'SliderControl',
|
||||
label,
|
||||
description,
|
||||
default: argClass.default,
|
||||
min: argClass.min,
|
||||
max: argClass.max,
|
||||
step: argClass.step ?? 1,
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Color control
|
||||
if (isColorArg(argClass)) {
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
const hexDefault = argClass.default ?? '#000000';
|
||||
return {
|
||||
type: 'ColorPickerControl',
|
||||
label,
|
||||
description,
|
||||
default: hexToRgba(hexDefault),
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Default to TextControl
|
||||
const textClass = argClass as typeof Text;
|
||||
return {
|
||||
type: 'TextControl',
|
||||
label,
|
||||
description,
|
||||
default: textClass.default ?? '',
|
||||
placeholder: textClass.placeholder ?? '',
|
||||
renderTrigger: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for control panel generation
|
||||
*/
|
||||
export interface ControlPanelOptions {
|
||||
/** Additional control rows for the query section */
|
||||
queryControls?: ControlSetRow[];
|
||||
/** Additional control rows for the chart options section */
|
||||
chartOptionsControls?: ControlSetRow[];
|
||||
/** Control overrides */
|
||||
controlOverrides?: Record<string, Record<string, unknown>>;
|
||||
/** Form data overrides function */
|
||||
formDataOverrides?: (
|
||||
formData: Record<string, unknown>,
|
||||
) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete ControlPanelConfig from glyph arguments
|
||||
*
|
||||
* This is the core function that converts semantic argument definitions
|
||||
* into Superset's control panel format.
|
||||
*/
|
||||
export function generateControlPanel(
|
||||
glyphArguments: GlyphArguments,
|
||||
options: ControlPanelOptions = {},
|
||||
): ControlPanelConfig {
|
||||
const queryControls: ControlSetRow[] = [];
|
||||
const chartOptionsControls: ControlSetRow[] = [];
|
||||
|
||||
// Process each argument
|
||||
for (const [paramName, argOrConfig] of glyphArguments) {
|
||||
const argClass = getArgClass(argOrConfig);
|
||||
const { visibility, resetOnHide } = getVisibilityConfig(argOrConfig);
|
||||
|
||||
// Data arguments go in Query section
|
||||
if (isMetricArg(argClass)) {
|
||||
queryControls.push(['metric']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDimensionArg(argClass)) {
|
||||
queryControls.push(['groupby']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTemporalArg(argClass)) {
|
||||
queryControls.push(['x_axis'], ['time_grain_sqla']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Style/visual arguments go in Chart Options section
|
||||
const controlConfig = getControlConfig(argClass, paramName);
|
||||
|
||||
// Add visibility if specified
|
||||
if (visibility) {
|
||||
controlConfig.visibility = visibility;
|
||||
controlConfig.resetOnHide = resetOnHide ?? false;
|
||||
}
|
||||
|
||||
chartOptionsControls.push([
|
||||
{
|
||||
name: paramName,
|
||||
config: controlConfig,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Add adhoc_filters to query section
|
||||
queryControls.push(['adhoc_filters']);
|
||||
|
||||
// Merge with additional controls from options
|
||||
const finalQueryControls = [
|
||||
...queryControls,
|
||||
...(options.queryControls || []),
|
||||
];
|
||||
const finalChartOptionsControls = [
|
||||
...chartOptionsControls,
|
||||
...(options.chartOptionsControls || []),
|
||||
];
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: finalQueryControls,
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: finalChartOptionsControls,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (options.controlOverrides) {
|
||||
config.controlOverrides = options.controlOverrides;
|
||||
}
|
||||
|
||||
if (options.formDataOverrides) {
|
||||
// Type assertion needed because SqlaFormData is more specific than Record<string, unknown>
|
||||
config.formDataOverrides =
|
||||
options.formDataOverrides as ControlPanelConfig['formDataOverrides'];
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for transformProps generation
|
||||
*/
|
||||
export interface TransformPropsOptions<TResult> {
|
||||
/** Custom transformation function that receives extracted values */
|
||||
transform?: (
|
||||
values: Record<string, unknown>,
|
||||
chartProps: ChartProps,
|
||||
) => TResult;
|
||||
/** Additional props to pass through from chartProps */
|
||||
passthrough?: (keyof ChartProps)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transformProps function from glyph arguments
|
||||
*
|
||||
* This extracts values from formData based on argument definitions,
|
||||
* applying type conversions as needed (e.g., RGBA to hex for colors).
|
||||
*/
|
||||
export function generateTransformProps<TResult = Record<string, unknown>>(
|
||||
glyphArguments: GlyphArguments,
|
||||
options: TransformPropsOptions<TResult> = {},
|
||||
): (chartProps: ChartProps) => TResult {
|
||||
return (chartProps: ChartProps) => {
|
||||
const { formData, width, height, queriesData } = chartProps;
|
||||
const values: Record<string, unknown> = {
|
||||
width,
|
||||
height,
|
||||
queriesData,
|
||||
};
|
||||
|
||||
// Add passthrough props
|
||||
if (options.passthrough) {
|
||||
for (const key of options.passthrough) {
|
||||
values[key] = chartProps[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract values from formData based on argument definitions
|
||||
for (const [paramName, argOrConfig] of glyphArguments) {
|
||||
const argClass = getArgClass(argOrConfig);
|
||||
|
||||
// Skip data arguments (metric, dimension, temporal) - these are handled differently
|
||||
if (
|
||||
isMetricArg(argClass) ||
|
||||
isDimensionArg(argClass) ||
|
||||
isTemporalArg(argClass)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get value from formData, using default if not present
|
||||
let value = formData[paramName];
|
||||
|
||||
// Color control: convert RGBA object to hex string
|
||||
if (isColorArg(argClass)) {
|
||||
const colorClass = argClass as typeof Color;
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
const defaultRgba = hexToRgba(colorClass.default ?? '#000000');
|
||||
const colorValue = value ?? defaultRgba;
|
||||
|
||||
if (
|
||||
typeof colorValue === 'object' &&
|
||||
colorValue !== null &&
|
||||
'r' in colorValue
|
||||
) {
|
||||
value = rgbaToHex(colorValue as RgbaColor);
|
||||
} else if (typeof colorValue === 'string') {
|
||||
value = colorValue;
|
||||
} else {
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
value = colorClass.default ?? '#000000';
|
||||
}
|
||||
}
|
||||
// Select control: use default if no value
|
||||
else if (isSelectArg(argClass)) {
|
||||
const selectClass = argClass as typeof Select;
|
||||
value = value ?? selectClass.default;
|
||||
}
|
||||
// Checkbox control: use default if no value
|
||||
else if (isCheckboxArg(argClass)) {
|
||||
const checkboxClass = argClass as typeof Checkbox;
|
||||
value = value ?? checkboxClass.default ?? false;
|
||||
}
|
||||
// Int control: use default if no value
|
||||
else if (isIntArg(argClass)) {
|
||||
const intClass = argClass as typeof Int;
|
||||
value = value ?? intClass.default ?? 0;
|
||||
}
|
||||
// Text control: use default if no value
|
||||
else {
|
||||
const textClass = argClass as typeof Text;
|
||||
value = value ?? textClass.default ?? '';
|
||||
}
|
||||
|
||||
values[paramName] = value;
|
||||
}
|
||||
|
||||
// Apply custom transformation if provided
|
||||
if (options.transform) {
|
||||
return options.transform(values, chartProps);
|
||||
}
|
||||
|
||||
return values as TResult;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined result of creating a glyph plugin
|
||||
*/
|
||||
export interface GlyphPluginDef<TProps> {
|
||||
controlPanel: ControlPanelConfig;
|
||||
transformProps: (chartProps: ChartProps) => TProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create both controlPanel and transformProps from a single argument definition
|
||||
*
|
||||
* This is the main entry point for the single-file viz pattern.
|
||||
*/
|
||||
export function createGlyphPlugin<TProps = Record<string, unknown>>(
|
||||
glyphArguments: GlyphArguments,
|
||||
controlPanelOptions: ControlPanelOptions = {},
|
||||
transformPropsOptions: TransformPropsOptions<TProps> = {},
|
||||
): GlyphPluginDef<TProps> {
|
||||
return {
|
||||
controlPanel: generateControlPanel(glyphArguments, controlPanelOptions),
|
||||
transformProps: generateTransformProps(
|
||||
glyphArguments,
|
||||
transformPropsOptions,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Core - A declarative visualization plugin framework
|
||||
*
|
||||
* This module enables single-file visualization plugins where:
|
||||
* 1. Arguments define both the chart's inputs AND the control panel
|
||||
* 2. transformProps is auto-generated from argument definitions
|
||||
* 3. The chart component is a simple function receiving typed arguments
|
||||
*
|
||||
* Features:
|
||||
* - Single-file chart definitions with defineChart()
|
||||
* - Declarative argument types (Metric, Dimension, Select, Checkbox, etc.)
|
||||
* - Conditional visibility with visibleWhen/disabledWhen
|
||||
* - Cross-filtering support with extractCrossFilterProps() and allEventHandlers()
|
||||
* - Reusable presets (ShowLegend, HeaderFontSize, etc.)
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* export default defineChart({
|
||||
* metadata: {
|
||||
* name: 'My Chart',
|
||||
* thumbnail,
|
||||
* behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy],
|
||||
* },
|
||||
* arguments: {
|
||||
* metric: Metric.with({ label: 'Metric' }),
|
||||
* groupby: Dimension.with({ label: 'Breakdowns' }),
|
||||
* fontSize: Select.with({
|
||||
* label: 'Font Size',
|
||||
* options: [{ label: 'Small', value: 0.2 }, { label: 'Large', value: 0.4 }],
|
||||
* default: 0.3,
|
||||
* }),
|
||||
* },
|
||||
* transform: (chartProps, argValues) => {
|
||||
* // Extract cross-filter props for interactive filtering
|
||||
* const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap, seriesNames);
|
||||
* return { transformedProps: { echartOptions, ...crossFilterProps } };
|
||||
* },
|
||||
* render: ({ transformedProps }) => {
|
||||
* const eventHandlers = allEventHandlers(transformedProps);
|
||||
* return <Echart eventHandlers={eventHandlers} ... />;
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Re-export everything
|
||||
export * from './types';
|
||||
export * from './arguments';
|
||||
export * from './generators';
|
||||
export * from './defineChart';
|
||||
export * from './presets';
|
||||
export * from './crossFilter';
|
||||
406
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
406
superset-frontend/packages/superset-ui-glyph-core/src/presets.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Glyph Presets - Reusable argument configurations
|
||||
*
|
||||
* This module contains pre-configured arguments that are commonly
|
||||
* used across multiple visualization types. Charts can import these
|
||||
* directly or use .with() to customize them further.
|
||||
*
|
||||
* Example usage:
|
||||
* ```typescript
|
||||
* import { HeaderFontSize, Subtitle } from '../../glyph-core/presets';
|
||||
*
|
||||
* arguments: {
|
||||
* headerFontSize: HeaderFontSize,
|
||||
* subtitle: Subtitle,
|
||||
* // Override defaults when needed:
|
||||
* customSize: HeaderFontSize.with({ default: 0.5 }),
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Select, Text, Checkbox } from './arguments';
|
||||
import { SelectOption } from './types';
|
||||
|
||||
// ============================================================================
|
||||
// Font Size Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Large font size options - for primary/header text elements
|
||||
* Values are multipliers of container height (0.2 = 20% of height)
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_LARGE: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.2 },
|
||||
{ label: t('Small'), value: 0.3 },
|
||||
{ label: t('Normal'), value: 0.4 },
|
||||
{ label: t('Large'), value: 0.5 },
|
||||
{ label: t('Huge'), value: 0.6 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Small font size options - for secondary text elements (subtitles, labels)
|
||||
* Values are multipliers of container height
|
||||
*/
|
||||
export const FONT_SIZE_OPTIONS_SMALL: SelectOption[] = [
|
||||
{ label: t('Tiny'), value: 0.125 },
|
||||
{ label: t('Small'), value: 0.15 },
|
||||
{ label: t('Normal'), value: 0.2 },
|
||||
{ label: t('Large'), value: 0.3 },
|
||||
{ label: t('Huge'), value: 0.4 },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Pre-configured Arguments
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Header/primary font size selector
|
||||
* Used for main display elements like big numbers, titles
|
||||
*/
|
||||
export const HeaderFontSize = Select.with({
|
||||
label: t('Font Size'),
|
||||
description: t('Font size for the primary display element'),
|
||||
options: FONT_SIZE_OPTIONS_LARGE,
|
||||
default: 0.4,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subheader/secondary font size selector
|
||||
* Used for subtitles, labels, secondary text
|
||||
*/
|
||||
export const SubheaderFontSize = Select.with({
|
||||
label: t('Subheader Font Size'),
|
||||
description: t('Font size for secondary text elements'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
/**
|
||||
* Subtitle text input
|
||||
* Generic subtitle/description field used by many chart types
|
||||
*/
|
||||
export const Subtitle = Text.with({
|
||||
label: t('Subtitle'),
|
||||
description: t('Description text displayed below the main content'),
|
||||
default: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* Show legend toggle
|
||||
* Common toggle for charts with legends
|
||||
*/
|
||||
export const ShowLegend = Checkbox.with({
|
||||
label: t('Show Legend'),
|
||||
description: t('Whether to display the chart legend'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Force timestamp formatting toggle
|
||||
* Used when a value might be a timestamp but isn't auto-detected
|
||||
*/
|
||||
export const ForceTimestampFormatting = Checkbox.with({
|
||||
label: t('Force Date Format'),
|
||||
description: t(
|
||||
'Use date formatting even when the value is not detected as a timestamp',
|
||||
),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Legend Options
|
||||
// ============================================================================
|
||||
|
||||
export const LEGEND_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Scroll'), value: 'scroll' },
|
||||
{ label: t('List'), value: 'plain' },
|
||||
];
|
||||
|
||||
export const LEGEND_ORIENTATION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
];
|
||||
|
||||
export const LEGEND_SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('No sort'), value: '' },
|
||||
{ label: t('Ascending'), value: 'asc' },
|
||||
{ label: t('Descending'), value: 'desc' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Legend type selector
|
||||
* Choose between scrollable or plain list legend
|
||||
*/
|
||||
export const LegendType = Select.with({
|
||||
label: t('Legend Type'),
|
||||
description: t('Type of legend display'),
|
||||
options: LEGEND_TYPE_OPTIONS,
|
||||
default: 'scroll',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend orientation selector
|
||||
* Position the legend relative to the chart
|
||||
*/
|
||||
export const LegendOrientation = Select.with({
|
||||
label: t('Legend Orientation'),
|
||||
description: t('Position of the legend'),
|
||||
options: LEGEND_ORIENTATION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
/**
|
||||
* Legend sort selector
|
||||
* Sort legend items alphabetically
|
||||
*/
|
||||
export const LegendSort = Select.with({
|
||||
label: t('Legend Sort'),
|
||||
description: t('Sort order for legend items'),
|
||||
options: LEGEND_SORT_OPTIONS,
|
||||
default: '',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show labels toggle
|
||||
* Common toggle for chart labels
|
||||
*/
|
||||
export const ShowLabels = Checkbox.with({
|
||||
label: t('Show Labels'),
|
||||
description: t('Whether to display labels on the chart'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Show value toggle
|
||||
* Common toggle for showing values on chart elements
|
||||
*/
|
||||
export const ShowValue = Checkbox.with({
|
||||
label: t('Show Value'),
|
||||
description: t('Whether to display values on the chart'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Metric Name Presets
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show metric name toggle
|
||||
* Used in BigNumber charts to optionally show the metric name
|
||||
*/
|
||||
export const ShowMetricName = Checkbox.with({
|
||||
label: t('Show Metric Name'),
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Metric name font size selector
|
||||
* Typically used with visibility tied to ShowMetricName
|
||||
*/
|
||||
export const MetricNameFontSize = Select.with({
|
||||
label: t('Metric Name Font Size'),
|
||||
description: t('Font size for the metric name'),
|
||||
options: FONT_SIZE_OPTIONS_SMALL,
|
||||
default: 0.15,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Type Options (shared by Pie, Funnel, etc.)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Standard label content type options
|
||||
* Used by Pie, Funnel, and other category-based charts
|
||||
*/
|
||||
export const LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Percentage'), value: 'percent' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
{ label: t('Category and Percentage'), value: 'key_percent' },
|
||||
{ label: t('Category, Value and Percentage'), value: 'key_value_percent' },
|
||||
{ label: t('Value and Percentage'), value: 'value_percent' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label type selector for category-based charts
|
||||
*/
|
||||
export const LabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sort Options
|
||||
// ============================================================================
|
||||
|
||||
export const SORT_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Descending'), value: 'descending' },
|
||||
{ label: t('Ascending'), value: 'ascending' },
|
||||
{ label: t('None'), value: 'none' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Sort by metric toggle
|
||||
* Common for charts that need to sort data by metric value
|
||||
*/
|
||||
export const SortByMetric = Checkbox.with({
|
||||
label: t('Sort by Metric'),
|
||||
description: t('Sort results by the selected metric'),
|
||||
default: true,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Label Position Options
|
||||
// ============================================================================
|
||||
|
||||
export const LABEL_POSITION_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Top'), value: 'top' },
|
||||
{ label: t('Left'), value: 'left' },
|
||||
{ label: t('Right'), value: 'right' },
|
||||
{ label: t('Bottom'), value: 'bottom' },
|
||||
{ label: t('Inside'), value: 'inside' },
|
||||
{ label: t('Inside Left'), value: 'insideLeft' },
|
||||
{ label: t('Inside Right'), value: 'insideRight' },
|
||||
{ label: t('Inside Top'), value: 'insideTop' },
|
||||
{ label: t('Inside Bottom'), value: 'insideBottom' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Label position selector
|
||||
* Position labels relative to chart elements
|
||||
*/
|
||||
export const LabelPosition = Select.with({
|
||||
label: t('Label Position'),
|
||||
description: t('Position of labels on the chart'),
|
||||
options: LABEL_POSITION_OPTIONS,
|
||||
default: 'top',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Simple Label Type (key/value variants only)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Simple label type options - for charts with fewer label display options
|
||||
* Used by Radar, Sunburst, etc.
|
||||
*/
|
||||
export const SIMPLE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Category Name'), value: 'key' },
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Simple label type selector
|
||||
* For charts that only need key/value/key_value options
|
||||
*/
|
||||
export const SimpleLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: SIMPLE_LABEL_TYPE_OPTIONS,
|
||||
default: 'key',
|
||||
});
|
||||
|
||||
/**
|
||||
* Value-only label type options - for charts like Radar
|
||||
*/
|
||||
export const VALUE_LABEL_TYPE_OPTIONS: SelectOption[] = [
|
||||
{ label: t('Value'), value: 'value' },
|
||||
{ label: t('Category and Value'), value: 'key_value' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Value label type selector
|
||||
* For charts that show value or category+value
|
||||
*/
|
||||
export const ValueLabelType = Select.with({
|
||||
label: t('Label Type'),
|
||||
description: t('What should be shown on the label?'),
|
||||
options: VALUE_LABEL_TYPE_OPTIONS,
|
||||
default: 'value',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Totals and Aggregates
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Show total toggle
|
||||
* For charts that can display aggregate totals
|
||||
*/
|
||||
export const ShowTotal = Checkbox.with({
|
||||
label: t('Show Total'),
|
||||
description: t('Whether to display the aggregate total'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Threshold Controls
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Label percentage threshold
|
||||
* Minimum percentage for showing labels (avoids clutter on small slices)
|
||||
*/
|
||||
export const LabelThreshold = Text.with({
|
||||
label: t('Percentage Threshold'),
|
||||
description: t('Minimum threshold in percentage points for showing labels'),
|
||||
default: '5',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Shape Options
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Circle shape toggle (used by Radar)
|
||||
*/
|
||||
export const CircleShape = Checkbox.with({
|
||||
label: t('Circle Shape'),
|
||||
description: t('Use circular shape instead of polygon'),
|
||||
default: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Data Zoom
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Enable data zoom toggle
|
||||
* For charts with zoomable data areas
|
||||
*/
|
||||
export const DataZoom = Checkbox.with({
|
||||
label: t('Data Zoom'),
|
||||
description: t('Enable data zooming controls'),
|
||||
default: false,
|
||||
});
|
||||
306
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
306
superset-frontend/packages/superset-ui-glyph-core/src/types.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
|
||||
/**
|
||||
* Option for Select controls
|
||||
*/
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Select argument type
|
||||
*/
|
||||
export interface SelectOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | number;
|
||||
options?: SelectOption[];
|
||||
clearable?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Text argument type
|
||||
*/
|
||||
export interface TextOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Checkbox argument type
|
||||
*/
|
||||
export interface CheckboxOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Int argument type (slider)
|
||||
*/
|
||||
export interface IntOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Color argument type
|
||||
*/
|
||||
export interface ColorOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Metric argument type
|
||||
*/
|
||||
export interface MetricOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Dimension argument type
|
||||
*/
|
||||
export interface DimensionOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for NumberFormat argument type
|
||||
*/
|
||||
export interface NumberFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency value structure
|
||||
*/
|
||||
export interface CurrencyValue {
|
||||
symbol?: string;
|
||||
symbolPosition?: 'prefix' | 'suffix';
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Currency argument type
|
||||
*/
|
||||
export interface CurrencyOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: CurrencyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for TimeFormat argument type
|
||||
*/
|
||||
export interface TimeFormatOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for ConditionalFormatting argument type
|
||||
*/
|
||||
export interface ConditionalFormattingOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Slider argument type (continuous float values)
|
||||
*/
|
||||
export interface SliderOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for Bounds argument type (min/max pairs)
|
||||
*/
|
||||
export interface BoundsOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: [number | null, number | null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds value type - tuple of [min, max] where either can be null
|
||||
*/
|
||||
export type BoundsValue = [number | null, number | null];
|
||||
|
||||
/**
|
||||
* Configuration options for ColorPicker argument type (RGBA colors)
|
||||
*/
|
||||
export interface ColorPickerOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: RgbaColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for RadioButton argument type
|
||||
*/
|
||||
export interface RadioButtonOptions {
|
||||
label?: string;
|
||||
description?: string;
|
||||
default?: string | boolean;
|
||||
options: RadioOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Option for RadioButton controls
|
||||
*/
|
||||
export interface RadioOption {
|
||||
label: string;
|
||||
value: string | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional formatting rule value
|
||||
*/
|
||||
export interface ConditionalFormattingRule {
|
||||
column?: string;
|
||||
operator?: '<' | '<=' | '>' | '>=' | '==' | '!=' | 'between';
|
||||
targetValue?: number;
|
||||
targetValueLeft?: number;
|
||||
targetValueRight?: number;
|
||||
colorScheme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column type enum for data arguments
|
||||
*/
|
||||
export enum ColumnType {
|
||||
Metric = 'metric',
|
||||
Dimension = 'dimension',
|
||||
Temporal = 'temporal',
|
||||
Argument = 'argument',
|
||||
}
|
||||
|
||||
/**
|
||||
* Base argument class interface
|
||||
*/
|
||||
export interface ArgumentClass {
|
||||
label: string | null;
|
||||
description: string | null;
|
||||
columnType?: ColumnType;
|
||||
controlType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RGBA color format used by Superset's ColorPickerControl
|
||||
*/
|
||||
export interface RgbaColor {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility function for conditional control display (legacy)
|
||||
*/
|
||||
export type VisibilityFn = (state: {
|
||||
controls: Record<string, { value: unknown }>;
|
||||
}) => boolean;
|
||||
|
||||
/**
|
||||
* Declarative condition for argument visibility/disabled state.
|
||||
*
|
||||
* Keys are argument names, values define the condition:
|
||||
* - Literal value: equality check (e.g., { showMetricName: true })
|
||||
* - Function: custom check (e.g., { subtitle: (val) => !!val })
|
||||
*
|
||||
* Multiple keys are AND'd together.
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true
|
||||
* visibleWhen: { showMetricName: true }
|
||||
*
|
||||
* @example
|
||||
* // Visible when subtitle is not empty
|
||||
* visibleWhen: { subtitle: (val) => !!val }
|
||||
*
|
||||
* @example
|
||||
* // Visible when showMetricName is true AND subtitle is not empty
|
||||
* visibleWhen: { showMetricName: true, subtitle: (val) => !!val }
|
||||
*/
|
||||
export type ArgumentCondition = Record<
|
||||
string,
|
||||
unknown | ((value: unknown) => boolean)
|
||||
>;
|
||||
|
||||
/**
|
||||
* Extended control configuration with visibility
|
||||
*/
|
||||
export interface ControlConfig {
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
visibility?: VisibilityFn;
|
||||
resetOnHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glyph chart definition
|
||||
*/
|
||||
export interface GlyphChartDef<TArgs extends Record<string, ArgumentClass>> {
|
||||
arguments: TArgs;
|
||||
sections?: {
|
||||
query?: ControlConfig[][];
|
||||
chartOptions?: ControlConfig[][];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of createGlyphPlugin
|
||||
*/
|
||||
export interface GlyphPluginResult<TFormData> {
|
||||
controlPanel: ControlPanelConfig;
|
||||
transformProps: (chartProps: ChartProps) => TFormData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to extract form data types from argument definitions
|
||||
*/
|
||||
export type ArgumentsToFormData<TArgs extends Record<string, ArgumentClass>> = {
|
||||
[K in keyof TArgs]: TArgs[K] extends { default: infer D } ? D : unknown;
|
||||
};
|
||||
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Argument,
|
||||
Bounds,
|
||||
Checkbox,
|
||||
Color,
|
||||
ColorPicker,
|
||||
ConditionalFormatting,
|
||||
Currency,
|
||||
Dimension,
|
||||
Int,
|
||||
isBoundsArg,
|
||||
isCheckboxArg,
|
||||
isColorArg,
|
||||
isColorPickerArg,
|
||||
isConditionalFormattingArg,
|
||||
isCurrencyArg,
|
||||
isDimensionArg,
|
||||
isIntArg,
|
||||
isMetricArg,
|
||||
isNumberFormatArg,
|
||||
isRadioButtonArg,
|
||||
isSelectArg,
|
||||
isSliderArg,
|
||||
isTemporalArg,
|
||||
isTextArg,
|
||||
isTimeFormatArg,
|
||||
Metric,
|
||||
NumberFormat,
|
||||
RadioButton,
|
||||
Select,
|
||||
Slider,
|
||||
Temporal,
|
||||
Text,
|
||||
TimeFormat,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import { ColumnType } from '@superset-ui/glyph-core/types';
|
||||
|
||||
describe('Argument base class', () => {
|
||||
test('stores its constructor value', () => {
|
||||
const a = new Argument(42);
|
||||
expect(a.value).toBe(42);
|
||||
});
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(Argument.label).toBeNull();
|
||||
expect(Argument.description).toBeNull();
|
||||
expect(Argument.columnType).toBe(ColumnType.Argument);
|
||||
expect(Argument.controlType).toBe('TextControl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metric', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Metric.label).toBe('Metric');
|
||||
expect(Metric.columnType).toBe(ColumnType.Metric);
|
||||
expect(Metric.controlType).toBe('MetricsControl');
|
||||
expect(Metric.multi).toBe(false);
|
||||
});
|
||||
|
||||
test('.with() overrides label, description, multi', () => {
|
||||
const M = Metric.with({
|
||||
label: 'Sales',
|
||||
description: 'Total sales',
|
||||
multi: true,
|
||||
});
|
||||
expect(M.label).toBe('Sales');
|
||||
expect(M.description).toBe('Total sales');
|
||||
expect(M.multi).toBe(true);
|
||||
// unaltered ancestor metadata still present
|
||||
expect(M.columnType).toBe(ColumnType.Metric);
|
||||
expect(M.controlType).toBe('MetricsControl');
|
||||
});
|
||||
|
||||
test('.with() falls back to parent defaults when option omitted', () => {
|
||||
const M = Metric.with({ label: 'X' });
|
||||
expect(M.label).toBe('X');
|
||||
expect(M.multi).toBe(Metric.multi);
|
||||
expect(M.description).toBe(Metric.description);
|
||||
});
|
||||
|
||||
test('isMetricArg type guard', () => {
|
||||
expect(isMetricArg(Metric)).toBe(true);
|
||||
expect(isMetricArg(Metric.with({ label: 'X' }))).toBe(true);
|
||||
expect(isMetricArg(Dimension)).toBe(false);
|
||||
expect(isMetricArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dimension', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Dimension.label).toBe('Dimension');
|
||||
expect(Dimension.columnType).toBe(ColumnType.Dimension);
|
||||
expect(Dimension.controlType).toBe('GroupByControl');
|
||||
expect(Dimension.multi).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() overrides label, description, multi', () => {
|
||||
const D = Dimension.with({
|
||||
label: 'Region',
|
||||
multi: false,
|
||||
});
|
||||
expect(D.label).toBe('Region');
|
||||
expect(D.multi).toBe(false);
|
||||
});
|
||||
|
||||
test('isDimensionArg type guard', () => {
|
||||
expect(isDimensionArg(Dimension)).toBe(true);
|
||||
expect(isDimensionArg(Dimension.with({ label: 'X' }))).toBe(true);
|
||||
expect(isDimensionArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Temporal', () => {
|
||||
test('has expected static metadata', () => {
|
||||
expect(Temporal.label).toBe('Time Column');
|
||||
expect(Temporal.columnType).toBe(ColumnType.Temporal);
|
||||
expect(Temporal.controlType).toBe('TemporalControl');
|
||||
});
|
||||
|
||||
test('.with() overrides label and description', () => {
|
||||
const T = Temporal.with({ label: 'Order Date' });
|
||||
expect(T.label).toBe('Order Date');
|
||||
});
|
||||
|
||||
test('isTemporalArg type guard', () => {
|
||||
expect(isTemporalArg(Temporal)).toBe(true);
|
||||
expect(isTemporalArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select', () => {
|
||||
const OPTIONS = [
|
||||
{ label: 'A', value: 'a' },
|
||||
{ label: 'B', value: 'b' },
|
||||
];
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(Select.label).toBe('Select');
|
||||
expect(Select.controlType).toBe('SelectControl');
|
||||
expect(Select.options).toEqual([]);
|
||||
expect(Select.default).toBe('');
|
||||
});
|
||||
|
||||
test('.with() applies label, default, options', () => {
|
||||
const S = Select.with({
|
||||
label: 'Choice',
|
||||
default: 'a',
|
||||
options: OPTIONS,
|
||||
});
|
||||
expect(S.label).toBe('Choice');
|
||||
expect(S.default).toBe('a');
|
||||
expect(S.options).toEqual(OPTIONS);
|
||||
});
|
||||
|
||||
test('isSelectArg type guard', () => {
|
||||
expect(isSelectArg(Select.with({ options: OPTIONS }))).toBe(true);
|
||||
expect(isSelectArg(Checkbox)).toBe(false);
|
||||
expect(isSelectArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Text.controlType).toBe('TextControl');
|
||||
expect(Text.default).toBe('');
|
||||
expect(Text.placeholder).toBe('');
|
||||
});
|
||||
|
||||
test('.with() applies label, default, placeholder', () => {
|
||||
const T = Text.with({
|
||||
label: 'Title',
|
||||
default: 'Untitled',
|
||||
placeholder: 'Enter title',
|
||||
});
|
||||
expect(T.label).toBe('Title');
|
||||
expect(T.default).toBe('Untitled');
|
||||
expect(T.placeholder).toBe('Enter title');
|
||||
});
|
||||
|
||||
test('isTextArg type guard accepts Text but not Select/Checkbox', () => {
|
||||
expect(isTextArg(Text)).toBe(true);
|
||||
expect(isTextArg(Text.with({ label: 'X' }))).toBe(true);
|
||||
expect(isTextArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkbox', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Checkbox.controlType).toBe('CheckboxControl');
|
||||
expect(Checkbox.default).toBe(false);
|
||||
});
|
||||
|
||||
test('.with() applies label, description, default', () => {
|
||||
const C = Checkbox.with({
|
||||
label: 'Show legend',
|
||||
default: true,
|
||||
});
|
||||
expect(C.label).toBe('Show legend');
|
||||
expect(C.default).toBe(true);
|
||||
});
|
||||
|
||||
test('isCheckboxArg type guard', () => {
|
||||
expect(isCheckboxArg(Checkbox)).toBe(true);
|
||||
expect(isCheckboxArg(Checkbox.with({ default: true }))).toBe(true);
|
||||
expect(isCheckboxArg(Text)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Int', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Int.controlType).toBe('SliderControl');
|
||||
expect(Int.default).toBe(0);
|
||||
expect(Int.min).toBe(0);
|
||||
expect(Int.max).toBe(100);
|
||||
expect(Int.step).toBe(1);
|
||||
});
|
||||
|
||||
test('.with() applies label, default, min, max, step', () => {
|
||||
const I = Int.with({
|
||||
label: 'Limit',
|
||||
default: 50,
|
||||
min: 10,
|
||||
max: 1000,
|
||||
step: 5,
|
||||
});
|
||||
expect(I.label).toBe('Limit');
|
||||
expect(I.default).toBe(50);
|
||||
expect(I.min).toBe(10);
|
||||
expect(I.max).toBe(1000);
|
||||
expect(I.step).toBe(5);
|
||||
});
|
||||
|
||||
test('isIntArg type guard', () => {
|
||||
expect(isIntArg(Int)).toBe(true);
|
||||
expect(isIntArg(Slider)).toBe(true); // Slider also has min/max
|
||||
expect(isIntArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Color', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Color.controlType).toBe('ColorPickerControl');
|
||||
expect(Color.default).toBe('#000000');
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const C = Color.with({ label: 'Fill', default: '#ff0000' });
|
||||
expect(C.label).toBe('Fill');
|
||||
expect(C.default).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('isColorArg type guard', () => {
|
||||
expect(isColorArg(Color)).toBe(true);
|
||||
expect(isColorArg(Color.with({ default: '#ff0000' }))).toBe(true);
|
||||
expect(isColorArg(Metric)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NumberFormat', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(NumberFormat.controlType).toBe('NumberFormatControl');
|
||||
expect(NumberFormat.default).toBe('SMART_NUMBER');
|
||||
expect(NumberFormat.FORMAT_OPTIONS.length).toBeGreaterThan(10);
|
||||
expect(
|
||||
NumberFormat.FORMAT_OPTIONS.some(o => o.value === 'SMART_NUMBER'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const N = NumberFormat.with({ label: 'Amount', default: '.2f' });
|
||||
expect(N.label).toBe('Amount');
|
||||
expect(N.default).toBe('.2f');
|
||||
});
|
||||
|
||||
test('isNumberFormatArg type guard', () => {
|
||||
expect(isNumberFormatArg(NumberFormat)).toBe(true);
|
||||
expect(isNumberFormatArg(TimeFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Currency', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Currency.controlType).toBe('CurrencyControl');
|
||||
expect(Currency.default).toEqual({});
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const C = Currency.with({
|
||||
label: 'Money',
|
||||
default: { symbol: 'USD', symbolPosition: 'prefix' },
|
||||
});
|
||||
expect(C.label).toBe('Money');
|
||||
expect(C.default).toEqual({ symbol: 'USD', symbolPosition: 'prefix' });
|
||||
});
|
||||
|
||||
test('isCurrencyArg type guard', () => {
|
||||
expect(isCurrencyArg(Currency)).toBe(true);
|
||||
expect(isCurrencyArg(NumberFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimeFormat', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(TimeFormat.controlType).toBe('TimeFormatControl');
|
||||
expect(TimeFormat.default).toBe('smart_date');
|
||||
expect(
|
||||
TimeFormat.FORMAT_OPTIONS.some(o => o.value === 'smart_date'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('.with() applies label, default', () => {
|
||||
const T = TimeFormat.with({ label: 'When', default: '%Y-%m-%d' });
|
||||
expect(T.label).toBe('When');
|
||||
expect(T.default).toBe('%Y-%m-%d');
|
||||
});
|
||||
|
||||
test('isTimeFormatArg type guard', () => {
|
||||
expect(isTimeFormatArg(TimeFormat)).toBe(true);
|
||||
expect(isTimeFormatArg(NumberFormat)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConditionalFormatting', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(ConditionalFormatting.controlType).toBe(
|
||||
'ConditionalFormattingControl',
|
||||
);
|
||||
expect(ConditionalFormatting.default).toEqual([]);
|
||||
});
|
||||
|
||||
test('.with() applies label and description (not default)', () => {
|
||||
const CF = ConditionalFormatting.with({ label: 'Format' });
|
||||
expect(CF.label).toBe('Format');
|
||||
});
|
||||
|
||||
test('isConditionalFormattingArg type guard', () => {
|
||||
expect(isConditionalFormattingArg(ConditionalFormatting)).toBe(true);
|
||||
expect(isConditionalFormattingArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slider', () => {
|
||||
test('has expected float-friendly defaults', () => {
|
||||
expect(Slider.controlType).toBe('SliderControl');
|
||||
expect(Slider.default).toBe(0);
|
||||
expect(Slider.min).toBe(0);
|
||||
expect(Slider.max).toBe(1);
|
||||
expect(Slider.step).toBe(0.1);
|
||||
});
|
||||
|
||||
test('.with() applies all numeric fields', () => {
|
||||
const S = Slider.with({
|
||||
label: 'Opacity',
|
||||
default: 0.8,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
});
|
||||
expect(S.label).toBe('Opacity');
|
||||
expect(S.default).toBe(0.8);
|
||||
expect(S.step).toBe(0.05);
|
||||
});
|
||||
|
||||
test('isSliderArg type guard requires float step', () => {
|
||||
expect(isSliderArg(Slider)).toBe(true);
|
||||
// Int is also SliderControl + has step but step is integer-valued — still
|
||||
// numeric so the guard recognizes it (current behavior); document it.
|
||||
expect(isSliderArg(Int)).toBe(true);
|
||||
expect(isSliderArg(Checkbox)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bounds', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(Bounds.controlType).toBe('BoundsControl');
|
||||
expect(Bounds.default).toEqual([null, null]);
|
||||
});
|
||||
|
||||
test('.with() applies default', () => {
|
||||
const B = Bounds.with({ label: 'Range', default: [0, 100] });
|
||||
expect(B.label).toBe('Range');
|
||||
expect(B.default).toEqual([0, 100]);
|
||||
});
|
||||
|
||||
test('isBoundsArg type guard', () => {
|
||||
expect(isBoundsArg(Bounds)).toBe(true);
|
||||
expect(isBoundsArg(Int)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ColorPicker', () => {
|
||||
test('has expected static defaults', () => {
|
||||
expect(ColorPicker.controlType).toBe('ColorPickerControl');
|
||||
expect(ColorPicker.default).toEqual({ r: 0, g: 0, b: 0, a: 1 });
|
||||
});
|
||||
|
||||
test('.with() applies default', () => {
|
||||
const CP = ColorPicker.with({
|
||||
label: 'Pick',
|
||||
default: { r: 255, g: 0, b: 0, a: 0.5 },
|
||||
});
|
||||
expect(CP.label).toBe('Pick');
|
||||
expect(CP.default).toEqual({ r: 255, g: 0, b: 0, a: 0.5 });
|
||||
});
|
||||
|
||||
test('isColorPickerArg distinguishes from Color (string)', () => {
|
||||
expect(isColorPickerArg(ColorPicker)).toBe(true);
|
||||
expect(isColorPickerArg(Color)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RadioButton', () => {
|
||||
const RADIO_OPTIONS = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
test('has expected static defaults', () => {
|
||||
expect(RadioButton.controlType).toBe('RadioButtonControl');
|
||||
expect(RadioButton.default).toBe('');
|
||||
expect(RadioButton.options).toEqual([]);
|
||||
});
|
||||
|
||||
test('.with() applies all fields', () => {
|
||||
const RB = RadioButton.with({
|
||||
label: 'Toggle',
|
||||
default: true,
|
||||
options: RADIO_OPTIONS,
|
||||
});
|
||||
expect(RB.label).toBe('Toggle');
|
||||
expect(RB.default).toBe(true);
|
||||
expect(RB.options).toEqual(RADIO_OPTIONS);
|
||||
});
|
||||
|
||||
test('isRadioButtonArg type guard', () => {
|
||||
expect(isRadioButtonArg(RadioButton)).toBe(true);
|
||||
expect(isRadioButtonArg(Select)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Argument inheritance via .with()', () => {
|
||||
test('chained .with() calls compose overrides', () => {
|
||||
const Base = Select.with({
|
||||
label: 'Pick one',
|
||||
options: [{ label: 'A', value: 'a' }],
|
||||
});
|
||||
const Tighter = Base.with({ label: 'Pick exactly one' });
|
||||
expect(Tighter.label).toBe('Pick exactly one');
|
||||
expect(Tighter.options).toEqual([{ label: 'A', value: 'a' }]);
|
||||
});
|
||||
|
||||
test('original class is unmodified after .with()', () => {
|
||||
const before = Metric.multi;
|
||||
Metric.with({ multi: !before });
|
||||
expect(Metric.multi).toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ChartProps, FilterState } from '@superset-ui/core';
|
||||
import {
|
||||
createLabelMap,
|
||||
createSelectedValuesMap,
|
||||
extractCrossFilterProps,
|
||||
isDataPointFiltered,
|
||||
} from '@superset-ui/glyph-core';
|
||||
|
||||
describe('createSelectedValuesMap', () => {
|
||||
test('returns empty object when filterState is undefined', () => {
|
||||
expect(createSelectedValuesMap(undefined, ['a', 'b'])).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty object when selectedValues is undefined', () => {
|
||||
expect(
|
||||
createSelectedValuesMap({} as FilterState, ['a', 'b']),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty object when selectedValues is empty', () => {
|
||||
expect(
|
||||
createSelectedValuesMap(
|
||||
{ selectedValues: [] } as unknown as FilterState,
|
||||
['a', 'b'],
|
||||
),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
test('maps selected value to its index in seriesNames', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['b'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 1: 'b' });
|
||||
});
|
||||
|
||||
test('maps multiple selected values to their indices', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['a', 'c'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 0: 'a', 2: 'c' });
|
||||
});
|
||||
|
||||
test('ignores selected values not in seriesNames', () => {
|
||||
const result = createSelectedValuesMap(
|
||||
{ selectedValues: ['x', 'a'] } as unknown as FilterState,
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
expect(result).toEqual({ 0: 'a' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDataPointFiltered', () => {
|
||||
test('returns false when no filterState', () => {
|
||||
expect(isDataPointFiltered(undefined, 'a')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when selectedValues is empty', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: [] } as unknown as FilterState,
|
||||
'a',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when name is in selectedValues', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
|
||||
'a',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when name is NOT in non-empty selectedValues', () => {
|
||||
expect(
|
||||
isDataPointFiltered(
|
||||
{ selectedValues: ['a', 'b'] } as unknown as FilterState,
|
||||
'c',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLabelMap', () => {
|
||||
test('returns empty object for empty data', () => {
|
||||
expect(createLabelMap([], ['col1'], () => 'label')).toEqual({});
|
||||
});
|
||||
|
||||
test('maps each record to its label and groupby column values', () => {
|
||||
const data = [
|
||||
{ country: 'USA', region: 'North' },
|
||||
{ country: 'Brazil', region: 'South' },
|
||||
];
|
||||
const result = createLabelMap(
|
||||
data,
|
||||
['country', 'region'],
|
||||
d => d.country as string,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
USA: ['USA', 'North'],
|
||||
Brazil: ['Brazil', 'South'],
|
||||
});
|
||||
});
|
||||
|
||||
test('last record wins when extractLabel collides', () => {
|
||||
const data = [
|
||||
{ name: 'X', value: 1 },
|
||||
{ name: 'X', value: 2 },
|
||||
];
|
||||
const result = createLabelMap(data, ['value'], d => d.name as string);
|
||||
// collision: later entry overwrites
|
||||
expect(result).toEqual({ X: [2] });
|
||||
});
|
||||
|
||||
test('groupbyLabels controls the columns extracted, not the label', () => {
|
||||
const data = [{ a: 1, b: 2, c: 3 }];
|
||||
const result = createLabelMap(data, ['c'], () => 'only-key');
|
||||
expect(result).toEqual({ 'only-key': [3] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCrossFilterProps', () => {
|
||||
const baseChartProps = {
|
||||
hooks: { setDataMask: jest.fn(), onContextMenu: jest.fn() },
|
||||
filterState: {
|
||||
selectedValues: ['USA'],
|
||||
} as unknown as FilterState,
|
||||
emitCrossFilters: true,
|
||||
formData: { viz_type: 'test' },
|
||||
} as unknown as ChartProps;
|
||||
|
||||
test('returns all expected fields', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{ USA: ['USA'] },
|
||||
['USA', 'Brazil'],
|
||||
);
|
||||
expect(result.groupby).toEqual(['country']);
|
||||
expect(result.labelMap).toEqual({ USA: ['USA'] });
|
||||
expect(result.selectedValues).toEqual({ 0: 'USA' });
|
||||
expect(result.emitCrossFilters).toBe(true);
|
||||
expect(result.setDataMask).toBe(baseChartProps.hooks!.setDataMask);
|
||||
expect(result.onContextMenu).toBe(baseChartProps.hooks!.onContextMenu);
|
||||
});
|
||||
|
||||
test('coltypeMapping pass-through when provided', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{},
|
||||
[],
|
||||
{ country: 1 },
|
||||
);
|
||||
expect(result.coltypeMapping).toEqual({ country: 1 });
|
||||
});
|
||||
|
||||
test('defaults setDataMask to a no-op when hooks omits it', () => {
|
||||
const chartProps = {
|
||||
...baseChartProps,
|
||||
hooks: {},
|
||||
} as unknown as ChartProps;
|
||||
const result = extractCrossFilterProps(chartProps, [], {}, []);
|
||||
expect(typeof result.setDataMask).toBe('function');
|
||||
// No throw when invoked
|
||||
expect(() =>
|
||||
result.setDataMask({ filterState: {} } as unknown as Parameters<
|
||||
typeof result.setDataMask
|
||||
>[0]),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('formData is included in the returned shape (for context menu formatting)', () => {
|
||||
const result = extractCrossFilterProps(
|
||||
baseChartProps,
|
||||
['country'],
|
||||
{},
|
||||
[],
|
||||
) as ReturnType<typeof extractCrossFilterProps> & { formData: unknown };
|
||||
expect(result.formData).toEqual({ viz_type: 'test' });
|
||||
});
|
||||
|
||||
test('selectedValues is empty when filterState has none', () => {
|
||||
const chartProps = {
|
||||
...baseChartProps,
|
||||
filterState: {} as FilterState,
|
||||
} as unknown as ChartProps;
|
||||
const result = extractCrossFilterProps(chartProps, [], {}, ['x']);
|
||||
expect(result.selectedValues).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,563 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Behavior,
|
||||
ChartLabel,
|
||||
ChartMetadata,
|
||||
ChartPlugin,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
defineChart,
|
||||
Dimension,
|
||||
evaluateGlyphCondition,
|
||||
getArgVisibleWhen,
|
||||
Metric,
|
||||
resolveArgClass,
|
||||
Select,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
|
||||
// Helper: instantiate a plugin and reach its controlPanel config.
|
||||
function instantiate(PluginClass: ReturnType<typeof defineChart>) {
|
||||
const plugin = new PluginClass();
|
||||
// ChartPlugin internals expose the panel under .controlPanel
|
||||
// (set via super({ controlPanel }) in defineChart's GlyphChartPlugin).
|
||||
return {
|
||||
plugin,
|
||||
controlPanel: (plugin as unknown as { controlPanel: Record<string, unknown> })
|
||||
.controlPanel,
|
||||
metadata: (plugin as unknown as { metadata: ChartMetadata }).metadata,
|
||||
};
|
||||
}
|
||||
|
||||
const MIN_THUMBNAIL = 'thumb.png';
|
||||
|
||||
describe('resolveArgClass', () => {
|
||||
test('returns the bare class form unchanged', () => {
|
||||
expect(resolveArgClass(Metric)).toBe(Metric);
|
||||
});
|
||||
|
||||
test('unwraps the { arg, visibleWhen } object form', () => {
|
||||
const M = Metric.with({ label: 'Sales' });
|
||||
const argDef = { arg: M, visibleWhen: { show: true } };
|
||||
expect(resolveArgClass(argDef)).toBe(M);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArgVisibleWhen', () => {
|
||||
test('returns undefined for bare class form', () => {
|
||||
expect(getArgVisibleWhen(Metric)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns the condition for object form', () => {
|
||||
const argDef = { arg: Metric, visibleWhen: { show: true } };
|
||||
expect(getArgVisibleWhen(argDef)).toEqual({ show: true });
|
||||
});
|
||||
|
||||
test('returns undefined when object form has no visibleWhen', () => {
|
||||
expect(getArgVisibleWhen({ arg: Metric })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateGlyphCondition', () => {
|
||||
test('returns true for empty condition', () => {
|
||||
expect(evaluateGlyphCondition({}, { foo: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true when equality check matches', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: true })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when equality check fails', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, { show: false })).toBe(false);
|
||||
});
|
||||
|
||||
test('handles missing formData keys as undefined', () => {
|
||||
expect(evaluateGlyphCondition({ show: true }, {})).toBe(false);
|
||||
});
|
||||
|
||||
test('supports function-valued conditions', () => {
|
||||
const cond = { subtitle: (val: unknown) => !!val };
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: 'hi' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { subtitle: '' })).toBe(false);
|
||||
});
|
||||
|
||||
test('requires all keys in the condition to pass (AND semantics)', () => {
|
||||
const cond = { a: true, b: 'x' };
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'x' })).toBe(true);
|
||||
expect(evaluateGlyphCondition(cond, { a: true, b: 'y' })).toBe(false);
|
||||
expect(evaluateGlyphCondition(cond, { a: false, b: 'x' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - basic plugin construction', () => {
|
||||
test('returns a ChartPlugin subclass', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const p = new Plugin();
|
||||
expect(p).toBeInstanceOf(ChartPlugin);
|
||||
});
|
||||
|
||||
test('plugin metadata is a ChartMetadata instance with required fields', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
description: 'A test chart',
|
||||
category: 'Charts',
|
||||
tags: ['test'],
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata).toBeInstanceOf(ChartMetadata);
|
||||
expect(metadata.name).toBe('Test');
|
||||
expect(metadata.description).toBe('A test chart');
|
||||
expect(metadata.category).toBe('Charts');
|
||||
expect(metadata.tags).toEqual(['test']);
|
||||
expect(metadata.thumbnail).toBe(MIN_THUMBNAIL);
|
||||
});
|
||||
|
||||
test('metadata defaults Behavior.InteractiveChart when omitted', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toContain(Behavior.InteractiveChart);
|
||||
});
|
||||
|
||||
test('metadata behaviors override the default when provided', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.behaviors).toEqual([
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
]);
|
||||
});
|
||||
|
||||
test('passes label, canBeAnnotationTypes, useLegacyApi, supportedAnnotationTypes through', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
label: ChartLabel.Deprecated,
|
||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||
useLegacyApi: true,
|
||||
supportedAnnotationTypes: ['FORMULA'],
|
||||
credits: ['https://example.com'],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.label).toBe(ChartLabel.Deprecated);
|
||||
expect(metadata.canBeAnnotationTypes).toEqual(['EVENT', 'INTERVAL']);
|
||||
expect(metadata.useLegacyApi).toBe(true);
|
||||
expect(metadata.supportedAnnotationTypes).toEqual(['FORMULA']);
|
||||
expect(metadata.credits).toEqual(['https://example.com']);
|
||||
});
|
||||
|
||||
test('exampleGallery + thumbnailDark are preserved', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: {
|
||||
name: 'Test',
|
||||
thumbnail: MIN_THUMBNAIL,
|
||||
thumbnailDark: 'thumb-dark.png',
|
||||
exampleGallery: [{ url: 'a.png', urlDark: 'a-dark.png' }],
|
||||
},
|
||||
arguments: {},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { metadata } = instantiate(Plugin);
|
||||
expect(metadata.thumbnailDark).toBe('thumb-dark.png');
|
||||
expect(metadata.exampleGallery).toEqual([
|
||||
{ url: 'a.png', urlDark: 'a-dark.png' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - controlPanel generation from arguments', () => {
|
||||
test('Query section is auto-generated from Metric/Dimension arguments', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// Query section should be auto-generated
|
||||
expect(sections.some(s => s?.label === 'Query')).toBe(true);
|
||||
});
|
||||
|
||||
test('suppressQuerySection: true skips the auto Query section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
},
|
||||
suppressQuerySection: true,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// The auto-generated Query section is suppressed.
|
||||
// (Charts using suppressQuerySection typically provide their own via
|
||||
// prependSections — see legacy nvd3 / deckgl consolidations.)
|
||||
const autoQuery = sections.find(s => s?.label === 'Query');
|
||||
expect(autoQuery).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Chart Options section is generated when there are non-data args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
|
||||
test('Chart Options section is hidden when there are no customize args', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
groupby: Dimension,
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
// No Customize-tab content → Chart Options auto-hides.
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - prependSections / middleSections / additionalSections', () => {
|
||||
test('prependSections appears before the auto Query section', () => {
|
||||
const TIME_SECTION = {
|
||||
label: 'Time',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
prependSections: [TIME_SECTION],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const timeIdx = sections.findIndex(s => s?.label === 'Time');
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
expect(timeIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(queryIdx).toBeGreaterThan(timeIdx);
|
||||
});
|
||||
|
||||
test('additionalSections appears after Chart Options', () => {
|
||||
const TIME_COMP = {
|
||||
label: 'Time Comparison',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
additionalSections: [TIME_COMP],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
const timeCompIdx = sections.findIndex(s => s?.label === 'Time Comparison');
|
||||
expect(chartOptsIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(timeCompIdx).toBeGreaterThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('middleSections appears between Query and Chart Options', () => {
|
||||
const MIDDLE = {
|
||||
label: 'Middle',
|
||||
controlSetRows: [],
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
middleSections: [MIDDLE],
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
const queryIdx = sections.findIndex(s => s?.label === 'Query');
|
||||
const middleIdx = sections.findIndex(s => s?.label === 'Middle');
|
||||
const chartOptsIdx = sections.findIndex(s => s?.label === 'Chart Options');
|
||||
expect(queryIdx).toBeLessThan(middleIdx);
|
||||
expect(middleIdx).toBeLessThan(chartOptsIdx);
|
||||
});
|
||||
|
||||
test('chartOptionsTabOverride sets tabOverride on the generated section', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
},
|
||||
chartOptionsTabOverride: 'data',
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
tabOverride?: string;
|
||||
}>;
|
||||
const chartOpts = sections.find(s => s?.label === 'Chart Options');
|
||||
expect(chartOpts?.tabOverride).toBe('data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - overrides + formDataOverrides + onInit', () => {
|
||||
test('additionalControlOverrides land on controlPanel.controlOverrides', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
additionalControlOverrides: {
|
||||
size: { label: 'Custom Size Label' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(
|
||||
(controlPanel.controlOverrides as Record<string, unknown>)?.size,
|
||||
).toEqual({ label: 'Custom Size Label' });
|
||||
});
|
||||
|
||||
test('controlOverrides + additionalControlOverrides merge', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
controlOverrides: {
|
||||
a: { label: 'A' },
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
b: { label: 'B' },
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
const merged = controlPanel.controlOverrides as Record<string, unknown>;
|
||||
expect(merged.a).toEqual({ label: 'A' });
|
||||
expect(merged.b).toEqual({ label: 'B' });
|
||||
});
|
||||
|
||||
test('formDataOverrides is preserved on controlPanel', () => {
|
||||
const fdo = (formData: Record<string, unknown>) => ({
|
||||
...formData,
|
||||
custom: 'extra',
|
||||
});
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
formDataOverrides: fdo,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('onInit is preserved on controlPanel', () => {
|
||||
const onInit = (state: unknown) => state;
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
onInit,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel.onInit).toBe(onInit);
|
||||
});
|
||||
|
||||
test('_glyphArgs is attached to the controlPanel for native rendering', () => {
|
||||
const args = {
|
||||
metric: Metric,
|
||||
showLegend: Checkbox.with({ label: 'Show', default: true }),
|
||||
};
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: args,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toEqual(args);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - custom buildQuery / transform', () => {
|
||||
test('custom buildQuery is invoked via the plugin loader', async () => {
|
||||
const customBuildQuery = jest.fn(() => ({ queries: [{ marker: 1 }] }));
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {},
|
||||
buildQuery: customBuildQuery as unknown as never,
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
// ChartPlugin stores it as a sanitized loader
|
||||
const loader = (p as unknown as { loadBuildQuery?: () => Promise<Function> })
|
||||
.loadBuildQuery;
|
||||
expect(loader).toBeDefined();
|
||||
const fn = await (loader as () => Promise<Function>)();
|
||||
fn({ viz_type: 'test', datasource: '1__table' });
|
||||
expect(customBuildQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('transform receives chartProps and argValues', async () => {
|
||||
const captured: unknown[] = [];
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'Test', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: { metric: Metric },
|
||||
transform: (chartProps, argValues) => {
|
||||
captured.push({ chartProps, argValues });
|
||||
return { transformed: true };
|
||||
},
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const p = new Plugin();
|
||||
const loader = (
|
||||
p as unknown as { loadTransformProps: () => Promise<Function> }
|
||||
).loadTransformProps;
|
||||
const transformProps = await loader();
|
||||
transformProps({
|
||||
width: 100,
|
||||
height: 100,
|
||||
formData: { metric: 'count' },
|
||||
queriesData: [{ data: [] }],
|
||||
});
|
||||
expect(captured).toHaveLength(1);
|
||||
expect((captured[0] as { chartProps: unknown }).chartProps).toBeDefined();
|
||||
expect((captured[0] as { argValues: unknown }).argValues).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - Text-only argument behavior', () => {
|
||||
test('a Text-only chart still wires up a working plugin', () => {
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'TextOnly', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
title: Text.with({ label: 'Title', default: 'Hi' }),
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel, metadata } = instantiate(Plugin);
|
||||
expect(metadata.name).toBe('TextOnly');
|
||||
// Customize args present → Chart Options shows up
|
||||
const sections = controlPanel.controlPanelSections as Array<{
|
||||
label?: string;
|
||||
}>;
|
||||
expect(sections.some(s => s?.label === 'Chart Options')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineChart - visibleWhen with object-form ArgDef', () => {
|
||||
test('attaches a visibility derivation to the underlying control', () => {
|
||||
// Build a plugin where one arg is visibleWhen another is true.
|
||||
const Plugin = defineChart({
|
||||
metadata: { name: 'V', thumbnail: MIN_THUMBNAIL },
|
||||
arguments: {
|
||||
showLegend: Checkbox.with({ label: 'Show legend', default: true }),
|
||||
legendPosition: {
|
||||
arg: Select.with({
|
||||
label: 'Position',
|
||||
default: 'right',
|
||||
options: [{ label: 'R', value: 'right' }],
|
||||
}),
|
||||
visibleWhen: { showLegend: true },
|
||||
},
|
||||
},
|
||||
transform: () => ({}),
|
||||
render: () => null as unknown as React.ReactElement,
|
||||
});
|
||||
const { controlPanel } = instantiate(Plugin);
|
||||
expect(controlPanel._glyphArgs).toBeDefined();
|
||||
const glyphArgs = controlPanel._glyphArgs as Record<string, unknown>;
|
||||
// The visibleWhen is preserved on the glyph args
|
||||
const lp = glyphArgs.legendPosition as { visibleWhen?: unknown };
|
||||
expect(lp.visibleWhen).toEqual({ showLegend: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
Color,
|
||||
createGlyphPlugin,
|
||||
Dimension,
|
||||
generateControlPanel,
|
||||
generateTransformProps,
|
||||
getControlConfig,
|
||||
Int,
|
||||
Metric,
|
||||
Select,
|
||||
Temporal,
|
||||
Text,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import type { GlyphArguments } from '@superset-ui/glyph-core/generators';
|
||||
|
||||
describe('getControlConfig - per argument type', () => {
|
||||
test('Select → SelectControl with options and clearable=false', () => {
|
||||
const S = Select.with({
|
||||
label: 'Choose',
|
||||
default: 'a',
|
||||
options: [{ label: 'A', value: 'a' }],
|
||||
});
|
||||
const cfg = getControlConfig(S, 'myParam');
|
||||
expect(cfg.type).toBe('SelectControl');
|
||||
expect(cfg.label).toBe('Choose');
|
||||
expect(cfg.default).toBe('a');
|
||||
expect(cfg.options).toEqual([{ label: 'A', value: 'a' }]);
|
||||
expect(cfg.clearable).toBe(false);
|
||||
expect(cfg.renderTrigger).toBe(true);
|
||||
});
|
||||
|
||||
test('Checkbox → CheckboxControl with default', () => {
|
||||
const C = Checkbox.with({ label: 'Show', default: true });
|
||||
const cfg = getControlConfig(C, 'show');
|
||||
expect(cfg.type).toBe('CheckboxControl');
|
||||
expect(cfg.label).toBe('Show');
|
||||
expect(cfg.default).toBe(true);
|
||||
expect(cfg.renderTrigger).toBe(true);
|
||||
});
|
||||
|
||||
test('Int → SliderControl with min/max/step', () => {
|
||||
const I = Int.with({ label: 'Limit', default: 50, min: 0, max: 1000, step: 5 });
|
||||
const cfg = getControlConfig(I, 'limit');
|
||||
expect(cfg.type).toBe('SliderControl');
|
||||
expect(cfg.label).toBe('Limit');
|
||||
expect(cfg.default).toBe(50);
|
||||
expect(cfg.min).toBe(0);
|
||||
expect(cfg.max).toBe(1000);
|
||||
expect(cfg.step).toBe(5);
|
||||
});
|
||||
|
||||
test('Color (hex) → ColorPickerControl with RGBA default', () => {
|
||||
const C = Color.with({ label: 'Fill', default: '#ff0000' });
|
||||
const cfg = getControlConfig(C, 'fill');
|
||||
expect(cfg.type).toBe('ColorPickerControl');
|
||||
expect(cfg.label).toBe('Fill');
|
||||
expect(cfg.default).toEqual({ r: 255, g: 0, b: 0, a: 1 });
|
||||
});
|
||||
|
||||
test('Text → TextControl with placeholder', () => {
|
||||
const T = Text.with({
|
||||
label: 'Title',
|
||||
default: 'Untitled',
|
||||
placeholder: 'Enter…',
|
||||
});
|
||||
const cfg = getControlConfig(T, 'title');
|
||||
expect(cfg.type).toBe('TextControl');
|
||||
expect(cfg.label).toBe('Title');
|
||||
expect(cfg.default).toBe('Untitled');
|
||||
expect(cfg.placeholder).toBe('Enter…');
|
||||
});
|
||||
|
||||
test('falls back to paramName when label is unset', () => {
|
||||
// Use the raw Text class (label: null on Argument)
|
||||
class Bare extends Text {
|
||||
static override label = null;
|
||||
}
|
||||
const cfg = getControlConfig(Bare, 'fallback_name');
|
||||
expect(cfg.label).toBe('fallback_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateControlPanel', () => {
|
||||
test('produces Query and Chart Options sections', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const labels = cp.controlPanelSections.map(s =>
|
||||
s && 'label' in s ? s.label : undefined,
|
||||
);
|
||||
expect(labels).toContain('Query');
|
||||
expect(labels).toContain('Chart Options');
|
||||
});
|
||||
|
||||
test('Metric args produce a [metric] row in Query', () => {
|
||||
const args: GlyphArguments = new Map([['m', Metric]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
expect(querySection).toBeDefined();
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['metric']);
|
||||
});
|
||||
|
||||
test('Dimension args produce a [groupby] row in Query', () => {
|
||||
const args: GlyphArguments = new Map([['d', Dimension]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['groupby']);
|
||||
});
|
||||
|
||||
test('Temporal args produce [x_axis] and [time_grain_sqla] rows in Query', () => {
|
||||
const args: GlyphArguments = new Map([['t', Temporal]]);
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['x_axis']);
|
||||
expect(rows).toContainEqual(['time_grain_sqla']);
|
||||
});
|
||||
|
||||
test('adhoc_filters is always added to Query', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const cp = generateControlPanel(args);
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const rows = (querySection as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toContainEqual(['adhoc_filters']);
|
||||
});
|
||||
|
||||
test('Non-data args become controls in Chart Options', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['showLegend', Checkbox.with({ label: 'Legend', default: true })],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const rows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toEqual([
|
||||
{
|
||||
name: 'showLegend',
|
||||
config: expect.objectContaining({ type: 'CheckboxControl' }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('GlyphArgConfig with visibility wires onto control', () => {
|
||||
const visibility = jest.fn(() => true);
|
||||
const args: GlyphArguments = new Map([
|
||||
[
|
||||
'subtitleSize',
|
||||
{
|
||||
arg: Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [{ label: 'M', value: 'm' }],
|
||||
}),
|
||||
visibility,
|
||||
resetOnHide: true,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const cp = generateControlPanel(args);
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const row = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows[0];
|
||||
const item = (row as Array<{ config: Record<string, unknown> }>)[0];
|
||||
expect(item.config.visibility).toBe(visibility);
|
||||
expect(item.config.resetOnHide).toBe(true);
|
||||
});
|
||||
|
||||
test('controlOverrides and formDataOverrides options pass through', () => {
|
||||
const fdo = (fd: Record<string, unknown>) => ({ ...fd, x: 1 });
|
||||
const cp = generateControlPanel(new Map(), {
|
||||
controlOverrides: { metric: { label: 'M' } },
|
||||
formDataOverrides: fdo,
|
||||
});
|
||||
expect(cp.controlOverrides).toEqual({ metric: { label: 'M' } });
|
||||
expect(cp.formDataOverrides).toBe(fdo);
|
||||
});
|
||||
|
||||
test('extra queryControls and chartOptionsControls are appended', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const cp = generateControlPanel(args, {
|
||||
queryControls: [['custom_filter']] as never,
|
||||
chartOptionsControls: [['custom_chart_opt']] as never,
|
||||
});
|
||||
const querySection = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Query',
|
||||
);
|
||||
const queryRows = (querySection as { controlSetRows: unknown[][] })
|
||||
.controlSetRows;
|
||||
expect(queryRows).toContainEqual(['custom_filter']);
|
||||
|
||||
const chartOpts = cp.controlPanelSections.find(
|
||||
s => s && 'label' in s && s.label === 'Chart Options',
|
||||
);
|
||||
const optRows = (chartOpts as { controlSetRows: unknown[][] }).controlSetRows;
|
||||
expect(optRows).toContainEqual(['custom_chart_opt']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTransformProps', () => {
|
||||
function makeChartProps(formData: Record<string, unknown>): ChartProps {
|
||||
return {
|
||||
width: 400,
|
||||
height: 300,
|
||||
queriesData: [{ data: [] }],
|
||||
formData,
|
||||
} as unknown as ChartProps;
|
||||
}
|
||||
|
||||
test('returns width/height/queriesData passthrough', () => {
|
||||
const transform = generateTransformProps(new Map());
|
||||
const out = transform(makeChartProps({}));
|
||||
expect(out).toMatchObject({ width: 400, height: 300 });
|
||||
});
|
||||
|
||||
test('extracts Select value from formData', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
[
|
||||
'size',
|
||||
Select.with({
|
||||
label: 'Size',
|
||||
default: 'm',
|
||||
options: [
|
||||
{ label: 'S', value: 's' },
|
||||
{ label: 'M', value: 'm' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ size: 's' }));
|
||||
expect((out as { size: unknown }).size).toBe('s');
|
||||
});
|
||||
|
||||
test('Select falls back to default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['size', Select.with({ label: 'Size', default: 'm', options: [] })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { size: unknown }).size).toBe('m');
|
||||
});
|
||||
|
||||
test('Checkbox uses formData value when present', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: false })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ flag: true }));
|
||||
expect((out as { flag: unknown }).flag).toBe(true);
|
||||
});
|
||||
|
||||
test('Checkbox falls back to default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: true })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { flag: unknown }).flag).toBe(true);
|
||||
});
|
||||
|
||||
test('Color: RGBA in formData → hex string', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#000000' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(
|
||||
makeChartProps({ fill: { r: 255, g: 0, b: 0, a: 1 } }),
|
||||
);
|
||||
expect((out as { fill: unknown }).fill).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('Color: string value in formData passes through', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#000000' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({ fill: '#abcdef' }));
|
||||
expect((out as { fill: unknown }).fill).toBe('#abcdef');
|
||||
});
|
||||
|
||||
test('Color: falls back to class default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['fill', Color.with({ label: 'Fill', default: '#112233' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
// default is hex string; transform converts the RGBA-formatted default back to hex
|
||||
expect((out as { fill: unknown }).fill).toBe('#112233');
|
||||
});
|
||||
|
||||
test('Int uses default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['n', Int.with({ label: 'N', default: 7, min: 0, max: 100 })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { n: unknown }).n).toBe(7);
|
||||
});
|
||||
|
||||
test('Text uses default when value missing', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['s', Text.with({ label: 'S', default: 'hi' })],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(makeChartProps({}));
|
||||
expect((out as { s: unknown }).s).toBe('hi');
|
||||
});
|
||||
|
||||
test('Metric/Dimension/Temporal args are NOT extracted (handled elsewhere)', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['groupby', Dimension],
|
||||
['t', Temporal],
|
||||
]);
|
||||
const transform = generateTransformProps(args);
|
||||
const out = transform(
|
||||
makeChartProps({ metric: 'count', groupby: 'a', t: 'date' }),
|
||||
);
|
||||
expect((out as Record<string, unknown>).metric).toBeUndefined();
|
||||
expect((out as Record<string, unknown>).groupby).toBeUndefined();
|
||||
expect((out as Record<string, unknown>).t).toBeUndefined();
|
||||
});
|
||||
|
||||
test('passthrough option copies named ChartProps fields onto the result', () => {
|
||||
const args: GlyphArguments = new Map();
|
||||
const transform = generateTransformProps(args, {
|
||||
passthrough: ['formData'],
|
||||
});
|
||||
const out = transform(makeChartProps({ marker: 1 })) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(out.formData).toEqual({ marker: 1 });
|
||||
});
|
||||
|
||||
test('custom transform option receives extracted values and chartProps', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['flag', Checkbox.with({ label: 'F', default: false })],
|
||||
]);
|
||||
const transform = generateTransformProps(args, {
|
||||
transform: (values, chartProps) => ({
|
||||
values,
|
||||
gotChartProps: !!chartProps,
|
||||
}),
|
||||
});
|
||||
const out = transform(makeChartProps({ flag: true })) as {
|
||||
values: { flag: boolean };
|
||||
gotChartProps: boolean;
|
||||
};
|
||||
expect(out.values.flag).toBe(true);
|
||||
expect(out.gotChartProps).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGlyphPlugin', () => {
|
||||
test('returns both controlPanel and transformProps', () => {
|
||||
const args: GlyphArguments = new Map([
|
||||
['metric', Metric],
|
||||
['show', Checkbox.with({ label: 'S', default: true })],
|
||||
]);
|
||||
const plugin = createGlyphPlugin(args);
|
||||
expect(plugin.controlPanel).toBeDefined();
|
||||
expect(plugin.controlPanel.controlPanelSections.length).toBe(2);
|
||||
expect(typeof plugin.transformProps).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Checkbox,
|
||||
CircleShape,
|
||||
DataZoom,
|
||||
ForceTimestampFormatting,
|
||||
HeaderFontSize,
|
||||
isCheckboxArg,
|
||||
isSelectArg,
|
||||
isTextArg,
|
||||
LabelPosition,
|
||||
LabelType,
|
||||
LabelThreshold,
|
||||
LegendOrientation,
|
||||
LegendSort,
|
||||
LegendType,
|
||||
MetricNameFontSize,
|
||||
Select,
|
||||
ShowLabels,
|
||||
ShowLegend,
|
||||
ShowMetricName,
|
||||
ShowTotal,
|
||||
ShowValue,
|
||||
SimpleLabelType,
|
||||
SortByMetric,
|
||||
Subtitle,
|
||||
SubheaderFontSize,
|
||||
Text,
|
||||
ValueLabelType,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import {
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
LABEL_TYPE_OPTIONS,
|
||||
LEGEND_ORIENTATION_OPTIONS,
|
||||
LEGEND_SORT_OPTIONS,
|
||||
LEGEND_TYPE_OPTIONS,
|
||||
SORT_OPTIONS,
|
||||
} from '@superset-ui/glyph-core/presets';
|
||||
|
||||
describe('Font-size presets', () => {
|
||||
test('HeaderFontSize is a Select with large font options', () => {
|
||||
expect(isSelectArg(HeaderFontSize)).toBe(true);
|
||||
expect((HeaderFontSize as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
});
|
||||
|
||||
test('SubheaderFontSize is a Select with small font options', () => {
|
||||
expect(isSelectArg(SubheaderFontSize)).toBe(true);
|
||||
expect((SubheaderFontSize as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_SMALL,
|
||||
);
|
||||
});
|
||||
|
||||
test('FONT_SIZE_OPTIONS_LARGE and _SMALL are non-empty option arrays', () => {
|
||||
expect(FONT_SIZE_OPTIONS_LARGE.length).toBeGreaterThan(0);
|
||||
expect(FONT_SIZE_OPTIONS_SMALL.length).toBeGreaterThan(0);
|
||||
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('label');
|
||||
expect(FONT_SIZE_OPTIONS_LARGE[0]).toHaveProperty('value');
|
||||
});
|
||||
|
||||
test('MetricNameFontSize is a Select preset', () => {
|
||||
expect(isSelectArg(MetricNameFontSize)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text presets', () => {
|
||||
test('Subtitle is a Text preset', () => {
|
||||
expect(isTextArg(Subtitle)).toBe(true);
|
||||
expect(Subtitle.prototype).toBeInstanceOf(Text);
|
||||
});
|
||||
|
||||
test('LabelThreshold is a Text preset', () => {
|
||||
expect(isTextArg(LabelThreshold)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Checkbox presets', () => {
|
||||
test.each([
|
||||
['ShowLegend', ShowLegend],
|
||||
['ShowLabels', ShowLabels],
|
||||
['ShowValue', ShowValue],
|
||||
['ShowMetricName', ShowMetricName],
|
||||
['ShowTotal', ShowTotal],
|
||||
['SortByMetric', SortByMetric],
|
||||
['CircleShape', CircleShape],
|
||||
['DataZoom', DataZoom],
|
||||
['ForceTimestampFormatting', ForceTimestampFormatting],
|
||||
])('%s is a Checkbox preset', (_name, preset) => {
|
||||
expect(isCheckboxArg(preset)).toBe(true);
|
||||
expect(preset.prototype).toBeInstanceOf(Checkbox);
|
||||
});
|
||||
|
||||
test('Checkbox presets have a label and a description', () => {
|
||||
[ShowLegend, ShowLabels, ShowValue, ShowMetricName, ShowTotal].forEach(
|
||||
preset => {
|
||||
expect(preset.label).toBeTruthy();
|
||||
expect(preset.description).toBeTruthy();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legend Select presets', () => {
|
||||
test('LegendType uses LEGEND_TYPE_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendType)).toBe(true);
|
||||
expect((LegendType as unknown as typeof Select).options).toBe(
|
||||
LEGEND_TYPE_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('LegendOrientation uses LEGEND_ORIENTATION_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendOrientation)).toBe(true);
|
||||
expect((LegendOrientation as unknown as typeof Select).options).toBe(
|
||||
LEGEND_ORIENTATION_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('LegendSort uses LEGEND_SORT_OPTIONS', () => {
|
||||
expect(isSelectArg(LegendSort)).toBe(true);
|
||||
expect((LegendSort as unknown as typeof Select).options).toBe(
|
||||
LEGEND_SORT_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('legend option sets are non-empty', () => {
|
||||
expect(LEGEND_TYPE_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(LEGEND_ORIENTATION_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(LEGEND_SORT_OPTIONS.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Label / value-label Select presets', () => {
|
||||
test('LabelType is a Select with LABEL_TYPE_OPTIONS', () => {
|
||||
expect(isSelectArg(LabelType)).toBe(true);
|
||||
expect((LabelType as unknown as typeof Select).options).toBe(
|
||||
LABEL_TYPE_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('SimpleLabelType is a Select preset', () => {
|
||||
expect(isSelectArg(SimpleLabelType)).toBe(true);
|
||||
});
|
||||
|
||||
test('ValueLabelType is a Select preset', () => {
|
||||
expect(isSelectArg(ValueLabelType)).toBe(true);
|
||||
});
|
||||
|
||||
test('LabelPosition is a Select preset', () => {
|
||||
expect(isSelectArg(LabelPosition)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sort options', () => {
|
||||
test('SORT_OPTIONS is non-empty', () => {
|
||||
expect(SORT_OPTIONS.length).toBeGreaterThan(0);
|
||||
expect(SORT_OPTIONS[0]).toHaveProperty('label');
|
||||
expect(SORT_OPTIONS[0]).toHaveProperty('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Preset extensibility', () => {
|
||||
test('ShowLegend.with() overrides label while keeping the Checkbox shape', () => {
|
||||
const Custom = ShowLegend.with({
|
||||
label: 'Display legend',
|
||||
default: false,
|
||||
});
|
||||
expect(isCheckboxArg(Custom)).toBe(true);
|
||||
expect(Custom.label).toBe('Display legend');
|
||||
expect(Custom.default).toBe(false);
|
||||
});
|
||||
|
||||
test('HeaderFontSize.with() overrides label, default keeps options', () => {
|
||||
const Custom = HeaderFontSize.with({
|
||||
label: 'Title size',
|
||||
default: 0.4,
|
||||
});
|
||||
expect(isSelectArg(Custom)).toBe(true);
|
||||
expect(Custom.label).toBe('Title size');
|
||||
expect(Custom.default).toBe(0.4);
|
||||
expect((Custom as unknown as typeof Select).options).toBe(
|
||||
FONT_SIZE_OPTIONS_LARGE,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
||||
"baseUrl": "../..",
|
||||
|
||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
||||
// but packages need paths relative to their own directory
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"declarationDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
|
||||
"references": [
|
||||
{ "path": "../superset-core" },
|
||||
{ "path": "../superset-ui-core" },
|
||||
{ "path": "../superset-ui-chart-controls" }
|
||||
]
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { legacyValidateInteger } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
['linear_color_scheme'],
|
||||
[
|
||||
{
|
||||
name: 'cell_size',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
default: 10,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
label: t('Cell Size'),
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cell_padding',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
label: t('Cell Padding'),
|
||||
description: t('The distance between cells, in pixels'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'cell_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 0,
|
||||
label: t('Cell Radius'),
|
||||
description: t('The pixel radius'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'steps',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 10,
|
||||
label: t('Color Steps'),
|
||||
description: t('The number color "steps"'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'y_axis_format',
|
||||
{
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_legend',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Legend'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_values',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Values'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Names'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
},
|
||||
},
|
||||
null,
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number Format'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Correlation'),
|
||||
credits: ['https://github.com/wa0x6e/cal-heatmap'],
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Calendar Heatmap'),
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Comparison'),
|
||||
t('Intensity'),
|
||||
t('Pattern'),
|
||||
t('Report'),
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CalendarChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCalendar'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { getNumberFormatter } from '@superset-ui/core';
|
||||
import {
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
defineChart,
|
||||
Int,
|
||||
Checkbox,
|
||||
TimeFormat,
|
||||
} from '@superset-ui/glyph-core';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactCalendar = require('./ReactCalendar').default;
|
||||
|
||||
type CalendarExtra = {
|
||||
timeFormatter: (ts: number | string) => string;
|
||||
valueFormatter: (val: unknown) => string;
|
||||
verboseMap: Record<string, string>;
|
||||
domainGranularity: string;
|
||||
subdomainGranularity: string;
|
||||
linearColorScheme: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, CalendarExtra>({
|
||||
metadata: {
|
||||
name: t('Calendar Heatmap'),
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
category: t('Correlation'),
|
||||
credits: ['https://github.com/wa0x6e/cal-heatmap'],
|
||||
tags: [
|
||||
t('Business'),
|
||||
t('Comparison'),
|
||||
t('Intensity'),
|
||||
t('Pattern'),
|
||||
t('Report'),
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
},
|
||||
arguments: {
|
||||
cell_size: Int.with({
|
||||
label: 'Cell Size',
|
||||
description: 'The size of the square cell, in pixels',
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 100,
|
||||
}),
|
||||
cell_padding: Int.with({
|
||||
label: 'Cell Padding',
|
||||
description: 'The distance between cells, in pixels',
|
||||
default: 2,
|
||||
min: 0,
|
||||
max: 20,
|
||||
}),
|
||||
cell_radius: Int.with({
|
||||
label: 'Cell Radius',
|
||||
description: 'The pixel radius',
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 50,
|
||||
}),
|
||||
steps: Int.with({
|
||||
label: 'Color Steps',
|
||||
description: 'The number color "steps"',
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 50,
|
||||
}),
|
||||
x_axis_time_format: TimeFormat.with({
|
||||
label: 'Time Format',
|
||||
description: D3_FORMAT_DOCS,
|
||||
default: 'smart_date',
|
||||
}),
|
||||
show_legend: Checkbox.with({
|
||||
label: 'Legend',
|
||||
description: 'Whether to display the legend (toggles)',
|
||||
default: true,
|
||||
}),
|
||||
show_values: Checkbox.with({
|
||||
label: 'Show Values',
|
||||
description: 'Whether to display the numerical values within the cells',
|
||||
default: false,
|
||||
}),
|
||||
show_metric_name: Checkbox.with({
|
||||
label: 'Show Metric Names',
|
||||
description: 'Whether to display the metric name as a title',
|
||||
default: true,
|
||||
}),
|
||||
},
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
],
|
||||
additionalControls: {
|
||||
queryBefore: [
|
||||
[
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['metrics'],
|
||||
],
|
||||
chartOptions: [['linear_color_scheme'], ['y_axis_format']],
|
||||
},
|
||||
chartOptionsTabOverride: 'customize',
|
||||
additionalControlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number Format'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
transform: (chartProps, { x_axis_time_format }) => {
|
||||
const { formData, datasource } = chartProps;
|
||||
const {
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
linearColorScheme,
|
||||
yAxisFormat,
|
||||
} = formData as Record<string, string>;
|
||||
|
||||
const verboseMap =
|
||||
(datasource as { verboseMap?: Record<string, string> })?.verboseMap ?? {};
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, x_axis_time_format as string);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
timeFormatter,
|
||||
valueFormatter: valueFormatter as (val: unknown) => string,
|
||||
verboseMap,
|
||||
domainGranularity: domainGranularity ?? 'month',
|
||||
subdomainGranularity: subdomainGranularity ?? 'day',
|
||||
linearColorScheme: linearColorScheme ?? '',
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
height,
|
||||
data,
|
||||
cell_size: cellSize,
|
||||
cell_padding: cellPadding,
|
||||
cell_radius: cellRadius,
|
||||
steps,
|
||||
show_legend: showLegend,
|
||||
show_values: showValues,
|
||||
show_metric_name: showMetricName,
|
||||
timeFormatter,
|
||||
valueFormatter,
|
||||
verboseMap,
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
linearColorScheme,
|
||||
}) => (
|
||||
<ReactCalendar
|
||||
height={height}
|
||||
data={data}
|
||||
cellSize={cellSize}
|
||||
cellPadding={cellPadding}
|
||||
cellRadius={cellRadius}
|
||||
steps={steps}
|
||||
showLegend={showLegend}
|
||||
showValues={showValues}
|
||||
showMetricName={showMetricName}
|
||||
timeFormatter={timeFormatter}
|
||||
valueFormatter={valueFormatter}
|
||||
verboseMap={verboseMap}
|
||||
domainGranularity={domainGranularity}
|
||||
subdomainGranularity={subdomainGranularity}
|
||||
linearColorScheme={linearColorScheme}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ChartProps, getNumberFormatter } from '@superset-ui/core';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
domainGranularity,
|
||||
linearColorScheme,
|
||||
showLegend,
|
||||
showMetricName,
|
||||
showValues,
|
||||
steps,
|
||||
subdomainGranularity,
|
||||
xAxisTimeFormat,
|
||||
yAxisFormat,
|
||||
} = formData;
|
||||
|
||||
const { verboseMap } = datasource;
|
||||
const timeFormatter = (ts: number | string) =>
|
||||
getFormattedUTCTime(ts, xAxisTimeFormat);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
return {
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
domainGranularity,
|
||||
linearColorScheme,
|
||||
showLegend,
|
||||
showMetricName,
|
||||
showValues,
|
||||
steps,
|
||||
subdomainGranularity,
|
||||
timeFormatter,
|
||||
valueFormatter,
|
||||
verboseMap,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*"
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/glyph-core": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
['columns'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
['row_limit'],
|
||||
['sort_by_metric'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => {
|
||||
const groupby = getStandardizedControls()
|
||||
.popAllColumns()
|
||||
.filter(col => !ensureIsArray(formData.columns).includes(col));
|
||||
return {
|
||||
...formData,
|
||||
groupby,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Flow'),
|
||||
credits: ['https://github.com/d3/d3-chord'],
|
||||
description: t(
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
name: t('Chord Diagram'),
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class ChordChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactChord'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray, validateNonEmpty } from '@superset-ui/core';
|
||||
import { getStandardizedControls } from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactChord = require('./ReactChord').default;
|
||||
|
||||
type ChordExtra = {
|
||||
colorScheme: string;
|
||||
numberFormat: string;
|
||||
sliceId: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, ChordExtra>({
|
||||
metadata: {
|
||||
name: t('Chord Diagram'),
|
||||
description: t(
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
category: t('Flow'),
|
||||
credits: ['https://github.com/d3/d3-chord'],
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [['groupby'], ['columns'], ['metric']],
|
||||
query: [['row_limit'], ['sort_by_metric']],
|
||||
chartOptions: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
additionalControlOverrides: {
|
||||
y_axis_format: {
|
||||
label: t('Number format'),
|
||||
description: t('Choose a number format'),
|
||||
},
|
||||
groupby: {
|
||||
label: t('Source'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a source'),
|
||||
},
|
||||
columns: {
|
||||
label: t('Target'),
|
||||
multi: false,
|
||||
validators: [validateNonEmpty],
|
||||
description: t('Choose a target'),
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => {
|
||||
const groupby = getStandardizedControls()
|
||||
.popAllColumns()
|
||||
.filter(
|
||||
(col: string) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!ensureIsArray((formData as any).columns).includes(col),
|
||||
);
|
||||
return {
|
||||
...formData,
|
||||
groupby,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
};
|
||||
},
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData as Record<
|
||||
string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any
|
||||
>;
|
||||
return {
|
||||
colorScheme: colorScheme ?? '',
|
||||
numberFormat: yAxisFormat ?? '',
|
||||
sliceId: sliceId ?? 0,
|
||||
};
|
||||
},
|
||||
render: ({ width, height, data, colorScheme, numberFormat, sliceId }) => (
|
||||
<ReactChord
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
colorScheme={colorScheme}
|
||||
numberFormat={numberFormat}
|
||||
sliceId={sliceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData;
|
||||
|
||||
return {
|
||||
colorScheme,
|
||||
data: queriesData[0].data,
|
||||
height,
|
||||
numberFormat: yAxisFormat,
|
||||
width,
|
||||
sliceId,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['entity'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['currency_format'],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
entity: {
|
||||
label: t('ISO 3166-2 Codes'),
|
||||
description: t(
|
||||
'Column containing ISO 3166-2 codes of region/province/department in your table.',
|
||||
),
|
||||
},
|
||||
metric: {
|
||||
label: t('Metric'),
|
||||
description: t('Metric to display bottom title'),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://bl.ocks.org/john-guerra'],
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
name: t('Country Map'),
|
||||
tags: [
|
||||
t('2D'),
|
||||
t('Comparison'),
|
||||
t('Geo'),
|
||||
t('Range'),
|
||||
t('Report'),
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CountryMapChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCountryMap'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { default as countries } from './countries';
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ReactCountryMap = require('./ReactCountryMap').default;
|
||||
|
||||
export { default as countries } from './countries';
|
||||
|
||||
type CountryMapExtra = {
|
||||
country: string | null;
|
||||
linearColorScheme: string;
|
||||
numberFormat: string;
|
||||
colorScheme: string;
|
||||
sliceId: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, CountryMapExtra>({
|
||||
metadata: {
|
||||
name: t('Country Map'),
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
category: t('Map'),
|
||||
credits: ['https://bl.ocks.org/john-guerra'],
|
||||
tags: [
|
||||
t('2D'),
|
||||
t('Comparison'),
|
||||
t('Geo'),
|
||||
t('Range'),
|
||||
t('Report'),
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
additionalControls: {
|
||||
queryBefore: [
|
||||
[
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
['entity'],
|
||||
['metric'],
|
||||
],
|
||||
chartOptions: [
|
||||
[
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
chartOptionsTabOverride: 'customize',
|
||||
additionalControlOverrides: {
|
||||
entity: {
|
||||
label: t('ISO 3166-2 Codes'),
|
||||
description: t(
|
||||
'Column containing ISO 3166-2 codes of region/province/department in your table.',
|
||||
),
|
||||
},
|
||||
metric: {
|
||||
label: t('Metric'),
|
||||
description: t('Metric to display bottom title'),
|
||||
},
|
||||
linear_color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const {
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
selectCountry,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
} = formData as Record<string, unknown>;
|
||||
return {
|
||||
country: selectCountry ? String(selectCountry).toLowerCase() : null,
|
||||
linearColorScheme: (linearColorScheme as string) ?? '',
|
||||
numberFormat: (numberFormat as string) ?? '',
|
||||
colorScheme: (colorScheme as string) ?? '',
|
||||
sliceId: (sliceId as number) ?? 0,
|
||||
};
|
||||
},
|
||||
render: ({
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
country,
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
}) => (
|
||||
<ReactCountryMap
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
country={country}
|
||||
linearColorScheme={linearColorScheme}
|
||||
numberFormat={numberFormat}
|
||||
colorScheme={colorScheme}
|
||||
sliceId={sliceId}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps, getValueFormatter } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData, datasource } = chartProps;
|
||||
const {
|
||||
linearColorScheme,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
selectCountry,
|
||||
colorScheme,
|
||||
sliceId,
|
||||
metric,
|
||||
} = formData;
|
||||
|
||||
const {
|
||||
currencyFormats = {},
|
||||
columnFormats = {},
|
||||
currencyCodeColumn,
|
||||
} = datasource;
|
||||
const { data, detected_currency: detectedCurrency } = queriesData[0];
|
||||
|
||||
const formatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
undefined, // key - not needed for single-metric charts
|
||||
data,
|
||||
currencyCodeColumn,
|
||||
detectedCurrency,
|
||||
);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data: queriesData[0].data,
|
||||
country: selectCountry ? String(selectCountry).toLowerCase() : null,
|
||||
linearColorScheme,
|
||||
numberFormat, // left for backward compatibility
|
||||
colorScheme,
|
||||
sliceId,
|
||||
formatter,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@superset-ui/glyph-core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
formatSelectOptions,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Distribution'),
|
||||
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Horizon Chart'),
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class HorizonChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./HorizonChart'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HorizonChart = require('./HorizonChart').default;
|
||||
|
||||
type HorizonExtra = {
|
||||
colorScale: string;
|
||||
seriesHeight: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<any, HorizonExtra>({
|
||||
metadata: {
|
||||
name: t('Horizon Chart'),
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
category: t('Distribution'),
|
||||
credits: ['http://kmandov.github.io/d3-horizon-chart/'],
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
useLegacyApi: true,
|
||||
},
|
||||
arguments: {},
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
],
|
||||
additionalControls: {
|
||||
queryBefore: [['metrics']],
|
||||
query: [
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
['row_limit', null],
|
||||
],
|
||||
chartOptions: [
|
||||
[
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
transform: chartProps => {
|
||||
const { formData } = chartProps;
|
||||
const { horizonColorScale, seriesHeight } = formData as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
return {
|
||||
colorScale: horizonColorScale ?? 'series',
|
||||
seriesHeight: parseInt(seriesHeight ?? '25', 10),
|
||||
};
|
||||
},
|
||||
render: ({ width, height, data, colorScale, seriesHeight }) => (
|
||||
<HorizonChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={data}
|
||||
colorScale={colorScale}
|
||||
seriesHeight={seriesHeight}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { height, width, formData, queriesData } = chartProps;
|
||||
const {
|
||||
horizon_color_scale: horizonColorScale,
|
||||
series_height: seriesHeight,
|
||||
} = formData;
|
||||
|
||||
// Only include colorScale if defined, otherwise let defaultProps apply
|
||||
return {
|
||||
...(horizonColorScale !== undefined && {
|
||||
colorScale: horizonColorScale as string,
|
||||
}),
|
||||
data: queriesData[0].data,
|
||||
height,
|
||||
seriesHeight: parseInt(String(seriesHeight ?? 20), 10),
|
||||
width,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"references": [
|
||||
{ "path": "../../packages/superset-core" },
|
||||
{ "path": "../../packages/superset-ui-core" },
|
||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||
{ "path": "../../packages/superset-ui-chart-controls" },
|
||||
{ "path": "../../packages/superset-ui-glyph-core" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
declare module "*.png" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
declare module "*.jpg" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user