Compare commits

..

65 Commits

Author SHA1 Message Date
Amin Ghadersohi
16d136ef8d fix(mcp): context-aware recovery hints and sanitize identifier in not-found errors
- In get_chart_preview, when identifier looks like a form_data_key (long
  non-numeric string), suggest regenerating the explore link rather than
  always pointing to list_charts, which is only relevant for chart IDs.
- Truncate request.identifier to 200 chars before embedding in error
  messages across get_chart_preview, get_chart_data, and update_chart
  to prevent injection via oversized attacker-controlled identifiers.
2026-05-09 00:26:11 +00:00
Amin Ghadersohi
c78658d852 fix(mcp): improve "not found" errors to suggest corresponding list_* tools
When MCP tools return "not found" errors for database, chart, dataset, or
dashboard IDs, include recovery guidance pointing to the appropriate list
tool (list_databases, list_charts, list_datasets, list_dashboards).

Affected tools: execute_sql, open_sql_lab_with_context, query_dataset,
get_chart_data, get_chart_preview, update_chart,
add_chart_to_existing_dashboard, generate_dashboard
2026-05-06 22:55:46 +00:00
bdonovan1
5b5dd01028 fix(sqla): parenthesize calculated column expressions in WHERE clause (#39793)
Co-authored-by: Brian Donovan <briand@netflix.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
2026-05-06 19:45:27 -03:00
bialkou
4aa4415d8f fix(i18n): update Russian translations (#39589)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
2026-05-06 13:05:23 -04:00
Sebastian Mohr
e667ceb6cf feat(themes): expose active theme mode via data-theme-mode attribute (#39063) 2026-05-06 18:17:54 +03:00
Enzo Martellucci
9aaa12c7d4 fix(reports): preserve urlParams in multi-tab report fan-out (#39884) 2026-05-06 16:29:45 +02:00
Alexandru Soare
adfbbf1433 fix(sql): quote identifiers in transpile_to_dialect to fix case-sensitive column filters (#39521) 2026-05-06 10:53:09 +03:00
dependabot[bot]
d7663a9a1c chore(deps-dev): update denodo-sqlalchemy requirement from ~=1.0.6 to >=1.0.6,<2.1.0 (#39832)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:17:21 -07:00
dependabot[bot]
7290d3c452 chore(deps-dev): update pyathena requirement from <3,>=2 to >=2,<4 (#39830)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:17:00 -07:00
dependabot[bot]
d7beffcec1 chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 0.9.3 to 0.10.0 in /superset-frontend (#39853)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:15:10 -07:00
dependabot[bot]
f018b67895 chore(deps-dev): update sqlalchemy-vertica-python requirement from <0.6,>=0.5.9 to >=0.5.9,<0.7 (#39831)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:14:08 -07:00
dependabot[bot]
5e2c6d8c9e chore(deps): bump nanoid from 5.1.9 to 5.1.11 in /superset-frontend (#39820)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:13:52 -07:00
dependabot[bot]
b305c8681c chore(deps-dev): update impyla requirement from <0.17,>0.16.2 to >0.16.2,<0.23 (#39833)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:09:37 -07:00
dependabot[bot]
d578fa1949 chore(deps): bump @deck.gl/mapbox from 9.3.1 to 9.3.2 in /superset-frontend (#39814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-05 22:09:33 -07:00
dependabot[bot]
14d28c34fd chore(deps-dev): update cx-oracle requirement from <8.1,>8.0.0 to >8.0.0,<8.4 (#39753)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:05:54 -07:00
dependabot[bot]
c06aee8513 chore(deps-dev): bump jsdom from 29.1.0 to 29.1.1 in /superset-frontend (#39815)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-05 22:04:47 -07:00
dependabot[bot]
d0ef19953a chore(deps): bump memoize-one from 5.2.1 to 6.0.0 in /superset-frontend/plugins/plugin-chart-ag-grid-table (#37910)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-05-05 21:38:49 -07:00
Vitor Avila
3745e37182 fix(OAuth2): Support OAuth2 exception with legacy endpoint (#39897) 2026-05-05 21:21:48 -03:00
Joe Li
4b17ac2629 fix(explore): add matrixify_enable guard to prevent stale validators on pre-revamp charts (#38765)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 16:45:38 -07:00
Amin Ghadersohi
4a21a5365f fix(mcp): validate column refs in generate_explore_link, update_chart_preview, and update_chart (#39797) 2026-05-05 19:12:31 -04:00
Richard Fogaca Nienkotter
9459bc7bf4 fix(mcp): warn on invalid chart preview form data key (#39891)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 16:40:00 -03:00
Beto Dealmeida
cb53745d43 feat: semantic layer extension (#37815) 2026-05-05 12:07:46 -04:00
jesperct
9e91ae8cff fix(colors): reassign colliding series when dashboard locks shared dimension color (#39297)
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
2026-05-05 08:38:19 -07:00
jesperct
5b5f23d127 test(plugin-chart-echarts): regression guards for temporal x-axis labels on timeseries charts (#39208) 2026-05-05 08:37:35 -07:00
Mehmet Salih Yavuz
8173cfe9e3 fix(CollectionControl): assign stable ids to keyless items (#39862) 2026-05-05 17:52:36 +03:00
Mehmet Salih Yavuz
586de12a05 fix(embedded): prevent duplicate React root on rehandshake (#39860) 2026-05-05 17:52:01 +03:00
dependabot[bot]
d6188374b4 chore(deps): bump docusaurus-theme-openapi-docs from 5.0.1 to 5.0.2 in /docs (#39846)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 09:16:53 +07:00
dependabot[bot]
2edae162f0 chore(deps): bump baseline-browser-mapping from 2.10.24 to 2.10.27 in /docs (#39848)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 09:16:33 +07:00
dependabot[bot]
e80207218b chore(deps-dev): bump eslint from 10.2.1 to 10.3.0 in /superset-websocket (#39843)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 17:02:27 -07:00
Beto Dealmeida
76955017eb chore: bump shillelagh to 1.4.4 (#39870) 2026-05-04 19:39:38 -04:00
Beto Dealmeida
5325b87e73 fix(clickhouse): prevent expensive table scan (#39867) 2026-05-04 19:39:10 -04:00
Đỗ Trọng Hải
e76318633e fix(helm): allow chart to work out-of-the-box with legacy Bitnami images (#39839)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-05-04 15:54:01 -07:00
Sam Firke
c2725e86f3 fix(markdown): Allow "target" attribute (#39868) 2026-05-04 18:27:43 -04:00
dependabot[bot]
2f605724e7 chore(deps-dev): bump globals from 17.5.0 to 17.6.0 in /superset-websocket (#39844)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:09:36 -07:00
dependabot[bot]
ebb02d0ecf chore(deps): bump @swc/core from 1.15.32 to 1.15.33 in /docs (#39845)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:09:21 -07:00
dependabot[bot]
319b8a1124 chore(deps-dev): bump globals from 17.5.0 to 17.6.0 in /docs (#39847)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:08:59 -07:00
dependabot[bot]
2be971ce77 chore(deps): bump docusaurus-plugin-openapi-docs from 5.0.1 to 5.0.2 in /docs (#39849)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:08:10 -07:00
dependabot[bot]
812f4ae080 chore(deps): update zod requirement from ^4.4.1 to ^4.4.3 in /superset-frontend/plugins/plugin-chart-echarts (#39850)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:07:55 -07:00
dependabot[bot]
af8d15fdfc chore(deps): bump yeoman-generator from 8.1.2 to 8.2.2 in /superset-frontend (#39852)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 15:07:41 -07:00
Amin Ghadersohi
673634f7af fix(mcp): point get_dataset_info url to explore view instead of legacy tablemodelview edit (#39838) 2026-05-04 13:39:05 -04:00
Mehmet Salih Yavuz
41a22d7918 chore: Upgrade to React 18 (#38563)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-04 19:19:36 +03:00
Amin Ghadersohi
28239c18d4 feat(mcp): warn when execute_sql template_params used with templating disabled (#39858) 2026-05-04 12:14:44 -04:00
dependabot[bot]
6205afbaa0 chore(deps-dev): bump webpack-sources from 3.4.0 to 3.4.1 in /superset-frontend (#39851) 2026-05-04 22:25:31 +07:00
EMMANUELA OPURUM
dc1c0f6ba1 docs: add user-facing Handlebars chart page with full helpers reference (#39591)
Co-authored-by: Emmanuela Opurum <youremail@example.com>
2026-05-02 13:16:39 -04:00
dependabot[bot]
ad73395c89 chore(deps-dev): bump yeoman-test from 11.3.1 to 11.4.2 in /superset-frontend (#39816)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 13:46:47 +07:00
Evan Rusackas
867e173427 chore(deps): drop stale legacy-plugin-chart-map-box lockfile entry (#39825)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 13:35:25 +07:00
dependabot[bot]
c90c8612ad chore(deps): bump @docusaurus/faster from 3.10.0 to 3.10.1 in /docs (#39804)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-05-02 13:32:37 +07:00
Abdul Rehman
b14cca15f6 fix(table): preserve decimals in totals row when Time Comparison is enabled (#39747) 2026-05-02 13:31:54 +07:00
dependabot[bot]
9d4384e49e chore(deps-dev): bump @babel/preset-env from 7.29.2 to 7.29.3 in /superset-frontend (#39822)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 12:38:54 +07:00
jesperct
d8dd2d99b3 fix(time-comparison): use chart row_limit instead of instance config in offset queries (#39490) 2026-05-01 16:24:59 -07:00
dependabot[bot]
dbe26d81ce chore(deps-dev): bump baseline-browser-mapping from 2.10.21 to 2.10.24 in /superset-frontend (#39759)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:24:23 -07:00
Elizabeth Thompson
98eaaaa6d6 fix(mcp): clear stale thread-local DB session in sync tool wrapper (#39798)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:24:48 -07:00
Jay Masiwal
cb74438865 fix(viz): correct table chart drill-to-detail temporal boundaries and null handling (#39668)
Co-authored-by: Samuelinto <samuel.mantilla@mail.utoronto.ca>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:46:18 -04:00
Danylo Korostil
e77fb5e3fc feat(i18n): updated Ukrainian translation (#39720) 2026-05-01 11:12:05 -04:00
dependabot[bot]
1ac113fd44 chore(deps): bump aws-actions/amazon-ecs-render-task-definition from 1.8.4 to 1.8.5 (#39809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 06:31:48 -07:00
dependabot[bot]
6bfdee98cd chore(deps-dev): bump @docusaurus/tsconfig from 3.10.0 to 3.10.1 in /docs (#39811)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:31:29 -04:00
dependabot[bot]
de45f3a928 chore(deps): bump aws-actions/amazon-ecs-deploy-task-definition from 2.6.1 to 2.6.2 (#39806)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:30:49 -04:00
dependabot[bot]
2ec53c0694 chore(deps): bump mapbox-gl from 3.22.0 to 3.23.0 in /superset-frontend (#39769)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 09:30:21 -04:00
Michael S. Molina
d23b0cad92 chore: Bump core packages to 0.1.0 RC3 (#39823) 2026-05-01 09:54:39 -03:00
Evan Rusackas
e585406fff chore(codeowners): notify @sfirke on translation changes (#39794)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-04-30 23:07:29 -04:00
Amin Ghadersohi
957b298ae1 fix(mcp): add default request parameter to list_charts and list_dashboards (#39730) 2026-04-30 18:04:39 -04:00
Amin Ghadersohi
f29d82b3b1 feat(mcp): add query_dataset tool to query datasets using semantic layer (#39727) 2026-04-30 18:03:41 -04:00
Vitor Avila
3f550f166f fix(GSheets OAuth2): Re-add UnauthenticatedError (#39785) 2026-04-30 18:57:00 -03:00
Vitor Avila
86eb6176d1 fix: Enforce per-user caching on legacy API endpoint (#39789) 2026-04-30 18:04:33 -03:00
Joe Li
4244ae87bf fix(deps): regenerate pinned requirements for psycopg2-binary 2.9.12 (#39790)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 17:46:23 -03:00
406 changed files with 36820 additions and 14871 deletions

4
.github/CODEOWNERS vendored
View File

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

View File

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

View File

@@ -54,6 +54,7 @@ jobs:
SUPERSET_SECRET_KEY: not-a-secret
run: |
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
pytest --durations-min=0.5 --cov=superset/semantic_layers/ ./tests/unit_tests/semantic_layers/ --cache-clear --cov-fail-under=100
- name: Upload code coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:

View File

@@ -46,6 +46,13 @@ The Deck.gl MapBox chart's **Opacity**, **Default longitude**, **Default latitud
**To restore fit-to-data behavior:** Open the chart in Explore, clear the **Default longitude**, **Default latitude**, and **Zoom** fields in the Viewport section, and re-save the chart.
### Combined datasource list endpoint
Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to serve datasets and semantic views in one response.
- The endpoint is available to users with at least one of `can_read` on `Dataset` or `SemanticView`.
- Semantic views are included only when the `SEMANTIC_LAYERS` feature flag is enabled.
- The endpoint enforces strict `order_column` validation and returns `400` for invalid sort columns.
### ClickHouse minimum driver version bump
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.

View File

@@ -105,7 +105,13 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
"SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

View File

@@ -224,3 +224,52 @@ async def analysis_guide(ctx: Context) -> str:
```
See [MCP Integration](./mcp) for implementation details.
### Semantic Layers
Extensions can register custom semantic layer implementations that allow Superset to connect to external data modeling frameworks. Each semantic layer defines how to authenticate, discover semantic views (tables/metrics/dimensions), and execute queries against the external system.
```python
from superset_core.semantic_layers.decorators import semantic_layer
from superset_core.semantic_layers.layer import SemanticLayer
from my_extension.config import MyConfig
from my_extension.view import MySemanticView
@semantic_layer(
id="my_platform",
name="My Data Platform",
description="Connect to My Data Platform's semantic layer",
)
class MySemanticLayer(SemanticLayer[MyConfig, MySemanticView]):
configuration_class = MyConfig
@classmethod
def from_configuration(cls, configuration: dict) -> "MySemanticLayer":
config = MyConfig.model_validate(configuration)
return cls(config)
@classmethod
def get_configuration_schema(cls, configuration=None) -> dict:
return MyConfig.model_json_schema()
@classmethod
def get_runtime_schema(cls, configuration=None, runtime_data=None) -> dict:
return {"type": "object", "properties": {}}
def get_semantic_views(self, runtime_configuration: dict) -> set[MySemanticView]:
# Return available views from the external platform
...
def get_semantic_view(self, name: str, additional_configuration: dict) -> MySemanticView:
# Return a specific view by name
...
```
**Note**: The `@semantic_layer` decorator automatically detects context and applies appropriate ID prefixing:
- **Extension context**: ID prefixed as `extensions.{publisher}.{name}.{id}`
- **Host context**: Original ID used as-is
The decorator registers the class in the semantic layers registry, making it available in the UI for users to create connections. The `configuration_class` should be a Pydantic model that defines the fields needed to connect (credentials, project, database, etc.). Superset uses the model's JSON schema to render the configuration form dynamically.

View File

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

View File

@@ -41,12 +41,12 @@
},
"dependencies": {
"@ant-design/icons": "^6.2.2",
"@docusaurus/core": "^3.10.0",
"@docusaurus/faster": "^3.10.0",
"@docusaurus/plugin-client-redirects": "^3.10.0",
"@docusaurus/preset-classic": "3.10.0",
"@docusaurus/theme-live-codeblock": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
"@docusaurus/core": "^3.10.1",
"@docusaurus/faster": "^3.10.1",
"@docusaurus/plugin-client-redirects": "^3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@docusaurus/theme-live-codeblock": "^3.10.1",
"@docusaurus/theme-mermaid": "^3.10.1",
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
@@ -67,12 +67,12 @@
"@storybook/preview-api": "^8.6.18",
"@storybook/theming": "^8.6.15",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.32",
"@swc/core": "^1.15.33",
"antd": "^6.3.7",
"baseline-browser-mapping": "^2.10.24",
"baseline-browser-mapping": "^2.10.27",
"caniuse-lite": "^1.0.30001791",
"docusaurus-plugin-openapi-docs": "^5.0.1",
"docusaurus-theme-openapi-docs": "^5.0.1",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.1.1",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
@@ -92,8 +92,8 @@
"unist-util-visit": "^5.1.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/module-type-aliases": "^3.10.1",
"@docusaurus/tsconfig": "^3.10.1",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
@@ -103,7 +103,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.5.0",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.1",
@@ -124,8 +124,7 @@
"resolutions": {
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"webpackbar": "^7.0.0"
"baseline-browser-mapping": "^2.9.19"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -81,6 +81,12 @@
"lifecycle": "development",
"description": "Expand nested types in Presto into extra columns/arrays. Experimental, doesn't work with all nested types."
},
{
"name": "SEMANTIC_LAYERS",
"default": false,
"lifecycle": "development",
"description": "Enable semantic layers and show semantic views alongside datasets"
},
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,

View File

@@ -1570,10 +1570,10 @@
"@docsearch/core" "4.6.2"
"@docsearch/css" "4.6.2"
"@docusaurus/babel@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.10.0.tgz#819819f107233dfcf50b59cd51158f23fb04878a"
integrity sha512-mqCJhCZNZUDg0zgDEaPTM4DnRsisa24HdqTy/qn/MQlbwhTb4WVaZg6ZyX6yIVKqTz8fS1hBMgM+98z+BeJJDg==
"@docusaurus/babel@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.10.1.tgz#2f714f682117658ba43d308e9b35b6a73a105227"
integrity sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg==
dependencies:
"@babel/core" "^7.25.9"
"@babel/generator" "^7.25.9"
@@ -1584,23 +1584,23 @@
"@babel/preset-typescript" "^7.25.9"
"@babel/runtime" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
babel-plugin-dynamic-import-node "^2.3.3"
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/bundler@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.10.0.tgz#878c4c46bfa3434671ea37a43da184238a6aae26"
integrity sha512-iONUGZGgp+lAkw/cJZH6irONcF4p8+278IsdRlq8lYhxGjkoNUs0w7F4gVXBYSNChq5KG5/JleTSsdJySShxow==
"@docusaurus/bundler@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.10.1.tgz#82fa5079f3787a67502e25f82d37d05ec5de0cc3"
integrity sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw==
dependencies:
"@babel/core" "^7.25.9"
"@docusaurus/babel" "3.10.0"
"@docusaurus/cssnano-preset" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/babel" "3.10.1"
"@docusaurus/cssnano-preset" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
babel-loader "^9.2.1"
clean-css "^5.3.3"
copy-webpack-plugin "^11.0.0"
@@ -1618,20 +1618,20 @@
tslib "^2.6.0"
url-loader "^4.1.1"
webpack "^5.95.0"
webpackbar "^6.0.1"
webpackbar "^7.0.0"
"@docusaurus/core@3.10.0", "@docusaurus/core@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.10.0.tgz#642e71a0209d62c3f5ef275ed9d74a881f40df39"
integrity sha512-mgLdQsO8xppnQZc3LPi+Mf+PkPeyxJeIx11AXAq/14fsaMefInQiMEZUUmrc7J+956G/f7MwE7tn8KZgi3iRcA==
"@docusaurus/core@3.10.1", "@docusaurus/core@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.10.1.tgz#3f8bdb97451b4df14f2a3b39ab0186366fbf8fbe"
integrity sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==
dependencies:
"@docusaurus/babel" "3.10.0"
"@docusaurus/bundler" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/babel" "3.10.1"
"@docusaurus/bundler" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
boxen "^6.2.1"
chalk "^4.1.2"
chokidar "^3.5.3"
@@ -1668,22 +1668,22 @@
webpack-dev-server "^5.2.2"
webpack-merge "^6.0.1"
"@docusaurus/cssnano-preset@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.0.tgz#be1b435c33df09d743473d3fadda67b4568dfae3"
integrity sha512-qzSshTO1DB3TYW+dPUal5KHM7XPc5YQfzF3Kdb2NDACJUyGbNcFtw3tGkCJlYwhNCRKbZcmwraKUS1i5dcHdGg==
"@docusaurus/cssnano-preset@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.1.tgz#4b6bafeca8bb9423364d2fd6683c28e2f85a4665"
integrity sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ==
dependencies:
cssnano-preset-advanced "^6.1.2"
postcss "^8.5.4"
postcss-sort-media-queries "^5.2.0"
tslib "^2.6.0"
"@docusaurus/faster@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.10.0.tgz#0758a93196f685537aa7700bde62faf926e6c817"
integrity sha512-GNPtVH14ISjHfSwnHu3KiFGf86ICmJSQDeSv/QaanpBgiZGOtgZaslnC5q8WiguxM1EVkwcGxPuD8BXF4eggKw==
"@docusaurus/faster@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.10.1.tgz#a63d89ae980c98e1eeab3ff15ee083f7c20ed353"
integrity sha512-XTZhE5C1gZ/DaYYMlSk02dwP5vhpQON5QHVz1s3892mSESAywgWanURpXEDAvt4GvGuq7s+XP8rTWHZvfaJmdQ==
dependencies:
"@docusaurus/types" "3.10.0"
"@docusaurus/types" "3.10.1"
"@rspack/core" "^1.7.10"
"@swc/core" "^1.7.39"
"@swc/html" "^1.13.5"
@@ -1694,22 +1694,22 @@
tslib "^2.6.0"
webpack "^5.95.0"
"@docusaurus/logger@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.10.0.tgz#2bacbd004dd78e3da926dbe8f6fa9a930856575d"
integrity sha512-9jrZzFuBH1LDRlZ7cznAhCLmAZ3HSDqgwdrSSZdGHq9SPUOQgXXu8mnxe2ZRB9NS1PCpMTIOVUqDtZPIhMafZg==
"@docusaurus/logger@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.10.1.tgz#34c964e32e18f120e30f80171a38cfefe72cfb4b"
integrity sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw==
dependencies:
chalk "^4.1.2"
tslib "^2.6.0"
"@docusaurus/mdx-loader@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.10.0.tgz#1d4b050d751389ecf38dee48bcb61e53df8ffb82"
integrity sha512-mQQV97080AH4PYNs087l202NMDqRopZA4mg5W76ZZyTFrmWhJ3mHg+8A+drJVENxw5/Q+wHMHLgsx+9z1nEs0A==
"@docusaurus/mdx-loader@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.10.1.tgz#050ae9bc614158a4ec07a628aa75fa9ae90d7e82"
integrity sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ==
dependencies:
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@mdx-js/mdx" "^3.0.0"
"@slorber/remark-comment" "^1.0.0"
escape-html "^1.0.3"
@@ -1732,12 +1732,12 @@
vfile "^6.0.1"
webpack "^5.88.1"
"@docusaurus/module-type-aliases@3.10.0", "@docusaurus/module-type-aliases@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.0.tgz#749928f104d563f11f046bf0c9ab6489a470c7c8"
integrity sha512-/1O0Zg8w3DFrYX/I6Fbss7OJrtZw1QoyjDhegiFNHVi9A9Y0gQ3jUAytVxF6ywpAWpLyLxch8nN8H/V3XfzdJQ==
"@docusaurus/module-type-aliases@3.10.1", "@docusaurus/module-type-aliases@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.1.tgz#22d39177c296786eb6e0d940699cd590cc93ca77"
integrity sha512-YoOZKUdGlp8xSYhuAkGdSo5Ydkbq4V4eK3sD8v0a2hloxCWdQbNBhkc+Ko9QyjpESc0BYcIGM5iHVAy5hdFV6w==
dependencies:
"@docusaurus/types" "3.10.0"
"@docusaurus/types" "3.10.1"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
@@ -1745,34 +1745,34 @@
react-helmet-async "npm:@slorber/react-helmet-async@1.3.0"
react-loadable "npm:@docusaurus/react-loadable@6.0.0"
"@docusaurus/plugin-client-redirects@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.0.tgz#4dd4619817fd69462d1e6d986580343aeb911111"
integrity sha512-P+VLoLoZTc74so8+IbsaPZ33/mkf2BWL1CYXQpPRkl0v1QVCN2CgfsZY/8QtbYjQnx2upXUnv45abDhNcSggNw==
"@docusaurus/plugin-client-redirects@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.1.tgz#e22ed20e5837b7c3a28258e3d1816c4239c82b36"
integrity sha512-LHgd+YDvkhfOHMAE6XtUng3DQNzVM765RqVRrMJgHtzAvfopQhY6ieprqjxDVBdv21cLma6I0jHr+YCZH8fL9A==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
eta "^2.2.0"
fs-extra "^11.1.1"
lodash "^4.17.21"
tslib "^2.6.0"
"@docusaurus/plugin-content-blog@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.0.tgz#10095291b637440847854ecb2c8afcd8746debd7"
integrity sha512-RuTz68DhB7CL96QO5UsFbciD7GPYq6QV+YMfF9V0+N4ZgLhJIBgpVAr8GobrKF6NRe5cyWWETU5z5T834piG9g==
"@docusaurus/plugin-content-blog@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.1.tgz#0bd8de700ccbd8e95d920df2613304ef59abe72b"
integrity sha512-mmkgE6Q2+K74tnkou7tXlpDLvoCU/qkSa2GSQ3XUiHWvcebCoDQzS670RR3tO8PmaWlIyWWISYWzZLuMfxunRA==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
cheerio "1.0.0-rc.12"
combine-promises "^1.1.0"
feed "^4.2.2"
@@ -1785,20 +1785,20 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@docusaurus/plugin-content-docs@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.0.tgz#9c4ea1d5a405340f28c281d2e4586c695a7c65a5"
integrity sha512-9BjHhf15ct8Z7TThTC0xRndKDVvMKmVsAGAN7W9FpNRzfMdScOGcXtLmcCWtJGvAezjOJIm6CxOYCy3Io5+RnQ==
"@docusaurus/plugin-content-docs@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz#261e0e982e4a937c05b462e3c5729374f433b752"
integrity sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@types/react-router-config" "^5.0.7"
combine-promises "^1.1.0"
fs-extra "^11.1.1"
@@ -1809,142 +1809,142 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@docusaurus/plugin-content-pages@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.0.tgz#7670cbb3c849f434949f542bfdfded1580a13165"
integrity sha512-5amX8kEJI+nIGtuLVjYk59Y5utEJ3CHETFOPEE4cooIRLA4xM4iBsA6zFgu4ljcopeYwvBzFEWf5g2I6Yb9SkA==
"@docusaurus/plugin-content-pages@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.1.tgz#8c6ffc2079ed0262548ecc4df1dea6add6aa9673"
integrity sha512-huJpaRPMl42nsFwuCXvV8bVDj2MazuwRJIUylI/RSlmZeJssVoZXeCjVf1y+1Drtpa9SKcdGn8yoJ76IRJijtw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
fs-extra "^11.1.1"
tslib "^2.6.0"
webpack "^5.88.1"
"@docusaurus/plugin-css-cascade-layers@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.0.tgz#71e318d842be95f92be6c3dca00ceea4971d0edb"
integrity sha512-6q1vtt5FJcg5osgkHeM1euErECNqEZ5Z1j69yiNx2luEBIso+nxCkS9nqj8w+MK5X7rvKEToGhFfOFWncs51pQ==
"@docusaurus/plugin-css-cascade-layers@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.1.tgz#440578d95cbe1a6120936fa83df868d2626cd1d8"
integrity sha512-r//fn+MNHkE1wCof8T29VAQezt1enGCpsFxoziBbvLgBM4JfXN2P3rxrBaavHmvLvm7lYkpJeitcDthwnmWCTw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
tslib "^2.6.0"
"@docusaurus/plugin-debug@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.10.0.tgz#e77f924604e1e09d5d90fe0bdf23a3be8ea3307e"
integrity sha512-XcljKN+G+nmmK69uQA1d9BlYU3ZftG3T3zpK8/7Hf/wrOlV7TA4Ampdrdwkg0jElKdKAoSnPhCO0/U3bQGsVQQ==
"@docusaurus/plugin-debug@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.10.1.tgz#b8b7b24d9a7d185fd8a56a030f90145d3bfd8239"
integrity sha512-9KqOpKNfAyqGZykRb9LhIT/vyRF6sm/ykhjj/39JvaJahDS+jZJE0Z1Wfz9q3DUNDTMNN0Q7u/kk4rKKU+IJuA==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
fs-extra "^11.1.1"
react-json-view-lite "^2.3.0"
tslib "^2.6.0"
"@docusaurus/plugin-google-analytics@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.0.tgz#22c7e976fe4d970c7cd1c73c9723d9a5786c6e37"
integrity sha512-hTEoodatpBZnUat5nFExbuTGA1lhWGy7vZGuTew5Q3QDtGKFpSJLYmZJhdTjvCFwv1+qQ67hgAVlKdJOB8TXow==
"@docusaurus/plugin-google-analytics@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.1.tgz#ac15afc77386e0352edb8a1698d993aa5de36ffc"
integrity sha512-8o0P1KtmgdYQHH+oInitPpRWI0Of5XednAX4+DMhQNSmGSRNrsEEHg1ebv35m9AgRClfAytCJ5jA9KvcASTyuA==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
tslib "^2.6.0"
"@docusaurus/plugin-google-gtag@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.0.tgz#c38a2ba638257851cc845b934506b80c08d47f96"
integrity sha512-iB/Zzjv/eelJRbdULZqzWCbgMgJ7ht4ONVjXtN3+BI/muil6S87gQ1OJyPwlXD+ELdKkitC7bWv5eJdYOZLhrQ==
"@docusaurus/plugin-google-gtag@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.1.tgz#0482b83b9bc411aa99a432be2b39d2e53a00e2e0"
integrity sha512-pu3xIUo5o/zCMLfUY9BO5KOwSH0zIsAGyFRPvXHayFSA5XIhCU/SFuB0g0ZNjFn9niZLCaNvoeAuOGFJZq0fdw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@types/gtag.js" "^0.0.20"
tslib "^2.6.0"
"@docusaurus/plugin-google-tag-manager@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.0.tgz#5469c923cc1ad4608399d0b17e5fcacd8e030d56"
integrity sha512-FEjZxqKgLHa+Wez/EgKxRwvArNCWIScfyEQD95rot7jkxp6nonjI5XIbGfO/iYhM5Qinwe8aIEQHP2KZtpqVuA==
"@docusaurus/plugin-google-tag-manager@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.1.tgz#eaf5765d6f82b4fb661d92a793d1883f9d1ec106"
integrity sha512-f6fyGHiCm7kJHBtAisGQS5oNBnpnMTYQZxDXeVrnw/3zWU+LMA22pr6UHGYkBKDbN+qPC5QHG3NuOfzQLq3+Lw==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
tslib "^2.6.0"
"@docusaurus/plugin-sitemap@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.0.tgz#35d59d46803f279f22aa64fc1bd18c048f12662b"
integrity sha512-DVTSLjB97hIjmayGnGcBfognCeI7ZuUKgEnU7Oz81JYqXtVg94mVTthDjq3QHTylYNeCUbkaW8VF0FDLcc8pPw==
"@docusaurus/plugin-sitemap@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.1.tgz#66a6974bb2fd1b9d8f5cb0f3c5ecd2201c118565"
integrity sha512-C26MbmmqgdjkDq1htaZ3aD7LzEDKFWXfpyQpt0EOUThuq5nV77zDaedV20yHcVo9p+3ey9aZ4pbHA0D3QcZTzg==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
fs-extra "^11.1.1"
sitemap "^7.1.1"
tslib "^2.6.0"
"@docusaurus/plugin-svgr@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.0.tgz#8ada2e6dd8318d20206a9b044fc091a5794ba3f0"
integrity sha512-lNljBESaETZqVBMPqkrGchr+UPT1eZzEPLmJhz8I76BxbjqgsUnRvrq6lQJ9sYjgmgX52KB7kkgczqd2yzoswQ==
"@docusaurus/plugin-svgr@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.1.tgz#c217c24d6d23fd2bc6f54d44c040635b49d6b36e"
integrity sha512-6SFxsmjWFkVLDmBUvFK6i72QjUwqyQFe4Ovz+SUJophJjOyVG3ZZG5IQpBC/kX/Gfv1yWeU9nWauH6F6Q7QX/Q==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@svgr/core" "8.1.0"
"@svgr/webpack" "^8.1.0"
tslib "^2.6.0"
webpack "^5.88.1"
"@docusaurus/preset-classic@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.10.0.tgz#74b6facdaf568bcd41ec90cae9aebb7ca0ac8619"
integrity sha512-kw/Ye02Hc6xP1OdTswy8yxQEHg0fdPpyWAQRxr5b2x3h7LlG2Zgbb5BDFROnXDDMpUxB7YejlocJIE5HIEfpNA==
"@docusaurus/preset-classic@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.10.1.tgz#faf330d96aedc9083a59bec09d966ae4dfc8b2fb"
integrity sha512-YO/FL8v1zmbxoTso6mjMz/RDjhaTJxb1UpFFTDdY5847LLDCeyYiYlrhyTbgN1RIN3xnkLKZ9Lj1x8hUzI4JOg==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/plugin-content-blog" "3.10.0"
"@docusaurus/plugin-content-docs" "3.10.0"
"@docusaurus/plugin-content-pages" "3.10.0"
"@docusaurus/plugin-css-cascade-layers" "3.10.0"
"@docusaurus/plugin-debug" "3.10.0"
"@docusaurus/plugin-google-analytics" "3.10.0"
"@docusaurus/plugin-google-gtag" "3.10.0"
"@docusaurus/plugin-google-tag-manager" "3.10.0"
"@docusaurus/plugin-sitemap" "3.10.0"
"@docusaurus/plugin-svgr" "3.10.0"
"@docusaurus/theme-classic" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-search-algolia" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/plugin-content-blog" "3.10.1"
"@docusaurus/plugin-content-docs" "3.10.1"
"@docusaurus/plugin-content-pages" "3.10.1"
"@docusaurus/plugin-css-cascade-layers" "3.10.1"
"@docusaurus/plugin-debug" "3.10.1"
"@docusaurus/plugin-google-analytics" "3.10.1"
"@docusaurus/plugin-google-gtag" "3.10.1"
"@docusaurus/plugin-google-tag-manager" "3.10.1"
"@docusaurus/plugin-sitemap" "3.10.1"
"@docusaurus/plugin-svgr" "3.10.1"
"@docusaurus/theme-classic" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-search-algolia" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/theme-classic@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.10.0.tgz#d937915c691189f27ced649c822994d839ea565b"
integrity sha512-9msCAsRdN+UG+RwPwCFb0uKy4tGoPh5YfBozXeGUtIeAgsMdn6f3G/oY861luZ3t8S2ET8S9Y/1GnpJAGWytww==
"@docusaurus/theme-classic@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.10.1.tgz#deed8cf73cc0f56113e53775cbb3b168c3c61566"
integrity sha512-VU1RK0qb2pab0si4r7HFK37cYco8VzqLj3u1PspVipSr/z/GPVKHO4/HXbnePqHoWDk8urjyGSeatH0NIMBM1A==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/plugin-content-blog" "3.10.0"
"@docusaurus/plugin-content-docs" "3.10.0"
"@docusaurus/plugin-content-pages" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-translations" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/plugin-content-blog" "3.10.1"
"@docusaurus/plugin-content-docs" "3.10.1"
"@docusaurus/plugin-content-pages" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-translations" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@mdx-js/react" "^3.0.0"
clsx "^2.0.0"
copy-text-to-clipboard "^3.2.0"
@@ -1959,15 +1959,15 @@
tslib "^2.6.0"
utility-types "^3.10.0"
"@docusaurus/theme-common@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.10.0.tgz#70b419ccfdf62f092299354a72d1692e81be597d"
integrity sha512-Dkp1YXKn16ByCJAdIjbDIOpVb4Z66MsVD694/ilX1vAAHaVEMrVsf/NPd9VgreyFx08rJ9GqV1MtzsbTcU73Kg==
"@docusaurus/theme-common@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.10.1.tgz#cbfec82b1b107be5c229811ed9caae14a501361c"
integrity sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==
dependencies:
"@docusaurus/mdx-loader" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/mdx-loader" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
@@ -1977,48 +1977,48 @@
tslib "^2.6.0"
utility-types "^3.10.0"
"@docusaurus/theme-live-codeblock@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.10.0.tgz#05a38c6bfac479fd698f18f27ca06ebb126633d9"
integrity sha512-1Ycxu0dBAhEXzXPQ1dQW01aY1MNi7TCTUOBtIF0GcNrQBFj74XxhDqv/T6GxYBsaN+6QnIDs1T+D43iV2/r2hQ==
"@docusaurus/theme-live-codeblock@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.10.1.tgz#29e6ddee467d816205ad611fd7bf10f00db5bdef"
integrity sha512-MKG/0zreelS6YlupQAoKmS5nCw9RRKwDHihJg2FinsU1+rqbrOYNYVq//eQy+m649k9b8XCazEw9VUMTFhpCTg==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-translations" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-translations" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
"@philpl/buble" "^0.19.7"
clsx "^2.0.0"
fs-extra "^11.1.1"
react-live "^4.1.6"
tslib "^2.6.0"
"@docusaurus/theme-mermaid@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.10.0.tgz#6581ccf16d27e4c02fe8c7cf15488862f27be9c8"
integrity sha512-Y2xrlwhIJ80oOZIO3PXL6A7J869splfcMI87E3NKpYsy3zJxOyV+BP1QMtGi59ajKgU868HPuyyn6J+6BZGOBg==
"@docusaurus/theme-mermaid@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.10.1.tgz#dada9c50c780524d246906234ace8a35446f26fc"
integrity sha512-2gxpmln8Pc4EN1oWzshQEx2HTs67jk14v7MmgqGs8ZU7Nm8oihg+fTouof2u4vN8DtB3Fln4cDJu4UprSX1S3Q==
dependencies:
"@docusaurus/core" "3.10.0"
"@docusaurus/module-type-aliases" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/module-type-aliases" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
mermaid ">=11.6.0"
tslib "^2.6.0"
"@docusaurus/theme-search-algolia@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.0.tgz#0ff57fe58db6abde8f5ad2877e459cd2fa6e7464"
integrity sha512-f5FPKI08e3JRG63vR/o4qeuUVHUHzFzM0nnF+AkB67soAZgNsKJRf2qmUZvlQkGwlV+QFkKe4D0ANMh1jToU3g==
"@docusaurus/theme-search-algolia@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.1.tgz#6f422058711629ce8d7c2f17e1e54efa075c626e"
integrity sha512-OTaARARVZj2GvkJQjB+1jOIxntRaXea+G+fMsNqrZBAU1O1vJKDW22R7kECOHW27oJCLFN9HKaZeRrfAUyviug==
dependencies:
"@algolia/autocomplete-core" "^1.19.2"
"@docsearch/react" "^3.9.0 || ^4.3.2"
"@docusaurus/core" "3.10.0"
"@docusaurus/logger" "3.10.0"
"@docusaurus/plugin-content-docs" "3.10.0"
"@docusaurus/theme-common" "3.10.0"
"@docusaurus/theme-translations" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-validation" "3.10.0"
"@docusaurus/core" "3.10.1"
"@docusaurus/logger" "3.10.1"
"@docusaurus/plugin-content-docs" "3.10.1"
"@docusaurus/theme-common" "3.10.1"
"@docusaurus/theme-translations" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-validation" "3.10.1"
algoliasearch "^5.37.0"
algoliasearch-helper "^3.26.0"
clsx "^2.0.0"
@@ -2028,23 +2028,23 @@
tslib "^2.6.0"
utility-types "^3.10.0"
"@docusaurus/theme-translations@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.10.0.tgz#8fdc23d29bd7f907db49c36cf65e2123d96be300"
integrity sha512-L9IbFLwTc5+XdgH45iQYufLn0SVZd6BUNelDbKIFlH+E4hhjuj/XHWAFMX/w2K59rfy8wak9McOaei7BSUfRPA==
"@docusaurus/theme-translations@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.10.1.tgz#c3119a015652290eea560ca45ac775963d6eb75b"
integrity sha512-cLMyaKivjBVWKMJuWqyFVVgtqe8DPJNPkog0bn8W1MDVAKcPdxRFycBfC1We1RaNp7Rdk513bmtW78RR6OBxBw==
dependencies:
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/tsconfig@^3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.10.0.tgz#f40a57248828f0503a5f355cf30aa59941c9baaa"
integrity sha512-TXdC3WXuPrdQAexLvjUJfnYf3YKEgEqAs5nK0Q88pRBCW7t7oN4ILvWYb3A5Z1wlSXyXGWW/mCUmLEhdWsjnDQ==
"@docusaurus/tsconfig@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.10.1.tgz#1db31b4a4a5c914bdffa80070a35b6365d34f2e8"
integrity sha512-rYvB7yqkdqWIpAbDzQljGfM4cDBkLTbhmagZBEcsyj6oPUsz47lmW2pYdN1j+7sGFgltbAmQH62xfbrij4Eh6Q==
"@docusaurus/types@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.10.0.tgz#a69232bba74b738fcf4671fd5f0f079366dd3d13"
integrity sha512-F0dOt3FOoO20rRaFK7whGFQZ3ggyrWEdQc/c8/UiRuzhtg4y1w9FspXH5zpCT07uMnJKBPGh+qNazbNlCQqvSw==
"@docusaurus/types@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.10.1.tgz#d42837938ae43ca2be0ca47e63e00476b5eb94be"
integrity sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw==
dependencies:
"@mdx-js/mdx" "^3.0.0"
"@types/history" "^4.7.11"
@@ -2057,36 +2057,36 @@
webpack "^5.95.0"
webpack-merge "^5.9.0"
"@docusaurus/utils-common@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.10.0.tgz#2a6dc76b312664fca7234d33607c085318ff1ae3"
integrity sha512-JyL7sb9QVDgYvudIS81Dv0lsWm7le0vGZSDwsztxWam1SPBqrnkvBy9UYL/amh6pbybkyYTd3CMTkO24oMlCSw==
"@docusaurus/utils-common@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.10.1.tgz#6350b4898691e765de750f90eade0e0fa7902d99"
integrity sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA==
dependencies:
"@docusaurus/types" "3.10.0"
"@docusaurus/types" "3.10.1"
tslib "^2.6.0"
"@docusaurus/utils-validation@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.10.0.tgz#a2418d7f31980d991fd3a1f39c8aad8820b36812"
integrity sha512-c+6n2+ZPOJtWWc8Bb/EYdpSDfjYEScdCu9fB/SNjOmSCf1IdVnGf2T53o0tsz0gDRtCL90tifTL0JE/oMuP1Mw==
"@docusaurus/utils-validation@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz#ddbcce997a5506424cdd16abf6845cc51692acae"
integrity sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==
dependencies:
"@docusaurus/logger" "3.10.0"
"@docusaurus/utils" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/utils" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
fs-extra "^11.2.0"
joi "^17.9.2"
js-yaml "^4.1.0"
lodash "^4.17.21"
tslib "^2.6.0"
"@docusaurus/utils@3.10.0":
version "3.10.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.10.0.tgz#ea7d7b0d325b60f728decc00bb3908d00ef86faf"
integrity sha512-T3B0WTigsIthe0D4LQa2k+7bJY+c3WS+Wq2JhcznOSpn1lSN64yNtHQXboCj3QnUs1EuAZszQG1SHKu5w5ZrlA==
"@docusaurus/utils@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.10.1.tgz#535968caa2c9bff69f997a081b98b95b3c5d3785"
integrity sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==
dependencies:
"@docusaurus/logger" "3.10.0"
"@docusaurus/types" "3.10.0"
"@docusaurus/utils-common" "3.10.0"
"@docusaurus/logger" "3.10.1"
"@docusaurus/types" "3.10.1"
"@docusaurus/utils-common" "3.10.1"
escape-string-regexp "^4.0.0"
execa "^5.1.1"
file-loader "^6.2.0"
@@ -4239,86 +4239,86 @@
dependencies:
apg-lite "^1.0.4"
"@swc/core-darwin-arm64@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.32.tgz#3592714588fdbb8b7a869f81ff96c7236fcf1c09"
integrity sha512-/YWMvJDPu+AAwuUsM2G+DNQ/7zhodURGzdQyewEqcvgklAdDHs3LwQmLLnyn6SJl8DT8UOxkbzK+D1PmPeelRg==
"@swc/core-darwin-arm64@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz#d84134fb80417d41128739f0b9014542e3ed9dd3"
integrity sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==
"@swc/core-darwin-x64@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.32.tgz#965044b632933146e319862ea7e4b717eb9f83dd"
integrity sha512-KOTXJXdAhWL+hZ77MYP3z+4pcMFaQhQ74yqyN1uz093q0YnbxpqMtYpPISbYvMHzVRNNx5kN+9RZAXEaadhWVA==
"@swc/core-darwin-x64@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz#0badb9834071f1c6005986571d4a96359c1d7cd0"
integrity sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==
"@swc/core-linux-arm-gnueabihf@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.32.tgz#70e70ad6ad961055f4a9be9e4947e455c18239e6"
integrity sha512-oOoxLweljlc0A4X8ybsgxV7cVaYTwBOg2iMDJcFR3Sr48C+lsv9VzSmqdK/IVIXF4W4GjLc3VqTAdSMXlfVLuQ==
"@swc/core-linux-arm-gnueabihf@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz#b7577a825b59d98b6a9a5c991d842046efe1c34a"
integrity sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==
"@swc/core-linux-arm64-gnu@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.32.tgz#7b82e2cc5995e8f919e29f6ce702285f5f1c3ad1"
integrity sha512-oDzEkdl6D6BAWdMtU5KGO7y3HR5fJcvByNLyEk9+ugj8nP5Ovb7P4kBcStBXc4MPExFGQryehiINMlmY8HlclA==
"@swc/core-linux-arm64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz#304c48321494a18c67b2913c273b08674ee70d8c"
integrity sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==
"@swc/core-linux-arm64-musl@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.32.tgz#16c581b9f859b0175a8bab5cbf694bef7dbf95b8"
integrity sha512-omcqjoZP/b8D8PuczVoRwJieC6ibj7qIxTftNYokz4/aSmKFHvsd7nIFfPk5ZvtzncbH4AY7+Dkr/Lp2gWxYeA==
"@swc/core-linux-arm64-musl@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz#d116cbc04ccb4f4ee810da6bca79d4423605dbcd"
integrity sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==
"@swc/core-linux-ppc64-gnu@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.32.tgz#420f7744dae327c8e4917c87ced5c1b3e0a38f96"
integrity sha512-KGkTMyz/Tbn3PBNu0AVZ4GTDFKnICrYcTiNPZq8DrvK42pnFsf3GNDrIG9E5AtQlTmC0YigkWKmu0eMcfTrmgA==
"@swc/core-linux-ppc64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz#f5354dba36db9414305bab344c817d57b8b457c2"
integrity sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==
"@swc/core-linux-s390x-gnu@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.32.tgz#9b563a3a73c544f29454e53894bfe533b9a27ffe"
integrity sha512-G3Aa4tVS/3OGZBkoNIwUF9F6RAy+Osb4GOlo62SinLmDiErz/ykmM7KH0wkz6l9kM8jJq1HyAM6atJTUEbBk7g==
"@swc/core-linux-s390x-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz#016df9f4c9d7fd65b85ca9c558c5aec341f06da0"
integrity sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==
"@swc/core-linux-x64-gnu@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.32.tgz#615c7bcc1890379dffcc74b6780e2277e65f4b61"
integrity sha512-ERsjfGcj6CBmj3vJnGDO8m8rTvw6RqMcWo1dogOtNx3/+/0+NNpJiXDobJrr1GwInI/BHAEkvSFIH6d2LqPcUQ==
"@swc/core-linux-x64-gnu@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz#49f36558ede072e71999aa37f123367daed2a662"
integrity sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==
"@swc/core-linux-x64-musl@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.32.tgz#038604d25bdebb1d1ad780d827a44654fa4b5bdd"
integrity sha512-N4Ggahe/8SUbTX50P6EdhbW9YWcgbZVb52R4cq6MK+zsoMjRq7rGvV5ztA05QnbaCYqMYx8rTY7KAIA3Crdo4Q==
"@swc/core-linux-x64-musl@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz#b096665f5cfeee2612325f301da5c1590b10d8f3"
integrity sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==
"@swc/core-win32-arm64-msvc@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.32.tgz#c82006e6ef92a998e96d2160b1657f5334af4d54"
integrity sha512-01yN0o9jvo8xBTP12aPK2wW8b41jmOlGbDDlAnoynotc4pO6xA0zby9f1z6j++qXDpGBttLySq1omgVrlQKYcw==
"@swc/core-win32-arm64-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz#f3101263a0dbaa173ec47638c9719d0b89838bd2"
integrity sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==
"@swc/core-win32-ia32-msvc@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.32.tgz#e2ae1c95bd6599322bc6e9a82685b7537a193f7b"
integrity sha512-fLagI9XZYNpTcmlqAcp3KBtmj7E19WCmYD80Jxj1Kn5tGNa7yxNLd3NNdWxuZGUPl5iC0/KqZru7g08gF6Fsrw==
"@swc/core-win32-ia32-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz#eb981ef5613d42c9220559bdb0c8bc58cf6c3eb9"
integrity sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==
"@swc/core-win32-x64-msvc@1.15.32":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.32.tgz#2535c791821054072a511dee0d13e5de9c5cd29b"
integrity sha512-gbc2bQ/T2CiR+w0OvcVKwLOFAcPZBvmWmolbwpg1E8UrpeC03DGtyMUApOHNXNYWA3SHFrYXCQtosrcMza1YFg==
"@swc/core-win32-x64-msvc@1.15.33":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz#a2fed9956933027ceb368857bac4bb4ee203d47c"
integrity sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==
"@swc/core@^1.15.32", "@swc/core@^1.7.39":
version "1.15.32"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.32.tgz#2333d66f4b8e7c4fded087ead13c135ff84ab9d6"
integrity sha512-/eWL0n43D64QWEUHLtTE+jDqjkJhyidjkDhv6f0uJohOUAhywxQ9wXYp845DNNds0JpCdI4Uo0a9bl+vbXf+ew==
"@swc/core@^1.15.33", "@swc/core@^1.7.39":
version "1.15.33"
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.33.tgz#2a6571c8aca961925f14beae52b3f43c18370fc6"
integrity sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==
dependencies:
"@swc/counter" "^0.1.3"
"@swc/types" "^0.1.26"
optionalDependencies:
"@swc/core-darwin-arm64" "1.15.32"
"@swc/core-darwin-x64" "1.15.32"
"@swc/core-linux-arm-gnueabihf" "1.15.32"
"@swc/core-linux-arm64-gnu" "1.15.32"
"@swc/core-linux-arm64-musl" "1.15.32"
"@swc/core-linux-ppc64-gnu" "1.15.32"
"@swc/core-linux-s390x-gnu" "1.15.32"
"@swc/core-linux-x64-gnu" "1.15.32"
"@swc/core-linux-x64-musl" "1.15.32"
"@swc/core-win32-arm64-msvc" "1.15.32"
"@swc/core-win32-ia32-msvc" "1.15.32"
"@swc/core-win32-x64-msvc" "1.15.32"
"@swc/core-darwin-arm64" "1.15.33"
"@swc/core-darwin-x64" "1.15.33"
"@swc/core-linux-arm-gnueabihf" "1.15.33"
"@swc/core-linux-arm64-gnu" "1.15.33"
"@swc/core-linux-arm64-musl" "1.15.33"
"@swc/core-linux-ppc64-gnu" "1.15.33"
"@swc/core-linux-s390x-gnu" "1.15.33"
"@swc/core-linux-x64-gnu" "1.15.33"
"@swc/core-linux-x64-musl" "1.15.33"
"@swc/core-win32-arm64-msvc" "1.15.33"
"@swc/core-win32-ia32-msvc" "1.15.33"
"@swc/core-win32-x64-msvc" "1.15.33"
"@swc/counter@^0.1.3":
version "0.1.3"
@@ -5794,10 +5794,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.24, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.24"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz#6dc320c7bf53859ec2bf55d54db6d2e5c078df16"
integrity sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==
baseline-browser-mapping@^2.10.27, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19:
version "2.10.27"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz#fee941c2a0b42cdf83c6427e4c830b1d0bdab2c3"
integrity sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==
batch@0.6.1:
version "0.6.1"
@@ -6062,12 +6062,7 @@ chalk@^4.0.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^5.0.1, chalk@^5.2.0:
version "5.6.0"
resolved "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz"
integrity sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==
chalk@^5.6.2:
chalk@^5.0.1, chalk@^5.2.0, chalk@^5.6.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea"
integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
@@ -7305,10 +7300,10 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
docusaurus-plugin-openapi-docs@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.0.1.tgz#2fe62b58fc1af11e3d947edc2f0d60e04f1aa149"
integrity sha512-OVfoDovRdiS78DQYWmr2BjuOF2A6kVmJ43mgkQaAEZxASyHbUft4zUIhvfa7gqema6KNL9pVKejDievZdZ3wGQ==
docusaurus-plugin-openapi-docs@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-5.0.2.tgz#f00028621deb9179065fe7d6a541256692ef941b"
integrity sha512-WCC2m6PpylXZfNga+ScelTG0a7jUGtbB9+AmbR9lUj93FPryTs8VHTMJ3fKtO0senJTWgOU3MDvZw0v+mE3ztA==
dependencies:
"@apidevtools/json-schema-ref-parser" "^15.3.3"
"@redocly/openapi-core" "^2.25.2"
@@ -7326,10 +7321,10 @@ docusaurus-plugin-openapi-docs@^5.0.1:
swagger2openapi "^7.0.8"
xml-formatter "^3.6.6"
docusaurus-theme-openapi-docs@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.0.1.tgz#a2c2c91346b6238f6d7862752cdb02611fb5396f"
integrity sha512-bVeb7hOqog9LKVrJzYXdNJ7/0N22lk0VE22QK+naAn5GuAvYo41JmpXW9hqLIPkEp2UbexTHoPW9SYVdUsyvvw==
docusaurus-theme-openapi-docs@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-5.0.2.tgz#2ab6f6b04fc2e494e24971d31432a9187c84a2fe"
integrity sha512-BD6WhbunR6kXqtoUUDlhxO4HlCNM2nYENGr/TbiTEknkgXYKQz+FEIhY4Hyz5GSLpuhPih0CDuNl7Xkfpcz0Yw==
dependencies:
"@hookform/error-message" "^2.0.1"
"@reduxjs/toolkit" "^2.8.2"
@@ -8474,10 +8469,10 @@ globals@^15.14.0:
resolved "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz"
integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
globals@^17.5.0:
version "17.5.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.5.0.tgz#a82c641d898f8dfbe0e81f66fdff7d0de43f88c6"
integrity sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==
globals@^17.6.0:
version "17.6.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.6.0.tgz#0f0be018d5cca8690e6375ead1f65c4bb96191fc"
integrity sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==
globalthis@^1.0.4:
version "1.0.4"
@@ -13328,7 +13323,7 @@ renderkid@^3.0.0:
repeat-string@^1.5.2:
version "1.6.1"
resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
require-directory@^2.1.1:
@@ -15368,7 +15363,7 @@ webpack@^5.106.2, webpack@^5.88.1, webpack@^5.95.0:
watchpack "^2.5.1"
webpack-sources "^3.3.4"
webpackbar@^6.0.1, webpackbar@^7.0.0:
webpackbar@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-7.0.0.tgz#7228d32881af2392381b6514499ddea73cdf218a"
integrity sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.15.4 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.15.5 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.15.4](https://img.shields.io/badge/Version-0.15.4-informational?style=flat-square)
![Version: 0.15.5](https://img.shields.io/badge/Version-0.15.5-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application

View File

@@ -844,6 +844,8 @@ postgresql:
database: superset
image:
registry: docker.io
repository: bitnamilegacy/postgresql
tag: "14.17.0-debian-12-r3"
## PostgreSQL Primary parameters
@@ -918,6 +920,11 @@ redis:
accessModes:
- ReadWriteOnce
image:
registry: docker.io
repository: bitnamilegacy/redis
tag: 7.0.10-debian-11-r4
nodeSelector: {}
tolerations: []

View File

@@ -95,7 +95,7 @@ dependencies = [
"redis>=5.0.0, <6.0",
"rison>=2.0.0, <3.0",
"selenium>=4.14.0, <5.0",
"shillelagh[gsheetsapi]>=1.4.3, <2.0",
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
"slack_sdk>=3.19.0, <4",
@@ -114,7 +114,7 @@ dependencies = [
[project.optional-dependencies]
athena = ["pyathena[pandas]>=2, <3"]
athena = ["pyathena[pandas]>=2, <4"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
@@ -135,7 +135,7 @@ databricks = [
"databricks-sqlalchemy==1.0.5",
]
db2 = ["ibm-db-sa>0.3.8, <=0.4.0"]
denodo = ["denodo-sqlalchemy~=1.0.6"]
denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.4, <2"]
druid = ["pydruid>=0.6.5,<0.7"]
@@ -149,7 +149,7 @@ fastmcp = ["fastmcp>=3.2.4,<4.0"]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.3, <2"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.4, <2"]
hana = ["hdbcli==2.4.162", "sqlalchemy_hana==0.4.0"]
hive = [
"pyhive[hive]>=0.6.5;python_version<'3.11'",
@@ -158,7 +158,7 @@ hive = [
"thrift>=0.14.1, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.17"]
impala = ["impyla>0.16.2, <0.23"]
kusto = ["sqlalchemy-kusto>=3.0.0, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"]
mssql = ["pymssql>=2.2.8, <3"]
@@ -171,7 +171,7 @@ ocient = [
"shapely",
"geojson",
]
oracle = ["cx-Oracle>8.0.0, <8.1"]
oracle = ["cx-Oracle>8.0.0, <8.4"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <6.0.0"]
playwright = ["playwright>=1.37.0, <2"]
@@ -181,7 +181,7 @@ trino = ["trino>=0.328.0"]
prophet = ["prophet>=1.1.6, <2"]
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.3, <2"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.2.4, <2"]
sqlite = ["syntaqlite>=0.1.0"]
@@ -197,7 +197,7 @@ tdengine = [
]
teradata = ["teradatasql>=16.20.0.23"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
vertica = ["sqlalchemy-vertica-python>= 0.5.9, < 0.7"]
netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.0.0"]
doris = ["pydoris>=1.0.0, <2.0.0"]
@@ -288,6 +288,7 @@ module = [
"superset.tags.filters",
"superset.commands.security.update",
"superset.commands.security.create",
"superset.semantic_layers.api",
]
warn_unused_ignores = false

View File

@@ -381,7 +381,7 @@ selenium==4.32.0
# via apache-superset (pyproject.toml)
setuptools==80.9.0
# via -r requirements/base.in
shillelagh==1.4.3
shillelagh==1.4.4
# via apache-superset (pyproject.toml)
simplejson==3.20.1
# via apache-superset (pyproject.toml)

View File

@@ -707,7 +707,7 @@ protobuf==4.25.8
# proto-plus
psutil==6.1.0
# via apache-superset
psycopg2-binary==2.9.9
psycopg2-binary==2.9.12
# via apache-superset
py-key-value-aio==0.4.4
# via fastmcp
@@ -931,7 +931,7 @@ setuptools==80.9.0
# pydata-google-auth
# zope-event
# zope-interface
shillelagh==1.4.3
shillelagh==1.4.4
# via
# -c requirements/base-constraint.txt
# apache-superset

View File

@@ -18,7 +18,7 @@
[project]
name = "apache-superset-core"
version = "0.1.0rc2"
version = "0.1.0rc3"
description = "Core Python package for building Apache Superset backend extensions and integrations"
readme = "README.md"
authors = [
@@ -43,6 +43,8 @@ classifiers = [
]
dependencies = [
"flask-appbuilder>=5.0.2,<6",
"isodate>=0.7.0",
"pyarrow>=16.0.0",
"pydantic>=2.8.0",
"sqlalchemy>=1.4.0,<2.0",
"sqlalchemy-utils>=0.38.0, <0.43", # expanding lowerbound to work with pydoris

View File

@@ -0,0 +1,73 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Any
from pydantic import BaseModel
def build_configuration_schema(
config_class: type[BaseModel],
configuration: BaseModel | None = None,
) -> dict[str, Any]:
"""
Build a JSON schema from a Pydantic configuration class.
Handles generic boilerplate that any semantic layer with dynamic fields needs:
- Reorders properties to match model field order (Pydantic sorts alphabetically)
- When ``configuration`` is None, sets ``enum: []`` on all ``x-dynamic`` properties
so the frontend renders them as empty dropdowns
Semantic layer implementations call this instead of
``model_json_schema()`` directly,
then only need to add their own dynamic population logic.
"""
schema = config_class.model_json_schema()
# Pydantic sorts properties alphabetically; restore model field order
field_order = [
field.alias or name for name, field in config_class.model_fields.items()
]
schema["properties"] = {
key: schema["properties"][key]
for key in field_order
if key in schema["properties"]
}
if configuration is None:
for prop_schema in schema["properties"].values():
if prop_schema.get("x-dynamic"):
prop_schema["enum"] = []
return schema
def check_dependencies(
prop_schema: dict[str, Any],
configuration: BaseModel,
) -> bool:
"""
Check whether a dynamic property's dependencies are satisfied.
Reads the ``x-dependsOn`` list from the property schema and returns ``True``
when every referenced attribute on ``configuration`` is truthy.
"""
dependencies = prop_schema.get("x-dependsOn", [])
return all(getattr(configuration, dep, None) for dep in dependencies)

View File

@@ -0,0 +1,169 @@
# 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.
"""
Semantic layer DAO interfaces for superset-core.
Provides abstract DAO classes for semantic layers and views that define the
interface contract. Host implementations replace these with concrete classes
backed by SQLAlchemy during initialization.
Usage:
from superset_core.semantic_layers.daos import (
AbstractSemanticLayerDAO,
AbstractSemanticViewDAO,
)
"""
from __future__ import annotations
from abc import abstractmethod
from typing import Any, ClassVar
from superset_core.common.daos import BaseDAO
from superset_core.semantic_layers.models import SemanticLayerModel, SemanticViewModel
class AbstractSemanticLayerDAO(BaseDAO[SemanticLayerModel]):
"""
Abstract DAO interface for SemanticLayer.
Host implementations will replace this class during initialization
with a concrete DAO providing actual database access.
"""
model_cls: ClassVar[type[Any] | None] = None
base_filter = None
id_column_name = "uuid"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def validate_uniqueness(cls, name: str) -> bool:
"""
Validate that a semantic layer name is unique.
:param name: Semantic layer name to validate
:return: True if the name is unique, False otherwise
"""
...
@classmethod
@abstractmethod
def validate_update_uniqueness(cls, layer_uuid: str, name: str) -> bool:
"""
Validate that a semantic layer name is unique for an update operation,
excluding the layer being updated.
:param layer_uuid: UUID of the semantic layer being updated
:param name: New name to validate
:return: True if the name is unique, False otherwise
"""
...
@classmethod
@abstractmethod
def find_by_name(cls, name: str) -> SemanticLayerModel | None:
"""
Find a semantic layer by name.
:param name: Semantic layer name
:return: SemanticLayerModel instance or None
"""
...
@classmethod
@abstractmethod
def get_semantic_views(cls, layer_uuid: str) -> list[SemanticViewModel]:
"""
Get all semantic views associated with a semantic layer.
:param layer_uuid: UUID of the semantic layer
:return: List of SemanticViewModel instances
"""
...
class AbstractSemanticViewDAO(BaseDAO[SemanticViewModel]):
"""
Abstract DAO interface for SemanticView.
Host implementations will replace this class during initialization
with a concrete DAO providing actual database access.
"""
model_cls: ClassVar[type[Any] | None] = None
base_filter = None
id_column_name = "id"
uuid_column_name = "uuid"
@classmethod
@abstractmethod
def validate_uniqueness(
cls,
name: str,
layer_uuid: str,
configuration: dict[str, Any],
) -> bool:
"""
Validate that a semantic view is unique within a semantic layer.
Uniqueness is determined by the combination of name, layer UUID, and
configuration.
:param name: View name
:param layer_uuid: UUID of the parent semantic layer
:param configuration: Configuration dict to compare
:return: True if unique, False otherwise
"""
...
@classmethod
@abstractmethod
def validate_update_uniqueness(
cls,
view_uuid: str,
name: str,
layer_uuid: str,
configuration: dict[str, Any],
) -> bool:
"""
Validate that a semantic view is unique within a semantic layer for an
update operation, excluding the view being updated.
:param view_uuid: UUID of the view being updated
:param name: New name to validate
:param layer_uuid: UUID of the parent semantic layer
:param configuration: Configuration dict to compare
:return: True if unique, False otherwise
"""
...
@classmethod
@abstractmethod
def find_by_name(cls, name: str, layer_uuid: str) -> SemanticViewModel | None:
"""
Find a semantic view by name within a semantic layer.
:param name: View name
:param layer_uuid: UUID of the parent semantic layer
:return: SemanticViewModel instance or None
"""
...
__all__ = ["AbstractSemanticLayerDAO", "AbstractSemanticViewDAO"]

View File

@@ -0,0 +1,102 @@
# 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.
"""
Semantic layer registration decorator for Superset.
This module provides a decorator interface to register semantic layer
implementations with the host application, enabling automatic discovery
by the extensions framework.
Usage:
from superset_core.semantic_layers.decorators import semantic_layer
@semantic_layer(
id="snowflake",
name="Snowflake Cortex",
description="Snowflake semantic layer via Cortex Analyst",
)
class SnowflakeSemanticLayer(SemanticLayer[SnowflakeConfig, SnowflakeView]):
...
# Or with minimal arguments:
@semantic_layer(id="dbt", name="dbt Semantic Layer")
class DbtSemanticLayer(SemanticLayer[DbtConfig, DbtView]):
...
"""
from __future__ import annotations
from typing import Callable, TypeVar
# Type variable for decorated semantic layer classes
T = TypeVar("T")
def semantic_layer(
id: str,
name: str,
description: str | None = None,
) -> Callable[[T], T]:
"""
Decorator to register a semantic layer implementation.
Automatically detects extension context and applies appropriate
namespacing to prevent ID conflicts between host and extension
semantic layers.
Host implementations will replace this function during initialization
with a concrete implementation providing actual functionality.
Args:
id: Unique semantic layer type identifier (e.g., "snowflake",
"dbt"). Used as the key in the semantic layers registry and
stored in the ``type`` column of the ``SemanticLayer`` model.
name: Human-readable display name (e.g., "Snowflake Cortex").
Shown in the UI when listing available semantic layer types.
description: Optional description for documentation and UI
tooltips.
Returns:
Decorated semantic layer class registered with the host
application.
Raises:
NotImplementedError: If called before host implementation is
initialized.
Example:
from superset_core.semantic_layers.decorators import semantic_layer
from superset_core.semantic_layers.layer import SemanticLayer
@semantic_layer(
id="snowflake",
name="Snowflake Cortex",
description="Connect to Snowflake Cortex Analyst",
)
class SnowflakeSemanticLayer(
SemanticLayer[SnowflakeConfig, SnowflakeView]
):
...
"""
raise NotImplementedError(
"Semantic layer decorator not initialized. "
"This decorator should be replaced during Superset startup."
)
__all__ = ["semantic_layer"]

View File

@@ -0,0 +1,129 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
from superset_core.semantic_layers.view import SemanticView
ConfigT = TypeVar("ConfigT", bound=BaseModel)
SemanticViewT = TypeVar("SemanticViewT", bound="SemanticView")
class SemanticLayer(ABC, Generic[ConfigT, SemanticViewT]):
"""
Abstract base class for semantic layers.
"""
configuration_class: type[BaseModel]
@classmethod
@abstractmethod
def from_configuration(
cls,
configuration: dict[str, Any],
) -> SemanticLayer[ConfigT, SemanticViewT]:
"""
Create a semantic layer from its configuration.
"""
raise NotImplementedError(
"Semantic layers must implement the from_configuration method"
)
@classmethod
@abstractmethod
def get_configuration_schema(
cls,
configuration: ConfigT | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the configuration needed to add the semantic layer.
A partial configuration `configuration` can be sent to improve the schema,
allowing for progressive validation and better UX. For example, a semantic
layer might require:
- auth information
- a database
If the user provides the auth information, a client can send the partial
configuration to this method, and the resulting JSON schema would include
the list of databases the user has access to, allowing a dropdown to be
populated.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are populated based on the provided connection info.
"""
raise NotImplementedError(
"Semantic layers must implement the get_configuration_schema method"
)
@classmethod
@abstractmethod
def get_runtime_schema(
cls,
configuration: ConfigT,
runtime_data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
Get the JSON schema for the runtime parameters needed to load semantic views.
This returns the schema needed to connect to a semantic view given the
configuration for the semantic layer. For example, a semantic layer might
be configured by:
- auth information
- an optional database
If the user does not provide a database when creating the semantic layer, the
runtime schema would require the database name to be provided before loading any
semantic views. This allows users to create semantic layers that connect to a
specific database (or project, account, etc.), or that allow users to select it
at query time.
The Snowflake semantic layer has an example implementation of this method, where
database and schema names are required if they were not provided in the initial
configuration.
"""
raise NotImplementedError(
"Semantic layers must implement the get_runtime_schema method"
)
@abstractmethod
def get_semantic_views(
self,
runtime_configuration: dict[str, Any],
) -> set[SemanticViewT]:
"""
Get the semantic views available in the semantic layer.
The runtime configuration can provide information like a given project or
schema, used to restrict the semantic views returned.
"""
@abstractmethod
def get_semantic_view(
self,
name: str,
additional_configuration: dict[str, Any],
) -> SemanticViewT:
"""
Get a specific semantic view by its name and additional configuration.
"""

View File

@@ -0,0 +1,85 @@
# 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.
"""
Semantic layer model interfaces for superset-core.
Provides abstract model classes for semantic layers and views that will be
replaced by the host implementation's concrete SQLAlchemy models during
initialization.
Usage:
from superset_core.semantic_layers.models import (
SemanticLayerModel,
SemanticViewModel,
)
"""
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from superset_core.common.models import CoreModel
class SemanticLayerModel(CoreModel):
"""
Abstract interface for the SemanticLayer database model.
Host implementations will replace this class during initialization
with a concrete SQLAlchemy model providing actual persistence.
"""
__abstract__ = True
# Type hints for expected column attributes
uuid: UUID
name: str
description: str | None
type: str
configuration: str
configuration_version: int
cache_timeout: int | None
created_on: datetime | None
changed_on: datetime | None
class SemanticViewModel(CoreModel):
"""
Abstract interface for the SemanticView database model.
Host implementations will replace this class during initialization
with a concrete SQLAlchemy model providing actual persistence.
"""
__abstract__ = True
# Type hints for expected column attributes
id: int
uuid: UUID
name: str
description: str | None
configuration: str
configuration_version: int
cache_timeout: int | None
semantic_layer_uuid: UUID
created_on: datetime | None
changed_on: datetime | None
__all__ = ["SemanticLayerModel", "SemanticViewModel"]

View File

@@ -0,0 +1,209 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import enum
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
import isodate
import pyarrow as pa
@dataclass(frozen=True)
class Grain:
"""
Represents a time grain (e.g., day, month, year).
Attributes:
name: Human-readable name of the grain (e.g., "Second")
representation: ISO 8601 duration (e.g., "PT1S", "P1D", "P1M")
"""
name: str
representation: str
def __post_init__(self) -> None:
isodate.parse_duration(self.representation)
def __eq__(self, other: object) -> bool:
if isinstance(other, Grain):
return self.representation == other.representation
return NotImplemented
def __hash__(self) -> int:
return hash(self.representation)
class Grains:
"""Pre-defined common grains and factory for custom ones."""
SECOND = Grain("Second", "PT1S")
MINUTE = Grain("Minute", "PT1M")
HOUR = Grain("Hour", "PT1H")
DAY = Grain("Day", "P1D")
WEEK = Grain("Week", "P1W")
MONTH = Grain("Month", "P1M")
QUARTER = Grain("Quarter", "P3M")
YEAR = Grain("Year", "P1Y")
_REGISTRY: dict[str, Grain] = {
"PT1S": SECOND,
"PT1M": MINUTE,
"PT1H": HOUR,
"P1D": DAY,
"P1W": WEEK,
"P1M": MONTH,
"P3M": QUARTER,
"P1Y": YEAR,
}
@classmethod
def get(cls, representation: str, name: str | None = None) -> Grain:
"""Return a pre-defined grain or create a custom one."""
if grain := cls._REGISTRY.get(representation):
return grain
return Grain(name or representation, representation)
@dataclass(frozen=True)
class Dimension:
id: str
name: str
type: pa.DataType
definition: str | None = None
description: str | None = None
grain: Grain | None = None
@dataclass(frozen=True)
class Metric:
id: str
name: str
type: pa.DataType
definition: str
description: str | None = None
@dataclass(frozen=True)
class AdhocExpression:
id: str
definition: str
class Operator(str, enum.Enum):
EQUALS = "="
NOT_EQUALS = "!="
GREATER_THAN = ">"
LESS_THAN = "<"
GREATER_THAN_OR_EQUAL = ">="
LESS_THAN_OR_EQUAL = "<="
IN = "IN"
NOT_IN = "NOT IN"
LIKE = "LIKE"
NOT_LIKE = "NOT LIKE"
IS_NULL = "IS NULL"
IS_NOT_NULL = "IS NOT NULL"
ADHOC = "ADHOC"
FilterValues = str | int | float | bool | datetime | date | time | timedelta | None
class PredicateType(enum.Enum):
WHERE = "WHERE"
HAVING = "HAVING"
@dataclass(frozen=True, order=True)
class Filter:
type: PredicateType
column: Dimension | Metric | None
operator: Operator
value: FilterValues | frozenset[FilterValues]
class OrderDirection(enum.Enum):
ASC = "ASC"
DESC = "DESC"
OrderTuple = tuple[Metric | Dimension | AdhocExpression, OrderDirection]
@dataclass(frozen=True)
class GroupLimit:
"""
Limit query to top/bottom N combinations of specified dimensions.
The `filters` parameter allows specifying separate filter constraints for the
group limit subquery. This is useful when you want to determine the top N groups
using different criteria (e.g., a different time range) than the main query.
For example, you might want to find the top 10 products by sales over the last
30 days, but then show daily sales for those products over the last 7 days.
"""
dimensions: list[Dimension]
top: int
metric: Metric | None
direction: OrderDirection = OrderDirection.DESC
group_others: bool = False
filters: set[Filter] | None = None
@dataclass(frozen=True)
class SemanticRequest:
"""
Represents a request made to obtain semantic results.
This could be a SQL query, an HTTP request, etc.
"""
type: str
definition: str
@dataclass(frozen=True)
class SemanticResult:
"""
Represents the results of a semantic query.
This includes any requests (SQL queries, HTTP requests) that were performed in order
to obtain the results, in order to help troubleshooting.
"""
requests: list[SemanticRequest]
results: pa.Table
@dataclass(frozen=True)
class SemanticQuery:
"""
Represents a semantic query.
"""
metrics: list[Metric]
dimensions: list[Dimension]
filters: set[Filter] | None = None
order: list[OrderTuple] | None = None
limit: int | None = None
offset: int | None = None
group_limit: GroupLimit | None = None

View File

@@ -0,0 +1,113 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import enum
from abc import ABC, abstractmethod
from superset_core.semantic_layers.types import (
Dimension,
Filter,
Metric,
SemanticQuery,
SemanticResult,
)
# TODO (betodealmeida): move to the extension JSON
class SemanticViewFeature(enum.Enum):
"""
Custom features supported by semantic layers.
"""
ADHOC_EXPRESSIONS_IN_ORDERBY = "ADHOC_EXPRESSIONS_IN_ORDERBY"
GROUP_LIMIT = "GROUP_LIMIT"
GROUP_OTHERS = "GROUP_OTHERS"
class SemanticView(ABC):
"""
Abstract base class for semantic views.
"""
features: frozenset[SemanticViewFeature]
# Implementations must expose a display name for the view.
# Declared here as a type annotation (not abstract) so that existing
# implementations are not required to add a formal @abstractmethod.
name: str
@abstractmethod
def uid(self) -> str:
"""
Returns a unique identifier for the semantic view.
"""
@abstractmethod
def get_dimensions(self) -> set[Dimension]:
"""
Get the dimensions defined in the semantic view.
"""
@abstractmethod
def get_metrics(self) -> set[Metric]:
"""
Get the metrics defined in the semantic view.
"""
@abstractmethod
def get_values(
self,
dimension: Dimension,
filters: set[Filter] | None = None,
) -> SemanticResult:
"""
Return distinct values for a dimension.
"""
@abstractmethod
def get_table(self, query: SemanticQuery) -> SemanticResult:
"""
Execute a semantic query and return the results.
"""
@abstractmethod
def get_row_count(self, query: SemanticQuery) -> SemanticResult:
"""
Execute a query and return the number of rows the result would have.
"""
@abstractmethod
def get_compatible_metrics(
self,
selected_metrics: set[Metric],
selected_dimensions: set[Dimension],
) -> set[Metric]:
"""
Return metrics compatible with the selected dimensions.
"""
@abstractmethod
def get_compatible_dimensions(
self,
selected_metrics: set[Metric],
selected_dimensions: set[Dimension],
) -> set[Dimension]:
"""
Return dimensions compatible with the selected metrics.
"""

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -117,7 +117,14 @@
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/antd": "^5.24.13",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.3",
"@rjsf/validator-ajv8": "^5.24.13",
@@ -183,24 +190,24 @@
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.18.1",
"mapbox-gl": "^3.22.0",
"mapbox-gl": "^3.23.0",
"markdown-to-jsx": "^9.7.16",
"match-sorter": "^8.3.0",
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.1.9",
"nanoid": "^5.1.11",
"ol": "^10.9.0",
"pretty-ms": "^9.3.0",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react": "^18.2.0",
"react-arborist": "^3.5.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.0.3",
"react-json-tree": "^0.20.0",
@@ -211,7 +218,6 @@
"react-reverse-portal": "^2.3.0",
"react-router-dom": "^5.3.4",
"react-search-input": "^0.11.3",
"react-sortable-hoc": "^2.0.0",
"react-split": "^2.0.9",
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5",
@@ -244,14 +250,13 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.29.2",
"@babel/runtime-corejs3": "^7.29.2",
"@babel/types": "^7.28.6",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
@@ -273,10 +278,9 @@
"@swc/core": "^1.15.32",
"@swc/plugin-emotion": "^14.9.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
@@ -286,8 +290,8 @@
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.6.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
@@ -306,7 +310,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"baseline-browser-mapping": "^2.10.21",
"baseline-browser-mapping": "^2.10.24",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^14.0.0",
@@ -326,7 +330,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.3",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
@@ -341,7 +345,7 @@
"jest-html-reporter": "^4.4.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^29.1.0",
"jsdom": "^29.1.1",
"lerna": "^9.0.4",
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
@@ -373,7 +377,7 @@
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.4.0",
"webpack-sources": "^3.4.1",
"webpack-visualizer-plugin2": "^2.0.0"
},
"peerDependencies": {

View File

@@ -30,14 +30,14 @@
"dependencies": {
"chalk": "^5.6.2",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.2.2",
"yeoman-generator": "^8.1.2",
"yosay": "^3.0.0"
},
"devDependencies": {
"cross-env": "^10.1.0",
"fs-extra": "^11.3.4",
"jest": "^30.3.0",
"yeoman-test": "^11.3.1"
"yeoman-test": "^11.4.2"
},
"engines": {
"npm": ">= 4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@apache-superset/core",
"version": "0.1.0-rc2",
"version": "0.1.0-rc3",
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
"sideEffects": false,
"main": "lib/index.js",
@@ -75,16 +75,15 @@
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-env": "^7.29.3",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.24",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -98,8 +97,8 @@
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.6",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"lodash": "^4.18.1",

View File

@@ -18,7 +18,7 @@
*/
import userEvent from '@testing-library/user-event';
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { render, RenderOptions, RenderResult } from '@testing-library/react';
import '@testing-library/jest-dom';
import { themeObject } from './theme';
@@ -33,7 +33,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => (
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) => render(ui, { wrapper: Providers, ...options });
): RenderResult => render(ui, { wrapper: Providers, ...options });
export {
createEvent,

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { ThemeProvider } from '@emotion/react';
import { theme as antdTheme } from 'antd';
import {

View File

@@ -33,17 +33,16 @@
"@ant-design/icons": "^5.6.1",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^17.0.2",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-dom": "^17.0.2"
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -18,6 +18,7 @@
*/
import { isMatrixifyVisible } from './matrixifyControls';
import type { ControlStateMapping } from '../types';
/**
* Helper to build a controls object matching the shape used by
@@ -25,7 +26,7 @@ import { isMatrixifyVisible } from './matrixifyControls';
*/
function makeControls(
overrides: Record<string, unknown> = {},
): Record<string, { value: unknown }> {
): ControlStateMapping {
const defaults: Record<string, unknown> = {
matrixify_enable: false,
matrixify_mode_rows: 'disabled',
@@ -36,7 +37,7 @@ function makeControls(
const merged = { ...defaults, ...overrides };
return Object.fromEntries(
Object.entries(merged).map(([k, v]) => [k, { value: v }]),
);
) as ControlStateMapping;
}
// ── matrixify_enable guard ──────────────────────────────────────────

View File

@@ -20,7 +20,7 @@
import { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { SharedControlConfig } from '../types';
import { ControlStateMapping, SharedControlConfig } from '../types';
import { dndAdhocMetricControl } from './dndControls';
import { defineSavedMetrics } from '../utils';
@@ -29,9 +29,12 @@ import { defineSavedMetrics } from '../utils';
* Controls for transforming charts into matrix/grid layouts
*/
// Utility function to check if matrixify controls should be visible
// Utility function to check if matrixify controls should be visible.
// Controls both visibility callbacks and validator injection via mapStateToProps.
// The matrixify_enable guard prevents hidden validators from firing on
// pre-revamp charts with stale matrixify_mode defaults (fix for #38519).
const isMatrixifyVisible = (
controls: any,
controls: ControlStateMapping | undefined,
axis: 'rows' | 'columns',
mode?: 'metrics' | 'dimensions',
selectionMode?: 'members' | 'topn' | 'all',

View File

@@ -0,0 +1,238 @@
/**
* 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.
*/
/**
* Tests for the matrixify_enable guard in isMatrixifyVisible() and
* validator injection via mapStateToProps on real matrixify control definitions.
*
* These are TDD tests for the fix to apache/superset#38519 regression:
* isMatrixifyVisible() must check matrixify_enable before evaluating mode,
* otherwise pre-revamp charts with stale matrixify_mode defaults trigger
* hidden validators that block save.
*/
import {
matrixifyControls,
isMatrixifyVisible,
} from '../../src/shared-controls/matrixifyControls';
import type { ControlPanelState, ControlStateMapping } from '../../src/types';
// Helper: build a minimal controls object for ControlPanelState
const buildControls = (
overrides: Record<string, any> = {},
): ControlStateMapping => {
const controls: Record<string, { value: any }> = {};
Object.entries(overrides).forEach(([key, value]) => {
controls[key] = { value };
});
return controls as ControlStateMapping;
};
// Helper: build a minimal ControlPanelState for mapStateToProps.
// Only provides fields that isMatrixifyVisible and mapStateToProps actually read.
const buildState = (
controlValues: Record<string, any> = {},
formData: Record<string, any> = {},
) =>
({
controls: buildControls(controlValues),
datasource: { columns: [], type: 'table' },
form_data: formData,
common: {},
metadata: {},
slice: { slice_id: 0 },
}) as unknown as ControlPanelState;
// ============================================================
// Validator injection tests via real mapStateToProps (rows)
// ============================================================
// --- matrixify_dimension_rows ---
test('matrixify_dimension_rows: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_dimension_rows;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'members',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_dimension_rows: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_dimension_rows;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'members',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// --- matrixify_topn_value_rows ---
test('matrixify_topn_value_rows: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_topn_value_rows;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_topn_value_rows: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_topn_value_rows;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// --- matrixify_topn_metric_rows ---
test('matrixify_topn_metric_rows: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_topn_metric_rows;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_topn_metric_rows: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_topn_metric_rows;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// ============================================================
// Validator injection tests via real mapStateToProps (columns)
// ============================================================
test('matrixify_dimension_columns: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_dimension_columns;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_columns: 'dimensions',
matrixify_dimension_selection_mode_columns: 'members',
},
{ matrixify_mode_columns: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_dimension_columns: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_dimension_columns;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_columns: 'dimensions',
matrixify_dimension_selection_mode_columns: 'members',
},
{ matrixify_mode_columns: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// ============================================================
// Direct isMatrixifyVisible guard tests
// ============================================================
test.each([
['undefined', undefined],
['null', null],
['false', false],
['0', 0],
])(
'isMatrixifyVisible returns false when matrixify_enable is %s',
(_, value) => {
const controls = buildControls({
matrixify_enable: value,
matrixify_mode_rows: 'dimensions',
});
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
},
);
test('isMatrixifyVisible returns true when matrixify_enable is true and mode matches', () => {
const controls = buildControls({
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
});
expect(isMatrixifyVisible(controls, 'rows', 'dimensions')).toBe(true);
});
test('isMatrixifyVisible returns false when matrixify_enable is true but mode is disabled', () => {
const controls = buildControls({
matrixify_enable: true,
matrixify_mode_rows: 'disabled',
});
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
});
test('isMatrixifyVisible returns true when matrixify_enable is true and any non-disabled mode (no mode filter)', () => {
const controls = buildControls({
matrixify_enable: true,
matrixify_mode_columns: 'metrics',
});
expect(isMatrixifyVisible(controls, 'columns')).toBe(true);
});

View File

@@ -91,10 +91,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -102,8 +101,8 @@
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
},

View File

@@ -94,11 +94,20 @@ class CategoricalColorScale extends ExtensibleFunction {
/**
* Increment the color range with analogous colors
*
* @param forceMinimumExpansion When true, expand at least once even if the
* ordinal domain is still shorter than the palette. Shared dashboard labels
* can resolve from the global map without entering the scale domain, so
* domain-based sizing alone would skip expansion while collision resolution
* still needs analogous colors.
*/
incrementColorRange() {
const multiple = Math.floor(
incrementColorRange(forceMinimumExpansion = false) {
const domainBasedMultiple = Math.floor(
this.domain().length / this.originColors.length,
);
const multiple = forceMinimumExpansion
? Math.max(domainBasedMultiple, 1)
: domainBasedMultiple;
// the domain has grown larger than the original range
// increments the range with analogous colors
if (multiple > this.multiple) {
@@ -144,6 +153,7 @@ class CategoricalColorScale extends ExtensibleFunction {
if (isFeatureEnabled(FeatureFlag.UseAnalogousColors)) {
this.incrementColorRange();
}
if (
// feature flag to be deprecated (will become standard behaviour)
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
@@ -154,6 +164,39 @@ class CategoricalColorScale extends ExtensibleFunction {
}
}
if (
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
source === LabelsColorMapSource.Dashboard &&
(forcedColor || isExistingLabel)
) {
const colliding = [...this.chartLabelsColorMap.entries()].filter(
([labelKey, c]) => c === color && labelKey !== cleanedValue,
);
if (
colliding.length > 0 &&
isFeatureEnabled(FeatureFlag.UseAnalogousColors)
) {
this.incrementColorRange(true);
}
for (const [otherLabel] of colliding) {
if (
Object.prototype.hasOwnProperty.call(this.forcedColors, otherLabel)
) {
continue;
}
const newColor = this.getNextAvailableColor(otherLabel, color);
this.chartLabelsColorMap.set(otherLabel, newColor);
if (sliceId) {
this.labelsColorMapInstance.addSlice(
otherLabel,
newColor,
sliceId,
appliedColorScheme,
);
}
}
}
// keep track of values in this slice
this.chartLabelsColorMap.set(cleanedValue, color);

View File

@@ -17,19 +17,15 @@
* under the License.
*/
import type { ReactElement } from 'react';
import {
Tooltip,
type TooltipPlacement,
type IconType,
} from '@superset-ui/core/components';
import type { ReactElement, ReactNode } from 'react';
import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components';
import { css, useTheme } from '@apache-superset/core/theme';
export interface ActionProps {
label: string;
tooltip?: string | ReactElement;
placement?: TooltipPlacement;
icon: IconType;
icon: ReactNode;
onClick: () => void;
}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useJsonValidation } from './useJsonValidation';
describe('useJsonValidation', () => {

View File

@@ -16,16 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
useEffect,
useState,
RefObject,
forwardRef,
ComponentType,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
} from 'react';
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
import { Loading } from '../Loading';
import type { PlaceholderProps } from './types';
@@ -93,15 +84,16 @@ export function AsyncEsmComponent<
return promise;
}
type AsyncComponent = ForwardRefExoticComponent<
PropsWithoutRef<FullProps> & RefAttributes<ComponentType<FullProps>>
type AsyncComponent = React.ForwardRefExoticComponent<
React.PropsWithoutRef<FullProps> & React.RefAttributes<unknown>
> & {
preload?: typeof waitForPromise;
};
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps,
ref: RefObject<ComponentType<FullProps>>,
ref,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {

View File

@@ -24,7 +24,6 @@ import type {
ButtonVariantType,
ButtonColorType,
} from 'antd/es/button';
import { IconType } from '@superset-ui/core/components/Icons/types';
import type { TooltipPlacement } from '../Tooltip/types';
export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
@@ -49,5 +48,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
buttonStyle?: ButtonStyle;
cta?: boolean;
showMarginRight?: boolean;
icon?: IconType;
icon?: ReactNode;
};

View File

@@ -73,7 +73,7 @@ export const Component = (props: DropdownContainerProps) => {
const [overflowingState, setOverflowingState] = useState<OverflowingState>();
const containerRef = useRef<DropdownRef>(null);
const onOverflowingStateChange = useCallback(
value => {
(value: OverflowingState) => {
if (!isEqual(overflowingState, value)) {
setItems(generateItems(value));
setOverflowingState(value);

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
import { IconType } from '../Icons';
/**
* Container item.
@@ -70,7 +69,7 @@ export interface DropdownContainerProps {
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: IconType;
dropdownTriggerIcon?: ReactNode;
/**
* Text of the dropdown trigger.
*/

View File

@@ -0,0 +1,80 @@
/**
* 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 { fireEvent, render, screen, userEvent } from '@superset-ui/core/spec';
import { useState } from 'react';
import { DynamicEditableTitle } from '.';
const Harness = ({ initialTitle = 'Original' }: { initialTitle?: string }) => {
const [title, setTitle] = useState(initialTitle);
return (
<DynamicEditableTitle
title={title}
placeholder="placeholder"
canEdit
label="Title"
onSave={setTitle}
/>
);
};
test('rapid typing then backspacing keeps every keystroke', async () => {
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'abc', { delay: 1 });
expect(input.value).toBe('Originalabc');
await userEvent.type(input, '{backspace}{backspace}{backspace}', {
delay: 1,
});
expect(input.value).toBe('Original');
});
test('a change event that arrives before isEditing flips is not dropped', () => {
// Reproduces the regression: the input is focused but `isEditing` is still
// false because no click has been registered yet (e.g. focus arrived via
// tab, autofocus, or programmatic focus). The pre-fix `handleChange`
// bailed out with `!isEditing`, dropping the keystroke. Because the
// input is controlled, antd's internal `useMergedState` then resyncs the
// DOM value back to the (stale) `props.value`, so the user sees their
// typed character disappear. This test fires a raw change event so it
// doesn't go through userEvent's implicit click.
const onSave = jest.fn();
render(
<DynamicEditableTitle
title="Foo"
placeholder="placeholder"
canEdit
label="Title"
onSave={onSave}
/>,
);
const input = screen.getByRole('textbox') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'FooX' } });
expect(input.value).toBe('FooX');
});
test('prop changes mid-edit do not clobber unsaved typing', async () => {
const { rerender } = render(<Harness initialTitle="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'X', { delay: 1 });
expect(input.value).toBe('FooX');
rerender(<Harness initialTitle="Foo" />);
expect(input.value).toBe('FooX');
});

View File

@@ -23,6 +23,7 @@ import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { t } from '@apache-superset/core/translation';
@@ -30,6 +31,7 @@ import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { useResizeDetector } from 'react-resize-detector';
import { Tooltip } from '../Tooltip';
import { Input } from '../Input';
import type { InputRef } from '../Input';
import type { DynamicEditableTitleProps } from './types';
const titleStyles = (theme: SupersetTheme) => css`
@@ -75,8 +77,10 @@ export const DynamicEditableTitle = memo(
const [isEditing, setIsEditing] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title || '');
const [inputWidth, setInputWidth] = useState<number>(0);
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
const sizerRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<InputRef>(null);
const { width: containerWidth, ref: containerRef } = useResizeDetector({
refreshMode: 'debounce',
});
@@ -85,27 +89,33 @@ export const DynamicEditableTitle = memo(
setCurrentTitle(title);
}, [title]);
useEffect(() => {
if (isEditing && sizerRef?.current) {
if (isEditing) {
// move cursor and scroll to the end
if (sizerRef.current.setSelectionRange) {
const { length } = sizerRef.current.value;
sizerRef.current.setSelectionRange(length, length);
sizerRef.current.scrollLeft = sizerRef.current.scrollWidth;
const inputElement = inputRef.current?.input;
if (inputElement) {
const { length } = inputElement.value;
inputElement.setSelectionRange(length, length);
inputElement.scrollLeft = inputElement.scrollWidth;
}
}
}, [isEditing]);
// a trick to make the input grow when user types text
// we make additional span component, place it somewhere out of view and copy input
// then we can measure the width of that span to resize the input element
// we make an additional span component, place it somewhere out of view and
// mirror the input value, then measure the span synchronously (pre-paint)
// to resize the input element. Reading offsetWidth in a useLayoutEffect
// forces a sync layout, so the input width updates in the same commit as
// the value change — preventing a flicker frame where the input is shown
// with new value but stale width.
useLayoutEffect(() => {
if (sizerRef?.current) {
if (sizerRef.current) {
sizerRef.current.textContent = currentTitle || placeholder;
setInputWidth(sizerRef.current.offsetWidth);
}
}, [currentTitle, placeholder, sizerRef]);
}, [currentTitle, placeholder]);
useEffect(() => {
const inputElement = sizerRef.current?.input;
const inputElement = inputRef.current?.input;
if (inputElement) {
if (inputElement.scrollWidth > inputElement.clientWidth) {
@@ -137,9 +147,17 @@ export const DynamicEditableTitle = memo(
const handleChange = useCallback(
(ev: ChangeEvent<HTMLInputElement>) => {
if (!canEdit || !isEditing) {
if (!canEdit) {
return;
}
// Any change implies the user is editing. Ensure isEditing is true
// even if the change event arrives before the click handler has
// committed (e.g. focus via tab, autofocus, or batched click+type
// events). Otherwise the keystroke would be dropped and the
// controlled input would revert to the previous value.
if (!isEditing) {
setIsEditing(true);
}
setCurrentTitle(ev.target.value);
},
[canEdit, isEditing],
@@ -168,6 +186,7 @@ export const DynamicEditableTitle = memo(
}
>
<Input
ref={inputRef}
data-test="editable-title-input"
variant="borderless"
aria-label={label ?? t('Title')}

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import type { ReactNode, SyntheticEvent } from 'react';
import type { IconType } from '@superset-ui/core/components';
export type EmptyStateSize = 'small' | 'medium' | 'large';
@@ -26,7 +25,7 @@ export type EmptyStateProps = {
description?: ReactNode;
image?: ReactNode | string;
buttonText?: ReactNode;
buttonIcon?: IconType;
buttonIcon?: ReactNode;
buttonAction?: (event: SyntheticEvent) => void;
/** Controls image size. Defaults to 'medium'. */
size?: EmptyStateSize;

View File

@@ -20,7 +20,7 @@ import { Form as AntdForm } from 'antd';
import { FormProps } from './types';
function CustomForm(props: FormProps) {
return <AntdForm {...props} />;
return <AntdForm {...(props as any)} />;
}
export const Form = Object.assign(CustomForm, {

View File

@@ -41,7 +41,6 @@ test('renders with monospace prop', () => {
// test stories from the storybook!
test('renders all the storybook gallery variants', () => {
// @ts-expect-error: Suppress TypeScript error for LabelGallery usage
const { container } = render(<LabelGallery />);
const nonInteractiveLabelCount = 4;
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;

View File

@@ -23,7 +23,7 @@ import { Label } from '..';
// Define the prop types for DatasetTypeLabel
interface DatasetTypeLabelProps {
datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
datasetType: 'physical' | 'virtual' | 'semantic_view';
}
const SIZE = 's'; // Define the size as a constant
@@ -32,6 +32,22 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType,
}) => {
const theme = useTheme();
if (datasetType === 'semantic_view') {
return (
<Label
icon={
<Icons.ApartmentOutlined
iconSize={SIZE}
iconColor={theme.colorInfo}
/>
}
type="info"
style={{ color: theme.colorInfo }}
>
{t('Semantic')}
</Label>
);
}
const isPhysical = datasetType === 'physical';
const label: string = isPhysical ? t('Physical') : t('Virtual');
const labelType = isPhysical ? 'primary' : 'default';

View File

@@ -21,6 +21,7 @@ import type { BackgroundPosition } from './ImageLoader';
export interface LinkProps {
to: string;
children?: ReactNode;
}
export interface ListViewCardProps {

View File

@@ -194,7 +194,7 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
}
const onResize = useCallback(
width => {
(width: number | undefined) => {
// Calculates the breakpoint width to collapse the bar.
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
const breakpoint =

View File

@@ -54,7 +54,7 @@ export function FormModal({
}, [onSave, resetForm]);
const handleFormSubmit = useCallback(
async values => {
async (values: object) => {
try {
setIsSaving(true);
await formSubmitHandler(values);

View File

@@ -104,6 +104,9 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
right: 0;
display: flex;
justify-content: center;
// Keep the close button clickable when modal body content uses
// position: sticky with elevated z-index (e.g. DatabaseModal header).
z-index: ${theme.zIndexPopupBase + 1};
}
.ant-modal-close:hover {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import type { CSSProperties, ReactNode } from 'react';
import type { ModalFuncProps } from 'antd';
import type { FormInstance, ModalFuncProps } from 'antd';
import type { ResizableProps } from 're-resizable';
import type { DraggableProps } from 'react-draggable';
import { ButtonStyle } from '../Button/types';
@@ -68,7 +68,8 @@ export interface StyledModalProps {
export type { ModalFuncProps };
export interface FormModalProps extends ModalProps {
export interface FormModalProps extends Omit<ModalProps, 'children'> {
children: ReactNode | ((form: FormInstance) => ReactNode);
initialValues?: object;
formSubmitHandler: (values: object) => Promise<void>;
onSave: () => void;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, ReactElement } from 'react';
import { ReactNode, ReactElement, memo } from 'react';
import { t } from '@apache-superset/core/translation';
import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
@@ -118,62 +118,64 @@ export type PageHeaderWithActionsProps = {
};
};
export const PageHeaderWithActions = ({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
showMenuDropdown = true,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<span>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>
</span>
</Dropdown>
export const PageHeaderWithActions = memo(
({
editableTitleProps,
showTitlePanelItems,
certificatiedBadgeProps,
showFaveStar,
faveStarProps,
titlePanelAdditionalItems,
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
showMenuDropdown = true,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
<div css={headerStyles} className="header-with-actions">
<div className="title-panel">
<DynamicEditableTitle {...editableTitleProps} />
{showTitlePanelItems && (
<div css={buttonsStyles}>
{certificatiedBadgeProps?.certifiedBy && (
<CertifiedBadge {...certificatiedBadgeProps} />
)}
{showFaveStar && <FaveStar {...faveStarProps} />}
{titlePanelAdditionalItems}
</div>
)}
</div>
<div className="right-button-panel">
{rightPanelAdditionalItems}
<div css={additionalActionsContainerStyles}>
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<span>
<Button
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconSize="l"
/>
</Button>
</span>
</Dropdown>
)}
</div>
</div>
</div>
</div>
);
};
);
},
);

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { render, screen, fireEvent } from '@superset-ui/core/spec';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { TableInstance, useTable } from 'react-table';
import TableCollection from '.';

View File

@@ -91,7 +91,7 @@ export function mapColumns<T extends object>(
return columns.map(column => {
const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id);
return {
title: column.Header,
title: column.Header as ReactNode,
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
hidden: column.hidden,
key: column.id,
@@ -121,7 +121,7 @@ export function mapColumns<T extends object>(
column,
});
}
return val;
return val as ReactNode;
},
className: column.className,
};

View File

@@ -19,6 +19,14 @@
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
import { TableView, TableViewProps } from '.';
// Mock window.scrollTo to prevent jsdom "Not implemented" errors
beforeAll(() => {
window.scrollTo = jest.fn();
});
afterAll(() => {
jest.restoreAllMocks();
});
const mockedProps: TableViewProps = {
columns: [
{
@@ -125,27 +133,25 @@ test('should change page when pagination is clicked', async () => {
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('321')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('Kate')).toBeInTheDocument();
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('321')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
const page1 = screen.getByRole('listitem', { name: '1' });
await userEvent.click(page1);
await userEvent.click(screen.getByTitle('Previous Page'));
await waitFor(() => {
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
});
test('should sort by age', async () => {
@@ -240,8 +246,7 @@ test('should handle server-side pagination', async () => {
render(<TableView {...serverPaginationProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(onServerPagination).toHaveBeenCalledWith({
@@ -301,9 +306,7 @@ test('should scroll to top when scrollTopOnPagination is true', async () => {
};
render(<TableView {...scrollProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
@@ -324,9 +327,7 @@ test('should NOT scroll to top when scrollTopOnPagination is false', async () =>
};
render(<TableView {...scrollProps} />);
// Click next page
const page2 = screen.getByRole('listitem', { name: '2' });
await userEvent.click(page2);
await userEvent.click(screen.getByTitle('Next Page'));
await waitFor(() => {
expect(screen.getByText('321')).toBeInTheDocument();

View File

@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
import { memo, useEffect, useRef, useMemo, useCallback, useState } from 'react';
import { isEqual } from 'lodash';
import { styled } from '@apache-superset/core/theme';
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
import { useFilters, useSortBy, useTable } from 'react-table';
import { Empty } from '@superset-ui/core/components';
import TableCollection from '@superset-ui/core/components/TableCollection';
import { TableSize } from '@superset-ui/core/components/Table';
@@ -117,43 +117,45 @@ const RawTableView = ({
...props
}: TableViewProps) => {
const tableRef = useRef<HTMLTableElement>(null);
const effectivePageSize = initialPageSize ?? DEFAULT_PAGE_SIZE;
const [pageIndex, setPageIndex] = useState(initialPageIndex ?? 0);
const initialState = useMemo(
() => ({
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageIndex: initialPageIndex ?? 0,
pageSize: effectivePageSize,
pageIndex: 0,
sortBy: initialSortBy,
}),
[initialPageSize, initialPageIndex, initialSortBy],
[effectivePageSize, initialSortBy],
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
rows,
prepareRow,
gotoPage,
setSortBy,
state: { pageIndex, sortBy },
state: { sortBy },
} = useTable(
{
columns,
data,
initialState,
manualPagination: serverPagination,
manualPagination: true,
manualSortBy: serverPagination,
pageCount: serverPagination
? Math.ceil(totalCount / initialState.pageSize)
: undefined,
autoResetSortBy: false,
},
useFilters,
useSortBy,
...(withPagination ? [usePagination] : []),
);
const content = useMemo(() => {
if (!withPagination || serverPagination) return rows;
const start = pageIndex * effectivePageSize;
return rows.slice(start, start + effectivePageSize);
}, [withPagination, serverPagination, rows, pageIndex, effectivePageSize]);
const EmptyWrapperComponent = useMemo(() => {
switch (emptyWrapperType) {
case EmptyWrapperType.Small:
@@ -164,11 +166,6 @@ const RawTableView = ({
}
}, [emptyWrapperType]);
const content = useMemo(
() => (withPagination ? page : rows),
[withPagination, page, rows],
);
const isEmpty = useMemo(
() => !loading && content.length === 0,
[loading, content.length],
@@ -192,10 +189,9 @@ const RawTableView = ({
const handlePageChange = useCallback(
(p: number) => {
if (scrollTopOnPagination) handleScrollToTop();
gotoPage(p);
setPageIndex(p);
},
[scrollTopOnPagination, handleScrollToTop, gotoPage],
[scrollTopOnPagination, handleScrollToTop],
);
const paginationProps = useMemo(() => {
@@ -211,7 +207,7 @@ const RawTableView = ({
if (serverPagination) {
return {
pageIndex,
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageSize: effectivePageSize,
totalCount,
onPageChange: handlePageChange,
};
@@ -219,7 +215,7 @@ const RawTableView = ({
return {
pageIndex,
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
pageSize: effectivePageSize,
totalCount: data.length,
onPageChange: handlePageChange,
};
@@ -227,28 +223,28 @@ const RawTableView = ({
withPagination,
serverPagination,
pageIndex,
initialPageSize,
effectivePageSize,
totalCount,
data.length,
handlePageChange,
]);
useEffect(() => {
if (serverPagination && pageIndex !== initialState.pageIndex) {
if (serverPagination && pageIndex !== (initialPageIndex ?? 0)) {
onServerPagination({
pageIndex,
});
}
}, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
}, [initialPageIndex, onServerPagination, pageIndex, serverPagination]);
useEffect(() => {
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
if (serverPagination && !isEqual(sortBy, initialSortBy)) {
onServerPagination({
pageIndex: 0,
sortBy,
});
}
}, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
}, [initialSortBy, onServerPagination, serverPagination, sortBy]);
return (
<TableViewStyles {...props} ref={tableRef}>

View File

@@ -97,8 +97,8 @@ const StyledPlus = styled.span`
export default function TruncatedList<ListItemType>({
items,
renderVisibleItem = item => item,
renderTooltipItem = item => item,
renderVisibleItem = item => item as ReactNode,
renderTooltipItem = item => item as ReactNode,
getKey = item => item as unknown as Key,
maxLinks = 20,
}: TruncatedListProps<ListItemType>) {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useChangeEffect } from './useChangeEffect';
test('call callback the first time with undefined and value', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useComponentDidMount } from './useComponentDidMount';
test('the effect should only be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useComponentDidUpdate } from './useComponentDidUpdate';
test('the effect should not be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { useElementOnScreen } from './useElementOnScreen';
const observeMock = jest.fn();
@@ -46,10 +46,9 @@ test('should return isSticky as true when intersectionRatio < 1', async () => {
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
);
const callback = IntersectionObserverMock.mock.calls[0][0];
const callBack = callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
const observer = new IntersectionObserverMock(callBack, {});
const newDiv = document.createElement('div');
observer.observe(newDiv);
act(() => {
callback([{ isIntersecting: true, intersectionRatio: 0.5 }]);
});
expect(hook.result.current[1]).toEqual(true);
});
@@ -58,10 +57,9 @@ test('should return isSticky as false when intersectionRatio >= 1', async () =>
useElementOnScreen({ rootMargin: '-50px 0px 0px 0px' }),
);
const callback = IntersectionObserverMock.mock.calls[0][0];
const callBack = callback([{ isIntersecting: true, intersectionRatio: 1 }]);
const observer = new IntersectionObserverMock(callBack, {});
const newDiv = document.createElement('div');
observer.observe(newDiv);
act(() => {
callback([{ isIntersecting: true, intersectionRatio: 1 }]);
});
expect(hook.result.current[1]).toEqual(false);
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { usePrevious } from './usePrevious';
test('get undefined on the first render when initialValue is not defined', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import useCSSTextTruncation from './useCSSTextTruncation';
afterEach(() => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { RefObject } from 'react';
import useChildElementTruncation from './useChildElementTruncation';

View File

@@ -19,6 +19,15 @@
import { DatasourceType } from './types/Datasource';
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
table: DatasourceType.Table,
query: DatasourceType.Query,
dataset: DatasourceType.Dataset,
sl_table: DatasourceType.SlTable,
saved_query: DatasourceType.SavedQuery,
semantic_view: DatasourceType.SemanticView,
};
export default class DatasourceKey {
readonly id: number;
@@ -27,8 +36,7 @@ export default class DatasourceKey {
constructor(key: string) {
const [idStr, typeStr] = key.split('__');
this.id = parseInt(idStr, 10);
this.type = DatasourceType.Table; // default to SqlaTable model
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
}
public toString() {

View File

@@ -26,6 +26,7 @@ export enum DatasourceType {
Dataset = 'dataset',
SlTable = 'sl_table',
SavedQuery = 'saved_query',
SemanticView = 'semantic_view',
}
export interface Currency {
@@ -40,6 +41,13 @@ export interface Datasource {
id: number;
name: string;
type: DatasourceType;
/**
* The parent resource that owns this datasource.
* For SQL-based datasets this is the database; for semantic views it is the
* semantic layer. Use this field instead of the legacy `database` field when
* you only need the display name.
*/
parent?: { name: string };
columns: Column[];
metrics: Metric[];
description?: string;

View File

@@ -249,7 +249,8 @@ export type Extensions = Partial<{
'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>;
'navbar.right': ComponentType;
'report-modal.dropdown.item.icon': ComponentType;
'root.context.provider': ComponentType;
'root.context.provider': ComponentType<{ children?: ReactNode }>;
'welcome.message': ComponentType;
'welcome.banner': ComponentType;
'welcome.main.replacement': ComponentType;

View File

@@ -61,6 +61,7 @@ export enum FeatureFlag {
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
Matrixify = 'MATRIXIFY',
ScheduledQueries = 'SCHEDULED_QUERIES',
SemanticLayers = 'SEMANTIC_LAYERS',
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
SshTunneling = 'SSH_TUNNELING',

View File

@@ -143,7 +143,7 @@ describe('SuperChart', () => {
);
expect(await screen.findByText('Custom Fallback!')).toBeInTheDocument();
expect(CustomFallbackComponent).toHaveBeenCalledTimes(1);
expect(CustomFallbackComponent).toHaveBeenCalled();
});
test('call onErrorBoundary', async () => {
expectedErrors = 1;

View File

@@ -21,6 +21,7 @@ import { ScaleOrdinal } from 'd3-scale';
import {
CategoricalColorScale,
FeatureFlag,
getLabelsColorMap,
LabelsColorMapSource,
} from '@superset-ui/core';
@@ -199,10 +200,42 @@ describe('CategoricalColorScale', () => {
const returnedColor = scale.getColor(value, sliceId);
expect(returnedColor).toBe(expectedColor);
});
test('reassigns colliding colors when no sliceId is provided', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
const labelsColorMap = chartAScale.labelsColorMapInstance;
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
try {
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE);
// Call getColor without sliceId (or with undefined)
chartBScale.getColor('Classic Cars', undefined, 'testScheme');
chartBScale.getColor('Trains', undefined, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBeDefined();
expect(classicCarsColor).not.toBe('red');
} finally {
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
}
});
test('conditionally calls getNextAvailableColor', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
scale.labelsColorMapInstance.source = LabelsColorMapSource.Explore;
scale.getColor('testValue1');
scale.getColor('testValue2');
@@ -225,6 +258,27 @@ describe('CategoricalColorScale', () => {
expect(getNextAvailableColorSpy).not.toHaveBeenCalled();
});
test('reassigns non-forced labels when a dashboard-synced label would duplicate their color', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const dashScale = new CategoricalColorScale(['red', 'blue', 'green']);
const sliceId = 501;
const colorScheme = 'preset';
dashScale.labelsColorMapInstance.source = LabelsColorMapSource.Dashboard;
jest
.spyOn(dashScale.labelsColorMapInstance, 'getColorMap')
.mockReturnValue(new Map([['Trains', 'red']]));
dashScale.getColor('Classic Cars', sliceId, colorScheme);
dashScale.getColor('Trains', sliceId, colorScheme);
expect(dashScale.chartLabelsColorMap.get('Trains')).toBe('red');
expect(dashScale.chartLabelsColorMap.get('Classic Cars')).not.toBe('red');
expect(dashScale.chartLabelsColorMap.get('Classic Cars')).toBeDefined();
});
});
describe('.setColor(value, forcedColor)', () => {
@@ -479,6 +533,131 @@ describe('CategoricalColorScale', () => {
});
});
describe('dashboard shared-dimension color collision', () => {
let labelsColorMap: ReturnType<typeof getLabelsColorMap>;
beforeEach(() => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
};
const sentinel = new CategoricalColorScale(['red', 'blue', 'green']);
labelsColorMap = sentinel.labelsColorMapInstance;
labelsColorMap.reset();
labelsColorMap.source = LabelsColorMapSource.Dashboard;
});
afterEach(() => {
jest.restoreAllMocks();
labelsColorMap.reset();
});
test('reproduces the bug without the fix: Classic Cars and Trains would both be red', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: false,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBe('red');
});
test('fix: Classic Cars is reassigned when Trains locks red from the dashboard', () => {
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
expect(labelsColorMap.getColorMap().get('Trains')).toBe('red');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const classicCarsColor =
chartBScale.chartLabelsColorMap.get('Classic Cars');
const trainsColor = chartBScale.chartLabelsColorMap.get('Trains');
expect(trainsColor).toBe('red');
expect(classicCarsColor).toBeDefined();
expect(classicCarsColor).not.toBe('red');
});
test('fix: no series in Chart B share a color when palette has enough colors', () => {
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
const colors = Array.from(chartBScale.chartLabelsColorMap.values());
const uniqueColors = new Set(colors);
expect(uniqueColors.size).toBe(colors.length);
});
test('fix: increments analogous color range for dashboard collisions when UseAnalogousColors is enabled', () => {
window.featureFlags = {
[FeatureFlag.AvoidColorsCollision]: true,
[FeatureFlag.UseAnalogousColors]: true,
};
const PALETTE = ['red', 'blue', 'green'];
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE);
const addSliceSpy = jest.spyOn(
chartBScale.labelsColorMapInstance,
'addSlice',
);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Model T', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
expect(chartBScale.chartLabelsColorMap.get('Trains')).toBe('red');
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).toBeDefined();
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).not.toBe(
'red',
);
expect(chartBScale.range()).toHaveLength(6);
expect(
addSliceSpy.mock.calls.some(
([label, color]) => label === 'Classic Cars' && color !== 'red',
),
).toBe(true);
});
test('fix: forced colors (user-set in dashboard JSON) are never reassigned', () => {
const PALETTE = ['red', 'blue', 'green'];
const forcedColors = { 'Classic Cars': 'red' };
const chartAScale = new CategoricalColorScale(PALETTE);
chartAScale.getColor('Trains', 101, 'testScheme');
const chartBScale = new CategoricalColorScale(PALETTE, forcedColors);
chartBScale.getColor('Classic Cars', 102, 'testScheme');
chartBScale.getColor('Trains', 102, 'testScheme');
expect(chartBScale.chartLabelsColorMap.get('Classic Cars')).toBe('red');
});
});
describe("is compatible with D3's ScaleOrdinal", () => {
test('passes type check', () => {
const scale: ScaleOrdinal<{ toString(): string }, string> =

View File

@@ -19,6 +19,7 @@
import fetchMock from 'fetch-mock';
import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
import type { SupersetClientInterface } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
beforeAll(() => fetchMock.mockGlobal());
@@ -31,6 +32,10 @@ describe('SupersetClient', () => {
afterEach(() => SupersetClient.reset());
const clientWithGetUrl = SupersetClient as SupersetClientInterface & {
getUrl: (...args: unknown[]) => string;
};
test('exposes configure, init, get, post, postForm, delete, put, request, reset, getGuestToken, getCSRFToken, getUrl, isAuthenticated, and reAuthenticate methods', () => {
expect(typeof SupersetClient.configure).toBe('function');
expect(typeof SupersetClient.init).toBe('function');
@@ -43,7 +48,7 @@ describe('SupersetClient', () => {
expect(typeof SupersetClient.reset).toBe('function');
expect(typeof SupersetClient.getGuestToken).toBe('function');
expect(typeof SupersetClient.getCSRFToken).toBe('function');
expect(typeof SupersetClient.getUrl).toBe('function');
expect(typeof clientWithGetUrl.getUrl).toBe('function');
expect(typeof SupersetClient.isAuthenticated).toBe('function');
expect(typeof SupersetClient.reAuthenticate).toBe('function');
});
@@ -58,7 +63,7 @@ describe('SupersetClient', () => {
expect(SupersetClient.request).toThrow();
expect(SupersetClient.getGuestToken).toThrow();
expect(SupersetClient.getCSRFToken).toThrow();
expect(SupersetClient.getUrl).toThrow();
expect(clientWithGetUrl.getUrl).toThrow();
expect(SupersetClient.isAuthenticated).toThrow();
expect(SupersetClient.reAuthenticate).toThrow();
expect(SupersetClient.configure).not.toThrow();
@@ -100,7 +105,7 @@ describe('SupersetClient', () => {
const getUrlSpy = jest.spyOn(SupersetClientClass.prototype, 'getUrl');
SupersetClient.configure({ appRoot: '/app' });
expect(SupersetClient.getUrl({ endpoint: '/some/path' })).toContain(
expect(clientWithGetUrl.getUrl({ endpoint: '/some/path' })).toContain(
'/app/some/path',
);
expect(getUrlSpy).toHaveBeenCalledTimes(1);

View File

@@ -28,10 +28,11 @@ test('DEFAULT_METRICS', () => {
});
test('DatasourceType', () => {
expect(Object.keys(DatasourceType).length).toBe(5);
expect(Object.keys(DatasourceType).length).toBe(6);
expect(DatasourceType.Table).toBe('table');
expect(DatasourceType.Query).toBe('query');
expect(DatasourceType.Dataset).toBe('dataset');
expect(DatasourceType.SlTable).toBe('sl_table');
expect(DatasourceType.SavedQuery).toBe('saved_query');
expect(DatasourceType.SemanticView).toBe('semantic_view');
});

View File

@@ -71,10 +71,16 @@ describe('TimeFormatter', () => {
// PivotData.processRecord coerces values with String(), turning numeric
// timestamps into strings.
const timestamp = PREVIEW_TIME.getTime().toString();
expect(formatter.format(timestamp)).toEqual('2017');
expect(formatter.format(timestamp as unknown as number | Date)).toEqual(
'2017',
);
});
test('handles ISO-8601 string without misinterpreting it as a number', () => {
expect(formatter.format('2017-02-14T11:22:33.000Z')).toEqual('2017');
expect(
formatter.format(
'2017-02-14T11:22:33.000Z' as unknown as number | Date,
),
).toEqual('2017');
});
test('otherwise returns formatted value', () => {
expect(formatter.format(PREVIEW_TIME)).toEqual('2017');

View File

@@ -33,7 +33,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -34,6 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -36,6 +36,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -32,9 +32,9 @@
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -32,7 +32,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,6 +39,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -43,6 +43,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"react": "^17.0.2"
"react": "^18.2.0"
}
}

View File

@@ -29,7 +29,7 @@
"classnames": "^2.5.1",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"memoize-one": "^5.2.1",
"memoize-one": "^6.0.0",
"react-table": "^7.8.0",
"regenerator-runtime": "^0.14.1",
"xss": "^1.0.15"
@@ -39,14 +39,13 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -247,7 +247,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
[serverPagination, debouncedSearch, searchId],
);
const handleColSort = (colId: string, sortDir: string) => {
const handleColSort = (colId: string, sortDir: string | null) => {
const isSortable = shouldSort({
colId,
sortDir,
@@ -301,10 +301,12 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
};
const handleColumnHeaderClick = useCallback(
params => {
(params: { column?: { colId?: string; sort?: string | null } }) => {
const colId = params?.column?.colId;
const sortDir = params?.column?.sort;
handleColSort(colId, sortDir);
if (colId && sortDir !== undefined) {
handleColSort(colId, sortDir);
}
},
[serverPagination, gridInitialState, percentMetrics, onSortChange],
);

View File

@@ -147,7 +147,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
]);
const handleColumnStateChange = useCallback(
agGridState => {
(agGridState: Record<string, unknown>) => {
if (onChartStateChange) {
onChartStateChange(agGridState);
}

View File

@@ -70,5 +70,9 @@ export const TextCellRenderer = (params: CellRendererProps) => {
}
}
return <div>{valueFormatted ?? value}</div>;
return (
<div>
{valueFormatted ?? (value instanceof Date ? value.toISOString() : value)}
</div>
);
};

View File

@@ -43,7 +43,7 @@ export const shouldSort = ({
gridInitialState,
}: {
colId: string;
sortDir: string;
sortDir: string | null;
percentMetrics: string[];
serverPagination: boolean;
gridInitialState: GridState;

View File

@@ -0,0 +1,99 @@
/**
* 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.
*/
/**
* Regression coverage for memoize-one v6 adoption.
*
* memoize-one v6 changed the signature of the (optional) custom `isEqual`
* callback from per-argument `(a, b) => bool` to arg-array
* `(newArgs, lastArgs) => bool`. Of the four memoizeOne callsites in
* `src/transformProps.ts` (`processComparisonDataRecords`,
* `processDataRecords`, `processColumns`, `getBasicColorFormatter`), only
* `processColumns` passes a custom comparator (`isEqualColumns`); its
* signature already takes arg-arrays and is compatible with v6. The other
* three rely on memoize-one's default referential-equality comparator, which
* is unchanged between v5 and v6.
*
* These tests lock those assumptions in by observing the memoization
* behavior through the public `transformProps` API: identical chart-props
* input references should produce referentially-equal `data` and `columns`
* arrays (cache hit), while inputs that differ on the sub-fields each
* memoizer actually compares should produce fresh arrays (cache miss).
*/
import transformProps from '../src/transformProps';
import testData from '../../plugin-chart-table/test/testData';
test('transformProps returns referentially-equal data/columns on identical input (cache hit)', () => {
// processColumns and processDataRecords are both wrapped by memoizeOne at
// module scope. Two consecutive calls with the same chartProps reference
// should hit both caches and yield the same output references.
const first = transformProps(testData.basic);
const second = transformProps(testData.basic);
expect(second.columns).toBe(first.columns);
expect(second.data).toBe(first.data);
});
test('transformProps busts its memoization caches when sub-field inputs change (cache miss)', () => {
const first = transformProps(testData.basic);
// `processColumns` is wrapped with a custom equality (`isEqualColumns`) that
// compares specific chartProps sub-fields by identity — mutating only the
// top-level props reference is NOT enough to bust it. Here we supply a fresh
// `datasource.columnFormats` reference, which `isEqualColumns` compares with
// `===`, forcing `processColumns` to recompute and return a new `columns`
// array.
//
// `processDataRecords` uses memoize-one's default referential equality on
// `(data, columns)`. We also hand it a fresh `queriesData[0].data` array, so
// together with the recomputed `columns` reference it too cache-misses.
const freshProps = {
...testData.basic,
datasource: {
...testData.basic.datasource,
columnFormats: {},
},
queriesData: [
{
...testData.basic.queriesData[0],
data: [...(testData.basic.queriesData[0].data || [])],
},
],
};
const second = transformProps(freshProps);
expect(second.columns).not.toBe(first.columns);
expect(second.data).not.toBe(first.data);
});
test('transformProps memoizes the comparison-mode data pipeline on identical input', () => {
// Exercises `processComparisonDataRecords` (the third of four memoizeOne
// callsites in transformProps.ts) via the `comparison` fixture, which has
// `time_compare` set and therefore flows through the comparison branch
// where `passedData = comparisonData`.
//
// Note: we don't assert reference equality on `columns` here because the
// comparison branch runs `comparisonColumns` through the non-memoized
// `processComparisonColumns` helper, which returns a fresh array on each
// call by design.
const first = transformProps(testData.comparison);
const second = transformProps(testData.comparison);
expect(second.data).toBe(first.data);
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { GenericDataType } from '@apache-superset/core/common';
import {
supersetTheme,

View File

@@ -47,7 +47,7 @@
"geostyler-wfs-parser": "^3.0.1",
"ol": "^10.8.0",
"polished": "*",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

@@ -19,7 +19,7 @@
import Layer from 'ol/layer/Layer';
import { FrameState } from 'ol/Map';
import { apply as applyTransform } from 'ol/transform';
import ReactDOM from 'react-dom';
import { createRoot, Root } from 'react-dom/client';
import { SupersetTheme } from '@apache-superset/core/theme';
import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types';
import { createChartComponent } from '../util/chartUtil';
@@ -31,7 +31,14 @@ import Loader from '../images/loading.gif';
* Custom OpenLayers layer that displays charts on given locations.
*/
export class ChartLayer extends Layer {
charts: any[] = [];
charts: {
htmlElement: HTMLDivElement;
root: Root;
coordinate: number[];
width: number;
height: number;
feature: any;
}[] = [];
chartConfigs: ChartConfig = {
type: 'FeatureCollection',
@@ -166,7 +173,7 @@ export class ChartLayer extends Layer {
*/
removeAllChartElements() {
this.charts.forEach(chart => {
ReactDOM.unmountComponentAtNode(chart.htmlElement);
chart.root.unmount();
chart.htmlElement.remove();
});
this.charts = [];
@@ -191,10 +198,12 @@ export class ChartLayer extends Layer {
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, container);
const root = createRoot(container);
root.render(chartComponent);
return {
htmlElement: container,
root,
coordinate: getProjectedCoordinateFromPointGeoJson(feature.geometry),
width: chartWidth,
height: chartHeight,
@@ -227,7 +236,7 @@ export class ChartLayer extends Layer {
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, chart.htmlElement);
chart.root.render(chartComponent);
return {
...chart,

View File

@@ -41,6 +41,11 @@ describe('ChartLayer', () => {
chartLayer.charts = [
{
htmlElement: document.createElement('div'),
root: { render: jest.fn(), unmount: jest.fn() } as any,
coordinate: [0, 0],
width: 100,
height: 100,
feature: {},
},
];

View File

@@ -29,7 +29,7 @@
"acorn": "^8.16.0",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"zod": "^4.4.1"
"zod": "^4.4.3"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -38,7 +38,7 @@
"dayjs": "^1.11.19",
"echarts": "*",
"memoize-one": "*",
"react": "^17.0.2"
"react": "^18.2.0"
},
"publishConfig": {
"access": "public"

View File

@@ -57,7 +57,7 @@ export default function EchartsMixedTimeseries({
);
const getCrossFilterDataMask = useCallback(
(seriesName, seriesIndex) => {
(seriesName: string, seriesIndex: number) => {
const selected: string[] = Object.values(selectedValues || {});
let values: string[];
if (selected.includes(seriesName)) {

View File

@@ -26,7 +26,7 @@ import {
import { useCallback } from 'react';
import Echart from '../components/Echart';
import { NULL_STRING } from '../constants';
import { EventHandlers } from '../types';
import { EventHandlers, TreePathInfo } from '../types';
import { extractTreePathInfo } from './constants';
import { TreemapTransformedProps } from './types';
import { formatSeriesName } from '../utils/series';
@@ -46,7 +46,7 @@ export default function EchartsTreemap({
coltypeMapping,
}: TreemapTransformedProps) {
const getCrossFilterDataMask = useCallback(
(data, treePathInfo) => {
(data: Record<string, unknown>, treePathInfo: TreePathInfo[]) => {
if (data?.children) {
return undefined;
}
@@ -96,7 +96,7 @@ export default function EchartsTreemap({
);
const handleChange = useCallback(
(data, treePathInfo) => {
(data: Record<string, unknown>, treePathInfo: TreePathInfo[]) => {
if (!emitCrossFilters || groupby.length === 0) {
return;
}

View File

@@ -20,12 +20,14 @@ import {
AnnotationStyle,
AnnotationType,
AnnotationSourceType,
AxisType,
DataRecord,
FormulaAnnotationLayer,
IntervalAnnotationLayer,
VizType,
ChartDataResponseResult,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import {
LegendOrientation,
LegendType,
@@ -496,3 +498,133 @@ test('should add a formula annotation when X-axis column has dataset-level label
expect(Array.isArray(formulaSeries?.data)).toBe(true);
expect((formulaSeries!.data as unknown[]).length).toBeGreaterThan(0);
});
test('numeric x coltype never gets silently coerced to the Time axis', () => {
// Regression guard for echarts-timeseries-epoch-x-axis-labels investigation.
// Mixed Timeseries must follow the reported coltype: Numeric values stay
// off the Time axis and are not silently reinterpreted as Date instances.
// A future change that coerces Numeric → Time would bring back the "NaN"
// label symptom we were investigating. We also assert that whichever
// formatter is picked, it produces a string and does not emit "NaN".
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const epochRows = [
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
];
const epochQueryData = createTestQueryData(epochRows, {
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [epochQueryData, epochQueryData],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['metric'],
metricsB: ['metric'],
groupby: [],
groupbyB: [],
},
queriesData: [epochQueryData, epochQueryData],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as {
type: string;
axisLabel: { formatter: (v: number) => string };
};
expect(xAxis.type).not.toBe(AxisType.Time);
const label = xAxis.axisLabel.formatter(ts1);
expect(typeof label).toBe('string');
expect(label).not.toMatch(/NaN/);
});
test('xAxisForceCategorical forces Category axis regardless of Numeric coltype', () => {
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const epochRows = [
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
];
const epochQueryData = createTestQueryData(epochRows, {
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [epochQueryData, epochQueryData],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['metric'],
metricsB: ['metric'],
groupby: [],
groupbyB: [],
xAxisForceCategorical: true,
},
queriesData: [epochQueryData, epochQueryData],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as { type: string };
expect(xAxis.type).toBe(AxisType.Category);
});
test('temporal x coltype wires the time formatter and Time axis', () => {
// Regression guard: the happy path for mixed-timeseries charts. Ensures
// Temporal coltype still routes through the TimeFormatter so the time axis
// rendering path is exercised by the test suite.
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const temporalRows = [
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
];
const temporalQueryData = createTestQueryData(temporalRows, {
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [temporalQueryData, temporalQueryData],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['metric'],
metricsB: ['metric'],
groupby: [],
groupbyB: [],
},
queriesData: [temporalQueryData, temporalQueryData],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as {
type: string;
axisLabel: { formatter: (v: Date) => string };
};
expect(xAxis.type).toBe(AxisType.Time);
const label = xAxis.axisLabel.formatter(new Date(ts1));
expect(typeof label).toBe('string');
expect(label).not.toMatch(/NaN/);
});

View File

@@ -20,6 +20,7 @@ import {
AnnotationSourceType,
AnnotationStyle,
AnnotationType,
AxisType,
ComparisonType,
DataRecord,
EventAnnotationLayer,
@@ -1472,6 +1473,118 @@ test('x-axis formatter deduplicates consecutive identical labels for coarse time
expect(label4).toBe('');
});
test('numeric x coltype routes through the number formatter (not the time formatter)', () => {
// Regression guard for echarts-timeseries-epoch-x-axis-labels investigation.
// When the query reports a Numeric x-axis coltype (including epoch-ms-like
// values), Timeseries transformProps must pick the Value axis and run the
// label through getNumberFormatter, not the time formatter. If this ever
// changes, epoch-ms values that arrive as Numeric would suddenly be treated
// as Date instances and could render "NaN" — the symptom that prompted this
// investigation.
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const chartProps = createTestChartProps({
formData: {
metrics: ['metric'],
granularity_sqla: 'ds',
x_axis: '__timestamp',
},
queriesData: [
createTestQueryData(
[
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
],
{
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
},
),
],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as {
type: string;
axisLabel: { formatter: (v: number) => string };
};
expect(xAxis.type).toBe(AxisType.Value);
const label = xAxis.axisLabel.formatter(ts1);
expect(typeof label).toBe('string');
expect(label).not.toMatch(/NaN/);
});
test('xAxisForceCategorical forces Category axis regardless of Numeric coltype', () => {
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const chartProps = createTestChartProps({
formData: {
metrics: ['metric'],
granularity_sqla: 'ds',
x_axis: '__timestamp',
xAxisForceCategorical: true,
},
queriesData: [
createTestQueryData(
[
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
],
{
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
},
),
],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as { type: string };
expect(xAxis.type).toBe(AxisType.Category);
});
test('temporal x coltype wires the time formatter and Time axis', () => {
// Regression guard: the happy path for time-series charts. Ensures that
// Temporal coltype keeps routing through the TimeFormatter so a refactor
// does not accidentally drop Date handling (the feared regression that
// sparked this investigation).
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const chartProps = createTestChartProps({
formData: {
metrics: ['metric'],
granularity_sqla: 'ds',
x_axis: '__timestamp',
},
queriesData: [
createTestQueryData(
[
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
],
{
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
},
),
],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as {
type: string;
axisLabel: { formatter: (v: Date) => string };
};
expect(xAxis.type).toBe(AxisType.Time);
const label = xAxis.axisLabel.formatter(new Date(ts1));
expect(typeof label).toBe('string');
expect(label).not.toMatch(/NaN/);
expect(label).not.toBe(String(ts1));
});
test('should assign distinct dash patterns for multiple time offsets consistently', () => {
const queriesDataWithMultipleOffsets = [
createTestQueryData([

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