diff --git a/.github/workflows/superset-docs-deploy.yml b/.github/workflows/superset-docs-deploy.yml index 12ae2a095da..c7dc57b4426 100644 --- a/.github/workflows/superset-docs-deploy.yml +++ b/.github/workflows/superset-docs-deploy.yml @@ -68,7 +68,7 @@ jobs: yarn install --check-cache - name: Download database diagnostics (if triggered by integration tests) if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' - uses: dawidd6/action-download-artifact@v16 + uses: dawidd6/action-download-artifact@v17 continue-on-error: true with: workflow: superset-python-integrationtest.yml @@ -77,7 +77,7 @@ jobs: path: docs/src/data/ - name: Try to download latest diagnostics (for push/dispatch triggers) if: github.event_name != 'workflow_run' - uses: dawidd6/action-download-artifact@v16 + uses: dawidd6/action-download-artifact@v17 continue-on-error: true with: workflow: superset-python-integrationtest.yml diff --git a/.github/workflows/superset-docs-verify.yml b/.github/workflows/superset-docs-verify.yml index 613b3ec3660..1306fec1bf1 100644 --- a/.github/workflows/superset-docs-verify.yml +++ b/.github/workflows/superset-docs-verify.yml @@ -111,12 +111,13 @@ jobs: run: | yarn install --check-cache - name: Download database diagnostics from integration tests - uses: dawidd6/action-download-artifact@v16 + uses: dawidd6/action-download-artifact@v17 with: workflow: superset-python-integrationtest.yml run_id: ${{ github.event.workflow_run.id }} name: database-diagnostics path: docs/src/data/ + if_no_artifact_found: 'warning' - name: Use fresh diagnostics run: | if [ -f "src/data/databases-diagnostics.json" ]; then diff --git a/UPDATING.md b/UPDATING.md index 455e9a10ba4..bbfd509ea44 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -24,6 +24,14 @@ assists people when migrating to a new version. ## Next +### Deck.gl MapBox viewport and opacity controls are functional + +The Deck.gl MapBox chart's **Opacity**, **Default longitude**, **Default latitude**, and **Zoom** controls were previously non-functional — changing them had no effect on the rendered map. These controls are now wired up correctly. + +**Behavior change for existing charts:** Previously, the viewport controls had hard-coded default values (`-122.405293`, `37.772123`, zoom `11` — San Francisco) that were stored in each chart's `form_data` but never applied. The map always used `fitBounds` to center on the data. With this fix, those stored values are now respected, which means existing MapBox charts may open centered on the old default coordinates instead of fitting to data bounds. + +**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. + ### 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. diff --git a/docs/developer_docs/extensions/mcp-server.md b/docs/admin_docs/configuration/mcp-server.mdx similarity index 96% rename from docs/developer_docs/extensions/mcp-server.md rename to docs/admin_docs/configuration/mcp-server.mdx index 769ada1bc0d..22a80d645b3 100644 --- a/docs/developer_docs/extensions/mcp-server.md +++ b/docs/admin_docs/configuration/mcp-server.mdx @@ -1,7 +1,7 @@ --- title: MCP Server Deployment & Authentication hide_title: true -sidebar_position: 9 +sidebar_position: 14 version: 1 --- @@ -30,6 +30,10 @@ Superset includes a built-in [Model Context Protocol (MCP)](https://modelcontext This guide covers how to run, secure, and deploy the MCP server. +:::tip Looking for user docs? +See **[Using AI with Superset](/user-docs/using-superset/using-ai-with-superset)** for a guide on what AI can do with Superset and how to connect your AI client. +::: + ```mermaid flowchart LR A["AI Client
(Claude, ChatGPT, etc.)"] -- "MCP protocol
(HTTP + JSON-RPC)" --> B["MCP Server
(:5008/mcp)"] @@ -668,12 +672,13 @@ MCP_CSRF_CONFIG = { - **Secrets management** -- Store `MCP_JWT_SECRET`, database credentials, and API keys in environment variables or a secrets manager, never in config files committed to version control - **Scoped tokens** -- Use `MCP_REQUIRED_SCOPES` to limit what operations a token can perform - **Network isolation** -- In Kubernetes, restrict MCP pod network policies to only allow traffic from your AI client endpoints -- Review the **[Security documentation](./security)** for additional extension security guidance +- Review the **[Security documentation](/developer-docs/extensions/security)** for additional extension security guidance --- ## Next Steps -- **[MCP Integration](./mcp)** -- Build custom MCP tools and prompts via Superset extensions -- **[Security](./security)** -- Security best practices for extensions -- **[Deployment](./deployment)** -- Package and deploy Superset extensions +- **[Using AI with Superset](/user-docs/using-superset/using-ai-with-superset)** -- What AI can do with Superset and how to get started +- **[MCP Integration](/developer-docs/extensions/mcp)** -- Build custom MCP tools and prompts via Superset extensions +- **[Security](/developer-docs/extensions/security)** -- Security best practices for extensions +- **[Deployment](/developer-docs/extensions/deployment)** -- Package and deploy Superset extensions diff --git a/docs/developer_docs/api.mdx b/docs/developer_docs/api.mdx index 09f3843087f..c97f8fb5648 100644 --- a/docs/developer_docs/api.mdx +++ b/docs/developer_docs/api.mdx @@ -47,10 +47,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get the CSRF token](./api/get-the-csrf-token) | `/api/v1/security/csrf_token/` | -| `POST` | [Get a guest token](./api/get-a-guest-token) | `/api/v1/security/guest_token/` | -| `POST` | [Create security login](./api/create-security-login) | `/api/v1/security/login` | -| `POST` | [Create security refresh](./api/create-security-refresh) | `/api/v1/security/refresh` | +| `GET` | [Get the CSRF token](/developer-docs/api/get-the-csrf-token) | `/api/v1/security/csrf_token/` | +| `POST` | [Get a guest token](/developer-docs/api/get-a-guest-token) | `/api/v1/security/guest_token/` | +| `POST` | [Create security login](/developer-docs/api/create-security-login) | `/api/v1/security/login` | +| `POST` | [Create security refresh](/developer-docs/api/create-security-refresh) | `/api/v1/security/refresh` | --- @@ -63,32 +63,32 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete dashboards](./api/bulk-delete-dashboards) | `/api/v1/dashboard/` | -| `GET` | [Get a list of dashboards](./api/get-a-list-of-dashboards) | `/api/v1/dashboard/` | -| `POST` | [Create a new dashboard](./api/create-a-new-dashboard) | `/api/v1/dashboard/` | -| `GET` | [Get metadata information about this API resource (dashboard--info)](./api/get-metadata-information-about-this-api-resource-dashboard-info) | `/api/v1/dashboard/_info` | -| `GET` | [Get a dashboard detail information](./api/get-a-dashboard-detail-information) | `/api/v1/dashboard/{id_or_slug}` | -| `GET` | [Get a dashboard's chart definitions.](./api/get-a-dashboard-s-chart-definitions) | `/api/v1/dashboard/{id_or_slug}/charts` | -| `POST` | [Create a copy of an existing dashboard](./api/create-a-copy-of-an-existing-dashboard) | `/api/v1/dashboard/{id_or_slug}/copy/` | -| `GET` | [Get dashboard's datasets](./api/get-dashboard-s-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` | -| `DELETE` | [Delete a dashboard's embedded configuration](./api/delete-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` | -| `GET` | [Get the dashboard's embedded configuration](./api/get-the-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` | -| `POST` | [Set a dashboard's embedded configuration](./api/set-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` | -| `PUT` | [Update dashboard by id_or_slug embedded](./api/update-dashboard-by-id-or-slug-embedded) | `/api/v1/dashboard/{id_or_slug}/embedded` | -| `GET` | [Get dashboard's tabs](./api/get-dashboard-s-tabs) | `/api/v1/dashboard/{id_or_slug}/tabs` | -| `DELETE` | [Delete a dashboard](./api/delete-a-dashboard) | `/api/v1/dashboard/{pk}` | -| `PUT` | [Update a dashboard](./api/update-a-dashboard) | `/api/v1/dashboard/{pk}` | -| `POST` | [Compute and cache a screenshot (dashboard-pk-cache-dashboard-screenshot)](./api/compute-and-cache-a-screenshot-dashboard-pk-cache-dashboard-screenshot) | `/api/v1/dashboard/{pk}/cache_dashboard_screenshot/` | -| `PUT` | [Update colors configuration for a dashboard.](./api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` | -| `DELETE` | [Remove the dashboard from the user favorite list](./api/remove-the-dashboard-from-the-user-favorite-list) | `/api/v1/dashboard/{pk}/favorites/` | -| `POST` | [Mark the dashboard as favorite for the current user](./api/mark-the-dashboard-as-favorite-for-the-current-user) | `/api/v1/dashboard/{pk}/favorites/` | -| `PUT` | [Update native filters configuration for a dashboard.](./api/update-native-filters-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/filters` | -| `GET` | [Get a computed screenshot from cache (dashboard-pk-screenshot-digest)](./api/get-a-computed-screenshot-from-cache-dashboard-pk-screenshot-digest) | `/api/v1/dashboard/{pk}/screenshot/{digest}/` | -| `GET` | [Get dashboard's thumbnail](./api/get-dashboard-s-thumbnail) | `/api/v1/dashboard/{pk}/thumbnail/{digest}/` | -| `GET` | [Download multiple dashboards as YAML files](./api/download-multiple-dashboards-as-yaml-files) | `/api/v1/dashboard/export/` | -| `GET` | [Check favorited dashboards for current user](./api/check-favorited-dashboards-for-current-user) | `/api/v1/dashboard/favorite_status/` | -| `POST` | [Import dashboard(s) with associated charts/datasets/databases](./api/import-dashboard-s-with-associated-charts-datasets-databases) | `/api/v1/dashboard/import/` | -| `GET` | [Get related fields data (dashboard-related-column-name)](./api/get-related-fields-data-dashboard-related-column-name) | `/api/v1/dashboard/related/{column_name}` | +| `DELETE` | [Bulk delete dashboards](/developer-docs/api/bulk-delete-dashboards) | `/api/v1/dashboard/` | +| `GET` | [Get a list of dashboards](/developer-docs/api/get-a-list-of-dashboards) | `/api/v1/dashboard/` | +| `POST` | [Create a new dashboard](/developer-docs/api/create-a-new-dashboard) | `/api/v1/dashboard/` | +| `GET` | [Get metadata information about this API resource (dashboard--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-dashboard-info) | `/api/v1/dashboard/_info` | +| `GET` | [Get a dashboard detail information](/developer-docs/api/get-a-dashboard-detail-information) | `/api/v1/dashboard/{id_or_slug}` | +| `GET` | [Get a dashboard's chart definitions.](/developer-docs/api/get-a-dashboard-s-chart-definitions) | `/api/v1/dashboard/{id_or_slug}/charts` | +| `POST` | [Create a copy of an existing dashboard](/developer-docs/api/create-a-copy-of-an-existing-dashboard) | `/api/v1/dashboard/{id_or_slug}/copy/` | +| `GET` | [Get dashboard's datasets](/developer-docs/api/get-dashboard-s-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` | +| `DELETE` | [Delete a dashboard's embedded configuration](/developer-docs/api/delete-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` | +| `GET` | [Get the dashboard's embedded configuration](/developer-docs/api/get-the-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` | +| `POST` | [Set a dashboard's embedded configuration](/developer-docs/api/set-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` | +| `PUT` | [Update dashboard by id_or_slug embedded](/developer-docs/api/update-dashboard-by-id-or-slug-embedded) | `/api/v1/dashboard/{id_or_slug}/embedded` | +| `GET` | [Get dashboard's tabs](/developer-docs/api/get-dashboard-s-tabs) | `/api/v1/dashboard/{id_or_slug}/tabs` | +| `DELETE` | [Delete a dashboard](/developer-docs/api/delete-a-dashboard) | `/api/v1/dashboard/{pk}` | +| `PUT` | [Update a dashboard](/developer-docs/api/update-a-dashboard) | `/api/v1/dashboard/{pk}` | +| `POST` | [Compute and cache a screenshot (dashboard-pk-cache-dashboard-screenshot)](/developer-docs/api/compute-and-cache-a-screenshot-dashboard-pk-cache-dashboard-screenshot) | `/api/v1/dashboard/{pk}/cache_dashboard_screenshot/` | +| `PUT` | [Update colors configuration for a dashboard.](/developer-docs/api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` | +| `DELETE` | [Remove the dashboard from the user favorite list](/developer-docs/api/remove-the-dashboard-from-the-user-favorite-list) | `/api/v1/dashboard/{pk}/favorites/` | +| `POST` | [Mark the dashboard as favorite for the current user](/developer-docs/api/mark-the-dashboard-as-favorite-for-the-current-user) | `/api/v1/dashboard/{pk}/favorites/` | +| `PUT` | [Update native filters configuration for a dashboard.](/developer-docs/api/update-native-filters-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/filters` | +| `GET` | [Get a computed screenshot from cache (dashboard-pk-screenshot-digest)](/developer-docs/api/get-a-computed-screenshot-from-cache-dashboard-pk-screenshot-digest) | `/api/v1/dashboard/{pk}/screenshot/{digest}/` | +| `GET` | [Get dashboard's thumbnail](/developer-docs/api/get-dashboard-s-thumbnail) | `/api/v1/dashboard/{pk}/thumbnail/{digest}/` | +| `GET` | [Download multiple dashboards as YAML files](/developer-docs/api/download-multiple-dashboards-as-yaml-files) | `/api/v1/dashboard/export/` | +| `GET` | [Check favorited dashboards for current user](/developer-docs/api/check-favorited-dashboards-for-current-user) | `/api/v1/dashboard/favorite_status/` | +| `POST` | [Import dashboard(s) with associated charts/datasets/databases](/developer-docs/api/import-dashboard-s-with-associated-charts-datasets-databases) | `/api/v1/dashboard/import/` | +| `GET` | [Get related fields data (dashboard-related-column-name)](/developer-docs/api/get-related-fields-data-dashboard-related-column-name) | `/api/v1/dashboard/related/{column_name}` | @@ -97,26 +97,26 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete charts](./api/bulk-delete-charts) | `/api/v1/chart/` | -| `GET` | [Get a list of charts](./api/get-a-list-of-charts) | `/api/v1/chart/` | -| `POST` | [Create a new chart](./api/create-a-new-chart) | `/api/v1/chart/` | -| `GET` | [Get metadata information about this API resource (chart--info)](./api/get-metadata-information-about-this-api-resource-chart-info) | `/api/v1/chart/_info` | -| `DELETE` | [Delete a chart](./api/delete-a-chart) | `/api/v1/chart/{pk}` | -| `GET` | [Get a chart detail information](./api/get-a-chart-detail-information) | `/api/v1/chart/{pk}` | -| `PUT` | [Update a chart](./api/update-a-chart) | `/api/v1/chart/{pk}` | -| `GET` | [Compute and cache a screenshot (chart-pk-cache-screenshot)](./api/compute-and-cache-a-screenshot-chart-pk-cache-screenshot) | `/api/v1/chart/{pk}/cache_screenshot/` | -| `GET` | [Return payload data response for a chart](./api/return-payload-data-response-for-a-chart) | `/api/v1/chart/{pk}/data/` | -| `DELETE` | [Remove the chart from the user favorite list](./api/remove-the-chart-from-the-user-favorite-list) | `/api/v1/chart/{pk}/favorites/` | -| `POST` | [Mark the chart as favorite for the current user](./api/mark-the-chart-as-favorite-for-the-current-user) | `/api/v1/chart/{pk}/favorites/` | -| `GET` | [Get a computed screenshot from cache (chart-pk-screenshot-digest)](./api/get-a-computed-screenshot-from-cache-chart-pk-screenshot-digest) | `/api/v1/chart/{pk}/screenshot/{digest}/` | -| `GET` | [Get chart thumbnail](./api/get-chart-thumbnail) | `/api/v1/chart/{pk}/thumbnail/{digest}/` | -| `POST` | [Return payload data response for the given query (chart-data)](./api/return-payload-data-response-for-the-given-query-chart-data) | `/api/v1/chart/data` | -| `GET` | [Return payload data response for the given query (chart-data-cache-key)](./api/return-payload-data-response-for-the-given-query-chart-data-cache-key) | `/api/v1/chart/data/{cache_key}` | -| `GET` | [Download multiple charts as YAML files](./api/download-multiple-charts-as-yaml-files) | `/api/v1/chart/export/` | -| `GET` | [Check favorited charts for current user](./api/check-favorited-charts-for-current-user) | `/api/v1/chart/favorite_status/` | -| `POST` | [Import chart(s) with associated datasets and databases](./api/import-chart-s-with-associated-datasets-and-databases) | `/api/v1/chart/import/` | -| `GET` | [Get related fields data (chart-related-column-name)](./api/get-related-fields-data-chart-related-column-name) | `/api/v1/chart/related/{column_name}` | -| `PUT` | [Warm up the cache for the chart](./api/warm-up-the-cache-for-the-chart) | `/api/v1/chart/warm_up_cache` | +| `DELETE` | [Bulk delete charts](/developer-docs/api/bulk-delete-charts) | `/api/v1/chart/` | +| `GET` | [Get a list of charts](/developer-docs/api/get-a-list-of-charts) | `/api/v1/chart/` | +| `POST` | [Create a new chart](/developer-docs/api/create-a-new-chart) | `/api/v1/chart/` | +| `GET` | [Get metadata information about this API resource (chart--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-chart-info) | `/api/v1/chart/_info` | +| `DELETE` | [Delete a chart](/developer-docs/api/delete-a-chart) | `/api/v1/chart/{pk}` | +| `GET` | [Get a chart detail information](/developer-docs/api/get-a-chart-detail-information) | `/api/v1/chart/{pk}` | +| `PUT` | [Update a chart](/developer-docs/api/update-a-chart) | `/api/v1/chart/{pk}` | +| `GET` | [Compute and cache a screenshot (chart-pk-cache-screenshot)](/developer-docs/api/compute-and-cache-a-screenshot-chart-pk-cache-screenshot) | `/api/v1/chart/{pk}/cache_screenshot/` | +| `GET` | [Return payload data response for a chart](/developer-docs/api/return-payload-data-response-for-a-chart) | `/api/v1/chart/{pk}/data/` | +| `DELETE` | [Remove the chart from the user favorite list](/developer-docs/api/remove-the-chart-from-the-user-favorite-list) | `/api/v1/chart/{pk}/favorites/` | +| `POST` | [Mark the chart as favorite for the current user](/developer-docs/api/mark-the-chart-as-favorite-for-the-current-user) | `/api/v1/chart/{pk}/favorites/` | +| `GET` | [Get a computed screenshot from cache (chart-pk-screenshot-digest)](/developer-docs/api/get-a-computed-screenshot-from-cache-chart-pk-screenshot-digest) | `/api/v1/chart/{pk}/screenshot/{digest}/` | +| `GET` | [Get chart thumbnail](/developer-docs/api/get-chart-thumbnail) | `/api/v1/chart/{pk}/thumbnail/{digest}/` | +| `POST` | [Return payload data response for the given query (chart-data)](/developer-docs/api/return-payload-data-response-for-the-given-query-chart-data) | `/api/v1/chart/data` | +| `GET` | [Return payload data response for the given query (chart-data-cache-key)](/developer-docs/api/return-payload-data-response-for-the-given-query-chart-data-cache-key) | `/api/v1/chart/data/{cache_key}` | +| `GET` | [Download multiple charts as YAML files](/developer-docs/api/download-multiple-charts-as-yaml-files) | `/api/v1/chart/export/` | +| `GET` | [Check favorited charts for current user](/developer-docs/api/check-favorited-charts-for-current-user) | `/api/v1/chart/favorite_status/` | +| `POST` | [Import chart(s) with associated datasets and databases](/developer-docs/api/import-chart-s-with-associated-datasets-and-databases) | `/api/v1/chart/import/` | +| `GET` | [Get related fields data (chart-related-column-name)](/developer-docs/api/get-related-fields-data-chart-related-column-name) | `/api/v1/chart/related/{column_name}` | +| `PUT` | [Warm up the cache for the chart](/developer-docs/api/warm-up-the-cache-for-the-chart) | `/api/v1/chart/warm_up_cache` | @@ -125,24 +125,24 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete datasets](./api/bulk-delete-datasets) | `/api/v1/dataset/` | -| `GET` | [Get a list of datasets](./api/get-a-list-of-datasets) | `/api/v1/dataset/` | -| `POST` | [Create a new dataset](./api/create-a-new-dataset) | `/api/v1/dataset/` | -| `GET` | [Get metadata information about this API resource (dataset--info)](./api/get-metadata-information-about-this-api-resource-dataset-info) | `/api/v1/dataset/_info` | -| `DELETE` | [Delete a dataset](./api/delete-a-dataset) | `/api/v1/dataset/{pk}` | -| `GET` | [Get a dataset](./api/get-a-dataset) | `/api/v1/dataset/{pk}` | -| `PUT` | [Update a dataset](./api/update-a-dataset) | `/api/v1/dataset/{pk}` | -| `DELETE` | [Delete a dataset column](./api/delete-a-dataset-column) | `/api/v1/dataset/{pk}/column/{column_id}` | -| `DELETE` | [Delete a dataset metric](./api/delete-a-dataset-metric) | `/api/v1/dataset/{pk}/metric/{metric_id}` | -| `PUT` | [Refresh and update columns of a dataset](./api/refresh-and-update-columns-of-a-dataset) | `/api/v1/dataset/{pk}/refresh` | -| `GET` | [Get charts and dashboards count associated to a dataset](./api/get-charts-and-dashboards-count-associated-to-a-dataset) | `/api/v1/dataset/{pk}/related_objects` | -| `GET` | [Get distinct values from field data (dataset-distinct-column-name)](./api/get-distinct-values-from-field-data-dataset-distinct-column-name) | `/api/v1/dataset/distinct/{column_name}` | -| `POST` | [Duplicate a dataset](./api/duplicate-a-dataset) | `/api/v1/dataset/duplicate` | -| `GET` | [Download multiple datasets as YAML files](./api/download-multiple-datasets-as-yaml-files) | `/api/v1/dataset/export/` | -| `POST` | [Retrieve a table by name, or create it if it does not exist](./api/retrieve-a-table-by-name-or-create-it-if-it-does-not-exist) | `/api/v1/dataset/get_or_create/` | -| `POST` | [Import dataset(s) with associated databases](./api/import-dataset-s-with-associated-databases) | `/api/v1/dataset/import/` | -| `GET` | [Get related fields data (dataset-related-column-name)](./api/get-related-fields-data-dataset-related-column-name) | `/api/v1/dataset/related/{column_name}` | -| `PUT` | [Warm up the cache for each chart powered by the given table](./api/warm-up-the-cache-for-each-chart-powered-by-the-given-table) | `/api/v1/dataset/warm_up_cache` | +| `DELETE` | [Bulk delete datasets](/developer-docs/api/bulk-delete-datasets) | `/api/v1/dataset/` | +| `GET` | [Get a list of datasets](/developer-docs/api/get-a-list-of-datasets) | `/api/v1/dataset/` | +| `POST` | [Create a new dataset](/developer-docs/api/create-a-new-dataset) | `/api/v1/dataset/` | +| `GET` | [Get metadata information about this API resource (dataset--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-dataset-info) | `/api/v1/dataset/_info` | +| `DELETE` | [Delete a dataset](/developer-docs/api/delete-a-dataset) | `/api/v1/dataset/{pk}` | +| `GET` | [Get a dataset](/developer-docs/api/get-a-dataset) | `/api/v1/dataset/{pk}` | +| `PUT` | [Update a dataset](/developer-docs/api/update-a-dataset) | `/api/v1/dataset/{pk}` | +| `DELETE` | [Delete a dataset column](/developer-docs/api/delete-a-dataset-column) | `/api/v1/dataset/{pk}/column/{column_id}` | +| `DELETE` | [Delete a dataset metric](/developer-docs/api/delete-a-dataset-metric) | `/api/v1/dataset/{pk}/metric/{metric_id}` | +| `PUT` | [Refresh and update columns of a dataset](/developer-docs/api/refresh-and-update-columns-of-a-dataset) | `/api/v1/dataset/{pk}/refresh` | +| `GET` | [Get charts and dashboards count associated to a dataset](/developer-docs/api/get-charts-and-dashboards-count-associated-to-a-dataset) | `/api/v1/dataset/{pk}/related_objects` | +| `GET` | [Get distinct values from field data (dataset-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-dataset-distinct-column-name) | `/api/v1/dataset/distinct/{column_name}` | +| `POST` | [Duplicate a dataset](/developer-docs/api/duplicate-a-dataset) | `/api/v1/dataset/duplicate` | +| `GET` | [Download multiple datasets as YAML files](/developer-docs/api/download-multiple-datasets-as-yaml-files) | `/api/v1/dataset/export/` | +| `POST` | [Retrieve a table by name, or create it if it does not exist](/developer-docs/api/retrieve-a-table-by-name-or-create-it-if-it-does-not-exist) | `/api/v1/dataset/get_or_create/` | +| `POST` | [Import dataset(s) with associated databases](/developer-docs/api/import-dataset-s-with-associated-databases) | `/api/v1/dataset/import/` | +| `GET` | [Get related fields data (dataset-related-column-name)](/developer-docs/api/get-related-fields-data-dataset-related-column-name) | `/api/v1/dataset/related/{column_name}` | +| `PUT` | [Warm up the cache for each chart powered by the given table](/developer-docs/api/warm-up-the-cache-for-each-chart-powered-by-the-given-table) | `/api/v1/dataset/warm_up_cache` | @@ -151,37 +151,37 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get a list of databases](./api/get-a-list-of-databases) | `/api/v1/database/` | -| `POST` | [Create a new database](./api/create-a-new-database) | `/api/v1/database/` | -| `GET` | [Get metadata information about this API resource (database--info)](./api/get-metadata-information-about-this-api-resource-database-info) | `/api/v1/database/_info` | -| `DELETE` | [Delete a database](./api/delete-a-database) | `/api/v1/database/{pk}` | -| `GET` | [Get a database](./api/get-a-database) | `/api/v1/database/{pk}` | -| `PUT` | [Change a database](./api/change-a-database) | `/api/v1/database/{pk}` | -| `GET` | [Get all catalogs from a database](./api/get-all-catalogs-from-a-database) | `/api/v1/database/{pk}/catalogs/` | -| `GET` | [Get a database connection info](./api/get-a-database-connection-info) | `/api/v1/database/{pk}/connection` | -| `GET` | [Get function names supported by a database](./api/get-function-names-supported-by-a-database) | `/api/v1/database/{pk}/function_names/` | -| `GET` | [Get charts and dashboards count associated to a database](./api/get-charts-and-dashboards-count-associated-to-a-database) | `/api/v1/database/{pk}/related_objects/` | -| `GET` | [The list of the database schemas where to upload information](./api/the-list-of-the-database-schemas-where-to-upload-information) | `/api/v1/database/{pk}/schemas_access_for_file_upload/` | -| `GET` | [Get all schemas from a database](./api/get-all-schemas-from-a-database) | `/api/v1/database/{pk}/schemas/` | -| `GET` | [Get database select star for table (database-pk-select-star-table-name)](./api/get-database-select-star-for-table-database-pk-select-star-table-name) | `/api/v1/database/{pk}/select_star/{table_name}/` | -| `GET` | [Get database select star for table (database-pk-select-star-table-name-schema-name)](./api/get-database-select-star-for-table-database-pk-select-star-table-name-schema-name) | `/api/v1/database/{pk}/select_star/{table_name}/{schema_name}/` | -| `DELETE` | [Delete a SSH tunnel](./api/delete-a-ssh-tunnel) | `/api/v1/database/{pk}/ssh_tunnel/` | -| `POST` | [Re-sync all permissions for a database connection](./api/re-sync-all-permissions-for-a-database-connection) | `/api/v1/database/{pk}/sync_permissions/` | -| `GET` | [Get table extra metadata (database-pk-table-extra-table-name-schema-name)](./api/get-table-extra-metadata-database-pk-table-extra-table-name-schema-name) | `/api/v1/database/{pk}/table_extra/{table_name}/{schema_name}/` | -| `GET` | [Get table metadata](./api/get-table-metadata) | `/api/v1/database/{pk}/table_metadata/` | -| `GET` | [Get table extra metadata (database-pk-table-metadata-extra)](./api/get-table-extra-metadata-database-pk-table-metadata-extra) | `/api/v1/database/{pk}/table_metadata/extra/` | -| `GET` | [Get database table metadata](./api/get-database-table-metadata) | `/api/v1/database/{pk}/table/{table_name}/{schema_name}/` | -| `GET` | [Get a list of tables for given database](./api/get-a-list-of-tables-for-given-database) | `/api/v1/database/{pk}/tables/` | -| `POST` | [Upload a file to a database table](./api/upload-a-file-to-a-database-table) | `/api/v1/database/{pk}/upload/` | -| `POST` | [Validate arbitrary SQL](./api/validate-arbitrary-sql) | `/api/v1/database/{pk}/validate_sql/` | -| `GET` | [Get names of databases currently available](./api/get-names-of-databases-currently-available) | `/api/v1/database/available/` | -| `GET` | [Download database(s) and associated dataset(s) as a zip file](./api/download-database-s-and-associated-dataset-s-as-a-zip-file) | `/api/v1/database/export/` | -| `POST` | [Import database(s) with associated datasets](./api/import-database-s-with-associated-datasets) | `/api/v1/database/import/` | -| `GET` | [Receive personal access tokens from OAuth2](./api/receive-personal-access-tokens-from-oauth2) | `/api/v1/database/oauth2/` | -| `GET` | [Get related fields data (database-related-column-name)](./api/get-related-fields-data-database-related-column-name) | `/api/v1/database/related/{column_name}` | -| `POST` | [Test a database connection](./api/test-a-database-connection) | `/api/v1/database/test_connection/` | -| `POST` | [Upload a file and returns file metadata](./api/upload-a-file-and-returns-file-metadata) | `/api/v1/database/upload_metadata/` | -| `POST` | [Validate database connection parameters](./api/validate-database-connection-parameters) | `/api/v1/database/validate_parameters/` | +| `GET` | [Get a list of databases](/developer-docs/api/get-a-list-of-databases) | `/api/v1/database/` | +| `POST` | [Create a new database](/developer-docs/api/create-a-new-database) | `/api/v1/database/` | +| `GET` | [Get metadata information about this API resource (database--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-database-info) | `/api/v1/database/_info` | +| `DELETE` | [Delete a database](/developer-docs/api/delete-a-database) | `/api/v1/database/{pk}` | +| `GET` | [Get a database](/developer-docs/api/get-a-database) | `/api/v1/database/{pk}` | +| `PUT` | [Change a database](/developer-docs/api/change-a-database) | `/api/v1/database/{pk}` | +| `GET` | [Get all catalogs from a database](/developer-docs/api/get-all-catalogs-from-a-database) | `/api/v1/database/{pk}/catalogs/` | +| `GET` | [Get a database connection info](/developer-docs/api/get-a-database-connection-info) | `/api/v1/database/{pk}/connection` | +| `GET` | [Get function names supported by a database](/developer-docs/api/get-function-names-supported-by-a-database) | `/api/v1/database/{pk}/function_names/` | +| `GET` | [Get charts and dashboards count associated to a database](/developer-docs/api/get-charts-and-dashboards-count-associated-to-a-database) | `/api/v1/database/{pk}/related_objects/` | +| `GET` | [The list of the database schemas where to upload information](/developer-docs/api/the-list-of-the-database-schemas-where-to-upload-information) | `/api/v1/database/{pk}/schemas_access_for_file_upload/` | +| `GET` | [Get all schemas from a database](/developer-docs/api/get-all-schemas-from-a-database) | `/api/v1/database/{pk}/schemas/` | +| `GET` | [Get database select star for table (database-pk-select-star-table-name)](/developer-docs/api/get-database-select-star-for-table-database-pk-select-star-table-name) | `/api/v1/database/{pk}/select_star/{table_name}/` | +| `GET` | [Get database select star for table (database-pk-select-star-table-name-schema-name)](/developer-docs/api/get-database-select-star-for-table-database-pk-select-star-table-name-schema-name) | `/api/v1/database/{pk}/select_star/{table_name}/{schema_name}/` | +| `DELETE` | [Delete a SSH tunnel](/developer-docs/api/delete-a-ssh-tunnel) | `/api/v1/database/{pk}/ssh_tunnel/` | +| `POST` | [Re-sync all permissions for a database connection](/developer-docs/api/re-sync-all-permissions-for-a-database-connection) | `/api/v1/database/{pk}/sync_permissions/` | +| `GET` | [Get table extra metadata (database-pk-table-extra-table-name-schema-name)](/developer-docs/api/get-table-extra-metadata-database-pk-table-extra-table-name-schema-name) | `/api/v1/database/{pk}/table_extra/{table_name}/{schema_name}/` | +| `GET` | [Get table metadata](/developer-docs/api/get-table-metadata) | `/api/v1/database/{pk}/table_metadata/` | +| `GET` | [Get table extra metadata (database-pk-table-metadata-extra)](/developer-docs/api/get-table-extra-metadata-database-pk-table-metadata-extra) | `/api/v1/database/{pk}/table_metadata/extra/` | +| `GET` | [Get database table metadata](/developer-docs/api/get-database-table-metadata) | `/api/v1/database/{pk}/table/{table_name}/{schema_name}/` | +| `GET` | [Get a list of tables for given database](/developer-docs/api/get-a-list-of-tables-for-given-database) | `/api/v1/database/{pk}/tables/` | +| `POST` | [Upload a file to a database table](/developer-docs/api/upload-a-file-to-a-database-table) | `/api/v1/database/{pk}/upload/` | +| `POST` | [Validate arbitrary SQL](/developer-docs/api/validate-arbitrary-sql) | `/api/v1/database/{pk}/validate_sql/` | +| `GET` | [Get names of databases currently available](/developer-docs/api/get-names-of-databases-currently-available) | `/api/v1/database/available/` | +| `GET` | [Download database(s) and associated dataset(s) as a zip file](/developer-docs/api/download-database-s-and-associated-dataset-s-as-a-zip-file) | `/api/v1/database/export/` | +| `POST` | [Import database(s) with associated datasets](/developer-docs/api/import-database-s-with-associated-datasets) | `/api/v1/database/import/` | +| `GET` | [Receive personal access tokens from OAuth2](/developer-docs/api/receive-personal-access-tokens-from-oauth2) | `/api/v1/database/oauth2/` | +| `GET` | [Get related fields data (database-related-column-name)](/developer-docs/api/get-related-fields-data-database-related-column-name) | `/api/v1/database/related/{column_name}` | +| `POST` | [Test a database connection](/developer-docs/api/test-a-database-connection) | `/api/v1/database/test_connection/` | +| `POST` | [Upload a file and returns file metadata](/developer-docs/api/upload-a-file-and-returns-file-metadata) | `/api/v1/database/upload_metadata/` | +| `POST` | [Validate database connection parameters](/developer-docs/api/validate-database-connection-parameters) | `/api/v1/database/validate_parameters/` | @@ -192,7 +192,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Assemble Explore related information in a single endpoint](./api/assemble-explore-related-information-in-a-single-endpoint) | `/api/v1/explore/` | +| `GET` | [Assemble Explore related information in a single endpoint](/developer-docs/api/assemble-explore-related-information-in-a-single-endpoint) | `/api/v1/explore/` | @@ -201,12 +201,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get the bootstrap data for SqlLab page](./api/get-the-bootstrap-data-for-sqllab-page) | `/api/v1/sqllab/` | -| `POST` | [Estimate the SQL query execution cost](./api/estimate-the-sql-query-execution-cost) | `/api/v1/sqllab/estimate/` | -| `POST` | [Execute a SQL query](./api/execute-a-sql-query) | `/api/v1/sqllab/execute/` | -| `GET` | [Export the SQL query results to a CSV](./api/export-the-sql-query-results-to-a-csv) | `/api/v1/sqllab/export/{client_id}/` | -| `POST` | [Format SQL code](./api/format-sql-code) | `/api/v1/sqllab/format_sql/` | -| `GET` | [Get the result of a SQL query execution](./api/get-the-result-of-a-sql-query-execution) | `/api/v1/sqllab/results/` | +| `GET` | [Get the bootstrap data for SqlLab page](/developer-docs/api/get-the-bootstrap-data-for-sqllab-page) | `/api/v1/sqllab/` | +| `POST` | [Estimate the SQL query execution cost](/developer-docs/api/estimate-the-sql-query-execution-cost) | `/api/v1/sqllab/estimate/` | +| `POST` | [Execute a SQL query](/developer-docs/api/execute-a-sql-query) | `/api/v1/sqllab/execute/` | +| `GET` | [Export the SQL query results to a CSV](/developer-docs/api/export-the-sql-query-results-to-a-csv) | `/api/v1/sqllab/export/{client_id}/` | +| `POST` | [Format SQL code](/developer-docs/api/format-sql-code) | `/api/v1/sqllab/format_sql/` | +| `GET` | [Get the result of a SQL query execution](/developer-docs/api/get-the-result-of-a-sql-query-execution) | `/api/v1/sqllab/results/` | @@ -215,23 +215,23 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get a list of queries](./api/get-a-list-of-queries) | `/api/v1/query/` | -| `GET` | [Get query detail information](./api/get-query-detail-information) | `/api/v1/query/{pk}` | -| `GET` | [Get distinct values from field data (query-distinct-column-name)](./api/get-distinct-values-from-field-data-query-distinct-column-name) | `/api/v1/query/distinct/{column_name}` | -| `GET` | [Get related fields data (query-related-column-name)](./api/get-related-fields-data-query-related-column-name) | `/api/v1/query/related/{column_name}` | -| `POST` | [Manually stop a query with client_id](./api/manually-stop-a-query-with-client-id) | `/api/v1/query/stop` | -| `GET` | [Get a list of queries that changed after last_updated_ms](./api/get-a-list-of-queries-that-changed-after-last-updated-ms) | `/api/v1/query/updated_since` | -| `DELETE` | [Bulk delete saved queries](./api/bulk-delete-saved-queries) | `/api/v1/saved_query/` | -| `GET` | [Get a list of saved queries](./api/get-a-list-of-saved-queries) | `/api/v1/saved_query/` | -| `POST` | [Create a saved query](./api/create-a-saved-query) | `/api/v1/saved_query/` | -| `GET` | [Get metadata information about this API resource (saved-query--info)](./api/get-metadata-information-about-this-api-resource-saved-query-info) | `/api/v1/saved_query/_info` | -| `DELETE` | [Delete a saved query](./api/delete-a-saved-query) | `/api/v1/saved_query/{pk}` | -| `GET` | [Get a saved query](./api/get-a-saved-query) | `/api/v1/saved_query/{pk}` | -| `PUT` | [Update a saved query](./api/update-a-saved-query) | `/api/v1/saved_query/{pk}` | -| `GET` | [Get distinct values from field data (saved-query-distinct-column-name)](./api/get-distinct-values-from-field-data-saved-query-distinct-column-name) | `/api/v1/saved_query/distinct/{column_name}` | -| `GET` | [Download multiple saved queries as YAML files](./api/download-multiple-saved-queries-as-yaml-files) | `/api/v1/saved_query/export/` | -| `POST` | [Import saved queries with associated databases](./api/import-saved-queries-with-associated-databases) | `/api/v1/saved_query/import/` | -| `GET` | [Get related fields data (saved-query-related-column-name)](./api/get-related-fields-data-saved-query-related-column-name) | `/api/v1/saved_query/related/{column_name}` | +| `GET` | [Get a list of queries](/developer-docs/api/get-a-list-of-queries) | `/api/v1/query/` | +| `GET` | [Get query detail information](/developer-docs/api/get-query-detail-information) | `/api/v1/query/{pk}` | +| `GET` | [Get distinct values from field data (query-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-query-distinct-column-name) | `/api/v1/query/distinct/{column_name}` | +| `GET` | [Get related fields data (query-related-column-name)](/developer-docs/api/get-related-fields-data-query-related-column-name) | `/api/v1/query/related/{column_name}` | +| `POST` | [Manually stop a query with client_id](/developer-docs/api/manually-stop-a-query-with-client-id) | `/api/v1/query/stop` | +| `GET` | [Get a list of queries that changed after last_updated_ms](/developer-docs/api/get-a-list-of-queries-that-changed-after-last-updated-ms) | `/api/v1/query/updated_since` | +| `DELETE` | [Bulk delete saved queries](/developer-docs/api/bulk-delete-saved-queries) | `/api/v1/saved_query/` | +| `GET` | [Get a list of saved queries](/developer-docs/api/get-a-list-of-saved-queries) | `/api/v1/saved_query/` | +| `POST` | [Create a saved query](/developer-docs/api/create-a-saved-query) | `/api/v1/saved_query/` | +| `GET` | [Get metadata information about this API resource (saved-query--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-saved-query-info) | `/api/v1/saved_query/_info` | +| `DELETE` | [Delete a saved query](/developer-docs/api/delete-a-saved-query) | `/api/v1/saved_query/{pk}` | +| `GET` | [Get a saved query](/developer-docs/api/get-a-saved-query) | `/api/v1/saved_query/{pk}` | +| `PUT` | [Update a saved query](/developer-docs/api/update-a-saved-query) | `/api/v1/saved_query/{pk}` | +| `GET` | [Get distinct values from field data (saved-query-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-saved-query-distinct-column-name) | `/api/v1/saved_query/distinct/{column_name}` | +| `GET` | [Download multiple saved queries as YAML files](/developer-docs/api/download-multiple-saved-queries-as-yaml-files) | `/api/v1/saved_query/export/` | +| `POST` | [Import saved queries with associated databases](/developer-docs/api/import-saved-queries-with-associated-databases) | `/api/v1/saved_query/import/` | +| `GET` | [Get related fields data (saved-query-related-column-name)](/developer-docs/api/get-related-fields-data-saved-query-related-column-name) | `/api/v1/saved_query/related/{column_name}` | @@ -240,7 +240,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get possible values for a datasource column](./api/get-possible-values-for-a-datasource-column) | `/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/` | +| `GET` | [Get possible values for a datasource column](/developer-docs/api/get-possible-values-for-a-datasource-column) | `/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/` | @@ -249,8 +249,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Return an AdvancedDataTypeResponse](./api/return-an-advanceddatatyperesponse) | `/api/v1/advanced_data_type/convert` | -| `GET` | [Return a list of available advanced data types](./api/return-a-list-of-available-advanced-data-types) | `/api/v1/advanced_data_type/types` | +| `GET` | [Return an AdvancedDataTypeResponse](/developer-docs/api/return-an-advanceddatatyperesponse) | `/api/v1/advanced_data_type/convert` | +| `GET` | [Return a list of available advanced data types](/developer-docs/api/return-a-list-of-available-advanced-data-types) | `/api/v1/advanced_data_type/types` | @@ -261,21 +261,21 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete tags](./api/bulk-delete-tags) | `/api/v1/tag/` | -| `GET` | [Get a list of tags](./api/get-a-list-of-tags) | `/api/v1/tag/` | -| `POST` | [Create a tag](./api/create-a-tag) | `/api/v1/tag/` | -| `GET` | [Get metadata information about tag API endpoints](./api/get-metadata-information-about-tag-api-endpoints) | `/api/v1/tag/_info` | -| `POST` | [Add tags to an object](./api/add-tags-to-an-object) | `/api/v1/tag/{object_type}/{object_id}/` | -| `DELETE` | [Delete a tagged object](./api/delete-a-tagged-object) | `/api/v1/tag/{object_type}/{object_id}/{tag}/` | -| `DELETE` | [Delete a tag](./api/delete-a-tag) | `/api/v1/tag/{pk}` | -| `GET` | [Get a tag detail information](./api/get-a-tag-detail-information) | `/api/v1/tag/{pk}` | -| `PUT` | [Update a tag](./api/update-a-tag) | `/api/v1/tag/{pk}` | -| `DELETE` | [Delete tag by pk favorites](./api/delete-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` | -| `POST` | [Create tag by pk favorites](./api/create-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` | -| `POST` | [Bulk create tags and tagged objects](./api/bulk-create-tags-and-tagged-objects) | `/api/v1/tag/bulk_create` | -| `GET` | [Get tag favorite status](./api/get-tag-favorite-status) | `/api/v1/tag/favorite_status/` | -| `GET` | [Get all objects associated with a tag](./api/get-all-objects-associated-with-a-tag) | `/api/v1/tag/get_objects/` | -| `GET` | [Get related fields data (tag-related-column-name)](./api/get-related-fields-data-tag-related-column-name) | `/api/v1/tag/related/{column_name}` | +| `DELETE` | [Bulk delete tags](/developer-docs/api/bulk-delete-tags) | `/api/v1/tag/` | +| `GET` | [Get a list of tags](/developer-docs/api/get-a-list-of-tags) | `/api/v1/tag/` | +| `POST` | [Create a tag](/developer-docs/api/create-a-tag) | `/api/v1/tag/` | +| `GET` | [Get metadata information about tag API endpoints](/developer-docs/api/get-metadata-information-about-tag-api-endpoints) | `/api/v1/tag/_info` | +| `POST` | [Add tags to an object](/developer-docs/api/add-tags-to-an-object) | `/api/v1/tag/{object_type}/{object_id}/` | +| `DELETE` | [Delete a tagged object](/developer-docs/api/delete-a-tagged-object) | `/api/v1/tag/{object_type}/{object_id}/{tag}/` | +| `DELETE` | [Delete a tag](/developer-docs/api/delete-a-tag) | `/api/v1/tag/{pk}` | +| `GET` | [Get a tag detail information](/developer-docs/api/get-a-tag-detail-information) | `/api/v1/tag/{pk}` | +| `PUT` | [Update a tag](/developer-docs/api/update-a-tag) | `/api/v1/tag/{pk}` | +| `DELETE` | [Delete tag by pk favorites](/developer-docs/api/delete-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` | +| `POST` | [Create tag by pk favorites](/developer-docs/api/create-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` | +| `POST` | [Bulk create tags and tagged objects](/developer-docs/api/bulk-create-tags-and-tagged-objects) | `/api/v1/tag/bulk_create` | +| `GET` | [Get tag favorite status](/developer-docs/api/get-tag-favorite-status) | `/api/v1/tag/favorite_status/` | +| `GET` | [Get all objects associated with a tag](/developer-docs/api/get-all-objects-associated-with-a-tag) | `/api/v1/tag/get_objects/` | +| `GET` | [Get related fields data (tag-related-column-name)](/developer-docs/api/get-related-fields-data-tag-related-column-name) | `/api/v1/tag/related/{column_name}` | @@ -284,20 +284,20 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Delete multiple annotation layers in a bulk operation](./api/delete-multiple-annotation-layers-in-a-bulk-operation) | `/api/v1/annotation_layer/` | -| `GET` | [Get a list of annotation layers (annotation-layer)](./api/get-a-list-of-annotation-layers-annotation-layer) | `/api/v1/annotation_layer/` | -| `POST` | [Create an annotation layer (annotation-layer)](./api/create-an-annotation-layer-annotation-layer) | `/api/v1/annotation_layer/` | -| `GET` | [Get metadata information about this API resource (annotation-layer--info)](./api/get-metadata-information-about-this-api-resource-annotation-layer-info) | `/api/v1/annotation_layer/_info` | -| `DELETE` | [Delete annotation layer (annotation-layer-pk)](./api/delete-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` | -| `GET` | [Get an annotation layer (annotation-layer-pk)](./api/get-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` | -| `PUT` | [Update an annotation layer (annotation-layer-pk)](./api/update-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` | -| `DELETE` | [Bulk delete annotation layers](./api/bulk-delete-annotation-layers) | `/api/v1/annotation_layer/{pk}/annotation/` | -| `GET` | [Get a list of annotation layers (annotation-layer-pk-annotation)](./api/get-a-list-of-annotation-layers-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` | -| `POST` | [Create an annotation layer (annotation-layer-pk-annotation)](./api/create-an-annotation-layer-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` | -| `DELETE` | [Delete annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/delete-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` | -| `GET` | [Get an annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/get-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` | -| `PUT` | [Update an annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/update-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` | -| `GET` | [Get related fields data (annotation-layer-related-column-name)](./api/get-related-fields-data-annotation-layer-related-column-name) | `/api/v1/annotation_layer/related/{column_name}` | +| `DELETE` | [Delete multiple annotation layers in a bulk operation](/developer-docs/api/delete-multiple-annotation-layers-in-a-bulk-operation) | `/api/v1/annotation_layer/` | +| `GET` | [Get a list of annotation layers (annotation-layer)](/developer-docs/api/get-a-list-of-annotation-layers-annotation-layer) | `/api/v1/annotation_layer/` | +| `POST` | [Create an annotation layer (annotation-layer)](/developer-docs/api/create-an-annotation-layer-annotation-layer) | `/api/v1/annotation_layer/` | +| `GET` | [Get metadata information about this API resource (annotation-layer--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-annotation-layer-info) | `/api/v1/annotation_layer/_info` | +| `DELETE` | [Delete annotation layer (annotation-layer-pk)](/developer-docs/api/delete-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` | +| `GET` | [Get an annotation layer (annotation-layer-pk)](/developer-docs/api/get-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` | +| `PUT` | [Update an annotation layer (annotation-layer-pk)](/developer-docs/api/update-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` | +| `DELETE` | [Bulk delete annotation layers](/developer-docs/api/bulk-delete-annotation-layers) | `/api/v1/annotation_layer/{pk}/annotation/` | +| `GET` | [Get a list of annotation layers (annotation-layer-pk-annotation)](/developer-docs/api/get-a-list-of-annotation-layers-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` | +| `POST` | [Create an annotation layer (annotation-layer-pk-annotation)](/developer-docs/api/create-an-annotation-layer-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` | +| `DELETE` | [Delete annotation layer (annotation-layer-pk-annotation-annotation-id)](/developer-docs/api/delete-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` | +| `GET` | [Get an annotation layer (annotation-layer-pk-annotation-annotation-id)](/developer-docs/api/get-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` | +| `PUT` | [Update an annotation layer (annotation-layer-pk-annotation-annotation-id)](/developer-docs/api/update-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` | +| `GET` | [Get related fields data (annotation-layer-related-column-name)](/developer-docs/api/get-related-fields-data-annotation-layer-related-column-name) | `/api/v1/annotation_layer/related/{column_name}` | @@ -306,14 +306,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete CSS templates](./api/bulk-delete-css-templates) | `/api/v1/css_template/` | -| `GET` | [Get a list of CSS templates](./api/get-a-list-of-css-templates) | `/api/v1/css_template/` | -| `POST` | [Create a CSS template](./api/create-a-css-template) | `/api/v1/css_template/` | -| `GET` | [Get metadata information about this API resource (css-template--info)](./api/get-metadata-information-about-this-api-resource-css-template-info) | `/api/v1/css_template/_info` | -| `DELETE` | [Delete a CSS template](./api/delete-a-css-template) | `/api/v1/css_template/{pk}` | -| `GET` | [Get a CSS template](./api/get-a-css-template) | `/api/v1/css_template/{pk}` | -| `PUT` | [Update a CSS template](./api/update-a-css-template) | `/api/v1/css_template/{pk}` | -| `GET` | [Get related fields data (css-template-related-column-name)](./api/get-related-fields-data-css-template-related-column-name) | `/api/v1/css_template/related/{column_name}` | +| `DELETE` | [Bulk delete CSS templates](/developer-docs/api/bulk-delete-css-templates) | `/api/v1/css_template/` | +| `GET` | [Get a list of CSS templates](/developer-docs/api/get-a-list-of-css-templates) | `/api/v1/css_template/` | +| `POST` | [Create a CSS template](/developer-docs/api/create-a-css-template) | `/api/v1/css_template/` | +| `GET` | [Get metadata information about this API resource (css-template--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-css-template-info) | `/api/v1/css_template/_info` | +| `DELETE` | [Delete a CSS template](/developer-docs/api/delete-a-css-template) | `/api/v1/css_template/{pk}` | +| `GET` | [Get a CSS template](/developer-docs/api/get-a-css-template) | `/api/v1/css_template/{pk}` | +| `PUT` | [Update a CSS template](/developer-docs/api/update-a-css-template) | `/api/v1/css_template/{pk}` | +| `GET` | [Get related fields data (css-template-related-column-name)](/developer-docs/api/get-related-fields-data-css-template-related-column-name) | `/api/v1/css_template/related/{column_name}` | @@ -324,8 +324,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | [Create a new dashboard's permanent link](./api/create-a-new-dashboard-s-permanent-link) | `/api/v1/dashboard/{pk}/permalink` | -| `GET` | [Get dashboard's permanent link state](./api/get-dashboard-s-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` | +| `POST` | [Create a new dashboard's permanent link](/developer-docs/api/create-a-new-dashboard-s-permanent-link) | `/api/v1/dashboard/{pk}/permalink` | +| `GET` | [Get dashboard's permanent link state](/developer-docs/api/get-dashboard-s-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` | @@ -334,8 +334,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | [Create a new permanent link (explore-permalink)](./api/create-a-new-permanent-link-explore-permalink) | `/api/v1/explore/permalink` | -| `GET` | [Get chart's permanent link state](./api/get-chart-s-permanent-link-state) | `/api/v1/explore/permalink/{key}` | +| `POST` | [Create a new permanent link (explore-permalink)](/developer-docs/api/create-a-new-permanent-link-explore-permalink) | `/api/v1/explore/permalink` | +| `GET` | [Get chart's permanent link state](/developer-docs/api/get-chart-s-permanent-link-state) | `/api/v1/explore/permalink/{key}` | @@ -344,8 +344,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | [Create a new permanent link (sqllab-permalink)](./api/create-a-new-permanent-link-sqllab-permalink) | `/api/v1/sqllab/permalink` | -| `GET` | [Get permanent link state for SQLLab editor.](./api/get-permanent-link-state-for-sqllab-editor) | `/api/v1/sqllab/permalink/{key}` | +| `POST` | [Create a new permanent link (sqllab-permalink)](/developer-docs/api/create-a-new-permanent-link-sqllab-permalink) | `/api/v1/sqllab/permalink` | +| `GET` | [Get permanent link state for SQLLab editor.](/developer-docs/api/get-permanent-link-state-for-sqllab-editor) | `/api/v1/sqllab/permalink/{key}` | @@ -354,7 +354,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get a report schedule log (embedded-dashboard-uuid)](./api/get-a-report-schedule-log-embedded-dashboard-uuid) | `/api/v1/embedded_dashboard/{uuid}` | +| `GET` | [Get a report schedule log (embedded-dashboard-uuid)](/developer-docs/api/get-a-report-schedule-log-embedded-dashboard-uuid) | `/api/v1/embedded_dashboard/{uuid}` | @@ -363,10 +363,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | [Create a dashboard's filter state](./api/create-a-dashboard-s-filter-state) | `/api/v1/dashboard/{pk}/filter_state` | -| `DELETE` | [Delete a dashboard's filter state value](./api/delete-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` | -| `GET` | [Get a dashboard's filter state value](./api/get-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` | -| `PUT` | [Update a dashboard's filter state value](./api/update-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` | +| `POST` | [Create a dashboard's filter state](/developer-docs/api/create-a-dashboard-s-filter-state) | `/api/v1/dashboard/{pk}/filter_state` | +| `DELETE` | [Delete a dashboard's filter state value](/developer-docs/api/delete-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` | +| `GET` | [Get a dashboard's filter state value](/developer-docs/api/get-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` | +| `PUT` | [Update a dashboard's filter state value](/developer-docs/api/update-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` | @@ -375,10 +375,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | [Create a new form_data](./api/create-a-new-form-data) | `/api/v1/explore/form_data` | -| `DELETE` | [Delete a form_data](./api/delete-a-form-data) | `/api/v1/explore/form_data/{key}` | -| `GET` | [Get a form_data](./api/get-a-form-data) | `/api/v1/explore/form_data/{key}` | -| `PUT` | [Update an existing form_data](./api/update-an-existing-form-data) | `/api/v1/explore/form_data/{key}` | +| `POST` | [Create a new form_data](/developer-docs/api/create-a-new-form-data) | `/api/v1/explore/form_data` | +| `DELETE` | [Delete a form_data](/developer-docs/api/delete-a-form-data) | `/api/v1/explore/form_data/{key}` | +| `GET` | [Get a form_data](/developer-docs/api/get-a-form-data) | `/api/v1/explore/form_data/{key}` | +| `PUT` | [Update an existing form_data](/developer-docs/api/update-an-existing-form-data) | `/api/v1/explore/form_data/{key}` | @@ -389,17 +389,17 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete report schedules](./api/bulk-delete-report-schedules) | `/api/v1/report/` | -| `GET` | [Get a list of report schedules](./api/get-a-list-of-report-schedules) | `/api/v1/report/` | -| `POST` | [Create a report schedule](./api/create-a-report-schedule) | `/api/v1/report/` | -| `GET` | [Get metadata information about this API resource (report--info)](./api/get-metadata-information-about-this-api-resource-report-info) | `/api/v1/report/_info` | -| `DELETE` | [Delete a report schedule](./api/delete-a-report-schedule) | `/api/v1/report/{pk}` | -| `GET` | [Get a report schedule](./api/get-a-report-schedule) | `/api/v1/report/{pk}` | -| `PUT` | [Update a report schedule](./api/update-a-report-schedule) | `/api/v1/report/{pk}` | -| `GET` | [Get a list of report schedule logs](./api/get-a-list-of-report-schedule-logs) | `/api/v1/report/{pk}/log/` | -| `GET` | [Get a report schedule log (report-pk-log-log-id)](./api/get-a-report-schedule-log-report-pk-log-log-id) | `/api/v1/report/{pk}/log/{log_id}` | -| `GET` | [Get related fields data (report-related-column-name)](./api/get-related-fields-data-report-related-column-name) | `/api/v1/report/related/{column_name}` | -| `GET` | [Get slack channels](./api/get-slack-channels) | `/api/v1/report/slack_channels/` | +| `DELETE` | [Bulk delete report schedules](/developer-docs/api/bulk-delete-report-schedules) | `/api/v1/report/` | +| `GET` | [Get a list of report schedules](/developer-docs/api/get-a-list-of-report-schedules) | `/api/v1/report/` | +| `POST` | [Create a report schedule](/developer-docs/api/create-a-report-schedule) | `/api/v1/report/` | +| `GET` | [Get metadata information about this API resource (report--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-report-info) | `/api/v1/report/_info` | +| `DELETE` | [Delete a report schedule](/developer-docs/api/delete-a-report-schedule) | `/api/v1/report/{pk}` | +| `GET` | [Get a report schedule](/developer-docs/api/get-a-report-schedule) | `/api/v1/report/{pk}` | +| `PUT` | [Update a report schedule](/developer-docs/api/update-a-report-schedule) | `/api/v1/report/{pk}` | +| `GET` | [Get a list of report schedule logs](/developer-docs/api/get-a-list-of-report-schedule-logs) | `/api/v1/report/{pk}/log/` | +| `GET` | [Get a report schedule log (report-pk-log-log-id)](/developer-docs/api/get-a-report-schedule-log-report-pk-log-log-id) | `/api/v1/report/{pk}/log/{log_id}` | +| `GET` | [Get related fields data (report-related-column-name)](/developer-docs/api/get-related-fields-data-report-related-column-name) | `/api/v1/report/related/{column_name}` | +| `GET` | [Get slack channels](/developer-docs/api/get-slack-channels) | `/api/v1/report/slack_channels/` | @@ -410,16 +410,16 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get security roles](./api/get-security-roles) | `/api/v1/security/roles/` | -| `POST` | [Create security roles](./api/create-security-roles) | `/api/v1/security/roles/` | -| `GET` | [Get security roles info](./api/get-security-roles-info) | `/api/v1/security/roles/_info` | -| `DELETE` | [Delete security roles by pk](./api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` | -| `GET` | [Get security roles by pk](./api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` | -| `PUT` | [Update security roles by pk](./api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` | -| `POST` | [Create security roles by role_id permissions](./api/create-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions` | -| `GET` | [Get security roles by role_id permissions](./api/get-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions/` | -| `PUT` | [Update security roles by role_id users](./api/update-security-roles-by-role-id-users) | `/api/v1/security/roles/{role_id}/users` | -| `GET` | [List roles](./api/list-roles) | `/api/v1/security/roles/search/` | +| `GET` | [Get security roles](/developer-docs/api/get-security-roles) | `/api/v1/security/roles/` | +| `POST` | [Create security roles](/developer-docs/api/create-security-roles) | `/api/v1/security/roles/` | +| `GET` | [Get security roles info](/developer-docs/api/get-security-roles-info) | `/api/v1/security/roles/_info` | +| `DELETE` | [Delete security roles by pk](/developer-docs/api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` | +| `GET` | [Get security roles by pk](/developer-docs/api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` | +| `PUT` | [Update security roles by pk](/developer-docs/api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` | +| `POST` | [Create security roles by role_id permissions](/developer-docs/api/create-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions` | +| `GET` | [Get security roles by role_id permissions](/developer-docs/api/get-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions/` | +| `PUT` | [Update security roles by role_id users](/developer-docs/api/update-security-roles-by-role-id-users) | `/api/v1/security/roles/{role_id}/users` | +| `GET` | [List roles](/developer-docs/api/list-roles) | `/api/v1/security/roles/search/` | @@ -428,12 +428,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get security users](./api/get-security-users) | `/api/v1/security/users/` | -| `POST` | [Create security users](./api/create-security-users) | `/api/v1/security/users/` | -| `GET` | [Get security users info](./api/get-security-users-info) | `/api/v1/security/users/_info` | -| `DELETE` | [Delete security users by pk](./api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` | -| `GET` | [Get security users by pk](./api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` | -| `PUT` | [Update security users by pk](./api/update-security-users-by-pk) | `/api/v1/security/users/{pk}` | +| `GET` | [Get security users](/developer-docs/api/get-security-users) | `/api/v1/security/users/` | +| `POST` | [Create security users](/developer-docs/api/create-security-users) | `/api/v1/security/users/` | +| `GET` | [Get security users info](/developer-docs/api/get-security-users-info) | `/api/v1/security/users/_info` | +| `DELETE` | [Delete security users by pk](/developer-docs/api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` | +| `GET` | [Get security users by pk](/developer-docs/api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` | +| `PUT` | [Update security users by pk](/developer-docs/api/update-security-users-by-pk) | `/api/v1/security/users/{pk}` | @@ -442,9 +442,9 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get security permissions](./api/get-security-permissions) | `/api/v1/security/permissions/` | -| `GET` | [Get security permissions info](./api/get-security-permissions-info) | `/api/v1/security/permissions/_info` | -| `GET` | [Get security permissions by pk](./api/get-security-permissions-by-pk) | `/api/v1/security/permissions/{pk}` | +| `GET` | [Get security permissions](/developer-docs/api/get-security-permissions) | `/api/v1/security/permissions/` | +| `GET` | [Get security permissions info](/developer-docs/api/get-security-permissions-info) | `/api/v1/security/permissions/_info` | +| `GET` | [Get security permissions by pk](/developer-docs/api/get-security-permissions-by-pk) | `/api/v1/security/permissions/{pk}` | @@ -453,12 +453,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get security resources](./api/get-security-resources) | `/api/v1/security/resources/` | -| `POST` | [Create security resources](./api/create-security-resources) | `/api/v1/security/resources/` | -| `GET` | [Get security resources info](./api/get-security-resources-info) | `/api/v1/security/resources/_info` | -| `DELETE` | [Delete security resources by pk](./api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` | -| `GET` | [Get security resources by pk](./api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` | -| `PUT` | [Update security resources by pk](./api/update-security-resources-by-pk) | `/api/v1/security/resources/{pk}` | +| `GET` | [Get security resources](/developer-docs/api/get-security-resources) | `/api/v1/security/resources/` | +| `POST` | [Create security resources](/developer-docs/api/create-security-resources) | `/api/v1/security/resources/` | +| `GET` | [Get security resources info](/developer-docs/api/get-security-resources-info) | `/api/v1/security/resources/_info` | +| `DELETE` | [Delete security resources by pk](/developer-docs/api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` | +| `GET` | [Get security resources by pk](/developer-docs/api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` | +| `PUT` | [Update security resources by pk](/developer-docs/api/update-security-resources-by-pk) | `/api/v1/security/resources/{pk}` | @@ -467,12 +467,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get security permissions resources](./api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` | -| `POST` | [Create security permissions resources](./api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` | -| `GET` | [Get security permissions resources info](./api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` | -| `DELETE` | [Delete security permissions resources by pk](./api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` | -| `GET` | [Get security permissions resources by pk](./api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` | -| `PUT` | [Update security permissions resources by pk](./api/update-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` | +| `GET` | [Get security permissions resources](/developer-docs/api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` | +| `POST` | [Create security permissions resources](/developer-docs/api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` | +| `GET` | [Get security permissions resources info](/developer-docs/api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` | +| `DELETE` | [Delete security permissions resources by pk](/developer-docs/api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` | +| `GET` | [Get security permissions resources by pk](/developer-docs/api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` | +| `PUT` | [Update security permissions resources by pk](/developer-docs/api/update-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` | @@ -481,14 +481,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `DELETE` | [Bulk delete RLS rules](./api/bulk-delete-rls-rules) | `/api/v1/rowlevelsecurity/` | -| `GET` | [Get a list of RLS](./api/get-a-list-of-rls) | `/api/v1/rowlevelsecurity/` | -| `POST` | [Create a new RLS rule](./api/create-a-new-rls-rule) | `/api/v1/rowlevelsecurity/` | -| `GET` | [Get metadata information about this API resource (rowlevelsecurity--info)](./api/get-metadata-information-about-this-api-resource-rowlevelsecurity-info) | `/api/v1/rowlevelsecurity/_info` | -| `DELETE` | [Delete an RLS](./api/delete-an-rls) | `/api/v1/rowlevelsecurity/{pk}` | -| `GET` | [Get an RLS](./api/get-an-rls) | `/api/v1/rowlevelsecurity/{pk}` | -| `PUT` | [Update an RLS rule](./api/update-an-rls-rule) | `/api/v1/rowlevelsecurity/{pk}` | -| `GET` | [Get related fields data (rowlevelsecurity-related-column-name)](./api/get-related-fields-data-rowlevelsecurity-related-column-name) | `/api/v1/rowlevelsecurity/related/{column_name}` | +| `DELETE` | [Bulk delete RLS rules](/developer-docs/api/bulk-delete-rls-rules) | `/api/v1/rowlevelsecurity/` | +| `GET` | [Get a list of RLS](/developer-docs/api/get-a-list-of-rls) | `/api/v1/rowlevelsecurity/` | +| `POST` | [Create a new RLS rule](/developer-docs/api/create-a-new-rls-rule) | `/api/v1/rowlevelsecurity/` | +| `GET` | [Get metadata information about this API resource (rowlevelsecurity--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-rowlevelsecurity-info) | `/api/v1/rowlevelsecurity/_info` | +| `DELETE` | [Delete an RLS](/developer-docs/api/delete-an-rls) | `/api/v1/rowlevelsecurity/{pk}` | +| `GET` | [Get an RLS](/developer-docs/api/get-an-rls) | `/api/v1/rowlevelsecurity/{pk}` | +| `PUT` | [Update an RLS rule](/developer-docs/api/update-an-rls-rule) | `/api/v1/rowlevelsecurity/{pk}` | +| `GET` | [Get related fields data (rowlevelsecurity-related-column-name)](/developer-docs/api/get-related-fields-data-rowlevelsecurity-related-column-name) | `/api/v1/rowlevelsecurity/related/{column_name}` | @@ -499,8 +499,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Export all assets](./api/export-all-assets) | `/api/v1/assets/export/` | -| `POST` | [Import multiple assets](./api/import-multiple-assets) | `/api/v1/assets/import/` | +| `GET` | [Export all assets](/developer-docs/api/export-all-assets) | `/api/v1/assets/export/` | +| `POST` | [Import multiple assets](/developer-docs/api/import-multiple-assets) | `/api/v1/assets/import/` | @@ -509,7 +509,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | [Invalidate cache records and remove the database records](./api/invalidate-cache-records-and-remove-the-database-records) | `/api/v1/cachekey/invalidate` | +| `POST` | [Invalidate cache records and remove the database records](/developer-docs/api/invalidate-cache-records-and-remove-the-database-records) | `/api/v1/cachekey/invalidate` | @@ -518,10 +518,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get a list of logs](./api/get-a-list-of-logs) | `/api/v1/log/` | -| `POST` | [Create log](./api/create-log) | `/api/v1/log/` | -| `GET` | [Get a log detail information](./api/get-a-log-detail-information) | `/api/v1/log/{pk}` | -| `GET` | [Get recent activity data for a user](./api/get-recent-activity-data-for-a-user) | `/api/v1/log/recent_activity/` | +| `GET` | [Get a list of logs](/developer-docs/api/get-a-list-of-logs) | `/api/v1/log/` | +| `POST` | [Create log](/developer-docs/api/create-log) | `/api/v1/log/` | +| `GET` | [Get a log detail information](/developer-docs/api/get-a-log-detail-information) | `/api/v1/log/{pk}` | +| `GET` | [Get recent activity data for a user](/developer-docs/api/get-recent-activity-data-for-a-user) | `/api/v1/log/recent_activity/` | @@ -532,8 +532,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get the user object](./api/get-the-user-object) | `/api/v1/me/` | -| `GET` | [Get the user roles](./api/get-the-user-roles) | `/api/v1/me/roles/` | +| `GET` | [Get the user object](/developer-docs/api/get-the-user-object) | `/api/v1/me/` | +| `GET` | [Get the user roles](/developer-docs/api/get-the-user-roles) | `/api/v1/me/roles/` | @@ -542,7 +542,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get the user avatar](./api/get-the-user-avatar) | `/api/v1/user/{user_id}/avatar.png` | +| `GET` | [Get the user avatar](/developer-docs/api/get-the-user-avatar) | `/api/v1/user/{user_id}/avatar.png` | @@ -551,7 +551,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get menu](./api/get-menu) | `/api/v1/menu/` | +| `GET` | [Get menu](/developer-docs/api/get-menu) | `/api/v1/menu/` | @@ -560,7 +560,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get all available domains](./api/get-all-available-domains) | `/api/v1/available_domains/` | +| `GET` | [Get all available domains](/developer-docs/api/get-all-available-domains) | `/api/v1/available_domains/` | @@ -569,7 +569,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Read off of the Redis events stream](./api/read-off-of-the-redis-events-stream) | `/api/v1/async_event/` | +| `GET` | [Read off of the Redis events stream](/developer-docs/api/read-off-of-the-redis-events-stream) | `/api/v1/async_event/` | @@ -578,7 +578,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | [Get api by version openapi](./api/get-api-by-version-openapi) | `/api/{version}/_openapi` | +| `GET` | [Get api by version openapi](/developer-docs/api/get-api-by-version-openapi) | `/api/{version}/_openapi` | diff --git a/docs/developer_docs/extensions/development.md b/docs/developer_docs/extensions/development.md index cd38f1e47d2..a10f7743110 100644 --- a/docs/developer_docs/extensions/development.md +++ b/docs/developer_docs/extensions/development.md @@ -39,7 +39,11 @@ superset-extensions bundle: Packages the extension into a .supx file. superset-extensions dev: Automatically rebuilds the extension as files change. -superset-extensions validate: Validates the extension structure and metadata. +superset-extensions validate: Validates the extension structure and metadata consistency. + +superset-extensions update: Updates derived and generated files in the extension project. + Use --version [] to update the version (prompts if no value given). + Use --license [] to update the license (prompts if no value given). ``` When creating a new extension with `superset-extensions init`, the CLI generates a standardized folder structure: diff --git a/docs/developer_docs/sidebars.js b/docs/developer_docs/sidebars.js index babdcd41025..7926d80cf61 100644 --- a/docs/developer_docs/sidebars.js +++ b/docs/developer_docs/sidebars.js @@ -52,7 +52,6 @@ module.exports = { 'extensions/development', 'extensions/deployment', 'extensions/mcp', - 'extensions/mcp-server', 'extensions/security', 'extensions/tasks', 'extensions/registry', diff --git a/docs/docs/using-superset/using-ai-with-superset.mdx b/docs/docs/using-superset/using-ai-with-superset.mdx new file mode 100644 index 00000000000..3969042bcd2 --- /dev/null +++ b/docs/docs/using-superset/using-ai-with-superset.mdx @@ -0,0 +1,245 @@ +--- +title: Using AI with Superset +hide_title: true +sidebar_position: 5 +version: 1 +--- + + + +# Using AI with Superset + +Superset supports AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). Connect Claude, ChatGPT, or other MCP-compatible clients to explore your data, build charts, create dashboards, and run SQL -- all through natural language. + +:::info +Requires Superset 5.0+. Your admin must enable and deploy the MCP server before you can connect. +See the **[MCP Server admin guide](/admin-docs/configuration/mcp-server)** for setup instructions. +::: + +--- + +## What Can AI Do with Superset? + +### Explore Your Data + +Ask your AI assistant to browse what's available in your Superset instance: + +- **List datasets** -- see all datasets you have access to, with filtering and search +- **Get dataset details** -- column names, types, available metrics, and filters +- **List charts and dashboards** -- find existing visualizations by name or keyword +- **Get chart and dashboard details** -- understand what a chart shows, its query, and configuration + +**Example prompts:** +> "What datasets are available?" +> "Show me the columns in the sales_orders dataset" +> "Find dashboards related to revenue" + +### Build Charts + +Describe the visualization you want and AI creates it for you: + +- **Create charts from natural language** -- describe what you want to see and AI picks the right chart type, metrics, and dimensions +- **Preview before saving** -- AI generates a preview so you can review before committing +- **Modify existing charts** -- update filters, change chart types, add metrics +- **Get Explore links** -- open any chart in Superset's Explore view for further refinement + +**Example prompts:** +> "Create a bar chart showing monthly revenue by region from the sales dataset" +> "Update chart 42 to use a line chart instead" +> "Give me a link to explore this chart further" + +### Create Dashboards + +Build dashboards from a collection of charts: + +- **Generate dashboards** -- create a new dashboard with a set of charts, automatically laid out +- **Add charts to existing dashboards** -- place a chart on an existing dashboard with automatic positioning + +**Example prompts:** +> "Create a dashboard called 'Q4 Sales Overview' with charts 10, 15, and 22" +> "Add the revenue trend chart to the executive dashboard" + +### Run SQL Queries + +Execute SQL directly through your AI assistant: + +- **Run queries** -- execute SQL with full Superset RBAC enforcement (you can only query data your roles allow) +- **Open SQL Lab** -- get a link to SQL Lab pre-populated with a query, ready to run and explore + +**Example prompts:** +> "Run this query: SELECT region, SUM(revenue) FROM sales GROUP BY region" +> "Open SQL Lab with a query to show the top 10 customers by order count" + +### Analyze Chart Data + +Pull the raw data behind any chart: + +- **Get chart data** -- retrieve the data a chart displays, with support for JSON, CSV, and Excel export formats +- **Inspect results** -- useful for verifying what a visualization shows or feeding data into other tools + +**Example prompts:** +> "Get the data behind chart 42" +> "Export chart 15 data as CSV" + +### Check Instance Status + +- **Health check** -- verify your Superset instance is up and the MCP connection is working +- **Instance info** -- get high-level statistics about your Superset instance (number of datasets, charts, dashboards) + +**Example prompts:** +> "Is Superset healthy?" +> "How many dashboards are in this instance?" + +--- + +## Connecting Your AI Client + +Once your admin has deployed the MCP server, connect your AI client using the instructions below. + +### Claude Desktop + +Edit your Claude Desktop config file: + +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` +- **Linux**: `~/.config/Claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "superset": { + "url": "http://localhost:5008/mcp" + } + } +} +``` + +Restart Claude Desktop. The hammer icon in the chat bar confirms the connection. + +If your admin has enabled JWT authentication, you may need to include a token: + +```json +{ + "mcpServers": { + "superset": { + "command": "npx", + "args": [ + "-y", + "mcp-remote@latest", + "http://your-superset-host:5008/mcp", + "--header", + "Authorization: Bearer YOUR_TOKEN" + ] + } + } +} +``` + +### Claude Code (CLI) + +Add to your project's `.mcp.json`: + +```json +{ + "mcpServers": { + "superset": { + "type": "url", + "url": "http://localhost:5008/mcp" + } + } +} +``` + +### ChatGPT + +1. Click your profile icon > **Settings** > **Apps and Connectors** +2. Enable **Developer Mode** in Advanced Settings +3. In the chat composer, press **+** > **Add sources** > **App** > **Connect more** > **Create app** +4. Enter a name and your MCP server URL +5. Click **I understand and continue** + +:::info +ChatGPT MCP connectors require a Pro, Team, Enterprise, or Edu plan. +::: + +Ask your admin for the MCP server URL and any authentication tokens you need. + +--- + +## Tips for Best Results + +- **Be specific** -- "Create a bar chart of monthly revenue by region from the sales dataset" works better than "Make me a chart" +- **Start with exploration** -- ask what datasets and charts exist before creating new ones +- **Review AI-generated content** -- always check chart configurations and SQL before saving or sharing +- **Use Explore for refinement** -- ask AI for an Explore link, then fine-tune interactively in the Superset UI +- **Check permissions if you get errors** -- AI respects Superset's RBAC, so you can only access data your roles allow + +--- + +## Available Tools Reference + +| Tool | Description | +|------|-------------| +| `health_check` | Verify the MCP server is running and connected | +| `get_instance_info` | Get instance statistics (dataset, chart, dashboard counts) | +| `get_schema` | Discover available charts, datasets, and dashboards with schema info | +| `list_datasets` | List datasets with filtering and search | +| `get_dataset_info` | Get dataset metadata (columns, metrics, filters) | +| `list_charts` | List charts with filtering and search | +| `get_chart_info` | Get chart metadata and configuration | +| `get_chart_data` | Retrieve chart data (JSON, CSV, or Excel) | +| `get_chart_preview` | Generate a chart preview (URL, ASCII, table, or Vega-Lite) | +| `generate_chart` | Create a new chart from a specification | +| `update_chart` | Modify an existing chart's configuration | +| `update_chart_preview` | Update a cached chart preview without saving | +| `list_dashboards` | List dashboards with filtering and search | +| `get_dashboard_info` | Get dashboard metadata and layout | +| `generate_dashboard` | Create a new dashboard with specified charts | +| `add_chart_to_existing_dashboard` | Add a chart to an existing dashboard | +| `execute_sql` | Run a SQL query with RBAC enforcement | +| `open_sql_lab_with_context` | Open SQL Lab with a pre-populated query | +| `generate_explore_link` | Generate an Explore URL for interactive visualization | + +--- + +## Troubleshooting + +### "Connection refused" or "Cannot connect" + +- Confirm the MCP server URL with your admin +- For Claude Desktop: fully quit the app (not just close the window) and restart after config changes +- Check that the URL path ends with `/mcp` (e.g., `http://localhost:5008/mcp`) + +### "Permission denied" or missing data + +- Superset's RBAC controls what you can access through AI, just like in the Superset UI +- Ask your admin to verify your roles and permissions +- Try accessing the same data through the Superset web UI to confirm your access + +### "Response too large" + +- Ask for smaller result sets: use filters, reduce `page_size`, or request specific columns +- Example: "Show me the top 10 rows from the sales dataset" instead of "Show me all sales data" + +### AI doesn't see Superset tools + +- Verify the connection in your AI client (e.g., the hammer icon in Claude Desktop) +- Ask the AI "What Superset tools are available?" to confirm the connection +- Restart your AI client if you recently changed the configuration diff --git a/docs/package.json b/docs/package.json index e4a31bf1294..50eff6532f5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -69,8 +69,8 @@ "@superset-ui/core": "^0.20.4", "@swc/core": "^1.15.17", "antd": "^6.3.2", - "baseline-browser-mapping": "^2.10.0", - "caniuse-lite": "^1.0.30001775", + "baseline-browser-mapping": "^2.10.7", + "caniuse-lite": "^1.0.30001778", "docusaurus-plugin-openapi-docs": "^4.6.0", "docusaurus-theme-openapi-docs": "^4.6.0", "js-yaml": "^4.1.1", diff --git a/docs/scripts/generate-api-index.mjs b/docs/scripts/generate-api-index.mjs index e5f6a7e6934..2fd9fddac63 100644 --- a/docs/scripts/generate-api-index.mjs +++ b/docs/scripts/generate-api-index.mjs @@ -202,7 +202,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\ mdx += `| Method | Endpoint | Description |\n`; mdx += `|--------|----------|-------------|\n`; for (const ep of tagEndpoints['Security']) { - mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`; + mdx += `| \`${ep.method}\` | [${ep.summary}](/developer-docs/api/${ep.slug}) | \`${ep.path}\` |\n`; } mdx += '\n'; renderedTags.add('Security'); @@ -229,7 +229,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\ mdx += `|--------|----------|-------------|\n`; for (const ep of endpoints) { - mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`; + mdx += `| \`${ep.method}\` | [${ep.summary}](/developer-docs/api/${ep.slug}) | \`${ep.path}\` |\n`; } mdx += `\n\n\n`; @@ -252,7 +252,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\ mdx += `|--------|----------|-------------|\n`; for (const ep of endpoints) { - mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`; + mdx += `| \`${ep.method}\` | [${ep.summary}](/developer-docs/api/${ep.slug}) | \`${ep.path}\` |\n`; } mdx += `\n\n\n`; diff --git a/docs/yarn.lock b/docs/yarn.lock index ba6b1120e85..edb44272dd2 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -5961,10 +5961,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.0, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19: - version "2.10.0" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" - integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== +baseline-browser-mapping@^2.10.7, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.19: + version "2.10.7" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz#2c017adffe4f7bbe93c2e55526cc1869d36f588c" + integrity sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw== batch@0.6.1: version "0.6.1" @@ -6209,15 +6209,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759: - version "1.0.30001770" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz#4dc47d3b263a50fbb243448034921e0a88591a84" - integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw== - -caniuse-lite@^1.0.30001775: - version "1.0.30001775" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz#9572266e3f7f77efee5deac1efeb4795879d1b7f" - integrity sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001778: + version "1.0.30001778" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz#79a8a124e3473a20b70616497b987c30d06570a0" + integrity sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg== ccount@^2.0.0: version "2.0.1" diff --git a/pyproject.toml b/pyproject.toml index 70feeeb4d3e..12d5224cb38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,7 @@ solr = ["sqlalchemy-solr >= 0.2.0"] elasticsearch = ["elasticsearch-dbapi>=0.2.12, <0.3.0"] exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"] excel = ["xlrd>=1.2.0, <1.3"] -fastmcp = ["fastmcp==2.14.3"] +fastmcp = ["fastmcp>=3.1.0,<4.0"] firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"] firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"] gevent = ["gevent>=23.9.1"] diff --git a/requirements/base.txt b/requirements/base.txt index c37afda03f8..240cf7d4eb1 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -120,7 +120,7 @@ flask==2.3.3 # flask-session # flask-sqlalchemy # flask-wtf -flask-appbuilder==5.0.2 +flask-appbuilder==5.1.0 # via # apache-superset (pyproject.toml) # apache-superset-core diff --git a/requirements/development.txt b/requirements/development.txt index e0a2b801182..2b9fffd474b 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -10,6 +10,8 @@ # via # -r requirements/development.in # apache-superset +aiofile==3.9.0 + # via py-key-value-aio alembic==1.15.2 # via # -c requirements/base-constraint.txt @@ -26,8 +28,10 @@ anyio==4.11.0 # via # httpx # mcp + # py-key-value-aio # sse-starlette # starlette + # watchfiles apispec==6.6.1 # via # -c requirements/base-constraint.txt @@ -65,9 +69,7 @@ bcrypt==4.3.0 # -c requirements/base-constraint.txt # paramiko beartype==0.22.5 - # via - # py-key-value-aio - # py-key-value-shared + # via py-key-value-aio billiard==4.2.1 # via # -c requirements/base-constraint.txt @@ -100,6 +102,8 @@ cachetools==6.2.1 # -c requirements/base-constraint.txt # google-auth # py-key-value-aio +caio==0.9.25 + # via aiofile cattrs==25.1.1 # via # -c requirements/base-constraint.txt @@ -138,7 +142,6 @@ click==8.2.1 # click-repl # flask # flask-appbuilder - # typer # uvicorn click-didyoumean==0.3.1 # via @@ -156,8 +159,6 @@ click-repl==0.3.0 # via # -c requirements/base-constraint.txt # celery -cloudpickle==3.1.2 - # via pydocket cmdstanpy==1.1.0 # via prophet colorama==0.4.6 @@ -206,8 +207,6 @@ deprecation==2.1.0 # apache-superset dill==0.4.0 # via pylint -diskcache==5.6.3 - # via py-key-value-aio distlib==0.3.8 # via virtualenv dnspython==2.7.0 @@ -237,9 +236,7 @@ et-xmlfile==2.0.0 # openpyxl exceptiongroup==1.3.0 # via fastmcp -fakeredis==2.32.1 - # via pydocket -fastmcp==2.14.3 +fastmcp==3.1.0 # via apache-superset filelock==3.20.3 # via @@ -262,7 +259,7 @@ flask==2.3.3 # flask-sqlalchemy # flask-testing # flask-wtf -flask-appbuilder==5.0.2 +flask-appbuilder==5.1.0 # via # -c requirements/base-constraint.txt # apache-superset @@ -474,6 +471,8 @@ jsonpath-ng==1.7.0 # via # -c requirements/base-constraint.txt # apache-superset +jsonref==1.1.0 + # via fastmcp jsonschema==4.23.0 # via # -c requirements/base-constraint.txt @@ -504,8 +503,6 @@ limits==5.1.0 # via # -c requirements/base-constraint.txt # flask-limiter -lupa==2.6 - # via fakeredis mako==1.3.10 # via # -c requirements/base-constraint.txt @@ -603,7 +600,7 @@ openpyxl==3.1.5 # -c requirements/base-constraint.txt # pandas opentelemetry-api==1.39.1 - # via pydocket + # via fastmcp ordered-set==4.1.0 # via # -c requirements/base-constraint.txt @@ -622,6 +619,7 @@ packaging==25.0 # deprecation # docker # duckdb-engine + # fastmcp # google-cloud-bigquery # gunicorn # limits @@ -653,8 +651,6 @@ parsedatetime==2.6 # apache-superset pathable==0.4.3 # via jsonschema-path -pathvalidate==3.3.1 - # via py-key-value-aio pgsanity==0.2.9 # via # -c requirements/base-constraint.txt @@ -691,8 +687,6 @@ prison==0.2.1 # flask-appbuilder progress==1.6 # via apache-superset -prometheus-client==0.23.1 - # via pydocket prompt-toolkit==3.0.51 # via # -c requirements/base-constraint.txt @@ -714,12 +708,8 @@ psutil==6.1.0 # via apache-superset psycopg2-binary==2.9.9 # via apache-superset -py-key-value-aio==0.3.0 - # via - # fastmcp - # pydocket -py-key-value-shared==0.3.0 - # via py-key-value-aio +py-key-value-aio==0.4.4 + # via fastmcp pyarrow==16.1.0 # via # -c requirements/base-constraint.txt @@ -758,8 +748,6 @@ pydantic-settings==2.10.1 # via mcp pydata-google-auth==1.9.0 # via pandas-gbq -pydocket==0.17.1 - # via fastmcp pydruid==0.6.9 # via apache-superset pyfakefs==5.3.5 @@ -844,8 +832,6 @@ python-dotenv==1.1.0 # apache-superset # fastmcp # pydantic-settings -python-json-logger==4.0.0 - # via pydocket python-ldap==3.4.4 # via apache-superset python-multipart==0.0.20 @@ -866,15 +852,13 @@ pyyaml==6.0.2 # -c requirements/base-constraint.txt # apache-superset # apispec + # fastmcp # jsonschema-path # pre-commit redis==5.3.1 # via # -c requirements/base-constraint.txt # apache-superset - # fakeredis - # py-key-value-aio - # pydocket referencing==0.36.2 # via # -c requirements/base-constraint.txt @@ -910,9 +894,7 @@ rich==13.9.4 # cyclopts # fastmcp # flask-limiter - # pydocket # rich-rst - # typer rich-rst==1.3.1 # via cyclopts rpds-py==0.25.0 @@ -944,8 +926,6 @@ setuptools==80.9.0 # pydata-google-auth # zope-event # zope-interface -shellingham==1.5.4 - # via typer shillelagh==1.4.3 # via # -c requirements/base-constraint.txt @@ -973,7 +953,6 @@ sniffio==1.3.1 sortedcontainers==2.4.0 # via # -c requirements/base-constraint.txt - # fakeredis # trio sqlalchemy==1.4.54 # via @@ -1017,6 +996,8 @@ tabulate==0.9.0 # via # -c requirements/base-constraint.txt # apache-superset +tomli-w==1.2.0 + # via apache-superset-extensions-cli tomlkit==0.13.3 # via pylint tqdm==4.67.1 @@ -1034,8 +1015,6 @@ trio-websocket==0.12.2 # via # -c requirements/base-constraint.txt # selenium -typer==0.20.0 - # via pydocket typing-extensions==4.15.0 # via # -c requirements/base-constraint.txt @@ -1048,16 +1027,14 @@ typing-extensions==4.15.0 # limits # mcp # opentelemetry-api - # py-key-value-shared + # py-key-value-aio # pydantic # pydantic-core - # pydocket # pyopenssl # referencing # selenium # shillelagh # starlette - # typer # typing-inspection typing-inspection==0.4.1 # via @@ -1072,6 +1049,8 @@ tzdata==2025.2 # pandas tzlocal==5.2 # via trino +uncalled-for==0.2.0 + # via fastmcp url-normalize==2.2.1 # via # -c requirements/base-constraint.txt @@ -1101,6 +1080,8 @@ watchdog==6.0.0 # -c requirements/base-constraint.txt # apache-superset # apache-superset-extensions-cli +watchfiles==1.1.1 + # via fastmcp wcwidth==0.2.13 # via # -c requirements/base-constraint.txt diff --git a/superset-core/src/superset_core/mcp/decorators.py b/superset-core/src/superset_core/mcp/decorators.py index d8c814a1acd..6ad29c9adc5 100644 --- a/superset-core/src/superset_core/mcp/decorators.py +++ b/superset-core/src/superset_core/mcp/decorators.py @@ -48,11 +48,14 @@ def tool( description: str | None = None, tags: list[str] | None = None, protect: bool = True, + class_permission_name: str | None = None, + method_permission_name: str | None = None, ) -> Any: # Use Any to avoid mypy issues with dependency injection """ Decorator to register an MCP tool with optional authentication. - This decorator combines FastMCP tool registration with optional authentication. + This decorator combines FastMCP tool registration with optional authentication + and RBAC permission checking. Can be used as: @tool @@ -69,6 +72,11 @@ def tool( description: Tool description (defaults to function docstring) tags: List of tags for categorizing the tool (defaults to empty list) protect: Whether to require Superset authentication (defaults to True) + class_permission_name: FAB view/resource name for RBAC checking + (e.g., "Chart", "Dashboard", "SQLLab"). When set, enables + permission checking via security_manager.can_access(). + method_permission_name: FAB action name (e.g., "read", "write"). + Defaults to "write" if tags includes "mutate", else "read". Returns: Decorator function that registers and wraps the tool, or the wrapped function @@ -90,6 +98,18 @@ def tool( def public_tool() -> str: '''Public tool accessible without auth''' return "Hello world" + + @tool(class_permission_name="Chart") # RBAC: requires can_read on Chart + def list_charts() -> list: + '''List charts the user can access''' + return [] + + @tool( # RBAC: can_write on Chart + tags=["mutate"], class_permission_name="Chart", + ) + def create_chart(name: str) -> dict: + '''Create a new chart''' + return {"name": name} """ raise NotImplementedError( "MCP tool decorator not initialized. " diff --git a/superset-extensions-cli/pyproject.toml b/superset-extensions-cli/pyproject.toml index 7a09fbe274e..8f853a86e36 100644 --- a/superset-extensions-cli/pyproject.toml +++ b/superset-extensions-cli/pyproject.toml @@ -17,7 +17,7 @@ [project] name = "apache-superset-extensions-cli" -version = "0.1.0rc1" +version = "0.1.0rc2" description = "Official command-line interface for building, bundling, and managing Apache Superset extensions" readme = "README.md" authors = [ @@ -49,6 +49,7 @@ dependencies = [ "jinja2>=3.1.6", "semver>=3.0.4", "tomli>=2.2.1; python_version < '3.11'", + "tomli-w>=1.2.0", "watchdog>=6.0.0", ] diff --git a/superset-extensions-cli/src/superset_extensions_cli/cli.py b/superset-extensions-cli/src/superset_extensions_cli/cli.py index 342baf637e7..feab2ab5d8a 100644 --- a/superset-extensions-cli/src/superset_extensions_cli/cli.py +++ b/superset-extensions-cli/src/superset_extensions_cli/cli.py @@ -50,6 +50,8 @@ from superset_extensions_cli.utils import ( validate_display_name, validate_publisher, validate_technical_name, + write_json, + write_toml, ) REMOTE_ENTRY_REGEX = re.compile(r"^remoteEntry\..+\.js$") @@ -292,6 +294,7 @@ def app() -> None: @app.command() def validate() -> None: + """Validate the extension structure and metadata consistency.""" validate_npm() cwd = Path.cwd() @@ -372,12 +375,167 @@ def validate() -> None: click.secho(" Convention requires: frontend/src/index.tsx", fg="yellow") sys.exit(1) + # Validate version and license consistency across extension.json, frontend, and backend + mismatches: list[str] = [] + frontend_pkg_path = cwd / "frontend" / "package.json" + frontend_pkg = None + if frontend_pkg_path.is_file(): + frontend_pkg = read_json(frontend_pkg_path) + if frontend_pkg: + if frontend_pkg.get("version") != extension.version: + mismatches.append( + f" frontend/package.json version: {frontend_pkg.get('version')} " + f"(expected {extension.version})" + ) + if extension.license and frontend_pkg.get("license") != extension.license: + mismatches.append( + f" frontend/package.json license: {frontend_pkg.get('license')} " + f"(expected {extension.license})" + ) + + backend_pyproject_path = cwd / "backend" / "pyproject.toml" + if backend_pyproject_path.is_file(): + backend_pyproject = read_toml(backend_pyproject_path) + if backend_pyproject: + project = backend_pyproject.get("project", {}) + if project.get("version") != extension.version: + mismatches.append( + f" backend/pyproject.toml version: {project.get('version')} " + f"(expected {extension.version})" + ) + if extension.license and project.get("license") != extension.license: + mismatches.append( + f" backend/pyproject.toml license: {project.get('license')} " + f"(expected {extension.license})" + ) + + if mismatches: + click.secho("❌ Metadata mismatch detected:", err=True, fg="red") + for mismatch in mismatches: + click.secho(mismatch, err=True, fg="red") + click.secho( + "Run `superset-extensions update` to sync from extension.json.", + fg="yellow", + ) + sys.exit(1) + click.secho("✅ Validation successful", fg="green") +@app.command() +@click.option( + "--version", + "version_opt", + is_flag=False, + flag_value="__prompt__", + default=None, + help="Set a new version. Prompts for value if none given.", +) +@click.option( + "--license", + "license_opt", + is_flag=False, + flag_value="__prompt__", + default=None, + help="Set a new license. Prompts for value if none given.", +) +def update(version_opt: str | None, license_opt: str | None) -> None: + """Update derived and generated files in the extension project.""" + cwd = Path.cwd() + + extension_json_path = cwd / "extension.json" + extension_data = read_json(extension_json_path) + if not extension_data: + click.secho("❌ extension.json not found.", err=True, fg="red") + sys.exit(1) + + try: + extension = ExtensionConfig.model_validate(extension_data) + except Exception as e: + click.secho(f"❌ Invalid extension.json: {e}", err=True, fg="red") + sys.exit(1) + + # Resolve version: prompt if flag used without value + if version_opt == "__prompt__": + version_opt = click.prompt("Version", default=extension.version) + target_version = ( + version_opt + if version_opt and version_opt != extension.version + else extension.version + ) + + # Resolve license: prompt if flag used without value + if license_opt == "__prompt__": + license_opt = click.prompt("License", default=extension.license or "") + target_license = ( + license_opt + if license_opt and license_opt != extension.license + else extension.license + ) + + updated: list[str] = [] + + # Update extension.json if version or license changed + ext_changed = False + if version_opt and version_opt != extension.version: + extension_data["version"] = target_version + ext_changed = True + if license_opt and license_opt != extension.license: + extension_data["license"] = target_license + ext_changed = True + if ext_changed: + try: + ExtensionConfig.model_validate(extension_data) + except Exception as e: + click.secho(f"❌ Invalid value: {e}", err=True, fg="red") + sys.exit(1) + write_json(extension_json_path, extension_data) + updated.append("extension.json") + + # Update frontend/package.json + frontend_pkg_path = cwd / "frontend" / "package.json" + if frontend_pkg_path.is_file(): + frontend_pkg = read_json(frontend_pkg_path) + if frontend_pkg: + pkg_changed = False + if frontend_pkg.get("version") != target_version: + frontend_pkg["version"] = target_version + pkg_changed = True + if target_license and frontend_pkg.get("license") != target_license: + frontend_pkg["license"] = target_license + pkg_changed = True + if pkg_changed: + write_json(frontend_pkg_path, frontend_pkg) + updated.append("frontend/package.json") + + # Update backend/pyproject.toml + backend_pyproject_path = cwd / "backend" / "pyproject.toml" + if backend_pyproject_path.is_file(): + backend_pyproject = read_toml(backend_pyproject_path) + if backend_pyproject: + project = backend_pyproject.setdefault("project", {}) + toml_changed = False + if project.get("version") != target_version: + project["version"] = target_version + toml_changed = True + if target_license and project.get("license") != target_license: + project["license"] = target_license + toml_changed = True + if toml_changed: + write_toml(backend_pyproject_path, backend_pyproject) + updated.append("backend/pyproject.toml") + + if updated: + for path in updated: + click.secho(f"✅ Updated {path}", fg="green") + else: + click.secho("✅ All files already up to date.", fg="green") + + @app.command() @click.pass_context def build(ctx: click.Context) -> None: + """Build extension assets.""" ctx.invoke(validate) cwd = Path.cwd() frontend_dir = cwd / "frontend" @@ -413,6 +571,7 @@ def build(ctx: click.Context) -> None: ) @click.pass_context def bundle(ctx: click.Context, output: Path | None) -> None: + """Package the extension into a .supx file.""" ctx.invoke(build) cwd = Path.cwd() @@ -453,6 +612,7 @@ def bundle(ctx: click.Context, output: Path | None) -> None: @app.command() @click.pass_context def dev(ctx: click.Context) -> None: + """Automatically rebuild the extension as files change.""" cwd = Path.cwd() frontend_dir = cwd / "frontend" backend_dir = cwd / "backend" @@ -647,6 +807,7 @@ def init( frontend_opt: bool | None, backend_opt: bool | None, ) -> None: + """Scaffold a new extension project.""" # Get extension names with graceful validation names = prompt_for_extension_info(display_name_opt, publisher_opt, name_opt) @@ -686,7 +847,7 @@ def init( click.secho("✅ Created extension.json", fg="green") # Create .gitignore - gitignore = env.get_template(".gitignore.j2").render(ctx) + gitignore = env.get_template("gitignore.j2").render(ctx) (target_dir / ".gitignore").write_text(gitignore) click.secho("✅ Created .gitignore", fg="green") diff --git a/superset-extensions-cli/src/superset_extensions_cli/templates/.gitignore.j2 b/superset-extensions-cli/src/superset_extensions_cli/templates/gitignore.j2 similarity index 100% rename from superset-extensions-cli/src/superset_extensions_cli/templates/.gitignore.j2 rename to superset-extensions-cli/src/superset_extensions_cli/templates/gitignore.j2 diff --git a/superset-extensions-cli/src/superset_extensions_cli/utils.py b/superset-extensions-cli/src/superset_extensions_cli/utils.py index bda0f3c4a80..c4fae04e6ec 100644 --- a/superset-extensions-cli/src/superset_extensions_cli/utils.py +++ b/superset-extensions-cli/src/superset_extensions_cli/utils.py @@ -21,6 +21,8 @@ import sys from pathlib import Path from typing import Any +import tomli_w + from superset_core.extensions.constants import ( DISPLAY_NAME_PATTERN, PUBLISHER_PATTERN, @@ -109,6 +111,14 @@ def read_json(path: Path) -> dict[str, Any] | None: return json.loads(path.read_text()) +def write_json(path: Path, data: dict[str, Any]) -> None: + path.write_text(json.dumps(data, indent=2) + "\n") + + +def write_toml(path: Path, data: dict[str, Any]) -> None: + path.write_text(tomli_w.dumps(data)) + + def _normalize_for_identifiers(name: str) -> str: """ Normalize display name to clean lowercase words. diff --git a/superset-extensions-cli/tests/conftest.py b/superset-extensions-cli/tests/conftest.py index 8c223be5051..a4cc7f7521c 100644 --- a/superset-extensions-cli/tests/conftest.py +++ b/superset-extensions-cli/tests/conftest.py @@ -17,10 +17,12 @@ from __future__ import annotations +import json import os from pathlib import Path import pytest +import tomli_w from click.testing import CliRunner @@ -138,3 +140,69 @@ def extension_setup_for_bundling(): (backend_dir / "__init__.py").write_text("# init") return _setup + + +@pytest.fixture +def extension_with_versions(): + """Create an extension directory structure with configurable versions and licenses.""" + + def _create( + base_path: Path, + ext_version: str = "1.0.0", + frontend_version: str | None = None, + backend_version: str | None = None, + ext_license: str | None = "Apache-2.0", + frontend_license: str | None = None, + backend_license: str | None = None, + ) -> None: + extension_json = { + "publisher": "test-org", + "name": "test-extension", + "displayName": "Test Extension", + "version": ext_version, + "permissions": [], + } + if ext_license is not None: + extension_json["license"] = ext_license + (base_path / "extension.json").write_text(json.dumps(extension_json)) + + if frontend_version is not None: + frontend_dir = base_path / "frontend" + frontend_dir.mkdir(exist_ok=True) + (frontend_dir / "src").mkdir(exist_ok=True) + (frontend_dir / "src" / "index.tsx").write_text("// entry") + pkg = { + "name": "@test-org/test-extension", + "version": frontend_version, + } + if frontend_license is not None: + pkg["license"] = frontend_license + elif ext_license is not None: + pkg["license"] = ext_license + (frontend_dir / "package.json").write_text(json.dumps(pkg, indent=2)) + + if backend_version is not None: + backend_dir = base_path / "backend" + backend_dir.mkdir(exist_ok=True) + src_dir = backend_dir / "src" / "test_org" / "test_extension" + src_dir.mkdir(parents=True, exist_ok=True) + (src_dir / "entrypoint.py").write_text("# entry") + project = { + "name": "test-org-test-extension", + "version": backend_version, + } + if backend_license is not None: + project["license"] = backend_license + elif ext_license is not None: + project["license"] = ext_license + pyproject = { + "project": project, + "tool": { + "apache_superset_extensions": { + "build": {"include": ["src/**/*.py"]} + } + }, + } + (backend_dir / "pyproject.toml").write_text(tomli_w.dumps(pyproject)) + + return _create diff --git a/superset-extensions-cli/tests/test_cli_build.py b/superset-extensions-cli/tests/test_cli_build.py index 90f63fdd5f5..5753eff4892 100644 --- a/superset-extensions-cli/tests/test_cli_build.py +++ b/superset-extensions-cli/tests/test_cli_build.py @@ -121,7 +121,7 @@ def test_build_command_success_flow( # Setup mocks mock_rebuild_frontend.return_value = "remoteEntry.abc123.js" mock_read_toml.return_value = { - "project": {"name": "test"}, + "project": {"name": "test", "version": "1.0.0"}, "tool": { "apache_superset_extensions": { "build": {"include": ["src/test_org/test_extension/**/*.py"]} @@ -162,7 +162,7 @@ def test_build_command_handles_frontend_build_failure( # Setup mocks mock_rebuild_frontend.return_value = None # Indicates failure mock_read_toml.return_value = { - "project": {"name": "test"}, + "project": {"name": "test", "version": "1.0.0"}, "tool": { "apache_superset_extensions": { "build": {"include": ["src/test_org/test_extension/**/*.py"]} diff --git a/superset-extensions-cli/tests/test_cli_update.py b/superset-extensions-cli/tests/test_cli_update.py new file mode 100644 index 00000000000..36b965fb54d --- /dev/null +++ b/superset-extensions-cli/tests/test_cli_update.py @@ -0,0 +1,172 @@ +# 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 pytest +from superset_extensions_cli.cli import app +from superset_extensions_cli.utils import read_json, read_toml + + +@pytest.mark.cli +def test_update_syncs_versions( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test update syncs frontend and backend versions from extension.json.""" + extension_with_versions( + isolated_filesystem, + ext_version="2.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ) + + result = cli_runner.invoke(app, ["update"]) + + assert result.exit_code == 0 + assert "Updated frontend/package.json" in result.output + assert "Updated backend/pyproject.toml" in result.output + + frontend_pkg = read_json(isolated_filesystem / "frontend" / "package.json") + assert frontend_pkg["version"] == "2.0.0" + + backend_pyproject = read_toml(isolated_filesystem / "backend" / "pyproject.toml") + assert backend_pyproject["project"]["version"] == "2.0.0" + + +@pytest.mark.cli +def test_update_noop_when_all_match( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test update reports no changes when everything already matches.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ) + + result = cli_runner.invoke(app, ["update"]) + + assert result.exit_code == 0 + assert "All files already up to date" in result.output + + +@pytest.mark.cli +def test_update_fails_without_extension_json(cli_runner, isolated_filesystem): + """Test update fails when extension.json is missing.""" + result = cli_runner.invoke(app, ["update"]) + + assert result.exit_code != 0 + assert "extension.json not found" in result.output + + +@pytest.mark.cli +def test_update_with_version_flag( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test --version updates extension.json first, then syncs all files.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ) + + result = cli_runner.invoke(app, ["update", "--version", "3.0.0"]) + + assert result.exit_code == 0 + assert "Updated extension.json" in result.output + assert "Updated frontend/package.json" in result.output + assert "Updated backend/pyproject.toml" in result.output + + ext = read_json(isolated_filesystem / "extension.json") + assert ext["version"] == "3.0.0" + + frontend_pkg = read_json(isolated_filesystem / "frontend" / "package.json") + assert frontend_pkg["version"] == "3.0.0" + + backend_pyproject = read_toml(isolated_filesystem / "backend" / "pyproject.toml") + assert backend_pyproject["project"]["version"] == "3.0.0" + + +@pytest.mark.cli +def test_update_with_license_flag( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test --license updates license across all files.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ext_license="Apache-2.0", + ) + + result = cli_runner.invoke(app, ["update", "--license", "MIT"]) + + assert result.exit_code == 0 + assert "Updated extension.json" in result.output + assert "Updated frontend/package.json" in result.output + assert "Updated backend/pyproject.toml" in result.output + + ext = read_json(isolated_filesystem / "extension.json") + assert ext["license"] == "MIT" + + frontend_pkg = read_json(isolated_filesystem / "frontend" / "package.json") + assert frontend_pkg["license"] == "MIT" + + backend_pyproject = read_toml(isolated_filesystem / "backend" / "pyproject.toml") + assert backend_pyproject["project"]["license"] == "MIT" + + +@pytest.mark.cli +def test_update_version_prompt_default( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test --version without value prompts with current version as default.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ) + + # Hit enter to accept default — nothing should change + result = cli_runner.invoke(app, ["update", "--version"], input="\n") + + assert result.exit_code == 0 + assert "All files already up to date" in result.output + + +@pytest.mark.cli +def test_update_rejects_invalid_version( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test --version with an invalid semver string exits with error.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + ) + + result = cli_runner.invoke(app, ["update", "--version", "not-a-version"]) + + assert result.exit_code != 0 + assert "Invalid value" in result.output + + # Verify extension.json was not modified + ext = read_json(isolated_filesystem / "extension.json") + assert ext["version"] == "1.0.0" diff --git a/superset-extensions-cli/tests/test_cli_validate.py b/superset-extensions-cli/tests/test_cli_validate.py index 970a2ce13ce..b796aedd930 100644 --- a/superset-extensions-cli/tests/test_cli_validate.py +++ b/superset-extensions-cli/tests/test_cli_validate.py @@ -207,3 +207,66 @@ def test_validate_npm_with_empty_version_output_raises_error(mock_run, mock_whic # semver.compare will raise ValueError for empty version with pytest.raises(ValueError): validate_npm() + + +# Version Consistency Tests +@pytest.mark.cli +def test_validate_fails_on_version_mismatch( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test validate fails when frontend/backend versions differ from extension.json.""" + extension_with_versions( + isolated_filesystem, + ext_version="2.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ) + + with patch("superset_extensions_cli.cli.validate_npm"): + result = cli_runner.invoke(app, ["validate"]) + + assert result.exit_code != 0 + assert "Metadata mismatch" in result.output + assert "superset-extensions update" in result.output + + +@pytest.mark.cli +def test_validate_passes_with_matching_versions( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test validate passes when all versions match extension.json.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ) + + with patch("superset_extensions_cli.cli.validate_npm"): + result = cli_runner.invoke(app, ["validate"]) + + assert result.exit_code == 0 + assert "Validation successful" in result.output + + +@pytest.mark.cli +def test_validate_fails_on_license_mismatch( + cli_runner, isolated_filesystem, extension_with_versions +): + """Test validate fails when frontend/backend licenses differ from extension.json.""" + extension_with_versions( + isolated_filesystem, + ext_version="1.0.0", + frontend_version="1.0.0", + backend_version="1.0.0", + ext_license="Apache-2.0", + frontend_license="MIT", + backend_license="MIT", + ) + + with patch("superset_extensions_cli.cli.validate_npm"): + result = cli_runner.invoke(app, ["validate"]) + + assert result.exit_code != 0 + assert "Metadata mismatch" in result.output + assert "license" in result.output diff --git a/superset-extensions-cli/tests/test_utils.py b/superset-extensions-cli/tests/test_utils.py index 1df07c72a60..4e87eaf9303 100644 --- a/superset-extensions-cli/tests/test_utils.py +++ b/superset-extensions-cli/tests/test_utils.py @@ -20,7 +20,7 @@ from __future__ import annotations import json import pytest -from superset_extensions_cli.utils import read_json, read_toml +from superset_extensions_cli.utils import read_json, read_toml, write_json, write_toml # Read JSON Tests @@ -269,3 +269,32 @@ def test_read_toml_with_permission_denied(isolated_filesystem): toml_file.chmod(0o644) except (OSError, PermissionError): pass + + +# Write JSON Tests +@pytest.mark.unit +def test_write_json_round_trip(isolated_filesystem): + """Test write_json then read_json round-trip preserves content.""" + data = {"name": "test-extension", "version": "2.0.0", "nested": {"key": "value"}} + json_file = isolated_filesystem / "output.json" + + write_json(json_file, data) + result = read_json(json_file) + + assert result == data + + +# Write TOML Tests +@pytest.mark.unit +def test_write_toml_round_trip(isolated_filesystem): + """Test write_toml then read_toml round-trip preserves content.""" + data = { + "project": {"name": "test-package", "version": "1.0.0"}, + "tool": {"apache_superset_extensions": {"build": {"include": ["src/**/*.py"]}}}, + } + toml_file = isolated_filesystem / "output.toml" + + write_toml(toml_file, data) + result = read_toml(toml_file) + + assert result == data diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index e0b12184eaf..d13cd1d03b0 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -69,7 +69,7 @@ module.exports = { ], coverageReporters: ['lcov', 'json-summary', 'html', 'text'], transformIgnorePatterns: [ - 'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued)', + 'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued)', ], preset: 'ts-jest', transform: { diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 6a6997b316f..e3a986c973f 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -89,11 +89,11 @@ "fs-extra": "^11.3.3", "fuse.js": "^7.1.0", "geolib": "^3.3.4", - "geostyler": "^14.1.3", + "geostyler": "^18.3.1", "geostyler-data": "^1.1.0", - "geostyler-openlayers-parser": "^4.3.0", - "geostyler-style": "7.5.0", - "geostyler-wfs-parser": "^2.0.3", + "geostyler-openlayers-parser": "^5.4.0", + "geostyler-style": "11.0.2", + "geostyler-wfs-parser": "^3.0.1", "google-auth-library": "^10.6.1", "immer": "^11.1.4", "interweave": "^13.1.1", @@ -224,7 +224,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.0", + "baseline-browser-mapping": "^2.10.7", "cheerio": "1.2.0", "concurrently": "^9.2.1", "copy-webpack-plugin": "^13.0.1", @@ -261,10 +261,10 @@ "js-yaml-loader": "^1.2.2", "jsdom": "^28.1.0", "lerna": "^8.2.3", - "lightningcss": "^1.31.1", - "mini-css-extract-plugin": "^2.10.0", + "lightningcss": "^1.32.0", + "mini-css-extract-plugin": "^2.10.1", "open-cli": "^8.0.0", - "oxlint": "^1.51.0", + "oxlint": "^1.53.0", "po2json": "^0.4.5", "prettier": "3.8.1", "prettier-plugin-packagejson": "^3.0.2", @@ -2391,23 +2391,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/polyfill": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz", - "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==", - "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.", - "license": "MIT", - "dependencies": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - } - }, - "node_modules/@babel/polyfill/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT" - }, "node_modules/@babel/preset-env": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", @@ -7471,29 +7454,26 @@ "license": "MIT" }, "node_modules/@monaco-editor/loader": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", - "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", "license": "MIT", "dependencies": { "state-local": "^1.0.6" - }, - "peerDependencies": { - "monaco-editor": ">= 0.21.0 < 1" } }, "node_modules/@monaco-editor/react": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", - "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", "license": "MIT", "dependencies": { - "@monaco-editor/loader": "^1.4.0" + "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -8563,9 +8543,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.51.0.tgz", - "integrity": "sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.53.0.tgz", + "integrity": "sha512-JC89/jAx4d2zhDIbK8MC4L659FN1WiMXMBkNg7b33KXSkYpUgcbf+0nz7+EPRg+VwWiZVfaoFkNHJ7RXYb5Neg==", "cpu": [ "arm" ], @@ -8580,9 +8560,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.51.0.tgz", - "integrity": "sha512-GtXyBCcH4ti98YdiMNCrpBNGitx87EjEWxevnyhcBK12k/Vu4EzSB45rzSC4fGFUD6sQgeaxItRCEEWeVwPafw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.53.0.tgz", + "integrity": "sha512-CY+pZfi+uyeU7AwFrEnjsNT+VfxYmKLMuk7bVxArd8f+09hQbJb8f7C7EpvTfNqrCK1J8zZlaYI4LltmEctgbQ==", "cpu": [ "arm64" ], @@ -8597,9 +8577,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.51.0.tgz", - "integrity": "sha512-3QJbeYaMHn6Bh2XeBXuITSsbnIctyTjvHf5nRjKYrT9pPeErNIpp5VDEeAXC0CZSwSVTsc8WOSDwgrAI24JolQ==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.53.0.tgz", + "integrity": "sha512-0aqsC4HDQ94oI6kMz64iaOJ1f3bCVArxvaHJGOScBvFz6CcQedXi5b70Xg09CYjKNaHA56dW0QJfoZ/111kz1A==", "cpu": [ "arm64" ], @@ -8614,9 +8594,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.51.0.tgz", - "integrity": "sha512-NzErhMaTEN1cY0E8C5APy74lw5VwsNfJfVPBMWPVQLqAbO0k4FFLjvHURvkUL+Y18Wu+8Vs1kbqPh2hjXYA4pg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.53.0.tgz", + "integrity": "sha512-e+KvuaWtnisyWojO/t5qKDbp2dvVpg+1dl4MGnTb21QpY4+4+9Y1XmZPaztcA2XNvy4BIaXFW+9JH9tMpSBqUg==", "cpu": [ "x64" ], @@ -8631,9 +8611,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.51.0.tgz", - "integrity": "sha512-msAIh3vPAoKoHlOE/oe6Q5C/n9umypv/k81lED82ibrJotn+3YG2Qp1kiR8o/Dg5iOEU97c6tl0utxcyFenpFw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.53.0.tgz", + "integrity": "sha512-hpU0ZHVeblFjmZDfgi9BxhhCpURh0KjoFy5V+Tvp9sg/fRcnMUEfaJrgz+jQfOX4jctlVWrAs1ANs91+5iV+zA==", "cpu": [ "x64" ], @@ -8648,9 +8628,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.51.0.tgz", - "integrity": "sha512-CqQPcvqYyMe9ZBot2stjGogEzk1z8gGAngIX7srSzrzexmXixwVxBdFZyxTVM0CjGfDeV+Ru0w25/WNjlMM2Hw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.53.0.tgz", + "integrity": "sha512-ccKxOpw+X4xa2pO+qbTOpxQ2x1+Ag3ViRQMnWt3gHp1LcpNgS1xd6GYc3OvehmHtrXqEV3YGczZ0I1qpBB4/2A==", "cpu": [ "arm" ], @@ -8665,9 +8645,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.51.0.tgz", - "integrity": "sha512-dstrlYQgZMnyOssxSbolGCge/sDbko12N/35RBNuqLpoPbft2aeBidBAb0dvQlyBd9RJ6u8D4o4Eh8Un6iTgyQ==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.53.0.tgz", + "integrity": "sha512-UBkBvmzSmlyH2ZObQMDKW/TuyTmUtP/XClPUyU2YLwj0qLopZTZxnDz4VG5d3wz1HQuZXO0o1QqsnQUW1v4a6Q==", "cpu": [ "arm" ], @@ -8682,9 +8662,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.51.0.tgz", - "integrity": "sha512-QEjUpXO7d35rP1/raLGGbAsBLLGZIzV3ZbeSjqWlD3oRnxpRIZ6iL4o51XQHkconn3uKssc+1VKdtHJ81BBhDA==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.53.0.tgz", + "integrity": "sha512-PQJJ1izoH9p61las6rZ0BWOznAhTDMmdUPL2IEBLuXFwhy2mSloYHvRkk39PSYJ1DyG+trqU5Z9ZbtHSGH6plg==", "cpu": [ "arm64" ], @@ -8699,9 +8679,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.51.0.tgz", - "integrity": "sha512-YSJua5irtG4DoMAjUapDTPhkQLHhBIY0G9JqlZS6/SZPzqDkPku/1GdWs0D6h/wyx0Iz31lNCfIaWKBQhzP0wQ==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.53.0.tgz", + "integrity": "sha512-GXI1o4Thn/rtnRIL38BwrDMwVcUbIHKCsOixIWf/CkU3fCG3MXFzFTtDMt+34ik0Qk452d8kcpksL0w/hUkMZA==", "cpu": [ "arm64" ], @@ -8716,9 +8696,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.51.0.tgz", - "integrity": "sha512-7L4Wj2IEUNDETKssB9IDYt16T6WlF+X2jgC/hBq3diGHda9vJLpAgb09+D3quFq7TdkFtI7hwz/jmuQmQFPc1Q==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.53.0.tgz", + "integrity": "sha512-Uahk7IVs2yBamCgeJ3XKpKT9Vh+de0pDKISFKnjEcI3c/w2CFHk1+W6Q6G3KI56HGwE9PWCp6ayhA9whXWkNIQ==", "cpu": [ "ppc64" ], @@ -8733,9 +8713,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.51.0.tgz", - "integrity": "sha512-cBUHqtOXy76G41lOB401qpFoKx1xq17qYkhWrLSM7eEjiHM9sOtYqpr6ZdqCnN9s6ZpzudX4EkeHOFH2E9q0vA==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.53.0.tgz", + "integrity": "sha512-sWtcU9UkrKMWsGKdFy8R6jkm9Q0VVG1VCpxVuh0HzRQQi3ENI1Nh5CkpsdfUs2MKRcOoHKbXqTscunuXjhxoxQ==", "cpu": [ "riscv64" ], @@ -8750,9 +8730,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.51.0.tgz", - "integrity": "sha512-WKbg8CysgZcHfZX0ixQFBRSBvFZUHa3SBnEjHY2FVYt2nbNJEjzTxA3ZR5wMU0NOCNKIAFUFvAh5/XJKPRJuJg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.53.0.tgz", + "integrity": "sha512-aXew1+HDvCdExijX/8NBVC854zJwxhKP3l9AHFSHQNo4EanlHtzDMIlIvP3raUkL0vXtFCkTFYezzU5HjstB8A==", "cpu": [ "riscv64" ], @@ -8767,9 +8747,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.51.0.tgz", - "integrity": "sha512-N1QRUvJTxqXNSu35YOufdjsAVmKVx5bkrggOWAhTWBc3J4qjcBwr1IfyLh/6YCg8sYRSR1GraldS9jUgJL/U4A==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.53.0.tgz", + "integrity": "sha512-rVpyBSqPGou9sITcsoXqUoGBUH74bxYLYOAGUqN599Zu6BQBlBU9hh3bJQ/20D1xrhhrsbiCpVPvXpLPM5nL1w==", "cpu": [ "s390x" ], @@ -8784,9 +8764,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.51.0.tgz", - "integrity": "sha512-e0Mz0DizsCoqNIjeOg6OUKe8JKJWZ5zZlwsd05Bmr51Jo3AOL4UJnPvwKumr4BBtBrDZkCmOLhCvDGm95nJM2g==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.53.0.tgz", + "integrity": "sha512-eOyeQ8qFQ2geXmlWJuXAOaek0hFhbMLlYsU457NMLKDRoC43Xf+eDPZ9Yk0n9jDaGJ5zBl/3Dy8wo41cnIXuLA==", "cpu": [ "x64" ], @@ -8801,9 +8781,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.51.0.tgz", - "integrity": "sha512-wD8HGTWhYBKXvRDvoBVB1y+fEYV01samhWQSy1Zkxq2vpezvMnjaFKRuiP6tBNITLGuffbNDEXOwcAhJ3gI5Ug==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.53.0.tgz", + "integrity": "sha512-S6rBArW/zD1tob8M9PwKYrRmz+j1ss1+wjbRAJCWKd7TC3JB6noDiA95pIj9zOZVVp04MIzy5qymnYusrEyXzg==", "cpu": [ "x64" ], @@ -8818,9 +8798,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.51.0.tgz", - "integrity": "sha512-5NSwQ2hDEJ0GPXqikjWtwzgAQCsS7P9aLMNenjjKa+gknN3lTCwwwERsT6lKXSirfU3jLjexA2XQvQALh5h27w==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.53.0.tgz", + "integrity": "sha512-sd/A0Ny5sN0D/MJtlk7w2jGY4bJQou7gToa9WZF7Sj6HTyVzvlzKJWiOHfr4SulVk4ndiFQ8rKmF9rXP0EcF3A==", "cpu": [ "arm64" ], @@ -8835,9 +8815,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.51.0.tgz", - "integrity": "sha512-JEZyah1M0RHMw8d+jjSSJmSmO8sABA1J1RtrHYujGPeCkYg1NeH0TGuClpe2h5QtioRTaF57y/TZfn/2IFV6fA==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.53.0.tgz", + "integrity": "sha512-QC3q7b51Er/ZurEFcFzc7RpQ/YEoEBLJuCp3WoOzhSHHH/nkUKFy+igOxlj1z3LayhEZPDQQ7sXvv2PM2cdG3Q==", "cpu": [ "arm64" ], @@ -8852,9 +8832,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.51.0.tgz", - "integrity": "sha512-q3cEoKH6kwjz/WRyHwSf0nlD2F5Qw536kCXvmlSu+kaShzgrA0ojmh45CA81qL+7udfCaZL2SdKCZlLiGBVFlg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.53.0.tgz", + "integrity": "sha512-3OvLgOqwd705hWHV2i8ni80pilvg6BUgpC2+xtVu++e/q28LKVohGh5J5QYJOrRMfWmxK0M/AUu43vUw62LAKQ==", "cpu": [ "ia32" ], @@ -8869,9 +8849,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.51.0.tgz", - "integrity": "sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.53.0.tgz", + "integrity": "sha512-xTiOkntexCdJytZ7ArIIgl3vGW5ujMM3sJNM7/+iqGAVJagCqjFFWn68HRWRLeyT66c95uR+CeFmQFI6mLQqDw==", "cpu": [ "x64" ], @@ -14395,12 +14375,14 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -15304,6 +15286,7 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/yauzl": { @@ -18434,9 +18417,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -18587,15 +18570,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/blob": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.1.0.tgz", - "integrity": "sha512-k+GwK+4Rj+MPNT4qu+y6+kHp+mPmmNd+28zdrIo69QM9UvypK5Vhcw7jnRiY4KaOMAiOdn0NtPQGTb+Ox1Dtng==", - "license": "MIT", - "dependencies": { - "esm": "^3.2.25" - } - }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -19532,9 +19506,9 @@ } }, "node_modules/chroma-js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", - "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", "license": "(BSD-3-Clause AND Apache-2.0)" }, "node_modules/chrome-trace-event": { @@ -23125,9 +23099,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -24823,15 +24797,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -25333,10 +25298,22 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", "funding": [ { "type": "github", @@ -25345,7 +25322,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^1.1.1" + "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" @@ -25875,9 +25852,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -26495,45 +26472,43 @@ "license": "MIT" }, "node_modules/geostyler": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/geostyler/-/geostyler-14.2.0.tgz", - "integrity": "sha512-mcHNeds2OBvyCs+VdLHkq4vX1PrR/HKN7hnmySUVpNiZCZ4b0kDnPKe3gUOsh2wEs9pOC1soS9AbNlVC6XpH7w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/geostyler/-/geostyler-18.3.1.tgz", + "integrity": "sha512-ZEZDwdAtP6Z4ANtlMfYtMwymdFMrr6n0kD4q8dMANA54taXgbeJKU2quC93MQlXlSo8Ka8Y1xI3qAliRasvWcw==", "license": "BSD-2-Clause", "dependencies": { - "@ant-design/icons": "^5.3.7", - "@babel/polyfill": "^7.12.1", + "@ant-design/icons": "^5.5.1", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", - "@monaco-editor/react": "^4.6.0", + "@monaco-editor/react": "^4.7.0", "@types/chroma-js": "^2.4.4", "@types/color": "^3.0.6", "@types/file-saver": "^2.0.7", "@types/geojson": "^7946.0.14", - "@types/lodash": "^4.17.4", + "@types/lodash": "^4.17.5", "@ungap/url-search-params": "^0.2.2", - "antd": "^5.17.3", - "blob": "^0.1.0", - "chroma-js": "^2.4.2", + "antd": "^5.25.4", + "chroma-js": "3.2.0", "color": "^4.2.3", "csstype": "^3.1.3", "file-saver": "^2.0.5", - "geostyler-cql-parser": "^3.0.2", - "geostyler-data": "^1.0.0", - "geostyler-geojson-parser": "^1.0.1", - "geostyler-mapbox-parser": "^5.0.1", - "geostyler-openlayers-parser": "^4.3.0", - "geostyler-qgis-parser": "^2.0.0", - "geostyler-sld-parser": "^5.3.1", - "geostyler-style": "^8.1.0", - "geostyler-wfs-parser": "^2.0.0", - "lodash": "^4.17.21", - "monaco-editor": "^0.49.0", + "geostyler-cql-parser": "^4.1.0", + "geostyler-data": "^1.1.0", + "geostyler-geojson-parser": "^2.0.0", + "geostyler-mapbox-parser": "^6.1.1", + "geostyler-openlayers-parser": "^5.4.0", + "geostyler-qgis-parser": "^4.0.2", + "geostyler-sld-parser": "^8.3.0", + "geostyler-style": "^11.0.2", + "geostyler-wfs-parser": "^3.0.1", + "lodash-es": "^4.17.21", + "monaco-editor": "^0.52.0", "proj4": "^2.11.0", - "typescript-json-schema": "^0.64.0" + "typescript-json-schema": "^0.65.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.6.0" }, "funding": { "url": "https://opencollective.com/geostyler" @@ -26547,13 +26522,32 @@ } }, "node_modules/geostyler-cql-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/geostyler-cql-parser/-/geostyler-cql-parser-3.0.2.tgz", - "integrity": "sha512-nfQOnWwFSrWtgl7DRmhwoa3r0BnpsMvEv7rb5D8ihpCUQS/YP09C2C1j6zKQVSnYRMl0545VnhtYtqejMgPaBg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/geostyler-cql-parser/-/geostyler-cql-parser-4.1.1.tgz", + "integrity": "sha512-k594MfqCtQfdaNdK3jyLCiZz5bLeg4pjfAk3qgzfM9dwLmI277BI01Ti68Q+Q72dybUV1VIyGU0ky8teUi5Cmg==", "license": "BSD-2-Clause", "dependencies": { - "geostyler-style": "^7.2.0", - "lodash": "^4.17.21" + "geostyler-style": "^10.0.0" + }, + "engines": { + "node": ">=20.6.0", + "npm": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, + "node_modules/geostyler-cql-parser/node_modules/geostyler-style": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-10.5.0.tgz", + "integrity": "sha512-XnKrjm5XY2hBc98h5uKaAdYAgy+R9YuBzf8aGo4SPmYoPtAlnNaGWkRZB8j/vnuzQg8/kpGVOJ94o+42WUhuhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.6.0", + "npm": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/geostyler" } }, "node_modules/geostyler-data": { @@ -26570,209 +26564,28 @@ } }, "node_modules/geostyler-geojson-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/geostyler-geojson-parser/-/geostyler-geojson-parser-1.0.1.tgz", - "integrity": "sha512-b7eJ2sCrYCC7fIDItxfbDH9r55dE58OXTQjPb/kIlXgH+7A2o2xp7pQlRXu5xCqM5lucQAAM9A7IfLLhbflznw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/geostyler-geojson-parser/-/geostyler-geojson-parser-2.0.0.tgz", + "integrity": "sha512-FBBoVfaa9musK/L679I+2l8y+wS6EJ5C73RE9qJ4drHMmHE/JwliLS0cMFnrgrp+NlQVyu/dbv6CQs6PLFKnWA==", "license": "BSD-2-Clause", "dependencies": { - "@babel/polyfill": "^7.4.4", - "@types/geojson": "^7946.0.7", - "@types/jest": "^24.0.18", - "@types/json-schema": "^7.0.3", - "@types/node": "^12.7.3", "geostyler-data": "^1.0.0" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/@jest/types": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", - "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^13.0.0" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/@types/jest": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", - "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", - "license": "MIT", - "dependencies": { - "jest-diff": "^24.3.0" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "license": "MIT" - }, - "node_modules/geostyler-geojson-parser/node_modules/@types/yargs": { - "version": "13.0.12", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.12.tgz", - "integrity": "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" + "node": ">=20.6.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/geostyler-geojson-parser/node_modules/diff-sequences": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", - "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/jest-diff": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", - "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", - "license": "MIT", - "dependencies": { - "chalk": "^2.0.1", - "diff-sequences": "^24.9.0", - "jest-get-type": "^24.9.0", - "pretty-format": "^24.9.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/jest-get-type": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", - "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/pretty-format": { - "version": "24.9.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", - "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", - "license": "MIT", - "dependencies": { - "@jest/types": "^24.9.0", - "ansi-regex": "^4.0.0", - "ansi-styles": "^3.2.0", - "react-is": "^16.8.4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/geostyler-geojson-parser/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/geostyler-geojson-parser/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://opencollective.com/geostyler" } }, "node_modules/geostyler-mapbox-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/geostyler-mapbox-parser/-/geostyler-mapbox-parser-5.0.1.tgz", - "integrity": "sha512-uL79jfXuULPcg/Yxj9EGDU5IX0xIQmhawUL239aCwbFv4FiM9KTuTdXG1M7P5r3RV6EZsbE21ETahx4ZzojVkw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/geostyler-mapbox-parser/-/geostyler-mapbox-parser-6.2.0.tgz", + "integrity": "sha512-aG7znml0nD62HAo+/H2MQz8uQIFnBRLiEcaZR2rf+ve/6MNGKidihCdXY231hAK/hw67BLeDYs22lxWub8wS9g==", "license": "BSD-2-Clause", "dependencies": { - "@types/mapbox-gl": "^2.7.18", - "geostyler-style": "^8.0.0" + "@types/mapbox-gl": "^3.4.1", + "geostyler-style": "^10.5.0" }, "engines": { "node": ">=18", @@ -26782,34 +26595,40 @@ "url": "https://opencollective.com/geostyler" } }, - "node_modules/geostyler-mapbox-parser/node_modules/geostyler-style": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", - "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", - "license": "BSD-2-Clause", + "node_modules/geostyler-mapbox-parser/node_modules/@types/mapbox-gl": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", + "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==", + "license": "MIT", "dependencies": { - "@types/lodash": "^4.14.201", - "lodash": "^4.17.21" - }, + "@types/geojson": "*" + } + }, + "node_modules/geostyler-mapbox-parser/node_modules/geostyler-style": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-10.5.0.tgz", + "integrity": "sha512-XnKrjm5XY2hBc98h5uKaAdYAgy+R9YuBzf8aGo4SPmYoPtAlnNaGWkRZB8j/vnuzQg8/kpGVOJ94o+42WUhuhw==", + "license": "BSD-2-Clause", "engines": { - "node": ">=16.0.0", - "npm": ">=6.0.0" + "node": ">=20.6.0", + "npm": ">=10.0.0" }, "funding": { "url": "https://opencollective.com/geostyler" } }, "node_modules/geostyler-openlayers-parser": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/geostyler-openlayers-parser/-/geostyler-openlayers-parser-4.3.0.tgz", - "integrity": "sha512-9LoctzoxorZogOpp6nXT6SZGnISRpNR9M+MLyhYVTFlOtDi4Gb4fWbUnpqVzDxqMNl8anMmv7cpMsmw1bjWsLw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/geostyler-openlayers-parser/-/geostyler-openlayers-parser-5.4.0.tgz", + "integrity": "sha512-BdlG10/wvok7KCOdtzM/NKfiUfkx+uqLDgesC3lNZnviz02S+zIQRtbAkb/Abin6/2boMFRL3i7YIt4D98xMNw==", "license": "BSD-2-Clause", "dependencies": { "css-font-parser": "^2.0.0", - "geostyler-style": "^8.1.0" + "geostyler-style": "^11.0.1", + "lodash": "^4.17.21" }, "engines": { - "node": ">=18" + "node": ">=20.6.0" }, "funding": { "url": "https://opencollective.com/geostyler" @@ -26818,34 +26637,17 @@ "ol": ">=7.4" } }, - "node_modules/geostyler-openlayers-parser/node_modules/geostyler-style": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", - "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", - "license": "BSD-2-Clause", - "dependencies": { - "@types/lodash": "^4.14.201", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=6.0.0" - }, - "funding": { - "url": "https://opencollective.com/geostyler" - } - }, "node_modules/geostyler-qgis-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/geostyler-qgis-parser/-/geostyler-qgis-parser-2.1.0.tgz", - "integrity": "sha512-0Xu3Dd1hBKSS7Qv8//ciHavVJI3Ez03JxrYfzF0HNZq32tNL/DITxxbD8nYb/uWawIsBWnJMFbmykxsqC1EEXA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/geostyler-qgis-parser/-/geostyler-qgis-parser-4.1.0.tgz", + "integrity": "sha512-LuATAdIrL2+6D3sCXSqjbztOJdbuDdDuAF+FJpIoMlcITSpXIYe1aBjoE/roQzNM/DwWxqSMb3yHTaozkytvnA==", "license": "BSD-2-Clause", "dependencies": { "buffer": "^6.0.3", "color": "^4.2.3", "core-js": "^3.26.1", - "geostyler-cql-parser": "4.0.0-beta.4", - "geostyler-style": "9.0.0-next.5", + "geostyler-cql-parser": "^4.0.0", + "geostyler-style": "^10.0.0", "string_decoder": "^1.3.0", "timers": "^0.1.1", "xml2js": "^0.6.0" @@ -26878,26 +26680,10 @@ "ieee754": "^1.2.1" } }, - "node_modules/geostyler-qgis-parser/node_modules/geostyler-cql-parser": { - "version": "4.0.0-beta.4", - "resolved": "https://registry.npmjs.org/geostyler-cql-parser/-/geostyler-cql-parser-4.0.0-beta.4.tgz", - "integrity": "sha512-aBR1oAx6gkmz6PZVbd8zmCQ0gwdeh/g7bkDL9v/YURZCInLAqH5o4oGP72ljt3Qjc6/zYwm5TYuJ3p+wwe2nkA==", - "license": "BSD-2-Clause", - "dependencies": { - "geostyler-style": "9.0.0-next.5" - }, - "engines": { - "node": ">=20.6.0", - "npm": ">=10.0.0" - }, - "funding": { - "url": "https://opencollective.com/geostyler" - } - }, "node_modules/geostyler-qgis-parser/node_modules/geostyler-style": { - "version": "9.0.0-next.5", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-9.0.0-next.5.tgz", - "integrity": "sha512-nbTyL5i4zAB303RUXpVRKhrXceSKu2sOMWqjXjitjII31eOIKybS0iKV5C4f1qktE3TweJY/xPtC6aP6rTsQAw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-10.5.0.tgz", + "integrity": "sha512-XnKrjm5XY2hBc98h5uKaAdYAgy+R9YuBzf8aGo4SPmYoPtAlnNaGWkRZB8j/vnuzQg8/kpGVOJ94o+42WUhuhw==", "license": "BSD-2-Clause", "engines": { "node": ">=20.6.0", @@ -26908,66 +26694,74 @@ } }, "node_modules/geostyler-sld-parser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-5.4.0.tgz", - "integrity": "sha512-TY/gwMoE/M8Xv8ykviC1NqygykrojvrFSCRIc/+CNww5VS0Qf9pu9JxiSQONGfR6QWDPtQrfFIoazyH67c0jTg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-8.4.0.tgz", + "integrity": "sha512-XR7o5L1n/+xBk/W1WVobX7ftoeoRy486o7qfI1D24PYcRFK3EBr5Dh1TZgGI2U7VXDWx2/S4ipl0hztqbjYnxw==", "license": "BSD-2-Clause", "dependencies": { - "fast-xml-parser": "^4.2.2", - "geostyler-style": "^8.1.0", - "i18next": "^23.11.5", + "fast-xml-parser": "^5.2.3", + "geostyler-style": "^11.0.2", "lodash": "^4.17.21" }, + "engines": { + "node": ">=20.6.0" + }, "funding": { "url": "https://opencollective.com/geostyler" } }, - "node_modules/geostyler-sld-parser/node_modules/geostyler-style": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", - "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", - "license": "BSD-2-Clause", + "node_modules/geostyler-sld-parser/node_modules/fast-xml-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz", + "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", "dependencies": { - "@types/lodash": "^4.14.201", - "lodash": "^4.17.21" + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" }, - "engines": { - "node": ">=16.0.0", - "npm": ">=6.0.0" - }, - "funding": { - "url": "https://opencollective.com/geostyler" + "bin": { + "fxparser": "src/cli/cli.js" } }, + "node_modules/geostyler-sld-parser/node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/geostyler-style": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-7.5.0.tgz", - "integrity": "sha512-0qlFOwDl9cdiiNUk6ijRSpBq+W3Kpry1aZS++BE8EPBxvbFgoTsKU6JIriKLmCqPqBmJIVnMFMUEk6Sf3n0ZIA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-11.0.2.tgz", + "integrity": "sha512-HDCjVf7qFIX1FRcp1+8uKD+Iqt/6rWzmwsZdzcOCBcdOoh5vFlLcAchjMP0eDgdVspPpwK6eZSSSYDabUGTZzA==", "license": "BSD-2-Clause", - "dependencies": { - "@types/lodash": "^4.14.201", - "lodash": "^4.17.21" - }, "engines": { - "node": ">=16.0.0", - "npm": ">=6.0.0" + "node": ">=20.6.0", + "npm": ">=10.0.0" }, "funding": { "url": "https://opencollective.com/geostyler" } }, "node_modules/geostyler-wfs-parser": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/geostyler-wfs-parser/-/geostyler-wfs-parser-2.0.3.tgz", - "integrity": "sha512-23gmufyveYB/jhVzxPj/eb/zkI+xWSzTKqGO7gcAh0NYUFPI+UG3WTJhzT4Dj9G+ZTpSUnXOSmkHr4M7jZsakA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/geostyler-wfs-parser/-/geostyler-wfs-parser-3.0.1.tgz", + "integrity": "sha512-U4Y9F6yTG0wRatkHnFLYBubcjXd1FwHs12zhFxLhZXzddOc4Gc25ffqe1o7x6ypmlbg0Tkb3aYsIXa3gfcNRfw==", "license": "BSD-2-Clause", "dependencies": { - "@types/geojson": "^7946.0.14", - "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.4", "fast-xml-parser": "^4.4.0", - "geostyler-data": "^1.0.0", - "lodash": "^4.17.21" + "geostyler-data": "^1.0.0" }, "funding": { "url": "https://opencollective.com/geostyler" @@ -26987,23 +26781,6 @@ "react": ">=16.8.0" } }, - "node_modules/geostyler/node_modules/geostyler-style": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-8.1.0.tgz", - "integrity": "sha512-8NgtzRc63bxC+1Vgqj/mMj77GX38CXXXWQ93PeZBdoMTkY9C/H0Anz38OrrlKdUgNVFZ/GJTNYwnX4wdaO5j6A==", - "license": "BSD-2-Clause", - "dependencies": { - "@types/lodash": "^4.14.201", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=6.0.0" - }, - "funding": { - "url": "https://opencollective.com/geostyler" - } - }, "node_modules/geotiff": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.3.tgz", @@ -28756,29 +28533,6 @@ "node": ">=10.18" } }, - "node_modules/i18next": { - "version": "23.16.8", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", - "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -35258,9 +35012,9 @@ } }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -35274,23 +35028,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -35309,9 +35063,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -35330,9 +35084,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -35351,9 +35105,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -35372,9 +35126,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -35393,9 +35147,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -35414,9 +35168,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -35435,9 +35189,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -35456,9 +35210,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -35477,9 +35231,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -35498,9 +35252,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -36954,9 +36708,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz", - "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.1.tgz", + "integrity": "sha512-k7G3Y5QOegl380tXmZ68foBRRjE9Ljavx835ObdvmZjQ639izvZD8CS7BkWw1qKPPzHsGL/JDhl0uyU1zc2rJw==", "dev": true, "license": "MIT", "dependencies": { @@ -37246,9 +37000,9 @@ } }, "node_modules/monaco-editor": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", - "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==", + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", "license": "MIT" }, "node_modules/mousetrap": { @@ -38781,9 +38535,9 @@ } }, "node_modules/oxlint": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.51.0.tgz", - "integrity": "sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.53.0.tgz", + "integrity": "sha512-TLW0PzGbpO1JxUnuy1pIqVPjQUGh4fNfxu5XJbdFIRFVaJ0UFzTjjk/hSFTMRxN6lZub53xL/IwJNEkrh7VtDg==", "dev": true, "license": "MIT", "bin": { @@ -38796,25 +38550,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.51.0", - "@oxlint/binding-android-arm64": "1.51.0", - "@oxlint/binding-darwin-arm64": "1.51.0", - "@oxlint/binding-darwin-x64": "1.51.0", - "@oxlint/binding-freebsd-x64": "1.51.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.51.0", - "@oxlint/binding-linux-arm-musleabihf": "1.51.0", - "@oxlint/binding-linux-arm64-gnu": "1.51.0", - "@oxlint/binding-linux-arm64-musl": "1.51.0", - "@oxlint/binding-linux-ppc64-gnu": "1.51.0", - "@oxlint/binding-linux-riscv64-gnu": "1.51.0", - "@oxlint/binding-linux-riscv64-musl": "1.51.0", - "@oxlint/binding-linux-s390x-gnu": "1.51.0", - "@oxlint/binding-linux-x64-gnu": "1.51.0", - "@oxlint/binding-linux-x64-musl": "1.51.0", - "@oxlint/binding-openharmony-arm64": "1.51.0", - "@oxlint/binding-win32-arm64-msvc": "1.51.0", - "@oxlint/binding-win32-ia32-msvc": "1.51.0", - "@oxlint/binding-win32-x64-msvc": "1.51.0" + "@oxlint/binding-android-arm-eabi": "1.53.0", + "@oxlint/binding-android-arm64": "1.53.0", + "@oxlint/binding-darwin-arm64": "1.53.0", + "@oxlint/binding-darwin-x64": "1.53.0", + "@oxlint/binding-freebsd-x64": "1.53.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.53.0", + "@oxlint/binding-linux-arm-musleabihf": "1.53.0", + "@oxlint/binding-linux-arm64-gnu": "1.53.0", + "@oxlint/binding-linux-arm64-musl": "1.53.0", + "@oxlint/binding-linux-ppc64-gnu": "1.53.0", + "@oxlint/binding-linux-riscv64-gnu": "1.53.0", + "@oxlint/binding-linux-riscv64-musl": "1.53.0", + "@oxlint/binding-linux-s390x-gnu": "1.53.0", + "@oxlint/binding-linux-x64-gnu": "1.53.0", + "@oxlint/binding-linux-x64-musl": "1.53.0", + "@oxlint/binding-openharmony-arm64": "1.53.0", + "@oxlint/binding-win32-arm64-msvc": "1.53.0", + "@oxlint/binding-win32-ia32-msvc": "1.53.0", + "@oxlint/binding-win32-x64-msvc": "1.53.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" @@ -44928,14 +44682,14 @@ } }, "node_modules/simple-git": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.5" + "debug": "^4.4.0" }, "funding": { "type": "github", @@ -48116,18 +47870,18 @@ } }, "node_modules/typescript-json-schema": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.64.0.tgz", - "integrity": "sha512-Sew8llkYSzpxaMoGjpjD6NMFCr6DoWFHLs7Bz1LU48pzzi8ok8W+GZs9cG87IMBpC0UI7qwBMUI2um0LGxxLOg==", + "version": "0.65.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.65.1.tgz", + "integrity": "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg==", "license": "BSD-3-Clause", "dependencies": { "@types/json-schema": "^7.0.9", - "@types/node": "^16.9.2", + "@types/node": "^18.11.9", "glob": "^7.1.7", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.2.0", "ts-node": "^10.9.1", - "typescript": "~5.1.0", + "typescript": "~5.5.0", "yargs": "^17.1.1" }, "bin": { @@ -48135,15 +47889,18 @@ } }, "node_modules/typescript-json-schema/node_modules/@types/node": { - "version": "16.18.126", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", - "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", - "license": "MIT" + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/typescript-json-schema/node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -48153,6 +47910,12 @@ "node": ">=14.17" } }, + "node_modules/typescript-json-schema/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -48198,16 +47961,15 @@ } }, "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "dev": true, + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", + "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", "dev": true, "license": "MIT", "engines": { @@ -51491,7 +51253,7 @@ "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "dayjs": "^1.11.19", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "fetch-retry": "^6.0.0", "handlebars": "^4.7.8", "jed": "^1.1.1", @@ -51505,7 +51267,7 @@ "react-js-cron": "^5.2.0", "react-markdown": "^8.0.7", "react-resize-detector": "^7.1.2", - "react-syntax-highlighter": "^16.1.0", + "react-syntax-highlighter": "^16.1.1", "react-ultimate-pagination": "^1.3.2", "regenerator-runtime": "^0.14.1", "rehype-raw": "^7.0.0", @@ -52836,12 +52598,6 @@ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "license": "ISC" }, - "plugins/legacy-preset-chart-deckgl/node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", - "license": "MIT" - }, "plugins/legacy-preset-chart-nvd3": { "name": "@superset-ui/legacy-preset-chart-nvd3", "version": "0.20.3", @@ -52849,7 +52605,7 @@ "dependencies": { "d3": "^3.5.17", "d3-tip": "^0.9.1", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "fast-safe-stringify": "^2.1.1", "lodash": "^4.17.23", "nvd3-fork": "^2.0.5", @@ -52923,11 +52679,11 @@ "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", "@types/react-redux": "*", - "geostyler": "^14.1.3", + "geostyler": "^18.3.1", "geostyler-data": "^1.0.0", - "geostyler-openlayers-parser": "^4.0.0", - "geostyler-style": "^7.2.0", - "geostyler-wfs-parser": "^2.0.0", + "geostyler-openlayers-parser": "^5.4.0", + "geostyler-style": "^11.0.2", + "geostyler-wfs-parser": "^3.0.1", "ol": "^10.8.0", "polished": "*", "react": "^17.0.2", @@ -52941,7 +52697,7 @@ "dependencies": { "@types/d3-array": "^3.2.2", "@types/react-redux": "^7.1.34", - "acorn": "^8.9.0", + "acorn": "^8.16.0", "d3-array": "^3.2.4", "lodash": "^4.17.23", "zod": "^4.3.6" @@ -52957,9 +52713,9 @@ } }, "plugins/plugin-chart-echarts/node_modules/acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 945b677c0e3..b1a82dd245e 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -170,11 +170,11 @@ "fs-extra": "^11.3.3", "fuse.js": "^7.1.0", "geolib": "^3.3.4", - "geostyler": "^14.1.3", + "geostyler": "^18.3.1", "geostyler-data": "^1.1.0", - "geostyler-openlayers-parser": "^4.3.0", - "geostyler-style": "7.5.0", - "geostyler-wfs-parser": "^2.0.3", + "geostyler-openlayers-parser": "^5.4.0", + "geostyler-style": "11.0.2", + "geostyler-wfs-parser": "^3.0.1", "google-auth-library": "^10.6.1", "immer": "^11.1.4", "interweave": "^13.1.1", @@ -305,7 +305,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.0", + "baseline-browser-mapping": "^2.10.7", "cheerio": "1.2.0", "concurrently": "^9.2.1", "copy-webpack-plugin": "^13.0.1", @@ -342,10 +342,10 @@ "js-yaml-loader": "^1.2.2", "jsdom": "^28.1.0", "lerna": "^8.2.3", - "lightningcss": "^1.31.1", - "mini-css-extract-plugin": "^2.10.0", + "lightningcss": "^1.32.0", + "mini-css-extract-plugin": "^2.10.1", "open-cli": "^8.0.0", - "oxlint": "^1.51.0", + "oxlint": "^1.53.0", "po2json": "^0.4.5", "prettier": "3.8.1", "prettier-plugin-packagejson": "^3.0.2", diff --git a/superset-frontend/packages/superset-core/src/editors/index.ts b/superset-frontend/packages/superset-core/src/editors/index.ts index a1ed3ed9653..0fea7c7fe71 100644 --- a/superset-frontend/packages/superset-core/src/editors/index.ts +++ b/superset-frontend/packages/superset-core/src/editors/index.ts @@ -369,6 +369,28 @@ export interface EditorProps { theme?: SupersetTheme; } +/** + * A single text change expressed as an offset-based replacement. + */ +export interface ContentChange { + /** Character offset in the document where the replaced range starts */ + rangeOffset: number; + /** Length in characters of the replaced range (0 for pure insertions) */ + rangeLength: number; + /** Text inserted at rangeOffset (empty string for pure deletions) */ + text: string; +} + +/** + * Payload delivered to `onDidChangeContent` listeners. + */ +export interface ContentChangeEvent { + /** Returns the full current content of the editor */ + getValue(): string; + /** The individual changes that occurred in this event */ + changes: ReadonlyArray; +} + /** * Imperative API for controlling the editor programmatically. * @@ -492,6 +514,27 @@ export interface EditorHandle { * - CodeMirror: editor.requestMeasure() */ resize(): void; + + /** + * Subscribe to content changes in the editor. + * + * The listener receives a {@link ContentChangeEvent} with: + * - `getValue()` — lazy accessor for the full content (call only when needed + * to avoid unnecessary O(n) string allocation on every keystroke) + * - `changes` — the individual edits that occurred, as offset-based replacements + * + * @param listener Called with a ContentChangeEvent on every change + * @param thisArgs Optional `this` context for the listener + * @returns A Disposable that unsubscribes the listener when disposed + * + * @example + * const disposable = editor.onDidChangeContent(e => { + * setStatements(parseStatements(e.getValue())); + * }); + * // Later, to unsubscribe: + * disposable.dispose(); + */ + onDidChangeContent: Event; } /** diff --git a/superset-frontend/packages/superset-core/src/sqlLab/index.ts b/superset-frontend/packages/superset-core/src/sqlLab/index.ts index cae3bc2d4e5..844c25f9ea6 100644 --- a/superset-frontend/packages/superset-core/src/sqlLab/index.ts +++ b/superset-frontend/packages/superset-core/src/sqlLab/index.ts @@ -252,6 +252,22 @@ export interface QueryResult { */ export declare const getActivePanel: () => Panel; +/** + * Switches the active panel in the SQL Lab south pane. + * Built-in panel IDs are 'Results' and 'History'. + * Pinned table panels use the table's ID as their panel ID. + * + * @param panelId The ID of the panel to activate + * @returns Promise that resolves when the panel is activated + * + * @example + * ```typescript + * // Focus the Results panel after running a query + * await setActivePanel('Results'); + * ``` + */ +export declare function setActivePanel(panelId: string): Promise; + /** * Gets the currently active tab in SQL Lab. * diff --git a/superset-frontend/packages/superset-core/src/theme/types.ts b/superset-frontend/packages/superset-core/src/theme/types.ts index a0b30ca5956..87bd226f874 100644 --- a/superset-frontend/packages/superset-core/src/theme/types.ts +++ b/superset-frontend/packages/superset-core/src/theme/types.ts @@ -426,6 +426,7 @@ export interface ThemeControllerOptions { canUpdateTheme?: () => boolean; canUpdateMode?: () => boolean; isGlobalContext?: boolean; + initialMode?: ThemeMode; } export interface ThemeContextType { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts index 852b4eb1ab8..9aae5445bc8 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts @@ -28,7 +28,9 @@ export const getTimeOffset = ( // offset is represented as , group by list series.name.includes(`${timeOffset},`) || // offset is represented as __ - series.name.includes(`__${timeOffset}`), + series.name.includes(`__${timeOffset}`) || + // offset is represented as , + series.name.includes(`, ${timeOffset}`), ); export const hasTimeOffset = ( @@ -45,10 +47,14 @@ export const getOriginalSeries = ( ): string => { let result = seriesName; timeCompare.forEach(compare => { - // offset is represented as , group by list + // offset in the middle: , , + result = result.replace(`, ${compare},`, ','); + // offset at start: , result = result.replace(`${compare},`, ''); - // offset is represented as __ + // offset with double underscore: __ result = result.replace(`__${compare}`, ''); + // offset at end: , + result = result.replace(`, ${compare}`, ''); }); return result.trim(); }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/matrixify.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/matrixify.tsx index dde1f0431db..928ec6331b8 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/matrixify.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/matrixify.tsx @@ -25,30 +25,17 @@ export const matrixifyEnableSection: ControlPanelSectionConfig = { controlSetRows: [ [ { - name: 'matrixify_enable_horizontal_layout', + name: 'matrixify_enable', config: { - type: 'CheckboxControl', - label: t('Enable horizontal layout (columns)'), - description: t( - 'Create matrix columns by placing charts side-by-side', - ), - default: false, - renderTrigger: true, - }, - }, - ], - [ - { - name: 'matrixify_enable_vertical_layout', - config: { - type: 'CheckboxControl', - label: t('Enable vertical layout (rows)'), - description: t('Create matrix rows by stacking charts vertically'), + type: 'SwitchControl', + label: t('Enable matrixify'), default: false, renderTrigger: true, }, }, ], + ['matrixify_mode_columns'], + ['matrixify_mode_rows'], ], tabOverride: 'matrixify', }; @@ -57,8 +44,11 @@ export const matrixifySection: ControlPanelSectionConfig = { label: t('Cell layout & styling'), expanded: false, visibility: ({ controls }) => - controls?.matrixify_enable_vertical_layout?.value === true || - controls?.matrixify_enable_horizontal_layout?.value === true, + controls?.matrixify_enable?.value === true && + (controls?.matrixify_mode_rows?.value === 'metrics' || + controls?.matrixify_mode_rows?.value === 'dimensions' || + controls?.matrixify_mode_columns?.value === 'metrics' || + controls?.matrixify_mode_columns?.value === 'dimensions'), controlSetRows: [ [ { @@ -119,13 +109,13 @@ export const matrixifySection: ControlPanelSectionConfig = { }; export const matrixifyRowSection: ControlPanelSectionConfig = { - label: t('Vertical layout (rows)'), expanded: false, visibility: ({ controls }) => - controls?.matrixify_enable_vertical_layout?.value === true, + controls?.matrixify_enable?.value === true && + (controls?.matrixify_mode_rows?.value === 'metrics' || + controls?.matrixify_mode_rows?.value === 'dimensions'), controlSetRows: [ ['matrixify_show_row_labels'], - ['matrixify_mode_rows'], ['matrixify_rows'], ['matrixify_dimension_rows'], ['matrixify_dimension_selection_mode_rows'], @@ -137,13 +127,13 @@ export const matrixifyRowSection: ControlPanelSectionConfig = { }; export const matrixifyColumnSection: ControlPanelSectionConfig = { - label: t('Horizontal layout (columns)'), expanded: false, visibility: ({ controls }) => - controls?.matrixify_enable_horizontal_layout?.value === true, + controls?.matrixify_enable?.value === true && + (controls?.matrixify_mode_columns?.value === 'metrics' || + controls?.matrixify_mode_columns?.value === 'dimensions'), controlSetRows: [ ['matrixify_show_column_headers'], - ['matrixify_mode_columns'], ['matrixify_columns'], ['matrixify_dimension_columns'], ['matrixify_dimension_selection_mode_columns'], diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/matrixifyControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/matrixifyControls.tsx index be93b6fec1c..48dcaa3dafc 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/matrixifyControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/matrixifyControls.tsx @@ -34,19 +34,18 @@ const isMatrixifyVisible = ( controls: any, axis: 'rows' | 'columns', mode?: 'metrics' | 'dimensions', - selectionMode?: 'members' | 'topn', + selectionMode?: 'members' | 'topn' | 'all', ) => { - const layoutControl = `matrixify_enable_${axis === 'rows' ? 'vertical' : 'horizontal'}_layout`; const modeControl = `matrixify_mode_${axis}`; const selectionModeControl = `matrixify_dimension_selection_mode_${axis}`; - const isLayoutEnabled = controls?.[layoutControl]?.value === true; + const modeValue = controls?.[modeControl]?.value; + const isLayoutEnabled = modeValue === 'metrics' || modeValue === 'dimensions'; if (!isLayoutEnabled) return false; if (mode) { - const isModeMatch = controls?.[modeControl]?.value === mode; - if (!isModeMatch) return false; + if (modeValue !== mode) return false; if (selectionMode && mode === 'dimensions') { return controls?.[selectionModeControl]?.value === selectionMode; @@ -66,22 +65,20 @@ const matrixifyControls: Record> = {}; matrixifyControls[`matrixify_mode_${axis}`] = { type: 'RadioButtonControl', - label: t(`Metrics / Dimensions`), - default: axis === 'columns' ? 'metrics' : 'dimensions', + default: 'disabled', renderTrigger: true, tabOverride: 'matrixify', - visibility: ({ controls }) => isMatrixifyVisible(controls, axis), mapStateToProps: ({ controls }) => { const otherAxisControlName = `matrixify_mode_${otherAxis}`; const otherAxisValue = - controls?.[otherAxisControlName]?.value ?? - (otherAxis === 'columns' ? 'metrics' : 'dimensions'); + controls?.[otherAxisControlName]?.value ?? 'disabled'; const isMetricsDisabled = otherAxisValue === 'metrics'; return { options: [ + { value: 'disabled', label: t('Disabled') }, { value: 'metrics', label: t('Metrics'), @@ -92,7 +89,7 @@ const matrixifyControls: Record> = {}; ) : undefined, }, - { value: 'dimensions', label: t('Dimension members') }, + { value: 'dimensions', label: t('Dimensions') }, ], }; }, @@ -125,6 +122,7 @@ const matrixifyControls: Record> = {}; `matrixify_topn_metric_${axis}`, `matrixify_topn_order_${axis}`, `matrixify_dimension_selection_mode_${axis}`, + `matrixify_all_sort_by_${axis}`, ]; return fieldsToCheck.some( @@ -161,7 +159,10 @@ const matrixifyControls: Record> = {}; selectionMode, topNMetric: getValue(`matrixify_topn_metric_${axis}`), topNValue: getValue(`matrixify_topn_value_${axis}`), - topNOrder: getValue(`matrixify_topn_order_${axis}`), + topNOrder: getValue(`matrixify_topn_order_${axis}`, true) + ? 'DESC' + : 'ASC', + allSortBy: getValue(`matrixify_all_sort_by_${axis}`, 'a_to_z'), formData: form_data, validators, }; @@ -187,19 +188,24 @@ const matrixifyControls: Record> = {}; visibility: () => false, }; - // Add selection mode control (Dimension Members vs TopN) + // Add selection mode control (Dimension Members / Top N / All) matrixifyControls[`matrixify_dimension_selection_mode_${axis}`] = { - type: 'RadioButtonControl', + type: 'VerticalRadioControl', label: t(`Selection method`), default: 'members', - options: [ - ['members', t('Dimension members')], - ['topn', t('Top n')], - ], renderTrigger: true, tabOverride: 'matrixify', visibility: ({ controls }) => isMatrixifyVisible(controls, axis, 'dimensions'), + options: [ + { value: 'members', label: t('Dimension members') }, + { value: 'topn', label: t('Top n') }, + { + value: 'all', + label: t('All dimensions'), + tooltip: t('Uses the first 25 values if the dimension has more.'), + }, + ], }; // TopN controls @@ -236,15 +242,15 @@ const matrixifyControls: Record> = {}; description: t(`Metric to use for ordering Top N values`), tabOverride: 'matrixify', visibility: ({ controls }) => - isMatrixifyVisible(controls, axis, 'dimensions', 'topn'), + isMatrixifyVisible(controls, axis, 'dimensions', 'topn') || + (isMatrixifyVisible(controls, axis, 'dimensions', 'all') && + controls?.[`matrixify_all_sort_by_${axis}`]?.value === 'metric'), mapStateToProps: (state, controlState) => { const { controls, datasource } = state; - const isVisible = isMatrixifyVisible( - controls, - axis, - 'dimensions', - 'topn', - ); + const isVisible = + isMatrixifyVisible(controls, axis, 'dimensions', 'topn') || + (isMatrixifyVisible(controls, axis, 'dimensions', 'all') && + controls?.[`matrixify_all_sort_by_${axis}`]?.value === 'metric'); const originalProps = dndAdhocMetricControl.mapStateToProps?.(state, controlState) || {}; @@ -261,17 +267,31 @@ const matrixifyControls: Record> = {}; }; matrixifyControls[`matrixify_topn_order_${axis}`] = { - type: 'RadioButtonControl', - label: t(`Sort order`), - default: 'desc', - options: [ - ['asc', t('Ascending')], - ['desc', t('Descending')], - ], + type: 'CheckboxControl', + label: t('Sort descending'), + default: true, renderTrigger: true, tabOverride: 'matrixify', visibility: ({ controls }) => - isMatrixifyVisible(controls, axis, 'dimensions', 'topn'), + isMatrixifyVisible(controls, axis, 'dimensions', 'topn') || + (isMatrixifyVisible(controls, axis, 'dimensions', 'all') && + controls?.[`matrixify_all_sort_by_${axis}`]?.value === 'metric'), + }; + + matrixifyControls[`matrixify_all_sort_by_${axis}`] = { + type: 'SelectControl', + label: t('Sort by'), + default: 'a_to_z', + clearable: false, + renderTrigger: true, + tabOverride: 'matrixify', + visibility: ({ controls }) => + isMatrixifyVisible(controls, axis, 'dimensions', 'all'), + choices: [ + ['a_to_z', t('A-Z')], + ['z_to_a', t('Z-A')], + ['metric', t('Metric')], + ], }; }); @@ -317,24 +337,6 @@ matrixifyControls.matrixify_charts_per_row = { !controls?.matrixify_fit_columns_dynamically?.value, }; -matrixifyControls.matrixify_enable_vertical_layout = { - type: 'CheckboxControl', - label: t('Enable vertical layout (rows)'), - description: t('Create matrix rows by stacking charts vertically'), - default: false, - renderTrigger: true, - tabOverride: 'matrixify', -}; - -matrixifyControls.matrixify_enable_horizontal_layout = { - type: 'CheckboxControl', - label: t('Enable horizontal layout (columns)'), - description: t('Create matrix columns by placing charts side-by-side'), - default: false, - renderTrigger: true, - tabOverride: 'matrixify', -}; - // Cell title control for Matrixify matrixifyControls.matrixify_cell_title_template = { type: 'TextControl', @@ -345,8 +347,8 @@ matrixifyControls.matrixify_cell_title_template = { default: '', renderTrigger: true, visibility: ({ controls }) => - controls?.matrixify_enable_vertical_layout?.value === true || - controls?.matrixify_enable_horizontal_layout?.value === true, + isMatrixifyVisible(controls, 'rows') || + isMatrixifyVisible(controls, 'columns'), }; // Matrix display controls @@ -357,8 +359,7 @@ matrixifyControls.matrixify_show_row_labels = { default: true, renderTrigger: true, tabOverride: 'matrixify', - visibility: ({ controls }) => - controls?.matrixify_enable_vertical_layout?.value === true, + visibility: ({ controls }) => isMatrixifyVisible(controls, 'rows'), }; matrixifyControls.matrixify_show_column_headers = { @@ -368,8 +369,7 @@ matrixifyControls.matrixify_show_column_headers = { default: true, renderTrigger: true, tabOverride: 'matrixify', - visibility: ({ controls }) => - controls?.matrixify_enable_horizontal_layout?.value === true, + visibility: ({ controls }) => isMatrixifyVisible(controls, 'columns'), }; export { matrixifyControls }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/timeOffset.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/timeOffset.test.ts index bee8c5bbbd2..bec42af6c9b 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/timeOffset.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/timeOffset.test.ts @@ -16,15 +16,101 @@ * specific language governing permissions and limitations * under the License. */ -import { getOriginalSeries } from '@superset-ui/chart-controls'; +import { + getOriginalSeries, + getTimeOffset, + hasTimeOffset, +} from '@superset-ui/chart-controls'; -test('returns the series name when time compare is empty', () => { +test('getOriginalSeries returns the series name when time compare is empty', () => { const seriesName = 'sum'; expect(getOriginalSeries(seriesName, [])).toEqual(seriesName); }); -test('returns the original series name', () => { +test('getOriginalSeries returns the original series name with __ pattern', () => { const seriesName = 'sum__1_month_ago'; const timeCompare = ['1_month_ago']; expect(getOriginalSeries(seriesName, timeCompare)).toEqual('sum'); }); + +test('getOriginalSeries returns the original series name with , pattern', () => { + const seriesName = '1 year ago, groupby_value'; + const timeCompare = ['1 year ago']; + expect(getOriginalSeries(seriesName, timeCompare)).toEqual('groupby_value'); +}); + +test('getOriginalSeries returns the original series name with , pattern', () => { + const seriesName = 'AVG(price_each), 1 year ago'; + const timeCompare = ['1 year ago']; + expect(getOriginalSeries(seriesName, timeCompare)).toEqual('AVG(price_each)'); +}); + +test('getOriginalSeries handles multiple time compares', () => { + const seriesName = 'count, 1 year ago'; + const timeCompare = ['1 month ago', '1 year ago']; + expect(getOriginalSeries(seriesName, timeCompare)).toEqual('count'); +}); + +test('getOriginalSeries strips offset in the middle with dimension', () => { + const seriesName = 'SUM(sales), 28 days ago, Medium'; + const timeCompare = ['28 days ago']; + expect(getOriginalSeries(seriesName, timeCompare)).toEqual( + 'SUM(sales), Medium', + ); +}); + +test('getOriginalSeries strips offset in the middle with multiple dimensions', () => { + const seriesName = 'SUM(sales), 1 year ago, Medium, 11'; + const timeCompare = ['1 year ago']; + expect(getOriginalSeries(seriesName, timeCompare)).toEqual( + 'SUM(sales), Medium, 11', + ); +}); + +test('getTimeOffset returns undefined when no time offset pattern matches', () => { + const series = { name: 'count' }; + const timeCompare = ['1 year ago']; + expect(getTimeOffset(series, timeCompare)).toBeUndefined(); +}); + +test('getTimeOffset detects __ pattern', () => { + const series = { name: 'count__1 year ago' }; + const timeCompare = ['1 year ago']; + expect(getTimeOffset(series, timeCompare)).toEqual('1 year ago'); +}); + +test('getTimeOffset detects , pattern', () => { + const series = { name: '1 year ago, groupby_value' }; + const timeCompare = ['1 year ago']; + expect(getTimeOffset(series, timeCompare)).toEqual('1 year ago'); +}); + +test('getTimeOffset detects , pattern', () => { + const series = { name: 'AVG(price_each), 1 year ago' }; + const timeCompare = ['1 year ago']; + expect(getTimeOffset(series, timeCompare)).toEqual('1 year ago'); +}); + +test('getTimeOffset detects , , pattern (offset in middle)', () => { + const series = { name: 'SUM(sales), 28 days ago, Medium' }; + const timeCompare = ['28 days ago']; + expect(getTimeOffset(series, timeCompare)).toEqual('28 days ago'); +}); + +test('hasTimeOffset returns false for original series', () => { + const series = { name: 'count' }; + const timeCompare = ['1 year ago']; + expect(hasTimeOffset(series, timeCompare)).toBe(false); +}); + +test('hasTimeOffset returns true for derived series with , pattern', () => { + const series = { name: 'AVG(price_each), 1 year ago' }; + const timeCompare = ['1 year ago']; + expect(hasTimeOffset(series, timeCompare)).toBe(true); +}); + +test('hasTimeOffset returns false when series name is not a string', () => { + const series = { name: 123 }; + const timeCompare = ['1 year ago']; + expect(hasTimeOffset(series, timeCompare)).toBe(false); +}); diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index b667e4c4371..3d4855ca3d0 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -41,7 +41,7 @@ "d3-scale": "^4.0.2", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "fetch-retry": "^6.0.0", "handlebars": "^4.7.8", "jed": "^1.1.1", diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts index a2915fef438..8f660e1a776 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts @@ -438,3 +438,23 @@ test('should handle metrics without labels', () => { expect(grid!.rowHeaders).toEqual(['']); expect(grid!.colHeaders).toEqual(['count']); }); + +test('should preserve slice_id and dashboardId for embedded dashboard permissions', () => { + const formDataWithDashboardContext: TestFormData = { + ...baseFormData, + slice_id: 42, + dashboardId: 123, + }; + + const grid = generateMatrixifyGrid(formDataWithDashboardContext); + + expect(grid).not.toBeNull(); + const cell = grid!.cells[0][0]; + + // slice_id must be preserved for embedded dashboard permission checks + // The backend uses slice_id to verify the chart belongs to the dashboard + expect(cell!.formData.slice_id).toBe(42); + + // dashboardId must be preserved for embedded dashboard context + expect(cell!.formData.dashboardId).toBe(123); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts index cc12d396b9e..59c5f9ed733 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts @@ -125,9 +125,9 @@ function generateCellFormData( }); // Override fields that could cause issues in grid cells + // Note: slice_id is intentionally preserved for embedded dashboard permission checks const overrides: Partial = { slice_name: undefined, - slice_id: undefined, header_font_size: undefined, subheader: undefined, show_title: undefined, diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.test.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.test.tsx index 7c1fc8d7bc7..588585372da 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.test.tsx @@ -22,6 +22,7 @@ import '@testing-library/jest-dom'; import { ThemeProvider } from '@apache-superset/core/theme'; import { supersetTheme } from '@apache-superset/core/theme'; import MatrixifyGridRenderer from './MatrixifyGridRenderer'; +import type { MatrixifyMode } from '../../types/matrixify'; import { generateMatrixifyGrid } from './MatrixifyGridGenerator'; // Mock the MatrixifyGridGenerator @@ -74,8 +75,9 @@ test('should create single group when fitting columns dynamically', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: true, matrixify_charts_per_row: 3, matrixify_show_row_labels: true, @@ -124,8 +126,9 @@ test('should create multiple groups when not fitting columns dynamically', () => const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: false, matrixify_charts_per_row: 3, matrixify_show_row_labels: true, @@ -160,8 +163,9 @@ test('should handle exact division of columns', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: false, matrixify_charts_per_row: 2, matrixify_show_row_labels: true, @@ -189,8 +193,9 @@ test('should handle case where charts_per_row exceeds total columns', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: false, matrixify_charts_per_row: 5, matrixify_show_row_labels: true, @@ -220,8 +225,9 @@ test('should show headers for each group when wrapping occurs', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: false, matrixify_charts_per_row: 2, matrixify_show_row_labels: true, @@ -255,8 +261,9 @@ test('should show headers only on first row when not wrapping', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: true, // No wrapping matrixify_show_row_labels: true, matrixify_show_column_headers: true, @@ -285,8 +292,9 @@ test('should hide headers when disabled', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_show_row_labels: false, matrixify_show_column_headers: false, }; @@ -313,8 +321,9 @@ test('should place cells correctly in wrapped layout', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, matrixify_fit_columns_dynamically: false, matrixify_charts_per_row: 2, matrixify_show_row_labels: true, @@ -344,8 +353,9 @@ test('should handle null grid gracefully', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, }; const { container } = renderWithTheme( @@ -366,8 +376,9 @@ test('should handle empty grid gracefully', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, }; const { container } = renderWithTheme( @@ -391,8 +402,9 @@ test('should use default values for missing configuration', () => { const formData = { viz_type: 'test_chart', - matrixify_enable_vertical_layout: true, - matrixify_enable_horizontal_layout: true, + matrixify_enable: true, + matrixify_mode_rows: 'metrics' as MatrixifyMode, + matrixify_mode_columns: 'metrics' as MatrixifyMode, // Missing optional configurations }; diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.tsx index 90402f8e782..fc4b6422459 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridRenderer.tsx @@ -130,10 +130,12 @@ function MatrixifyGridRenderer({ // Determine layout parameters - only show headers/labels if layout is enabled const showRowLabels = - formData.matrixify_enable_vertical_layout === true && + formData.matrixify_mode_rows !== undefined && + formData.matrixify_mode_rows !== 'disabled' && (formData.matrixify_show_row_labels ?? true); const showColumnHeaders = - formData.matrixify_enable_horizontal_layout === true && + formData.matrixify_mode_columns !== undefined && + formData.matrixify_mode_columns !== 'disabled' && (formData.matrixify_show_column_headers ?? true); const rowHeight = formData.matrixify_row_height || DEFAULT_ROW_HEIGHT; const fitColumnsDynamically = diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts index 6fe052a5a74..b0eaf837fcd 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts @@ -37,12 +37,11 @@ test('isMatrixifyEnabled should return false when no matrixify configuration exi expect(isMatrixifyEnabled(formData)).toBe(false); }); -test('isMatrixifyEnabled should return false when layout controls are false', () => { +test('isMatrixifyEnabled should return false when layout controls are disabled', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: false, - matrixify_enable_horizontal_layout: false, - matrixify_mode_rows: 'metrics', + matrixify_mode_rows: 'disabled', + matrixify_mode_columns: 'disabled', matrixify_rows: [createMetric('Revenue')], } as MatrixifyFormData; @@ -52,7 +51,7 @@ test('isMatrixifyEnabled should return false when layout controls are false', () test('isMatrixifyEnabled should return true for valid metrics mode configuration', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'metrics', matrixify_mode_columns: 'metrics', matrixify_rows: [createMetric('Revenue')], @@ -65,7 +64,7 @@ test('isMatrixifyEnabled should return true for valid metrics mode configuration test('isMatrixifyEnabled should return true for valid dimensions mode configuration', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'dimensions', matrixify_mode_columns: 'dimensions', matrixify_dimension_rows: { dimension: 'country', values: ['USA'] }, @@ -78,7 +77,7 @@ test('isMatrixifyEnabled should return true for valid dimensions mode configurat test('isMatrixifyEnabled should return true for mixed mode configuration', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'metrics', matrixify_mode_columns: 'dimensions', matrixify_rows: [createMetric('Revenue')], @@ -91,7 +90,7 @@ test('isMatrixifyEnabled should return true for mixed mode configuration', () => test('isMatrixifyEnabled should return true for topn dimension selection mode', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'dimensions', matrixify_mode_columns: 'dimensions', matrixify_dimension_rows: { @@ -110,7 +109,7 @@ test('isMatrixifyEnabled should return true for topn dimension selection mode', test('isMatrixifyEnabled should return false when both axes have empty metrics arrays', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'metrics', matrixify_mode_columns: 'metrics', matrixify_rows: [], @@ -123,7 +122,7 @@ test('isMatrixifyEnabled should return false when both axes have empty metrics a test('isMatrixifyEnabled should return false when both dimensions have empty values and no topn mode', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'dimensions', matrixify_mode_columns: 'dimensions', matrixify_dimension_rows: { dimension: 'country', values: [] }, @@ -141,7 +140,6 @@ test('getMatrixifyConfig should return null when no matrixify configuration exis test('getMatrixifyConfig should return valid config for metrics mode', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'metrics', matrixify_mode_columns: 'metrics', matrixify_rows: [createMetric('Revenue')], @@ -159,7 +157,6 @@ test('getMatrixifyConfig should return valid config for metrics mode', () => { test('getMatrixifyConfig should return valid config for dimensions mode', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'dimensions', matrixify_mode_columns: 'dimensions', matrixify_dimension_rows: { dimension: 'country', values: ['USA'] }, @@ -183,7 +180,6 @@ test('getMatrixifyConfig should return valid config for dimensions mode', () => test('getMatrixifyConfig should handle topn selection mode', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'dimensions', matrixify_mode_columns: 'dimensions', matrixify_dimension_rows: { @@ -204,8 +200,8 @@ test('getMatrixifyConfig should handle topn selection mode', () => { test('getMatrixifyValidationErrors should return empty array when matrixify is not enabled', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: false, - matrixify_enable_horizontal_layout: false, + matrixify_mode_rows: 'disabled', + matrixify_mode_columns: 'disabled', } as MatrixifyFormData; expect(getMatrixifyValidationErrors(formData)).toEqual([]); @@ -214,7 +210,6 @@ test('getMatrixifyValidationErrors should return empty array when matrixify is n test('getMatrixifyValidationErrors should return empty array when properly configured', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'metrics', matrixify_mode_columns: 'metrics', matrixify_rows: [createMetric('Revenue')], @@ -227,17 +222,16 @@ test('getMatrixifyValidationErrors should return empty array when properly confi test('getMatrixifyValidationErrors should return error when enabled but no configuration exists', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', } as MatrixifyFormData; const errors = getMatrixifyValidationErrors(formData); - expect(errors).toContain('Please configure at least one row or column axis'); + expect(errors.length).toBeGreaterThan(0); }); test('getMatrixifyValidationErrors should return error when metrics mode has no metrics', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'metrics', matrixify_rows: [], matrixify_columns: [], @@ -260,19 +254,28 @@ test('should handle empty form data object', () => { expect(isMatrixifyEnabled(formData)).toBe(false); }); -test('isMatrixifyEnabled should return false when layout enabled but no axis modes configured', () => { +test('isMatrixifyEnabled should return false when no axis modes configured', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, // No matrixify_mode_rows or matrixify_mode_columns set } as MatrixifyFormData; expect(isMatrixifyEnabled(formData)).toBe(false); }); +test('isMatrixifyEnabled should return false when switch is off even with valid axis config', () => { + const formData = { + viz_type: 'table', + matrixify_enable: false, + matrixify_mode_rows: 'metrics', + matrixify_rows: [createMetric('Revenue')], + } as MatrixifyFormData; + expect(isMatrixifyEnabled(formData)).toBe(false); +}); + test('getMatrixifyValidationErrors should return dimension error for rows when dimension has no data', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'dimensions', // No matrixify_dimension_rows set matrixify_mode_columns: 'metrics', @@ -286,7 +289,6 @@ test('getMatrixifyValidationErrors should return dimension error for rows when d test('getMatrixifyValidationErrors should return metric error for columns when metrics array is empty', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'metrics', matrixify_rows: [createMetric('Revenue')], matrixify_mode_columns: 'metrics', @@ -300,7 +302,6 @@ test('getMatrixifyValidationErrors should return metric error for columns when m test('getMatrixifyValidationErrors should return dimension error for columns when no dimension data', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'metrics', matrixify_rows: [createMetric('Revenue')], matrixify_mode_columns: 'dimensions', @@ -311,10 +312,9 @@ test('getMatrixifyValidationErrors should return dimension error for columns whe expect(errors).toContain('Please select a dimension and values for columns'); }); -test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is not set (line 240 false, line 279 || false)', () => { +test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is not set', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, // No matrixify_mode_rows — hasRowMode = false matrixify_mode_columns: 'metrics', matrixify_columns: [createMetric('Q1')], @@ -324,10 +324,9 @@ test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is n expect(errors).toEqual([]); }); -test('getMatrixifyValidationErrors evaluates full && expression when dimension is set but values are empty (lines 244, 264, 283, 291 true branches)', () => { +test('getMatrixifyValidationErrors evaluates full && expression when dimension is set but values are empty', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, matrixify_mode_rows: 'dimensions', matrixify_dimension_rows: { dimension: 'country', values: [] }, matrixify_mode_columns: 'dimensions', @@ -345,7 +344,7 @@ test('getMatrixifyValidationErrors evaluates full && expression when dimension i test('should handle partial configuration with one axis only', () => { const formData = { viz_type: 'table', - matrixify_enable_vertical_layout: true, + matrixify_enable: true, matrixify_mode_rows: 'metrics', matrixify_rows: [createMetric('Revenue')], // No columns configuration diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts index 8c01651ca60..e43dbe31406 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts @@ -23,6 +23,7 @@ import { AdhocMetric } from '../../query'; * Constants for Matrixify filter generation * These match the literal types used in Filter.ts */ + export const MatrixifyFilterConstants = { // Filter expression types ExpressionType: { @@ -46,12 +47,12 @@ export const MatrixifyFilterConstants = { /** * Mode for selecting matrix axis values */ -export type MatrixifyMode = 'metrics' | 'dimensions'; +export type MatrixifyMode = 'disabled' | 'metrics' | 'dimensions'; /** * Selection method for dimension values */ -export type MatrixifySelectionMode = 'members' | 'topn'; +export type MatrixifySelectionMode = 'members' | 'topn' | 'all'; /** * Sort order for top N selection @@ -96,18 +97,18 @@ export interface MatrixifyAxisConfig { * Complete Matrixify configuration in form data */ export interface MatrixifyFormData { - // Layout enable controls - matrixify_enable_vertical_layout?: boolean; - matrixify_enable_horizontal_layout?: boolean; + // Global enable switch + matrixify_enable?: boolean; - // Row axis configuration + // Row axis configuration (mode 'disabled' means axis is off) matrixify_mode_rows?: MatrixifyMode; matrixify_rows?: AdhocMetric[]; matrixify_dimension_selection_mode_rows?: MatrixifySelectionMode; matrixify_dimension_rows?: MatrixifyDimensionValue; matrixify_topn_value_rows?: number; matrixify_topn_metric_rows?: AdhocMetric; - matrixify_topn_order_rows?: MatrixifySortOrder; + matrixify_topn_order_rows?: boolean; + matrixify_all_sort_by_rows?: 'a_to_z' | 'z_to_a' | 'metric'; // Column axis configuration matrixify_mode_columns?: MatrixifyMode; @@ -116,7 +117,8 @@ export interface MatrixifyFormData { matrixify_dimension_columns?: MatrixifyDimensionValue; matrixify_topn_value_columns?: number; matrixify_topn_metric_columns?: AdhocMetric; - matrixify_topn_order_columns?: MatrixifySortOrder; + matrixify_topn_order_columns?: boolean; + matrixify_all_sort_by_columns?: 'a_to_z' | 'z_to_a' | 'metric'; // Grid layout configuration matrixify_row_height?: number; @@ -139,75 +141,85 @@ export interface MatrixifyConfig { columns: MatrixifyAxisConfig; } +/** + * Check if a given axis mode is active (not disabled) + */ +function isAxisEnabled(mode?: MatrixifyMode): boolean { + return mode === 'metrics' || mode === 'dimensions'; +} + /** * Helper function to extract Matrixify configuration from form data */ export function getMatrixifyConfig( formData: MatrixifyFormData & any, ): MatrixifyConfig | null { - const hasRowConfig = formData.matrixify_mode_rows; - const hasColumnConfig = formData.matrixify_mode_columns; + const rowEnabled = isAxisEnabled(formData.matrixify_mode_rows); + const colEnabled = isAxisEnabled(formData.matrixify_mode_columns); - if (!hasRowConfig && !hasColumnConfig) { + if (!rowEnabled && !colEnabled) { return null; } return { rows: { - mode: formData.matrixify_mode_rows || 'metrics', + mode: formData.matrixify_mode_rows || 'disabled', metrics: formData.matrixify_rows, selectionMode: formData.matrixify_dimension_selection_mode_rows, dimension: formData.matrixify_dimension_rows, topnValue: formData.matrixify_topn_value_rows, topnMetric: formData.matrixify_topn_metric_rows, - topnOrder: formData.matrixify_topn_order_rows, + topnOrder: formData.matrixify_topn_order_rows === false ? 'asc' : 'desc', }, columns: { - mode: formData.matrixify_mode_columns || 'metrics', + mode: formData.matrixify_mode_columns || 'disabled', metrics: formData.matrixify_columns, selectionMode: formData.matrixify_dimension_selection_mode_columns, dimension: formData.matrixify_dimension_columns, topnValue: formData.matrixify_topn_value_columns, topnMetric: formData.matrixify_topn_metric_columns, - topnOrder: formData.matrixify_topn_order_columns, + topnOrder: + formData.matrixify_topn_order_columns === false ? 'asc' : 'desc', }, }; } -/** - * Check if Matrixify is enabled and properly configured - */ export function isMatrixifyEnabled(formData: MatrixifyFormData): boolean { - // Check if either vertical or horizontal layout is enabled - const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true; - const hasHorizontalLayout = - formData.matrixify_enable_horizontal_layout === true; - - if (!hasVerticalLayout && !hasHorizontalLayout) { + if (formData.matrixify_enable !== true) { + return false; + } + + const rowEnabled = isAxisEnabled(formData.matrixify_mode_rows); + const colEnabled = isAxisEnabled(formData.matrixify_mode_columns); + + if (!rowEnabled && !colEnabled) { return false; } - // Then validate that we have proper configuration const config = getMatrixifyConfig(formData); if (!config) { return false; } const hasRowData = - config.rows.mode === 'metrics' + rowEnabled && + (config.rows.mode === 'metrics' ? config.rows.metrics && config.rows.metrics.length > 0 : config.rows.dimension?.dimension && (config.rows.selectionMode === 'topn' || + config.rows.selectionMode === 'all' || (config.rows.dimension.values && - config.rows.dimension.values.length > 0)); + config.rows.dimension.values.length > 0))); const hasColumnData = - config.columns.mode === 'metrics' + colEnabled && + (config.columns.mode === 'metrics' ? config.columns.metrics && config.columns.metrics.length > 0 : config.columns.dimension?.dimension && (config.columns.selectionMode === 'topn' || + config.columns.selectionMode === 'all' || (config.columns.dimension.values && - config.columns.dimension.values.length > 0)); + config.columns.dimension.values.length > 0))); return Boolean(hasRowData || hasColumnData); } @@ -220,12 +232,10 @@ export function getMatrixifyValidationErrors( ): string[] { const errors: string[] = []; - // Only validate if matrixify is enabled - const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true; - const hasHorizontalLayout = - formData.matrixify_enable_horizontal_layout === true; + const rowEnabled = isAxisEnabled(formData.matrixify_mode_rows); + const colEnabled = isAxisEnabled(formData.matrixify_mode_columns); - if (!hasVerticalLayout && !hasHorizontalLayout) { + if (!rowEnabled && !colEnabled) { return errors; } @@ -243,6 +253,7 @@ export function getMatrixifyValidationErrors( ? config.rows.metrics && config.rows.metrics.length > 0 : config.rows.dimension?.dimension && (config.rows.selectionMode === 'topn' || + config.rows.selectionMode === 'all' || (config.rows.dimension.values && config.rows.dimension.values.length > 0)); @@ -263,6 +274,7 @@ export function getMatrixifyValidationErrors( ? config.columns.metrics && config.columns.metrics.length > 0 : config.columns.dimension?.dimension && (config.columns.selectionMode === 'topn' || + config.columns.selectionMode === 'all' || (config.columns.dimension.values && config.columns.dimension.values.length > 0)); @@ -281,6 +293,7 @@ export function getMatrixifyValidationErrors( ? config.rows.metrics && config.rows.metrics.length > 0 : config.rows.dimension?.dimension && (config.rows.selectionMode === 'topn' || + config.rows.selectionMode === 'all' || (config.rows.dimension.values && config.rows.dimension.values.length > 0)); @@ -289,6 +302,7 @@ export function getMatrixifyValidationErrors( ? config.columns.metrics && config.columns.metrics.length > 0 : config.columns.dimension?.dimension && (config.columns.selectionMode === 'topn' || + config.columns.selectionMode === 'all' || (config.columns.dimension.values && config.columns.dimension.values.length > 0)); diff --git a/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/ContentConfig.tsx b/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/ContentConfig.tsx index 225549989b5..4a9af897d73 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/ContentConfig.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/ContentConfig.tsx @@ -23,7 +23,7 @@ import { Icons } from '@superset-ui/core/components/Icons'; import { ContentType, MetadataType } from '.'; const Header = styled.div` - font-weight: ${({ theme }) => theme.fontWeightStrong}; + font-weight: ${({ theme }) => theme.fontWeightBold}; `; const Info = ({ diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts index 0d2cb5b59ba..98695555068 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Operator.ts @@ -30,6 +30,7 @@ const BINARY_OPERATORS = [ '<=', 'ILIKE', 'LIKE', + 'NOT ILIKE', 'NOT LIKE', 'REGEX', 'TEMPORAL_RANGE', diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx index 361a27610c9..a69659de0b3 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx @@ -61,6 +61,9 @@ interface MapBoxProps { renderWhileDragging?: boolean; rgb?: (string | number)[]; bounds?: [[number, number], [number, number]]; // May be undefined for empty datasets + viewportLongitude?: number; + viewportLatitude?: number; + viewportZoom?: number; } interface MapBoxState { @@ -82,30 +85,10 @@ class MapBox extends Component { constructor(props: MapBoxProps) { super(props); - const { width = 400, height = 400, bounds } = this.props; - // Get a viewport that fits the given bounds, which all marks to be clustered. - // Derive lat, lon and zoom from this viewport. This is only done on initial - // render as the bounds don't update as we pan/zoom in the current design. - - let latitude = 0; - let longitude = 0; - let zoom = 1; - - // Guard against empty datasets where bounds may be undefined - if (bounds && bounds[0] && bounds[1]) { - const mercator = new WebMercatorViewport({ - width, - height, - }).fitBounds(bounds); - ({ latitude, longitude, zoom } = mercator); - } + const fitBounds = this.computeFitBoundsViewport(); this.state = { - viewport: { - longitude, - latitude, - zoom, - }, + viewport: this.mergeViewportWithProps(fitBounds), }; this.handleViewportChange = this.handleViewportChange.bind(this); } @@ -116,6 +99,75 @@ class MapBox extends Component { onViewportChange!(viewport); } + mergeViewportWithProps( + fitBounds: Viewport, + viewport: Viewport = fitBounds, + props: MapBoxProps = this.props, + useFitBoundsForUnset = true, + ): Viewport { + const { viewportLongitude, viewportLatitude, viewportZoom } = props; + + return { + ...viewport, + longitude: + viewportLongitude ?? + (useFitBoundsForUnset ? fitBounds.longitude : viewport.longitude), + latitude: + viewportLatitude ?? + (useFitBoundsForUnset ? fitBounds.latitude : viewport.latitude), + zoom: + viewportZoom ?? (useFitBoundsForUnset ? fitBounds.zoom : viewport.zoom), + }; + } + + computeFitBoundsViewport(): Viewport { + const { width = 400, height = 400, bounds } = this.props; + if (bounds && bounds[0] && bounds[1]) { + const mercator = new WebMercatorViewport({ width, height }).fitBounds( + bounds, + ); + return { + latitude: mercator.latitude, + longitude: mercator.longitude, + zoom: mercator.zoom, + }; + } + return { latitude: 0, longitude: 0, zoom: 1 }; + } + + componentDidUpdate(prevProps: MapBoxProps) { + const { viewport } = this.state; + const fitBoundsInputsChanged = + prevProps.width !== this.props.width || + prevProps.height !== this.props.height || + prevProps.bounds !== this.props.bounds; + const viewportPropsChanged = + prevProps.viewportLongitude !== this.props.viewportLongitude || + prevProps.viewportLatitude !== this.props.viewportLatitude || + prevProps.viewportZoom !== this.props.viewportZoom; + + if (!fitBoundsInputsChanged && !viewportPropsChanged) { + return; + } + + const fitBounds = this.computeFitBoundsViewport(); + const nextViewport = this.mergeViewportWithProps( + fitBounds, + viewport, + this.props, + fitBoundsInputsChanged || viewportPropsChanged, + ); + + const viewportChanged = + nextViewport.longitude !== viewport.longitude || + nextViewport.latitude !== viewport.latitude || + nextViewport.zoom !== viewport.zoom; + + if (viewportChanged) { + this.setState({ viewport: nextViewport }); + } + } + render() { const { width, diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/controlPanel.ts index 80d6ab0c7f6..8b1c846e048 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/controlPanel.ts @@ -241,6 +241,7 @@ const config: ControlPanelConfig = { label: t('Opacity'), default: 1, isFloat: true, + renderTrigger: true, description: t( 'Opacity of all clusters, points, and labels. Between 0 and 1.', ), @@ -273,7 +274,7 @@ const config: ControlPanelConfig = { type: 'TextControl', label: t('Default longitude'), renderTrigger: true, - default: -122.405293, + default: '', isFloat: true, description: t('Longitude of default viewport'), places: 8, @@ -287,7 +288,7 @@ const config: ControlPanelConfig = { type: 'TextControl', label: t('Default latitude'), renderTrigger: true, - default: 37.772123, + default: '', isFloat: true, description: t('Latitude of default viewport'), places: 8, @@ -304,7 +305,7 @@ const config: ControlPanelConfig = { label: t('Zoom'), renderTrigger: true, isFloat: true, - default: 11, + default: '', description: t('Zoom level of the map'), places: 8, // Viewport zoom shouldn't prompt user to re-run query diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts index caaeb9b3f55..45eca8d4448 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts @@ -23,6 +23,30 @@ import { ChartProps } from '@superset-ui/core'; import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapBox'; const NOOP = () => {}; +const MIN_LONGITUDE = -180; +const MAX_LONGITUDE = 180; +const MIN_LATITUDE = -90; +const MAX_LATITUDE = 90; +const MIN_ZOOM = 0; + +function toFiniteNumber( + value: string | number | null | undefined, +): number | undefined { + if (value === null || value === undefined) return undefined; + const normalizedValue = typeof value === 'string' ? value.trim() : value; + if (normalizedValue === '') return undefined; + const num = Number(normalizedValue); + return Number.isFinite(num) ? num : undefined; +} + +function clampNumber( + value: number | undefined, + min: number, + max: number, +): number | undefined { + if (value === undefined) return undefined; + return Math.min(max, Math.max(min, value)); +} interface ClusterProperties { metric: number; @@ -45,6 +69,9 @@ export default function transformProps(chartProps: ChartProps) { pandasAggfunc, pointRadiusUnit, renderWhileDragging, + viewportLongitude, + viewportLatitude, + viewportZoom, } = formData; // Validate mapbox color @@ -93,7 +120,6 @@ export default function transformProps(chartProps: ChartProps) { aggregatorName: pandasAggfunc, bounds, clusterer, - globalOpacity, hasCustomMetric, mapboxApiKey, mapStyle: mapboxStyle, @@ -116,5 +142,21 @@ export default function transformProps(chartProps: ChartProps) { pointRadiusUnit, renderWhileDragging, rgb, + viewportLongitude: clampNumber( + toFiniteNumber(viewportLongitude), + MIN_LONGITUDE, + MAX_LONGITUDE, + ), + viewportLatitude: clampNumber( + toFiniteNumber(viewportLatitude), + MIN_LATITUDE, + MAX_LATITUDE, + ), + viewportZoom: clampNumber( + toFiniteNumber(viewportZoom), + MIN_ZOOM, + DEFAULT_MAX_ZOOM, + ), + globalOpacity: Math.min(1, Math.max(0, toFiniteNumber(globalOpacity) ?? 1)), }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/test/MapBox.test.tsx b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/MapBox.test.tsx new file mode 100644 index 00000000000..f0782f5fc14 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/MapBox.test.tsx @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { type ReactNode } from 'react'; +import { render } from '@testing-library/react'; +import MapBox from '../src/MapBox'; + +// Capture the most recent viewport props passed to MapGL +let lastMapGLProps: Record = {}; +const mockFitBounds = jest.fn(); + +jest.mock('react-map-gl', () => { + const MockMapGL = (props: Record) => { + lastMapGLProps = props; + return
{props.children as ReactNode}
; + }; + return { __esModule: true, default: MockMapGL }; +}); + +jest.mock('@math.gl/web-mercator', () => ({ + WebMercatorViewport: jest + .fn() + .mockImplementation( + ({ width, height }: { width: number; height: number }) => ({ + fitBounds: (bounds: [[number, number], [number, number]]) => + mockFitBounds(bounds, width, height), + }), + ), +})); + +jest.mock('../src/ScatterPlotGlowOverlay', () => { + const MockOverlay = (props: Record) => ( +
+ ); + return { __esModule: true, default: MockOverlay }; +}); + +const defaultProps = { + width: 800, + height: 600, + clusterer: { + getClusters: jest.fn().mockReturnValue([]), + }, + globalOpacity: 1, + mapboxApiKey: 'test-key', + mapStyle: 'mapbox://styles/mapbox/light-v9', + pointRadius: 60, + pointRadiusUnit: 'Pixels', + renderWhileDragging: true, + rgb: ['', 255, 0, 0] as (string | number)[], + hasCustomMetric: false, + bounds: [ + [-74.0, 40.7], + [-73.9, 40.8], + ] as [[number, number], [number, number]], + onViewportChange: jest.fn(), +}; + +beforeEach(() => { + lastMapGLProps = {}; + jest.clearAllMocks(); + mockFitBounds.mockImplementation( + ( + bounds: [[number, number], [number, number]], + width: number, + height: number, + ) => ({ + latitude: Number(((bounds[0][1] + bounds[1][1]) / 2).toFixed(2)), + longitude: Number(((bounds[0][0] + bounds[1][0]) / 2).toFixed(2)), + zoom: Number((10 + width / 1000 + height / 10000).toFixed(2)), + }), + ); +}); + +test('initializes viewport from bounds', () => { + render(); + expect(lastMapGLProps.latitude).toBe(40.75); + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.zoom).toBe(10.86); +}); + +test('updates viewport when viewport props change', () => { + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(lastMapGLProps.longitude).toBe(-122.4); + expect(lastMapGLProps.latitude).toBe(37.8); + expect(lastMapGLProps.zoom).toBe(5); +}); + +test('does not loop when viewport state matches new props', () => { + const { rerender } = render( + , + ); + + // Re-render with same props that match the initial viewport state + rerender( + , + ); + + // Viewport should still be the fitBounds-computed values since props didn't change + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.latitude).toBe(40.75); + expect(lastMapGLProps.zoom).toBe(10); +}); + +test('passes globalOpacity to ScatterPlotGlowOverlay', () => { + const { getByTestId } = render( + , + ); + const overlay = getByTestId('scatter-overlay'); + expect(overlay.dataset.opacity).toBe('0.5'); +}); + +test('initializes viewport from props when provided', () => { + render( + , + ); + expect(lastMapGLProps.longitude).toBe(-122.4); + expect(lastMapGLProps.latitude).toBe(37.8); + expect(lastMapGLProps.zoom).toBe(5); +}); + +test('handles undefined bounds gracefully', () => { + render(); + expect(lastMapGLProps.longitude).toBe(0); + expect(lastMapGLProps.latitude).toBe(0); + expect(lastMapGLProps.zoom).toBe(1); +}); + +test('applies partial viewport props on update', () => { + const { rerender } = render(); + + rerender(); + + expect(lastMapGLProps.longitude).toBe(-122.4); + expect(lastMapGLProps.latitude).toBe(40.75); + expect(lastMapGLProps.zoom).toBe(10.86); +}); + +test('restores fitBounds when viewport props are cleared', () => { + const { rerender } = render( + , + ); + + // Clear all viewport props (simulates user clearing the controls) + rerender(); + + // Should revert to fitBounds values + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.latitude).toBe(40.75); + expect(lastMapGLProps.zoom).toBe(10.86); +}); + +test('restores only cleared viewport props, keeps the rest', () => { + const { rerender } = render( + , + ); + + // Clear only longitude, keep lat/zoom + rerender( + , + ); + + // Longitude reverts to fitBounds, lat/zoom stay + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.latitude).toBe(37.8); + expect(lastMapGLProps.zoom).toBe(5); +}); + +test('applies changed viewport props even when another is cleared simultaneously', () => { + const { rerender } = render( + , + ); + + // Clear longitude, change latitude simultaneously + rerender( + , + ); + + // Longitude reverts to fitBounds, latitude should be the NEW value + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.latitude).toBe(40.0); + expect(lastMapGLProps.zoom).toBe(5); +}); + +test('falls back to default viewport when cleared with undefined bounds', () => { + const { rerender } = render( + , + ); + + // Clear viewport props — no bounds to fitBounds to + rerender(); + + // Should fall back to {0, 0, 1} + expect(lastMapGLProps.longitude).toBe(0); + expect(lastMapGLProps.latitude).toBe(0); + expect(lastMapGLProps.zoom).toBe(1); +}); + +test('recomputes fitBounds when bounds change and no explicit viewport is set', () => { + const { rerender } = render(); + + rerender( + , + ); + + expect(lastMapGLProps.longitude).toBe(-122.5); + expect(lastMapGLProps.latitude).toBe(37.3); + expect(lastMapGLProps.zoom).toBe(10.86); +}); + +test('recomputes fitBounds when chart size changes and no explicit viewport is set', () => { + const { rerender } = render(); + + rerender(); + + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.latitude).toBe(40.75); + expect(lastMapGLProps.zoom).toBe(11.29); +}); + +test('recomputes only implicit viewport fields when bounds change', () => { + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(lastMapGLProps.longitude).toBe(-122.4); + expect(lastMapGLProps.latitude).toBe(37.3); + expect(lastMapGLProps.zoom).toBe(10.86); +}); + +test('recomputes only implicit viewport fields when chart size changes', () => { + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(lastMapGLProps.longitude).toBe(-73.95); + expect(lastMapGLProps.latitude).toBe(37.8); + expect(lastMapGLProps.zoom).toBe(11.29); +}); + +test('recomputes implicit position when zoom stays explicit across bounds changes', () => { + const { rerender } = render(); + + rerender( + , + ); + + expect(lastMapGLProps.longitude).toBe(-122.5); + expect(lastMapGLProps.latitude).toBe(37.3); + expect(lastMapGLProps.zoom).toBe(5); +}); + +test('does not recompute fitBounds on bounds change when an explicit viewport is set', () => { + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(lastMapGLProps.longitude).toBe(-122.4); + expect(lastMapGLProps.latitude).toBe(37.8); + expect(lastMapGLProps.zoom).toBe(5); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/test/controlPanel.test.ts b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/controlPanel.test.ts new file mode 100644 index 00000000000..2a72db46366 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/controlPanel.test.ts @@ -0,0 +1,81 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import type { + ControlPanelConfig, + CustomControlItem, +} from '@superset-ui/chart-controls'; +import controlPanel from '../src/controlPanel'; + +type ControlConfig = Required; + +function isCustomControlItem( + controlItem: unknown, +): controlItem is CustomControlItem & { config: ControlConfig } { + return ( + typeof controlItem === 'object' && + controlItem !== null && + 'name' in controlItem && + 'config' in controlItem + ); +} + +function getControl( + panel: ControlPanelConfig, + controlName: string, +): CustomControlItem & { config: ControlConfig } { + const item = (panel.controlPanelSections || []) + .flatMap(section => section?.controlSetRows || []) + .flat() + .find( + controlItem => + isCustomControlItem(controlItem) && controlItem.name === controlName, + ); + + if (!isCustomControlItem(item)) { + throw new Error(`Control "${controlName}" not found`); + } + + return item; +} + +test('viewport controls default to empty values and rerender without query refresh', () => { + const longitudeControl = getControl(controlPanel, 'viewport_longitude'); + const latitudeControl = getControl(controlPanel, 'viewport_latitude'); + const zoomControl = getControl(controlPanel, 'viewport_zoom'); + + expect(longitudeControl.config.default).toBe(''); + expect(latitudeControl.config.default).toBe(''); + expect(zoomControl.config.default).toBe(''); + + expect(longitudeControl.config.renderTrigger).toBe(true); + expect(latitudeControl.config.renderTrigger).toBe(true); + expect(zoomControl.config.renderTrigger).toBe(true); + + expect(longitudeControl.config.dontRefreshOnChange).toBe(true); + expect(latitudeControl.config.dontRefreshOnChange).toBe(true); + expect(zoomControl.config.dontRefreshOnChange).toBe(true); +}); + +test('opacity control rerenders immediately when changed', () => { + const opacityControl = getControl(controlPanel, 'global_opacity'); + + expect(opacityControl.config.default).toBe(1); + expect(opacityControl.config.renderTrigger).toBe(true); + expect(opacityControl.config.isFloat).toBe(true); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/test/transformProps.test.ts b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/transformProps.test.ts new file mode 100644 index 00000000000..483f64963dc --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/transformProps.test.ts @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartProps } from '@superset-ui/core'; +import { supersetTheme } from '@apache-superset/core/theme'; + +jest.mock('supercluster', () => { + const MockSupercluster = jest.fn().mockImplementation(() => ({ + load: jest.fn(), + getClusters: jest.fn().mockReturnValue([]), + })); + return { __esModule: true, default: MockSupercluster }; +}); + +// Import after mocking supercluster to avoid ESM parse error +// eslint-disable-next-line import/first +import transformProps from '../src/transformProps'; + +type TransformPropsResult = { + globalOpacity?: number; + onViewportChange?: (viewport: { + latitude: number; + longitude: number; + zoom: number; + }) => void; + viewportLongitude?: number; + viewportLatitude?: number; + viewportZoom?: number; +}; + +const baseFormData = { + clusteringRadius: 60, + globalOpacity: 0.8, + mapboxColor: 'rgb(0, 139, 139)', + mapboxStyle: 'mapbox://styles/mapbox/light-v9', + pandasAggfunc: 'sum', + pointRadiusUnit: 'Pixels', + renderWhileDragging: true, + viewportLongitude: -73.935242, + viewportLatitude: 40.73061, + viewportZoom: 9, +}; + +const baseQueriesData = [ + { + data: { + bounds: [ + [-74.0, 40.7], + [-73.9, 40.8], + ] as [[number, number], [number, number]], + geoJSON: { features: [] }, + hasCustomMetric: false, + mapboxApiKey: 'test-api-key', + }, + }, +]; + +function createChartProps(overrides: Record = {}) { + return new ChartProps({ + formData: { ...baseFormData, ...overrides }, + width: 800, + height: 600, + queriesData: baseQueriesData, + theme: supersetTheme, + }); +} + +function getTransformPropsResult( + overrides: Record = {}, +): TransformPropsResult { + return transformProps(createChartProps(overrides)) as TransformPropsResult; +} + +test('extracts globalOpacity from formData', () => { + const result = getTransformPropsResult({ globalOpacity: 0.5 }); + expect(result.globalOpacity).toBe(0.5); +}); + +test('extracts viewport values from formData', () => { + const result = getTransformPropsResult({ + viewportLongitude: -122.4, + viewportLatitude: 37.8, + viewportZoom: 12, + }); + expect(result).toEqual( + expect.objectContaining({ + viewportLongitude: -122.4, + viewportLatitude: 37.8, + viewportZoom: 12, + }), + ); +}); + +test('clamps viewport values to safe map ranges', () => { + const result = getTransformPropsResult({ + viewportLongitude: 190, + viewportLatitude: -100, + viewportZoom: 99, + }); + expect(result).toEqual( + expect.objectContaining({ + viewportLongitude: 180, + viewportLatitude: -90, + viewportZoom: 16, + }), + ); +}); + +test('provides onViewportChange callback that updates control values', () => { + const setControlValue = jest.fn(); + const chartProps = new ChartProps({ + formData: baseFormData, + width: 800, + height: 600, + queriesData: baseQueriesData, + hooks: { setControlValue }, + theme: supersetTheme, + }); + const result = transformProps(chartProps) as TransformPropsResult; + expect(result.onViewportChange).toBeDefined(); + + result.onViewportChange!({ + latitude: 51.5, + longitude: -0.12, + zoom: 10, + }); + + expect(setControlValue).toHaveBeenCalledWith('viewport_longitude', -0.12); + expect(setControlValue).toHaveBeenCalledWith('viewport_latitude', 51.5); + expect(setControlValue).toHaveBeenCalledWith('viewport_zoom', 10); +}); + +test('normalizes string viewport values to numbers', () => { + const result = getTransformPropsResult({ + viewportLongitude: '-122.4', + viewportLatitude: '37.8', + viewportZoom: '12', + }); + expect(result.viewportLongitude).toBe(-122.4); + expect(result.viewportLatitude).toBe(37.8); + expect(result.viewportZoom).toBe(12); +}); + +test('normalizes empty viewport values to undefined', () => { + const result = getTransformPropsResult({ + viewportLongitude: '', + viewportLatitude: '', + viewportZoom: '', + }); + expect(result.viewportLongitude).toBeUndefined(); + expect(result.viewportLatitude).toBeUndefined(); + expect(result.viewportZoom).toBeUndefined(); +}); + +test('normalizes whitespace-only viewport values to undefined', () => { + const result = getTransformPropsResult({ + viewportLongitude: ' ', + viewportLatitude: '\t', + viewportZoom: ' \n ', + }); + expect(result.viewportLongitude).toBeUndefined(); + expect(result.viewportLatitude).toBeUndefined(); + expect(result.viewportZoom).toBeUndefined(); +}); + +test('normalizes string opacity to number', () => { + const result = getTransformPropsResult({ globalOpacity: '0.5' }); + expect(result.globalOpacity).toBe(0.5); +}); + +test('defaults empty opacity to 1', () => { + const result = getTransformPropsResult({ globalOpacity: '' }); + expect(result.globalOpacity).toBe(1); +}); + +test('defaults whitespace-only opacity to 1', () => { + const result = getTransformPropsResult({ globalOpacity: ' ' }); + expect(result.globalOpacity).toBe(1); +}); + +test('clamps opacity to [0, 1] range', () => { + expect(getTransformPropsResult({ globalOpacity: 5 }).globalOpacity).toBe(1); + expect(getTransformPropsResult({ globalOpacity: -1 }).globalOpacity).toBe(0); +}); + +test('passes through numeric values unchanged', () => { + const result = getTransformPropsResult({ + viewportLongitude: -122.4, + viewportLatitude: 37.8, + viewportZoom: 12, + globalOpacity: 0.8, + }); + expect(result.viewportLongitude).toBe(-122.4); + expect(result.viewportLatitude).toBe(37.8); + expect(result.viewportZoom).toBe(12); + expect(result.globalOpacity).toBe(0.8); +}); + +test('calls onError and returns empty object for invalid color', () => { + const onError = jest.fn(); + const chartProps = new ChartProps({ + formData: { ...baseFormData, mapboxColor: 'invalid-color' }, + width: 800, + height: 600, + queriesData: baseQueriesData, + hooks: { onError }, + theme: supersetTheme, + }); + const result = transformProps(chartProps); + expect(onError).toHaveBeenCalledWith( + "Color field must be of form 'rgb(%d, %d, %d)'", + ); + expect(result).toEqual({}); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts index e6c7bc2679d..9388c379dd1 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts @@ -150,14 +150,20 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void { fillColor: colorFn(d.name, sliceId), })); } else { - colorFn = getSequentialSchemeRegistry() - .get(linearColorScheme) - .createLinearScale(d3Extent(filteredData, d => d.m1)); + const rawExtents = d3Extent(filteredData, d => d.m1); + const extents: [number, number] = + rawExtents[0] != null && rawExtents[1] != null + ? [rawExtents[0], rawExtents[1]] + : [0, 1]; + const colorSchemeObj = getSequentialSchemeRegistry().get(linearColorScheme); + colorFn = colorSchemeObj + ? colorSchemeObj.createLinearScale(extents) + : () => theme.colorBorder; processedData = filteredData.map(d => ({ ...d, radius: radiusScale(Math.sqrt(d.m2)), - fillColor: colorFn(d.m1), + fillColor: colorFn(d.m1) ?? theme.colorBorder, })); } diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts index 5a53aab9e82..4476bcefe85 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts @@ -489,6 +489,59 @@ test('popupTemplate returns tooltip HTML when country data exists', () => { expect(tooltipHtml).toContain('hoverinfo'); }); +test('assigns fill colors from sequential scheme when colorBy is metric', () => { + WorldMap(container, { + ...baseProps, + colorBy: ColorBy.Metric, + }); + + const data = lastDatamapConfig?.data as Record< + string, + WorldMapDataEntry & { fillColor: string } + >; + expect(data).toHaveProperty('USA'); + expect(data).toHaveProperty('CAN'); + expect(data.USA).toMatchObject({ + country: 'USA', + name: 'United States', + m1: 100, + }); + // fillColor should be a valid color string from the sequential scale + expect(data.USA.fillColor).toMatch(/^(#|rgb)/); + expect(data.CAN.fillColor).toMatch(/^(#|rgb)/); +}); + +test('falls back to theme.colorBorder when metric values are null', () => { + WorldMap(container, { + ...baseProps, + colorBy: ColorBy.Metric, + data: [ + { + country: 'USA', + name: 'United States', + m1: null as unknown as number, + m2: 200, + code: 'US', + latitude: 37.0902, + longitude: -95.7129, + }, + ], + } as any); + + const data = lastDatamapConfig?.data as Record; + expect(data.USA.fillColor).toBe('#e0e0e0'); +}); + +test('does not throw with empty data and metric coloring', () => { + expect(() => { + WorldMap(container, { + ...baseProps, + colorBy: ColorBy.Metric, + data: [], + }); + }).not.toThrow(); +}); + test('popupTemplate handles null/undefined country data gracefully', () => { WorldMap(container, baseProps); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx index f1e50c5b589..abce3761ad3 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx @@ -605,3 +605,140 @@ describe('DeckMulti Component Rendering', () => { }); }); }); + +test('includes parent_slice_id in child slice requests when parent has slice_id', async () => { + jest.clearAllMocks(); + const mockGet = jest.fn().mockResolvedValue({ + json: { + result: { + form_data: { + viz_type: 'deck_scatter', + datasource: '1__table', + }, + }, + data: { + features: [], + }, + }, + }); + (SupersetClient.get as jest.Mock) = mockGet; + const parentSliceId = 99; + const dashboardId = 5; + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + slice_id: parentSliceId, + dashboardId, + }, + }; + + renderWithProviders(); + + await waitFor(() => { + expect(mockGet).toHaveBeenCalled(); + }); + + // Check that the child slice requests include parent_slice_id + const { calls } = mockGet.mock; + calls.forEach(call => { + const { endpoint } = call[0]; + if (endpoint.includes('api/v1/explore/form_data')) { + const body = JSON.parse(call[0].body); + expect(body.form_data).toMatchObject({ + dashboardId, + parent_slice_id: parentSliceId, + }); + } + }); +}); + +test('includes parent_slice_id in embedded mode', async () => { + jest.clearAllMocks(); + const mockGet = jest.fn().mockResolvedValue({ + json: { + result: { + form_data: { + viz_type: 'deck_scatter', + datasource: '1__table', + }, + }, + data: { + features: [], + }, + }, + }); + (SupersetClient.get as jest.Mock) = mockGet; + const parentSliceId = 200; + const dashboardId = 10; + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + slice_id: parentSliceId, + dashboardId, + embedded: true, + }, + }; + + renderWithProviders(); + + await waitFor(() => { + expect(mockGet).toHaveBeenCalled(); + }); + + // Verify parent_slice_id is included in embedded mode + const { calls } = mockGet.mock; + calls.forEach(call => { + const { endpoint } = call[0]; + if (endpoint.includes('api/v1/explore/form_data')) { + const body = JSON.parse(call[0].body); + expect(body.form_data.parent_slice_id).toBe(parentSliceId); + } + }); +}); + +test('does not include parent_slice_id when parent has no slice_id', async () => { + jest.clearAllMocks(); + const mockGet = jest.fn().mockResolvedValue({ + json: { + result: { + form_data: { + viz_type: 'deck_scatter', + datasource: '1__table', + }, + }, + data: { + features: [], + }, + }, + }); + (SupersetClient.get as jest.Mock) = mockGet; + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + // No slice_id in parent + dashboardId: 5, + }, + }; + + renderWithProviders(); + + await waitFor(() => { + expect(mockGet).toHaveBeenCalled(); + }); + + // Verify parent_slice_id is not included when parent has no slice_id + const { calls } = mockGet.mock; + calls.forEach(call => { + const { endpoint } = call[0]; + if (endpoint.includes('api/v1/explore/form_data')) { + const body = JSON.parse(call[0].body); + expect(body.form_data.parent_slice_id).toBeUndefined(); + } + }); +}); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx index c361eb755c8..3c52464a247 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx @@ -289,6 +289,8 @@ const DeckMulti = (props: DeckMultiProps) => { adhoc_filters: adhocFilters, // Preserve dashboard context for embedded mode permissions ...(formData.dashboardId && { dashboardId: formData.dashboardId }), + // Include parent multilayer chart ID for security checks + ...(formData.slice_id && { parent_slice_id: formData.slice_id }), }, } as any as JsonObject & { slice_id: number }; diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts index a7b7be3ab9a..4069a474eff 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts @@ -289,6 +289,126 @@ describe('Polygon transformProps', () => { expect(features[0]?.elevation).toBeUndefined(); }); + test('should render polygons when boundary column contains GeoJSON Feature format', () => { + const geojsonFeatureProps = { + ...mockChartProps, + queriesData: [ + { + data: [ + { + geom: JSON.stringify({ + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-122.4, 37.8], + [-122.3, 37.8], + [-122.3, 37.9], + [-122.4, 37.9], + [-122.4, 37.8], + ], + ], + }, + properties: { name: 'test' }, + }), + }, + ], + }, + ], + }; + + const result = transformProps(geojsonFeatureProps as ChartProps); + + const features = result.payload.data.features as PolygonFeature[]; + expect(features).toHaveLength(1); + expect(features[0]?.polygon).toEqual([ + [-122.4, 37.8], + [-122.3, 37.8], + [-122.3, 37.9], + [-122.4, 37.9], + [-122.4, 37.8], + ]); + }); + + test('should render polygons when boundary column contains GeoJSON Geometry format', () => { + const geojsonGeometryProps = { + ...mockChartProps, + queriesData: [ + { + data: [ + { + geom: JSON.stringify({ + type: 'Polygon', + coordinates: [ + [ + [-122.4, 37.8], + [-122.3, 37.8], + [-122.3, 37.9], + [-122.4, 37.9], + [-122.4, 37.8], + ], + ], + }), + }, + ], + }, + ], + }; + + const result = transformProps(geojsonGeometryProps as ChartProps); + + const features = result.payload.data.features as PolygonFeature[]; + expect(features).toHaveLength(1); + expect(features[0]?.polygon).toEqual([ + [-122.4, 37.8], + [-122.3, 37.8], + [-122.3, 37.9], + [-122.4, 37.9], + [-122.4, 37.8], + ]); + }); + + test('should render polygons when boundary column contains JSON with nested geometry', () => { + // Real-world format: {"type":"Polygon","geometry":{"type":"Polygon","coordinates":[...]}} + const nonStandardProps = { + ...mockChartProps, + queriesData: [ + { + data: [ + { + geom: JSON.stringify({ + type: 'Polygon', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [79.7912, 8.4641], + [79.7959, 8.4629], + [79.7994, 8.4535], + [79.7912, 8.4641], + ], + ], + }, + }), + }, + ], + }, + ], + }; + + const result = transformProps(nonStandardProps as ChartProps); + + const features = result.payload.data.features as PolygonFeature[]; + expect(features).toHaveLength(1); + expect(features[0]?.polygon).toEqual([ + [79.7912, 8.4641], + [79.7959, 8.4629], + [79.7994, 8.4535], + [79.7912, 8.4641], + ]); + }); + test('should handle geohash decoding successfully', () => { const props = { ...mockedChartPropsWithGeoHash, diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts index bb070d92e36..5a9f71b2a53 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts @@ -115,6 +115,10 @@ function processPolygonData( if (parsed.coordinates) { polygonCoords = parsed.coordinates[0] || parsed.coordinates; + } else if (parsed.geometry?.coordinates) { + // Non-standard format with nested geometry + polygonCoords = + parsed.geometry.coordinates[0] || parsed.geometry.coordinates; } else if (Array.isArray(parsed)) { polygonCoords = parsed; } else { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json b/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json index 640a338a831..aa4526ebb7a 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json @@ -34,7 +34,7 @@ "fast-safe-stringify": "^2.1.1", "lodash": "^4.17.23", "nvd3-fork": "^2.0.5", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "prop-types": "^15.8.1", "urijs": "^1.19.11" }, diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx index b4eae18de41..0673bdd483a 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTableChart.tsx @@ -85,6 +85,7 @@ export default function TableChart( width, onChartStateChange, chartState, + metricSqlExpressions, } = props; const [searchOptions, setSearchOptions] = useState([]); @@ -187,6 +188,7 @@ export default function TableChart( lastFilteredColumn: completeFilterState.lastFilteredColumn, lastFilteredInputPosition: completeFilterState.inputPosition, currentPage: 0, // Reset to first page when filtering + metricSqlExpressions, }; updateTableOwnState(setDataMask, modifiedOwnState); @@ -197,6 +199,7 @@ export default function TableChart( serverPaginationData, onChartStateChange, chartState, + metricSqlExpressions, ], ); diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts index 8122c86f825..3d04b6c1d1c 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/buildQuery.ts @@ -482,12 +482,77 @@ const buildQuery: BuildQuery = ( }; } - // Add AG Grid complex WHERE clause from ownState (non-metric filters) + // Map metric/column labels to SQL expressions for WHERE/HAVING resolution + const sqlExpressionMap: Record = {}; + (metrics || []).forEach((m: QueryFormMetric) => { + if (typeof m === 'object' && 'expressionType' in m) { + const label = getMetricLabel(m); + if (m.expressionType === 'SQL' && m.sqlExpression) { + sqlExpressionMap[label] = m.sqlExpression; + } else if ( + m.expressionType === 'SIMPLE' && + m.aggregate && + m.column?.column_name + ) { + sqlExpressionMap[label] = `${m.aggregate}(${m.column.column_name})`; + } + } + }); + // Map dimension columns with custom SQL expressions + (columns || []).forEach((col: QueryFormColumn) => { + if (typeof col === 'object' && 'sqlExpression' in col) { + const label = getColumnLabel(col); + if (col.sqlExpression) { + sqlExpressionMap[label] = col.sqlExpression; + } + } + }); + // Merge datasource-level saved metrics and calculated columns + if (ownState.metricSqlExpressions) { + Object.entries( + ownState.metricSqlExpressions as Record, + ).forEach(([label, expression]) => { + if (!sqlExpressionMap[label]) { + sqlExpressionMap[label] = expression; + } + }); + } + + const resolveLabelsToSQL = (clause: string): string => { + let resolved = clause; + // Sort by label length descending to prevent substring false positives + const sortedEntries = Object.entries(sqlExpressionMap).sort( + ([a], [b]) => b.length - a.length, + ); + sortedEntries.forEach(([label, expression]) => { + if (resolved.includes(label)) { + const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Wrap complex expressions in parentheses for valid SQL + const isExpression = + expression.includes('(') || + expression.toUpperCase().includes('CASE') || + expression.includes('\n'); + const wrappedExpression = isExpression + ? `(${expression})` + : expression; + resolved = resolved.replace( + new RegExp(`\\b${escapedLabel}\\b`, 'g'), + wrappedExpression, + ); + } + }); + return resolved; + }; + + // Resolve and apply AG Grid WHERE clause if (ownState.agGridComplexWhere && ownState.agGridComplexWhere.trim()) { + const resolvedWhere = resolveLabelsToSQL(ownState.agGridComplexWhere); + (ownState as Record).agGridComplexWhere = + resolvedWhere; const existingWhere = queryObject.extras?.where; const combinedWhere = existingWhere - ? `${existingWhere} AND ${ownState.agGridComplexWhere}` - : ownState.agGridComplexWhere; + ? `${existingWhere} AND ${resolvedWhere}` + : resolvedWhere; queryObject = { ...queryObject, @@ -498,12 +563,15 @@ const buildQuery: BuildQuery = ( }; } - // Add AG Grid HAVING clause from ownState (metric filters only) + // Resolve and apply AG Grid HAVING clause if (ownState.agGridHavingClause && ownState.agGridHavingClause.trim()) { + const resolvedHaving = resolveLabelsToSQL(ownState.agGridHavingClause); + (ownState as Record).agGridHavingClause = + resolvedHaving; const existingHaving = queryObject.extras?.having; const combinedHaving = existingHaving - ? `${existingHaving} AND ${ownState.agGridHavingClause}` - : ownState.agGridHavingClause; + ? `${existingHaving} AND ${resolvedHaving}` + : resolvedHaving; queryObject = { ...queryObject, diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts index be174cb5a21..c27bd856d89 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts @@ -34,6 +34,8 @@ import { SMART_DATE_ID, TimeFormats, TimeFormatter, + AgGridChartState, + AgGridFilterModel, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/common'; import { isEmpty, isEqual, merge } from 'lodash'; @@ -456,6 +458,9 @@ const getPageSize = ( return numRecords * numColumns > 5000 ? 200 : 0; }; +// Tracks slice_ids that have already applied their saved chartState filter on mount +const savedFilterAppliedSet = new Set(); + const transformProps = ( chartProps: TableChartProps, ): AgGridTableChartTransformedProps => { @@ -710,6 +715,36 @@ const transformProps = ( : totalQuery?.data[0] : undefined; + // Map saved metric/calculated column labels to their SQL expressions for filter resolution + const metricSqlExpressions: Record = {}; + chartProps.datasource.metrics.forEach(metric => { + if (metric.metric_name && metric.expression) { + metricSqlExpressions[metric.metric_name] = metric.expression; + } + }); + chartProps.datasource.columns.forEach(col => { + if (col.column_name && col.expression) { + metricSqlExpressions[col.column_name] = col.expression; + if (col.verbose_name && col.verbose_name !== col.column_name) { + metricSqlExpressions[col.verbose_name] = col.expression; + } + } + }); + + // Strip saved filter from chartState after initial application to prevent re-injection + let chartState = serverPaginationData?.chartState as + | AgGridChartState + | undefined; + const chartStateHasFilter = !!( + chartState?.filterModel && Object.keys(chartState.filterModel).length > 0 + ); + + if (chartStateHasFilter && savedFilterAppliedSet.has(slice_id)) { + chartState = { ...chartState!, filterModel: {} as AgGridFilterModel }; + } else if (chartStateHasFilter) { + savedFilterAppliedSet.add(slice_id); + } + return { height, width, @@ -742,7 +777,8 @@ const transformProps = ( basicColorColumnFormatters, basicColorFormatters, formData, - chartState: serverPaginationData?.chartState, + metricSqlExpressions, + chartState, onChartStateChange, }; }; diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts index d97f36b267f..e3d6bf07079 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/types.ts @@ -128,6 +128,7 @@ export interface AgGridTableChartTransformedProps< basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[]; basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[]; formData: TableChartFormData; + metricSqlExpressions: Record; onChartStateChange?: (chartState: JsonObject) => void; chartState?: AgGridChartState; } diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/buildQuery.test.ts index 0778d6fde58..acec22c6965 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/buildQuery.test.ts @@ -1090,4 +1090,258 @@ describe('plugin-chart-ag-grid-table', () => { expect(query.metrics).toEqual([]); }); }); + + describe('buildQuery - label-to-SQL resolution in WHERE/HAVING', () => { + test('should resolve inline SQL metric labels in WHERE clause', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'SUM(revenue)', + label: 'Total Revenue', + }, + ], + }, + { + ownState: { + agGridComplexWhere: 'Total Revenue > 1000', + }, + }, + ).queries[0]; + + expect(query.extras?.where).toBe('(SUM(revenue)) > 1000'); + }); + + test('should resolve SIMPLE metric labels in HAVING clause', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + metrics: [ + { + expressionType: 'SIMPLE', + aggregate: 'SUM', + column: { column_name: 'revenue' }, + label: 'Total Revenue', + }, + ], + }, + { + ownState: { + agGridHavingClause: 'Total Revenue > 1000', + }, + }, + ).queries[0]; + + expect(query.extras?.having).toBe('(SUM(revenue)) > 1000'); + }); + + test('should resolve adhoc column SQL expressions in WHERE clause', () => { + const adhocColumn = createAdhocColumn('UPPER(city)', 'City Upper'); + + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + groupby: [adhocColumn], + }, + { + ownState: { + agGridComplexWhere: "City Upper = 'NEW YORK'", + }, + }, + ).queries[0]; + + expect(query.extras?.where).toContain('UPPER(city)'); + }); + + test('should wrap CASE expressions in parentheses', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + }, + { + ownState: { + agGridComplexWhere: "degree_level = 'High'", + metricSqlExpressions: { + degree_level: + "CASE WHEN degree = 'PhD' THEN 'High' ELSE 'Low' END", + }, + }, + }, + ).queries[0]; + + expect(query.extras?.where).toBe( + "(CASE WHEN degree = 'PhD' THEN 'High' ELSE 'Low' END) = 'High'", + ); + }); + + test('should wrap aggregate expressions in parentheses', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + }, + { + ownState: { + agGridHavingClause: 'total_count > 100', + metricSqlExpressions: { + total_count: 'COUNT(*)', + }, + }, + }, + ).queries[0]; + + expect(query.extras?.having).toBe('(COUNT(*)) > 100'); + }); + + test('should quote simple column names without parentheses', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + }, + { + ownState: { + agGridComplexWhere: "status = 'active'", + metricSqlExpressions: { + status: 'user_status', + }, + }, + }, + ).queries[0]; + + expect(query.extras?.where).toBe("user_status = 'active'"); + }); + + test('should resolve longer labels before shorter ones', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'COUNT(*)', + label: 'count', + }, + { + expressionType: 'SQL', + sqlExpression: 'COUNT(DISTINCT id)', + label: 'count_distinct', + }, + ], + }, + { + ownState: { + agGridHavingClause: 'count_distinct > 5 AND count > 10', + }, + }, + ).queries[0]; + + expect(query.extras?.having).toContain('(COUNT(DISTINCT id)) > 5'); + expect(query.extras?.having).toContain('(COUNT(*)) > 10'); + }); + + test('should prefer query-level expressions over datasource-level', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'SUM(amount)', + label: 'total', + }, + ], + }, + { + ownState: { + agGridHavingClause: 'total > 500', + metricSqlExpressions: { + total: 'SUM(old_amount)', + }, + }, + }, + ).queries[0]; + + // Query-level SUM(amount) should win over datasource-level SUM(old_amount) + expect(query.extras?.having).toBe('(SUM(amount)) > 500'); + }); + + test('should not modify clause when no labels match', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + }, + { + ownState: { + agGridComplexWhere: 'physical_column > 10', + }, + }, + ).queries[0]; + + expect(query.extras?.where).toBe('physical_column > 10'); + }); + + test('should resolve labels in both WHERE and HAVING simultaneously', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: true, + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'SUM(sales)', + label: 'Total Sales', + }, + ], + }, + { + ownState: { + agGridComplexWhere: "region = 'West'", + agGridHavingClause: 'Total Sales > 1000', + metricSqlExpressions: { + region: "CASE WHEN area = 'W' THEN 'West' ELSE 'East' END", + }, + }, + }, + ).queries[0]; + + expect(query.extras?.where).toContain('CASE WHEN'); + expect(query.extras?.having).toBe('(SUM(sales)) > 1000'); + }); + + test('should not resolve labels when server pagination is disabled', () => { + const query = buildQuery( + { + ...basicFormData, + server_pagination: false, + metrics: [ + { + expressionType: 'SQL', + sqlExpression: 'SUM(revenue)', + label: 'Total Revenue', + }, + ], + }, + { + ownState: { + agGridComplexWhere: 'Total Revenue > 1000', + metricSqlExpressions: { + some_col: 'COUNT(*)', + }, + }, + }, + ).queries[0]; + + expect(query.extras?.where || undefined).toBeUndefined(); + }); + }); }); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json index 505d11bb3bf..e467c2103db 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json @@ -40,11 +40,11 @@ "@superset-ui/core": "*", "@apache-superset/core": "*", "@types/react-redux": "*", - "geostyler": "^14.1.3", + "geostyler": "^18.3.1", "geostyler-data": "^1.0.0", - "geostyler-openlayers-parser": "^4.0.0", - "geostyler-style": "^7.2.0", - "geostyler-wfs-parser": "^2.0.0", + "geostyler-openlayers-parser": "^5.4.0", + "geostyler-style": "^11.0.2", + "geostyler-wfs-parser": "^3.0.1", "ol": "^10.8.0", "polished": "*", "react": "^17.0.2", diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts index 05f63e9b8a5..a4141d89543 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts @@ -36,6 +36,7 @@ describe('layerUtil', () => { describe('createWfsLayer', () => { test('properly applies style', async () => { const colorToExpect = '#123456'; + const fillColor = '#ff0000'; const wfsLayerConf: WfsLayerConf = { title: 'osm:osm-fuel', @@ -61,7 +62,7 @@ describe('layerUtil', () => { }, { kind: 'Fill', - color: '#000000', + color: fillColor, }, ], }, @@ -76,8 +77,8 @@ describe('layerUtil', () => { expect(style!.length).toEqual(3); // @ts-expect-error upgrade `ol` package for better type of StyleLike type. - const colorAtLayer = style![1].getImage().getFill().getColor(); - expect(colorToExpect).toEqual(colorAtLayer); + const colorAtLayer = style![2].getFill().getColor(); + expect(colorAtLayer).toEqual(fillColor); }); }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json index c56ac505132..459e3ee7a9e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/package.json +++ b/superset-frontend/plugins/plugin-chart-echarts/package.json @@ -26,7 +26,7 @@ "dependencies": { "@types/d3-array": "^3.2.2", "@types/react-redux": "^7.1.34", - "acorn": "^8.9.0", + "acorn": "^8.16.0", "d3-array": "^3.2.4", "lodash": "^4.17.23", "zod": "^4.3.6" diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx index b290596d153..4c52463936b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx @@ -158,7 +158,7 @@ const defaultFormData: EchartsTimeseriesFormData & { xAxisTitle: '', xAxisTitleMargin: 0, yAxisTitle: '', - yAxisTitleMargin: 0, + yAxisTitleMargin: 15, yAxisTitlePosition: '', time_range: 'No filter', granularity: undefined, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts index c07be715216..472d5fc6525 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts @@ -46,7 +46,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = { xAxisTitle: '', xAxisTitleMargin: 0, yAxisTitle: '', - yAxisTitleMargin: 0, + yAxisTitleMargin: 15, yAxisTitlePosition: 'Top', // Now that the weird bug workaround is over, here's the rest... ...DEFAULT_SORT_SERIES_DATA, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 6eff01ca80b..f7d90cc3f5a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -336,6 +336,7 @@ export default function transformProps( chartProps.rawFormData, seriesName, ); + const lineStyle: LineStyleOption = {}; if (derivedSeries) { // Get the time offset for this series to assign different dash patterns @@ -370,7 +371,21 @@ export default function transformProps( let colorScaleKey = getOriginalSeries(seriesName, array); - // If series name exactly matches a time offset (single metric case), + // When there's a single metric with dimensions, the backend replaces the metric + // with the time offset in derived series (e.g., "28 days ago, Medium" instead of + // "SUM(sales), 28 days ago, Medium"). To match colors, strip the metric label + // from original series so both produce the same key (e.g., "Medium"). + if ( + groupby && + groupby.length > 0 && + array.length > 0 && + metrics?.length === 1 + ) { + const metricLabel = getMetricLabel(metrics[0]); + colorScaleKey = colorScaleKey.replace(`${metricLabel}, `, ''); + } + + // If series name exactly matches a time offset (single metric case, no dimensions), // find the original series for color matching if (derivedSeries && array.includes(seriesName)) { const originalSeries = rawSeries.find( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index 286f611e6d3..76de9217868 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -104,7 +104,7 @@ export const DEFAULT_TITLE_FORM_DATA: TitleFormData = { xAxisTitle: '', xAxisTitleMargin: 0, yAxisTitle: '', - yAxisTitleMargin: 0, + yAxisTitleMargin: 15, yAxisTitlePosition: 'Top', }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts index f17924ec66e..e8707d0c8c2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts @@ -112,7 +112,7 @@ const formData: EchartsMixedTimeseriesFormData = { yAxisBounds: [undefined, undefined], yAxisBoundsSecondary: [undefined, undefined], yAxisTitle: '', - yAxisTitleMargin: 0, + yAxisTitleMargin: 15, yAxisTitlePosition: '', yAxisTitleSecondary: '', zoomable: false, diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 510228596d7..7c33304890a 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -118,7 +118,7 @@ export interface ResultSetProps { const ResultContainer = styled.div` display: flex; flex-direction: column; - row-gap: ${({ theme }) => theme.sizeUnit * 2}px; + row-gap: ${({ theme }) => theme.sizeUnit * 3}px; height: 100%; `; diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx index 5ee5b172f6d..114a2345a2f 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx @@ -63,6 +63,7 @@ const TABS_KEYS = { const StyledPane = styled.div` width: 100%; height: 100%; + .ant-tabs .ant-tabs-content-holder { overflow: visible; } @@ -79,6 +80,7 @@ const StyledPane = styled.div` ${({ theme }) => theme.sizeUnit * 2}px; } .ant-tabs-tabpane { + padding-top: ${({ theme }) => theme.sizeUnit * 3}px; .scrollable { overflow-y: auto; } diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx index ecb52a7d87d..cdab2f54af5 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx @@ -187,8 +187,10 @@ const ChartContextMenu = ( canDrillBy && isDisplayed(ContextMenuItem.DrillBy) && !( - formData.matrixify_enable_vertical_layout === true || - formData.matrixify_enable_horizontal_layout === true + (formData.matrixify_mode_rows !== undefined && + formData.matrixify_mode_rows !== 'disabled') || + (formData.matrixify_mode_columns !== undefined && + formData.matrixify_mode_columns !== 'disabled') ); // Disable drill by when matrixify is enabled const datasetResource = useDatasetDrillInfo( diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx index 1c35af4efc8..9fd400fd562 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx @@ -163,7 +163,7 @@ test('should detect changes in matrixify properties', () => { ...requiredProps.formData, datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', matrixify_dimension_x: { dimension: 'country', values: ['USA'] }, matrixify_dimension_y: { dimension: 'category', values: ['Tech'] }, matrixify_charts_per_row: 3, @@ -177,9 +177,9 @@ test('should detect changes in matrixify properties', () => { // Since we can't directly test shouldComponentUpdate, we verify the component // correctly identifies matrixify-related properties by checking the implementation - expect( - (initialProps.formData as JsonObject).matrixify_enable_vertical_layout, - ).toBe(true); + expect((initialProps.formData as JsonObject).matrixify_mode_rows).toBe( + 'metrics', + ); expect((initialProps.formData as JsonObject).matrixify_dimension_x).toEqual({ dimension: 'country', values: ['USA'], @@ -212,7 +212,7 @@ test('should identify matrixify property changes correctly', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', matrixify_dimension_x: { dimension: 'country', values: ['USA'] }, matrixify_charts_per_row: 3, }, @@ -234,7 +234,7 @@ test('should identify matrixify property changes correctly', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', matrixify_dimension_x: { dimension: 'country', values: ['USA', 'Canada'], // Changed @@ -277,7 +277,7 @@ test('should handle matrixify-related form data changes', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, // This is a significant change + matrixify_mode_rows: 'metrics', // This is a significant change regular_control: 'value1', }, }; @@ -296,7 +296,7 @@ test('should detect matrixify property addition', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', // No matrixify_dimension_x initially }, queriesResponse: [{ data: 'current' } as unknown as JsonObject], @@ -317,7 +317,7 @@ test('should detect matrixify property addition', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', matrixify_dimension_x: { dimension: 'country', values: ['USA'] }, // Added }, }; @@ -336,7 +336,7 @@ test('should detect nested matrixify property changes', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', matrixify_dimension_x: { dimension: 'country', values: ['USA'], @@ -361,7 +361,7 @@ test('should detect nested matrixify property changes', () => { formData: { datasource: '', viz_type: VizType.Table, - matrixify_enable_vertical_layout: true, + matrixify_mode_rows: 'metrics', matrixify_dimension_x: { dimension: 'country', values: ['USA'], diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx index 33528f78a38..b58b3966b36 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx @@ -275,8 +275,10 @@ class ChartRenderer extends Component { const nextFormData = nextProps.formData as JsonObject; const currentFormData = this.props.formData as JsonObject; const isMatrixifyEnabled = - nextFormData.matrixify_enable_vertical_layout === true || - nextFormData.matrixify_enable_horizontal_layout === true; + (nextFormData.matrixify_mode_rows !== undefined && + nextFormData.matrixify_mode_rows !== 'disabled') || + (nextFormData.matrixify_mode_columns !== undefined && + nextFormData.matrixify_mode_columns !== 'disabled'); if (!isMatrixifyEnabled) return false; // Check all matrixify-related properties diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx index 3b60e756911..1a7f23a9e4a 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx @@ -272,9 +272,9 @@ test('When menu item is clicked, call onSelection with clicked column and drill ); }); -test('matrixify_enable_vertical_layout should not render component', () => { +test('matrixify_mode_rows enabled should not render component', () => { const { container } = renderSubmenu({ - formData: { ...defaultFormData, matrixify_enable_vertical_layout: true }, + formData: { ...defaultFormData, matrixify_mode_rows: 'metrics' }, }); expect(container).toBeEmptyDOMElement(); }); diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx index 8bb347e7dfa..40e18a264be 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx @@ -179,8 +179,10 @@ export const DrillBySubmenu = ({ } if ( - formData.matrixify_enable_vertical_layout === true || - formData.matrixify_enable_horizontal_layout === true + (formData.matrixify_mode_rows !== undefined && + formData.matrixify_mode_rows !== 'disabled') || + (formData.matrixify_mode_columns !== undefined && + formData.matrixify_mode_columns !== 'disabled') ) { return null; } diff --git a/superset-frontend/src/components/Chart/chartReducers.test.ts b/superset-frontend/src/components/Chart/chartReducers.test.ts index ee5010358cb..d5b660dd810 100644 --- a/superset-frontend/src/components/Chart/chartReducers.test.ts +++ b/superset-frontend/src/components/Chart/chartReducers.test.ts @@ -35,36 +35,35 @@ describe('chart reducers', () => { }); test('should update endtime on fail', () => { - const controller = new AbortController(); - charts[chartKey] = { - ...charts[chartKey], - queryController: controller, - }; - const newState = chartReducer( - charts, - actions.chartUpdateStopped(chartKey, controller), - ); + const newState = chartReducer(charts, actions.chartUpdateStopped(chartKey)); expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0); expect(newState[chartKey].chartStatus).toEqual('stopped'); }); - test('should ignore stopped updates from stale controllers', () => { - const controller = new AbortController(); - const staleController = new AbortController(); - charts[chartKey] = { - ...charts[chartKey], - chartStatus: 'loading', - queryController: controller, - }; - - const newState = chartReducer( - charts, - actions.chartUpdateStopped(chartKey, staleController), + test('should handle chartUpdateStopped without queryController', () => { + const newState = chartReducer(charts, actions.chartUpdateStopped(chartKey)); + expect(newState[chartKey].chartStatus).toEqual('stopped'); + expect(newState[chartKey].chartAlert).toContain( + 'Updating chart was stopped', ); + expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0); + }); - expect(newState[chartKey].chartStatus).toEqual('loading'); - expect(newState[chartKey].chartUpdateEndTime).toEqual( - charts[chartKey].chartUpdateEndTime, + test('chartUpdateStopped sets state correctly', () => { + const chartsWithController = { + [chartKey]: { + ...testChart, + queryController: new AbortController(), + }, + }; + const newState = chartReducer( + chartsWithController, + actions.chartUpdateStopped(chartKey), + ); + // Verify the chart status and alert are set + expect(newState[chartKey].chartStatus).toEqual('stopped'); + expect(newState[chartKey].chartAlert).toContain( + 'Updating chart was stopped', ); }); diff --git a/superset-frontend/src/core/editors/AceEditorProvider.tsx b/superset-frontend/src/core/editors/AceEditorProvider.tsx index abdf76d14f1..3deb345a217 100644 --- a/superset-frontend/src/core/editors/AceEditorProvider.tsx +++ b/superset-frontend/src/core/editors/AceEditorProvider.tsx @@ -55,6 +55,8 @@ type Range = editors.Range; type Selection = editors.Selection; type EditorAnnotation = editors.EditorAnnotation; type CompletionProvider = editors.CompletionProvider; +type ContentChange = editors.ContentChange; +type ContentChangeEvent = editors.ContentChangeEvent; /** * Maps EditorLanguage to the corresponding Ace editor component. @@ -117,10 +119,14 @@ const createAceEditorHandle = ( }, moveCursorToPosition: (position: Position) => { - aceEditorRef.current?.editor?.moveCursorToPosition({ - row: position.line, - column: position.column, - }); + const editor = aceEditorRef.current?.editor; + if (editor) { + editor.clearSelection(); + editor.moveCursorToPosition({ + row: position.line, + column: position.column, + }); + } }, getSelections: (): Selection[] => { @@ -186,6 +192,33 @@ const createAceEditorHandle = ( resize: () => { aceEditorRef.current?.editor?.resize(); }, + + onDidChangeContent: (listener, thisArgs?) => { + const editor = aceEditorRef.current?.editor; + if (!editor) return new Disposable(() => {}); + const bound = (thisArgs ? listener.bind(thisArgs) : listener) as ( + e: ContentChangeEvent, + ) => void; + const handler = (delta: { + action: 'insert' | 'remove'; + start: { row: number; column: number }; + lines: string[]; + }) => { + const rangeOffset = editor.session.doc.positionToIndex(delta.start); + const changeText = delta.lines.join( + editor.session.doc.getNewLineCharacter(), + ); + const change: ContentChange = + delta.action === 'insert' + ? { rangeOffset, rangeLength: 0, text: changeText } + : { rangeOffset, rangeLength: changeText.length, text: '' }; + bound({ getValue: () => editor.getValue(), changes: [change] }); + }; + editor.session.on('change', handler); + return new Disposable(() => { + editor.session.off('change', handler); + }); + }, }); /** diff --git a/superset-frontend/src/core/sqlLab/index.ts b/superset-frontend/src/core/sqlLab/index.ts index 8e16a41613a..b14be7efd07 100644 --- a/superset-frontend/src/core/sqlLab/index.ts +++ b/superset-frontend/src/core/sqlLab/index.ts @@ -694,6 +694,12 @@ const setSchema: typeof sqlLabApi.setSchema = async (schema: string | null) => { store.dispatch(queryEditorSetSchema(queryEditor ?? null, schema)); }; +const setActivePanel: typeof sqlLabApi.setActivePanel = async ( + panelId: string, +) => { + store.dispatch({ type: SET_ACTIVE_SOUTHPANE_TAB, tabId: panelId }); +}; + export const sqlLab: typeof sqlLabApi = { CTASMethod, getActivePanel, @@ -719,6 +725,7 @@ export const sqlLab: typeof sqlLabApi = { setDatabase, setCatalog, setSchema, + setActivePanel, }; // Export all models diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx index 6fae1d5c6ed..10650a79ade 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Header/index.tsx @@ -33,6 +33,7 @@ const TitleArea = styled.div` justify-content: space-between; margin: 0; padding: 0 ${theme.sizeUnit * 2}px ${theme.sizeUnit * 2}px; + padding-bottom: 0; /* Works with other changes in PR https://github.com/apache/superset/pull/38646 to reduces space between filter header and 1st filter */ & > span { font-size: ${theme.fontSizeLG}px; @@ -56,9 +57,8 @@ const HeaderButton = styled(Button)` const Wrapper = styled.div` ${({ theme }) => ` - padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 2}px ${ - theme.sizeUnit - }px; + padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 2}px; + padding-bottom: 0; /* Works with other changes in PR https://github.com/apache/superset/pull/38646 to reduces space between filter header and 1st filter */ `} `; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index 9db9b8b0579..fa9ab3c4183 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -119,6 +119,7 @@ const FilterControlsWrapper = styled.div` flex-direction: column; gap: ${theme.sizeUnit * 2}px; padding: ${theme.sizeUnit * 4}px; + padding-top: 0; /* Works with other changes in PR https://github.com/apache/superset/pull/38646 to reduces space between filter header and 1st filter */ // 108px padding to make room for buttons with position: absolute padding-bottom: ${theme.sizeUnit * 27}px; `} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index ab6a9f22e69..cc53dcbcb61 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -45,12 +45,16 @@ import { useEffect, useImperativeHandle, useMemo, + useRef, useState, RefObject, memo, } from 'react'; import rison from 'rison'; -import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types'; +import { + PluginFilterSelectCustomizeProps, + SelectFilterOperatorType, +} from 'src/filters/components/Select/types'; import { useSelector } from 'react-redux'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import { @@ -624,6 +628,49 @@ const FiltersConfigForm = ( forceUpdate(); }; + const currentOperatorType: SelectFilterOperatorType = + formFilter?.controlValues?.operatorType ?? + filterToEdit?.controlValues?.operatorType ?? + SelectFilterOperatorType.Exact; + + const selectedColumnIsString = useMemo(() => { + const columnName = formFilter?.column; + if (!columnName || !datasetDetails?.columns) return true; + const colMeta = datasetDetails.columns.find( + (c: { column_name: string }) => c.column_name === columnName, + ); + if (!colMeta) return true; + return colMeta.type_generic === GenericDataType.String; + }, [formFilter?.column, datasetDetails?.columns]); + + const onOperatorTypeChanged = (value: SelectFilterOperatorType) => { + const previous = form.getFieldValue('filters')?.[filterId].controlValues; + setNativeFilterFieldValues(form, filterId, { + controlValues: { + ...previous, + operatorType: value, + }, + defaultDataMask: null, + }); + formChanged(); + forceUpdate(); + }; + + const prevColumnRef = useRef(formFilter?.column); + const datasetLoaded = !!datasetDetails?.columns; + useEffect(() => { + const columnChanged = prevColumnRef.current !== formFilter?.column; + if ( + (columnChanged || datasetLoaded) && + !selectedColumnIsString && + currentOperatorType !== SelectFilterOperatorType.Exact + ) { + onOperatorTypeChanged(SelectFilterOperatorType.Exact); + } + prevColumnRef.current = formFilter?.column; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formFilter?.column, selectedColumnIsString, datasetLoaded]); + const validatePreFilter = () => setTimeout( () => @@ -685,6 +732,7 @@ const FiltersConfigForm = ( 'columns.filterable', 'columns.is_dttm', 'columns.type', + 'columns.type_generic', 'columns.verbose_name', 'database.id', 'database.database_name', @@ -1501,6 +1549,67 @@ const FiltersConfigForm = ( hidden initialValue={null} /> + {!isChartCustomization && + itemTypeField === 'filter_select' && ( + + {t('Match type')} +   + + + } + > + (parentRef?.current as HTMLElement) || document.body - : (trigger: HTMLElement) => - (trigger?.parentNode as HTMLElement) || document.body - } - showSearch={showSearch} - mode={multiSelect ? 'multiple' : 'single'} - placeholder={placeholderText} - onClear={() => onSearch('')} - onSearch={onSearch} - onBlur={handleBlur} - onFocus={setFocusedFilter} - onMouseEnter={setHoveredFilter} - onMouseLeave={unsetHoveredFilter} - // @ts-expect-error - onChange={handleChange} - ref={inputRef} - loading={isRefreshing} - oneLine={filterBarOrientation === FilterBarOrientation.Horizontal} - invertSelection={inverseSelection && excludeFilterValues} - options={options} - sortComparator={sortComparator} - onOpenChange={setFilterActive} - className="select-container" - /> + {isLikeOperator ? ( + + ) : ( +