Compare commits

..
Author SHA1 Message Date
Elizabeth ThompsonandClaude Opus 4.8 c18c91a89d fix(chart): catch JSONDecodeError when parsing params on chart create
CreateChartCommand.__init__ called json.loads on the client-supplied
params string without guarding it. A malformed params value in
POST /api/v1/chart raised a raw JSONDecodeError out of __init__ --
before run()'s @transaction or validate() ran -- which the api.py
create() handler does not catch, surfacing as an opaque 500.

Wrap the parse and raise ChartInvalidError(exceptions=[...]) instead,
following the existing *ValidationError idiom, so the existing
except ChartInvalidError branch returns a 422. Adds a unit test
covering both the invalid-JSON and valid-JSON (happy path) cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 16:40:26 +00:00
382 changed files with 46552 additions and 67051 deletions
-30
View File
@@ -84,18 +84,6 @@ updates:
- "@swc/core"
- "@swc/plugin-emotion"
- "@swc/plugin-transform-imports"
jsonforms:
patterns:
- "@jsonforms/*"
visx:
patterns:
- "@visx/*"
emotion:
patterns:
- "@emotion/*"
fontsource:
patterns:
- "@fontsource/*"
open-pull-requests-limit: 30
versioning-strategy: increase
cooldown:
@@ -192,21 +180,3 @@ updates:
versioning-strategy: increase
cooldown:
default-days: 7
- package-ecosystem: "npm"
directory: "/superset-embedded-sdk/"
schedule:
interval: "daily"
labels:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
babel:
patterns:
- "@babel/*"
versioning-strategy: increase
cooldown:
default-days: 7
@@ -1,71 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: Check OpenAPI spec drift
on:
push:
branches:
- "master"
- "[0-9].[0-9]*"
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
# No `paths:` filter on purpose, matching enforce-single-migration-head: a
# required check that never runs for a given PR blocks that PR forever. The
# job is ~10s, so it fires on every PR rather than guessing which file edits
# can move the spec.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check-openapi-spec-drift:
runs-on: ubuntu-26.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: ./.github/actions/setup-backend/
with:
# base.txt pins apispec, which decides the generated output: 6.10.0
# renders marshmallow 4's unknown=RAISE as "additionalProperties":
# false while the pinned 6.6.1 does not. Regenerating off-pin
# produces a spec no CI run can reproduce.
requirements-type: base
- name: Regenerate the spec
env:
# No SUPERSET_CONFIG_PATH: the published spec documents the routes a
# default deployment registers. A config enabling feature flags adds
# paths that would 404 for everyone who has not enabled them.
SUPERSET__SQLALCHEMY_DATABASE_URI: "sqlite:///:memory:"
FLASK_APP: "superset.app:create_app()"
run: superset update-api-docs
- name: Assert the published spec is up to date
run: |
if ! git diff --quiet -- docs/static/resources/openapi.json; then
echo "::error::docs/static/resources/openapi.json is stale."
echo "Regenerate it on the pinned requirements, with no config file:"
echo " SUPERSET__SQLALCHEMY_DATABASE_URI='sqlite:///:memory:' \\"
echo " FLASK_APP='superset.app:create_app()' superset update-api-docs"
git diff --stat -- docs/static/resources/openapi.json
exit 1
fi
+2 -2
View File
@@ -67,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -78,6 +78,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -266,7 +266,7 @@ jobs:
actions-timeline:
needs: [docker-build, docker-compose-image-tag]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: "temurin"
java-version: "11"
@@ -19,7 +19,7 @@ concurrency:
jobs:
validate-all-ghas:
runs-on: ubuntu-26.04 # Don't switch to ubuntu-slim as zizmore-action requires Docker
runs-on: ubuntu-26.04
permissions:
contents: read
# Required for the zizmor action to upload its SARIF results to
@@ -34,7 +34,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
node-version: "20"
- name: Install Dependencies
# Versions are pinned to avoid ad-hoc, unpinned package installs.
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
cancel-on-hold:
name: Cancel CI runs when hold label applied
if: github.event.action == 'labeled' && startsWith(github.event.label.name, 'hold')
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: write
pull-requests: read
@@ -69,7 +69,7 @@ jobs:
rerun-on-unhold:
name: Re-run CI when hold label removed
if: github.event.action == 'unlabeled' && startsWith(github.event.label.name, 'hold')
runs-on: ubuntu-slim
runs-on: ubuntu-24.04
permissions:
actions: write
pull-requests: read
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: "temurin"
java-version: "11"
+1 -1
View File
@@ -202,7 +202,7 @@ jobs:
actions-timeline:
needs: pre-commit
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
with:
+1 -1
View File
@@ -118,7 +118,7 @@ jobs:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: "zulu"
java-version: "21"
+1 -1
View File
@@ -354,7 +354,7 @@ jobs:
actions-timeline:
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
@@ -74,7 +74,7 @@ jobs:
actions-timeline:
needs: test-superset-extensions-cli-package
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
with:
+2 -3
View File
@@ -97,7 +97,6 @@ jobs:
mkdir -p ${{ github.workspace }}/superset-frontend/coverage
docker run \
-v ${{ github.workspace }}/superset-frontend/coverage:/app/superset-frontend/coverage \
-e CI=true \
--rm $TAG \
bash -c \
"npm run test -- --coverage --shard=${{ matrix.shard }}/8 --coverageReporters=json"
@@ -111,7 +110,7 @@ jobs:
report-coverage:
needs: [sharded-jest-tests]
if: needs.frontend-build.outputs.should-run == 'true'
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
timeout-minutes: 15
permissions:
id-token: write
@@ -206,7 +205,7 @@ jobs:
actions-timeline:
needs: [report-coverage, lint-frontend, validate-frontend, test-storybook]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
+1 -1
View File
@@ -188,7 +188,7 @@ jobs:
actions-timeline:
needs: playwright-tests-experimental
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
@@ -257,7 +257,7 @@ jobs:
actions-timeline:
needs: [test-mysql, test-postgres, test-sqlite, test-postgres-required]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
@@ -165,7 +165,7 @@ jobs:
actions-timeline:
needs: [test-postgres-presto, test-postgres-hive]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
@@ -125,7 +125,7 @@ jobs:
unit-tests-required:
needs: [changes, unit-tests]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
@@ -20,7 +20,7 @@ permissions:
jobs:
post-comment:
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
# Only act when the Translations workflow failed (which means a regression
# was detected — the workflow exits 1 on regression).
if: github.event.workflow_run.conclusion == 'failure'
+1 -1
View File
@@ -160,7 +160,7 @@ jobs:
actions-timeline:
needs: [frontend-check-translations, babel-extract]
if: always()
runs-on: ubuntu-slim
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
+3 -3
View File
@@ -35,10 +35,10 @@ jobs:
- name: Install dependencies
working-directory: ./superset-websocket
run: npm ci
- name: lint
- name: eslint
working-directory: ./superset-websocket
run: npm run lint-check
- name: typescript check
run: npm run eslint -- . --quiet
- name: typescript checks
working-directory: ./superset-websocket
run: npm run type
- name: code format check
+3 -3
View File
@@ -88,9 +88,9 @@ repos:
language: system
pass_filenames: true
files: ^superset-frontend/.*\.(js|jsx|ts|tsx)$
- id: oxlint-docs
name: oxlint (docs)
entry: bash -c 'cd docs && FILES=$(printf "%s\n" "$@" | sed "s|^docs/||" | tr "\n" " ") && yarn lint --fix --quiet $FILES'
- id: eslint-docs
name: eslint (docs)
entry: bash -c 'cd docs && FILES=$(printf "%s\n" "$@" | sed "s|^docs/||" | tr "\n" " ") && yarn eslint --fix --quiet $FILES'
language: system
pass_filenames: true
files: ^docs/.*\.(js|jsx|ts|tsx)$
-5
View File
@@ -441,11 +441,6 @@ categories:
url: https://bestpair.info/
contributors: ["@stevensuting"]
- name: Veremes
url: https://www.veremes.com/
logo: veremes.svg
contributors: ["@verdier"]
- name: Virtuoso QA
url: https://www.virtuosoqa.com
+2 -47
View File
@@ -24,52 +24,9 @@ assists people when migrating to a new version.
## Next
- **[BREAKING] `SemanticLayer` and `SemanticView` are now classified in the
Flask-AppBuilder role sets**, so `sync_role_definitions` (run on
`superset init` and on startup) stops granting the built-in **Gamma** role
write access to them. `SemanticLayer` is treated like `Database`
(`READ_ONLY_MODEL_VIEWS`): create/edit/delete become **admin-only**, while
read stays broadly available (its configuration is returned masked).
`SemanticView` is treated like `Dataset` (`GAMMA_READ_ONLY_MODEL_VIEWS`):
writes are Alpha-tier, reads Gamma-tier. Its custom read endpoints
(`views`, `connections`) are mapped to `can_read` so they remain
accessible under the read-only classification. A deployment relying on
Gamma users creating or editing semantic layers/views must grant those
permissions through a custom role. A migration retires the now-unused
`can_views` / `can_connections` permissions left on the `SemanticLayer`
view menu by earlier builds. Two upgrade-time notes on that migration:
it seeds the `SemanticLayer` view menu and its `can_read` PVM if absent, so
even a fresh or flag-off install gains that permission (harmless — the
endpoints 404 while `SEMANTIC_LAYERS` is off); and retiring the stale
permissions remaps any role that held them onto `can_read`, a small
widening — a custom role granted only `can_views` or `can_connections` gains
`can_read` (the semantic-layer list and its masked-configuration detail),
which it could not previously reach. Operators who hand-rolled semantic-layer
roles should re-audit them after upgrading. The feature remains gated behind
the default-off `SEMANTIC_LAYERS` flag.
### Archived dataset purge requires impact confirmation
`GET /api/v1/dataset/<uuid>/purge-impact` returns the charts and distinct
dashboards affected by permanently deleting an archived dataset, together with
an opaque `impact_token`. The dataset purge endpoint now requires that token in
the JSON body as `confirmed_impact_token`. API clients that call
`POST /api/v1/dataset/<uuid>/purge` must fetch and display the impact first;
requests with a missing or malformed token are rejected with 400.
The server rechecks the dependency identities immediately before mutation. If
they changed, purge performs no deletion and returns 409 with a refreshed impact
payload. Clients must display the new impact and obtain renewed confirmation
before retrying. Preview or recheck failures fail closed rather than treating
unknown impact as zero. Chart and dashboard purge endpoints are unchanged.
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed should `pip uninstall cockroachdb && pip install sqlalchemy-cockroachdb` (or simply reinstall the extra) to restore CockroachDB connectivity.
### Native Value filter "Select all" always targets the whole column
The native "Value" filter's bulk "Select all" / "Clear" controls now operate on the entire loaded set of column values regardless of any text typed into the filter's search box. Previously the "Select all (N)" count briefly flickered to the search-scoped count before settling on the full-column count, and clicking "Select all" while searching could select only the currently matching subset. Search-scoped bulk selection was never a supported feature; the count is now stable and always matches what "Select all" selects (the full column). No configuration change is required.
### MCP tool results preserve stored string values
Structured MCP tool results no longer add `<UNTRUSTED-CONTENT>` wrappers or
@@ -273,10 +230,8 @@ Behavior changes to be aware of:
fail fast at the first phase check rather than erroring at setup.
- Dashboard reports whose charts have not mounted are no longer captured
blank: readiness is polled until the deadline, and the report fails loudly
if charts never mount. Large tiled reports also retry Chromium screenshot
stalls and suspicious uniform tiles, while persistent screenshot timeouts
fail loudly. Large tiled thumbnails use the same bounded retries, but retain
their previous failure contract after a persistent timeout.
if charts never mount. Thumbnails and non-report screenshots keep their
previous behavior.
### Embedded (guest token) API responses no longer echo database errors
+3 -3
View File
@@ -62,8 +62,8 @@ yarn version:remove:developer_docs <version> # Remove developer docs version
yarn version:remove:components <version> # Remove components version
# Quality Checks
yarn typecheck # TypeScript validation
yarn lint # Lint TypeScript/JavaScript files
yarn typecheck # TypeScript validation
yarn eslint # Lint TypeScript/JavaScript files
```
## 📁 Documentation Structure
@@ -431,7 +431,7 @@ yarn build
yarn typecheck
# Linting issues
yarn lint
yarn eslint
```
### Version Issues
@@ -566,55 +566,6 @@ def FLASK_APP_MUTATOR(app: Flask) -> None:
app.before_request_funcs.setdefault(None, []).append(make_session_permanent)
```
## Carrying extra data through chart and dashboard exports
Deployments often attach their own metadata to charts and dashboards — an owning
team, a catalogue entry, a cost centre — and need it to survive an export/import
round trip between environments. `EXTRA_ASSET_EXPORT_FIELDS` and
`EXTRA_ASSET_IMPORT_HANDLER` let you do that without forking the export commands.
The export hook receives the model and the asset type (`"chart"` or `"dashboard"`)
and returns a mapping, which is serialised under the `extra` key of the asset's
YAML. The import hook receives the model, the asset type and that same mapping,
once the asset exists and has an id:
```python
# superset_config.py
def _export_fields(model, asset_type):
return {"owning_team": lookup_team(model)}
def _import_handler(model, asset_type, extra):
if team := extra.get("owning_team"):
assign_team(model, team)
EXTRA_ASSET_EXPORT_FIELDS = _export_fields
EXTRA_ASSET_IMPORT_HANDLER = _import_handler
```
The exported YAML then carries:
```yaml
slice_name: Revenue by region
...
extra:
owning_team: analytics-platform
```
A few things worth knowing:
- **Both hooks are optional and default to `None`.** With neither configured,
exported files are byte-for-byte what they were before, and imports behave
identically.
- **Everything lives under the single `extra` key.** The import schemas reject
unknown top-level fields, so namespacing under `extra` keeps that strictness
while leaving you free to change the shape of your own payload later.
- **An export hook returning `None` or an empty mapping writes nothing**, so
assets without your metadata do not gain an empty `extra` block.
- **The import handler runs after the asset is created or updated**, which means
you can rely on `model.id`. Raising from it will fail the import.
## Customizing the landing page (index view)
The page served at `/` is rendered by an index view. By default Superset registers
@@ -253,58 +253,6 @@ def my_custom_auth_factory(app):
MCP_AUTH_FACTORY = my_custom_auth_factory
```
### Embedded Guest Authentication
Superset's [embedded dashboards](/user-docs/using-superset/embedding) feature mints short-lived **guest tokens** for anonymous/embedded viewers. The MCP server can accept these same guest tokens, so an embedded guest (e.g. an in-app chatbot next to an embedded dashboard) can call MCP tools scoped to the dashboards/resources named in its token.
This is opt-in and reuses the existing core guest-token configuration -- there is no MCP-specific guest secret or audience.
```python
# superset_config.py
FEATURE_FLAGS = {"EMBEDDED_SUPERSET": True} # required -- guest tokens only exist when this is on
MCP_EMBEDDED_GUEST_AUTH_ENABLED = True # opt-in for the MCP transport (default False)
```
Present the guest token the same way as any other bearer token:
```bash
curl -X POST http://localhost:5008/mcp \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_GUEST_TOKEN' \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
```
**How it works**
- A dedicated guest-token verifier validates the token against the same `GUEST_TOKEN_JWT_SECRET` / `GUEST_TOKEN_JWT_ALGO` / `GUEST_TOKEN_JWT_AUDIENCE` config used by embedded dashboards, replays the embedded structural checks, and enforces revocation (global version bumps and per-dashboard `guest_token_revoked_before` cutoffs). It runs *before* the JWT verifier described above, since guest tokens are signed with a different key/algorithm and would otherwise be rejected at the transport.
- A verified guest resolves to a Superset guest user as the highest-priority identity, so it's never downgraded to API-key / `MCP_DEV_USERNAME` / dev-mode resolution. Data access is scoped by the same checks (dataset allowlist, dashboard access, row-level security) that apply to embedded dashboard views.
- Guests are restricted to a default-deny allow-list, `MCP_GUEST_ALLOWED_TOOLS`, regardless of `MCP_RBAC_ENABLED`. Sensitive enumeration tools like `find_users` and `get_instance_info` are denied simply by being absent from the default list.
- Setting `MCP_AUTH_FACTORY` bypasses this whole path: a configured factory is tried first, and the default factory that wires up the guest-token verifier is never reached. If you rely on a custom auth factory (e.g. your own OIDC provider) alongside guest auth, that factory must verify guest tokens itself -- otherwise they're rejected regardless of `MCP_EMBEDDED_GUEST_AUTH_ENABLED`.
```python
# superset_config.py
MCP_GUEST_ALLOWED_TOOLS = {
"get_dashboard_info",
"get_dashboard_layout",
"list_dashboards",
"list_charts",
"get_chart_info",
"get_chart_data",
"get_chart_preview",
} # default
```
**Deployment requirements**
- The MCP server and the service that mints guest tokens (the Superset web app) must share `GUEST_TOKEN_JWT_SECRET` and `GUEST_TOKEN_JWT_AUDIENCE`. Set `GUEST_TOKEN_JWT_AUDIENCE` explicitly -- if it's unset, audience validation falls back to the URL host, which can differ between the two services and cause every guest token to fail validation.
- The `GUEST_ROLE_NAME` role (default `Public`) must exist -- a guest token is rejected if it does not.
- Don't set `MCP_DEV_USERNAME` on a deployment that also serves embedded guests.
- Restart the MCP process after toggling `EMBEDDED_SUPERSET` or `MCP_EMBEDDED_GUEST_AUTH_ENABLED` -- guest auth is wired up once at startup.
:::warning
`GUEST_TOKEN_JWT_SECRET` guards both the web embedding and MCP guest-auth surfaces. With `MCP_EMBEDDED_GUEST_AUTH_ENABLED` on, leaving it at its insecure default isn't just a forgery risk -- the MCP server refuses to start (`MCPAuthConfigError`) until you set a real secret shared with the guest-token minting service.
:::
---
## Connecting AI Clients
@@ -575,8 +523,6 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
| `MCP_JWT_DEBUG_ERRORS` | `False` | Log detailed JWT errors server-side (never exposed in HTTP responses per RFC 6750) |
| `MCP_AUTH_FACTORY` | `None` | Custom auth provider factory `(flask_app) -> auth_provider`. Takes precedence over built-in JWT |
| `MCP_USER_RESOLVER` | `None` | Custom function `(app, access_token) -> username` to extract a Superset username from a validated JWT token. When `None`, the default resolver checks `preferred_username`, `username`, `email`, and `sub` claims in that order. |
| `MCP_EMBEDDED_GUEST_AUTH_ENABLED` | `False` | Accept embedded [guest tokens](#embedded-guest-authentication) as Bearer auth. Also requires the `EMBEDDED_SUPERSET` feature flag. |
| `MCP_GUEST_ALLOWED_TOOLS` | see [default list](#embedded-guest-authentication) | The only tool names callable by embedded guests (default-deny), regardless of `MCP_RBAC_ENABLED`. |
### Response Size Guard
-33
View File
@@ -240,39 +240,6 @@ Font URLs are validated against a configurable allowlist. By default, fonts from
This feature works with the stock Docker image - no custom build required!
## Results Grid Configuration Overrides
Superset exposes a handful of opt-in tokens that customize the appearance of
the results grid in SQL Lab. These tokens have no effect unless explicitly
set, since the results grid otherwise falls back to its built-in defaults.
```python
THEME_DEFAULT = {
"token": {
"colorPrimary": "#2893B3",
# ... other Ant Design tokens
# Results grid overrides
"resultsGridRowHeight": 32,
"resultsGridHeaderFontSize": 13,
"resultsGridHeaderFontWeight": 600,
"resultsGridBorderRadius": 4,
"resultsGridNoStriping": True,
}
}
```
| Token | Type | Description |
| --- | --- | --- |
| `resultsGridRowHeight` | `number` | Row and header height, in pixels. |
| `resultsGridHeaderFontSize` | `number` | Header cell font size, in pixels. |
| `resultsGridHeaderFontWeight` | `number` | Header cell font weight. |
| `resultsGridBorderRadius` | `number` | Border radius applied to the grid and its wrapper, in pixels. |
| `resultsGridNoStriping` | `boolean` | When `true`, disables alternating row background striping. |
These tokens can also be set through the theme CRUD interface's JSON editor,
alongside any other Superset-specific tokens.
## ECharts Configuration Overrides
:::note
@@ -114,8 +114,8 @@ function MyExtension() {
## Source Links
- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/components/Alert/Alert.stories.tsx)
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/components/Alert/index.tsx)
- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.stories.tsx)
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx)
---
@@ -47,8 +47,8 @@ export function MyExtensionPanel() {
Components in `@apache-superset/core/components` are automatically documented here. To add a new extension component:
1. Add the component to `superset-frontend/packages/superset-core/src/components/`
2. Export it from `superset-frontend/packages/superset-core/src/components/index.ts`
1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/`
2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts`
3. Create a Storybook story with an `Interactive` export:
```tsx
@@ -49,10 +49,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------- | ------------------------------- |
| `GET` | [Get the CSRF token](/developer-docs/6.1.0/api/get-the-csrf-token) | `/api/v1/security/csrf_token/` |
| `POST` | [Get a guest token](/developer-docs/6.1.0/api/get-a-guest-token) | `/api/v1/security/guest_token/` |
| `POST` | [Create security login](/developer-docs/6.1.0/api/create-security-login) | `/api/v1/security/login` |
| `POST` | [Create security refresh](/developer-docs/6.1.0/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` |
---
@@ -65,34 +65,34 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `DELETE` | [Bulk delete dashboards](/developer-docs/6.1.0/api/bulk-delete-dashboards) | `/api/v1/dashboard/` |
| `GET` | [Get a list of dashboards](/developer-docs/6.1.0/api/get-a-list-of-dashboards) | `/api/v1/dashboard/` |
| `POST` | [Create a new dashboard](/developer-docs/6.1.0/api/create-a-new-dashboard) | `/api/v1/dashboard/` |
| `GET` | [Get metadata information about this API resource (dashboard--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-dashboard-info) | `/api/v1/dashboard/_info` |
| `GET` | [Get a dashboard detail information](/developer-docs/6.1.0/api/get-a-dashboard-detail-information) | `/api/v1/dashboard/{id_or_slug}` |
| `GET` | [Get a dashboard's chart definitions.](/developer-docs/6.1.0/api/get-a-dashboards-chart-definitions) | `/api/v1/dashboard/{id_or_slug}/charts` |
| `POST` | [Create a copy of an existing dashboard](/developer-docs/6.1.0/api/create-a-copy-of-an-existing-dashboard) | `/api/v1/dashboard/{id_or_slug}/copy/` |
| `GET` | [Get dashboard's datasets](/developer-docs/6.1.0/api/get-dashboards-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` |
| `DELETE` | [Delete a dashboard's embedded configuration](/developer-docs/6.1.0/api/delete-a-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
| `GET` | [Get the dashboard's embedded configuration](/developer-docs/6.1.0/api/get-the-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
| `POST` | [Set a dashboard's embedded configuration](/developer-docs/6.1.0/api/set-a-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
| `PUT` | [Update dashboard by id_or_slug embedded](/developer-docs/6.1.0/api/update-dashboard-by-id-or-slug-embedded) | `/api/v1/dashboard/{id_or_slug}/embedded` |
| `GET` | [Get dashboard's tabs](/developer-docs/6.1.0/api/get-dashboards-tabs) | `/api/v1/dashboard/{id_or_slug}/tabs` |
| `DELETE` | [Delete a dashboard](/developer-docs/6.1.0/api/delete-a-dashboard) | `/api/v1/dashboard/{pk}` |
| `PUT` | [Update a dashboard](/developer-docs/6.1.0/api/update-a-dashboard) | `/api/v1/dashboard/{pk}` |
| `POST` | [Compute and cache a screenshot (dashboard-pk-cache-dashboard-screenshot)](/developer-docs/6.1.0/api/compute-and-cache-a-screenshot-dashboard-pk-cache-dashboard-screenshot) | `/api/v1/dashboard/{pk}/cache_dashboard_screenshot/` |
| `PUT` | [Update chart customizations configuration for a dashboard.](/developer-docs/6.1.0/api/update-chart-customizations-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/chart_customizations` |
| `PUT` | [Update colors configuration for a dashboard.](/developer-docs/6.1.0/api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` |
| `GET` | [Export dashboard as example bundle](/developer-docs/6.1.0/api/export-dashboard-as-example-bundle) | `/api/v1/dashboard/{pk}/export_as_example/` |
| `DELETE` | [Remove the dashboard from the user favorite list](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/api/get-a-computed-screenshot-from-cache-dashboard-pk-screenshot-digest) | `/api/v1/dashboard/{pk}/screenshot/{digest}/` |
| `GET` | [Get dashboard's thumbnail](/developer-docs/6.1.0/api/get-dashboards-thumbnail) | `/api/v1/dashboard/{pk}/thumbnail/{digest}/` |
| `GET` | [Download multiple dashboards as YAML files](/developer-docs/6.1.0/api/download-multiple-dashboards-as-yaml-files) | `/api/v1/dashboard/export/` |
| `GET` | [Check favorited dashboards for current user](/developer-docs/6.1.0/api/check-favorited-dashboards-for-current-user) | `/api/v1/dashboard/favorite_status/` |
| `POST` | [Import dashboard(s) with associated charts/datasets/databases](/developer-docs/6.1.0/api/import-dashboard-s-with-associated-charts-datasets-databases) | `/api/v1/dashboard/import/` |
| `GET` | [Get related fields data (dashboard-related-column-name)](/developer-docs/6.1.0/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-dashboards-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-dashboards-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` |
| `DELETE` | [Delete a dashboard's embedded configuration](/developer-docs/api/delete-a-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
| `GET` | [Get the dashboard's embedded configuration](/developer-docs/api/get-the-dashboards-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
| `POST` | [Set a dashboard's embedded configuration](/developer-docs/api/set-a-dashboards-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-dashboards-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 chart customizations configuration for a dashboard.](/developer-docs/api/update-chart-customizations-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/chart_customizations` |
| `PUT` | [Update colors configuration for a dashboard.](/developer-docs/api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` |
| `GET` | [Export dashboard as example bundle](/developer-docs/api/export-dashboard-as-example-bundle) | `/api/v1/dashboard/{pk}/export_as_example/` |
| `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-dashboards-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}` |
</details>
@@ -101,26 +101,26 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `DELETE` | [Bulk delete charts](/developer-docs/6.1.0/api/bulk-delete-charts) | `/api/v1/chart/` |
| `GET` | [Get a list of charts](/developer-docs/6.1.0/api/get-a-list-of-charts) | `/api/v1/chart/` |
| `POST` | [Create a new chart](/developer-docs/6.1.0/api/create-a-new-chart) | `/api/v1/chart/` |
| `GET` | [Get metadata information about this API resource (chart--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-chart-info) | `/api/v1/chart/_info` |
| `GET` | [Get a chart detail information](/developer-docs/6.1.0/api/get-a-chart-detail-information) | `/api/v1/chart/{id_or_uuid}` |
| `DELETE` | [Delete a chart](/developer-docs/6.1.0/api/delete-a-chart) | `/api/v1/chart/{pk}` |
| `PUT` | [Update a chart](/developer-docs/6.1.0/api/update-a-chart) | `/api/v1/chart/{pk}` |
| `GET` | [Compute and cache a screenshot (chart-pk-cache-screenshot)](/developer-docs/6.1.0/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/6.1.0/api/return-payload-data-response-for-a-chart) | `/api/v1/chart/{pk}/data/` |
| `DELETE` | [Remove the chart from the user favorite list](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/get-a-computed-screenshot-from-cache-chart-pk-screenshot-digest) | `/api/v1/chart/{pk}/screenshot/{digest}/` |
| `GET` | [Get chart thumbnail](/developer-docs/6.1.0/api/get-chart-thumbnail) | `/api/v1/chart/{pk}/thumbnail/{digest}/` |
| `POST` | [Return payload data response for the given query (chart-data)](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/download-multiple-charts-as-yaml-files) | `/api/v1/chart/export/` |
| `GET` | [Check favorited charts for current user](/developer-docs/6.1.0/api/check-favorited-charts-for-current-user) | `/api/v1/chart/favorite_status/` |
| `POST` | [Import chart(s) with associated datasets and databases](/developer-docs/6.1.0/api/import-chart-s-with-associated-datasets-and-databases) | `/api/v1/chart/import/` |
| `GET` | [Get related fields data (chart-related-column-name)](/developer-docs/6.1.0/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/6.1.0/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` |
| `GET` | [Get a chart detail information](/developer-docs/api/get-a-chart-detail-information) | `/api/v1/chart/{id_or_uuid}` |
| `DELETE` | [Delete a chart](/developer-docs/api/delete-a-chart) | `/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` |
</details>
@@ -129,25 +129,25 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `DELETE` | [Bulk delete datasets](/developer-docs/6.1.0/api/bulk-delete-datasets) | `/api/v1/dataset/` |
| `GET` | [Get a list of datasets](/developer-docs/6.1.0/api/get-a-list-of-datasets) | `/api/v1/dataset/` |
| `POST` | [Create a new dataset](/developer-docs/6.1.0/api/create-a-new-dataset) | `/api/v1/dataset/` |
| `GET` | [Get metadata information about this API resource (dataset--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-dataset-info) | `/api/v1/dataset/_info` |
| `GET` | [Get a dataset](/developer-docs/6.1.0/api/get-a-dataset) | `/api/v1/dataset/{id_or_uuid}` |
| `GET` | [Get charts and dashboards count associated to a dataset](/developer-docs/6.1.0/api/get-charts-and-dashboards-count-associated-to-a-dataset) | `/api/v1/dataset/{id_or_uuid}/related_objects` |
| `DELETE` | [Delete a dataset](/developer-docs/6.1.0/api/delete-a-dataset) | `/api/v1/dataset/{pk}` |
| `PUT` | [Update a dataset](/developer-docs/6.1.0/api/update-a-dataset) | `/api/v1/dataset/{pk}` |
| `DELETE` | [Delete a dataset column](/developer-docs/6.1.0/api/delete-a-dataset-column) | `/api/v1/dataset/{pk}/column/{column_id}` |
| `GET` | [Get dataset drill info](/developer-docs/6.1.0/api/get-dataset-drill-info) | `/api/v1/dataset/{pk}/drill_info/` |
| `DELETE` | [Delete a dataset metric](/developer-docs/6.1.0/api/delete-a-dataset-metric) | `/api/v1/dataset/{pk}/metric/{metric_id}` |
| `PUT` | [Refresh and update columns of a dataset](/developer-docs/6.1.0/api/refresh-and-update-columns-of-a-dataset) | `/api/v1/dataset/{pk}/refresh` |
| `GET` | [Get distinct values from field data (dataset-distinct-column-name)](/developer-docs/6.1.0/api/get-distinct-values-from-field-data-dataset-distinct-column-name) | `/api/v1/dataset/distinct/{column_name}` |
| `POST` | [Duplicate a dataset](/developer-docs/6.1.0/api/duplicate-a-dataset) | `/api/v1/dataset/duplicate` |
| `GET` | [Download multiple datasets as YAML files](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/import-dataset-s-with-associated-databases) | `/api/v1/dataset/import/` |
| `GET` | [Get related fields data (dataset-related-column-name)](/developer-docs/6.1.0/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/6.1.0/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` |
| `GET` | [Get a dataset](/developer-docs/api/get-a-dataset) | `/api/v1/dataset/{id_or_uuid}` |
| `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/{id_or_uuid}/related_objects` |
| `DELETE` | [Delete a dataset](/developer-docs/api/delete-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}` |
| `GET` | [Get dataset drill info](/developer-docs/api/get-dataset-drill-info) | `/api/v1/dataset/{pk}/drill_info/` |
| `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 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` |
</details>
@@ -156,36 +156,36 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `GET` | [Get a list of databases](/developer-docs/6.1.0/api/get-a-list-of-databases) | `/api/v1/database/` |
| `POST` | [Create a new database](/developer-docs/6.1.0/api/create-a-new-database) | `/api/v1/database/` |
| `GET` | [Get metadata information about this API resource (database--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-database-info) | `/api/v1/database/_info` |
| `DELETE` | [Delete a database](/developer-docs/6.1.0/api/delete-a-database) | `/api/v1/database/{pk}` |
| `GET` | [Get a database](/developer-docs/6.1.0/api/get-a-database) | `/api/v1/database/{pk}` |
| `PUT` | [Change a database](/developer-docs/6.1.0/api/change-a-database) | `/api/v1/database/{pk}` |
| `GET` | [Get all catalogs from a database](/developer-docs/6.1.0/api/get-all-catalogs-from-a-database) | `/api/v1/database/{pk}/catalogs/` |
| `GET` | [Get a database connection info](/developer-docs/6.1.0/api/get-a-database-connection-info) | `/api/v1/database/{pk}/connection` |
| `GET` | [Get function names supported by a database](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/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}/` |
| `POST` | [Re-sync all permissions for a database connection](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/get-table-metadata) | `/api/v1/database/{pk}/table_metadata/` |
| `GET` | [Get table extra metadata (database-pk-table-metadata-extra)](/developer-docs/6.1.0/api/get-table-extra-metadata-database-pk-table-metadata-extra) | `/api/v1/database/{pk}/table_metadata/extra/` |
| `GET` | [Get database table metadata](/developer-docs/6.1.0/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/6.1.0/api/get-a-list-of-tables-for-given-database) | `/api/v1/database/{pk}/tables/` |
| `POST` | [Upload a file to a database table](/developer-docs/6.1.0/api/upload-a-file-to-a-database-table) | `/api/v1/database/{pk}/upload/` |
| `POST` | [Validate arbitrary SQL](/developer-docs/6.1.0/api/validate-arbitrary-sql) | `/api/v1/database/{pk}/validate_sql/` |
| `GET` | [Get names of databases currently available](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/import-database-s-with-associated-datasets) | `/api/v1/database/import/` |
| `GET` | [Receive personal access tokens from OAuth2](/developer-docs/6.1.0/api/receive-personal-access-tokens-from-o-auth-2) | `/api/v1/database/oauth2/` |
| `GET` | [Get related fields data (database-related-column-name)](/developer-docs/6.1.0/api/get-related-fields-data-database-related-column-name) | `/api/v1/database/related/{column_name}` |
| `POST` | [Test a database connection](/developer-docs/6.1.0/api/test-a-database-connection) | `/api/v1/database/test_connection/` |
| `POST` | [Upload a file and returns file metadata](/developer-docs/6.1.0/api/upload-a-file-and-returns-file-metadata) | `/api/v1/database/upload_metadata/` |
| `POST` | [Validate database connection parameters](/developer-docs/6.1.0/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}/` |
| `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-o-auth-2) | `/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/` |
</details>
@@ -196,7 +196,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
| `GET` | [Assemble Explore related information in a single endpoint](/developer-docs/6.1.0/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/` |
</details>
@@ -205,13 +205,13 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `GET` | [Get the bootstrap data for SqlLab page](/developer-docs/6.1.0/api/get-the-bootstrap-data-for-sql-lab-page) | `/api/v1/sqllab/` |
| `POST` | [Estimate the SQL query execution cost](/developer-docs/6.1.0/api/estimate-the-sql-query-execution-cost) | `/api/v1/sqllab/estimate/` |
| `POST` | [Execute a SQL query](/developer-docs/6.1.0/api/execute-a-sql-query) | `/api/v1/sqllab/execute/` |
| `POST` | [Export SQL query results to CSV with streaming](/developer-docs/6.1.0/api/export-sql-query-results-to-csv-with-streaming) | `/api/v1/sqllab/export_streaming/` |
| `GET` | [Export the SQL query results to a CSV](/developer-docs/6.1.0/api/export-the-sql-query-results-to-a-csv) | `/api/v1/sqllab/export/{client_id}/` |
| `POST` | [Format SQL code](/developer-docs/6.1.0/api/format-sql-code) | `/api/v1/sqllab/format_sql/` |
| `GET` | [Get the result of a SQL query execution](/developer-docs/6.1.0/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-sql-lab-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/` |
| `POST` | [Export SQL query results to CSV with streaming](/developer-docs/api/export-sql-query-results-to-csv-with-streaming) | `/api/v1/sqllab/export_streaming/` |
| `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/` |
</details>
@@ -220,23 +220,23 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
| `GET` | [Get a list of queries](/developer-docs/6.1.0/api/get-a-list-of-queries) | `/api/v1/query/` |
| `GET` | [Get query detail information](/developer-docs/6.1.0/api/get-query-detail-information) | `/api/v1/query/{pk}` |
| `GET` | [Get distinct values from field data (query-distinct-column-name)](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/api/get-a-list-of-queries-that-changed-after-last-updated-ms) | `/api/v1/query/updated_since` |
| `DELETE` | [Bulk delete saved queries](/developer-docs/6.1.0/api/bulk-delete-saved-queries) | `/api/v1/saved_query/` |
| `GET` | [Get a list of saved queries](/developer-docs/6.1.0/api/get-a-list-of-saved-queries) | `/api/v1/saved_query/` |
| `POST` | [Create a saved query](/developer-docs/6.1.0/api/create-a-saved-query) | `/api/v1/saved_query/` |
| `GET` | [Get metadata information about this API resource (saved-query--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-saved-query-info) | `/api/v1/saved_query/_info` |
| `DELETE` | [Delete a saved query](/developer-docs/6.1.0/api/delete-a-saved-query) | `/api/v1/saved_query/{pk}` |
| `GET` | [Get a saved query](/developer-docs/6.1.0/api/get-a-saved-query) | `/api/v1/saved_query/{pk}` |
| `PUT` | [Update a saved query](/developer-docs/6.1.0/api/update-a-saved-query) | `/api/v1/saved_query/{pk}` |
| `GET` | [Get distinct values from field data (saved-query-distinct-column-name)](/developer-docs/6.1.0/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/6.1.0/api/download-multiple-saved-queries-as-yaml-files) | `/api/v1/saved_query/export/` |
| `POST` | [Import saved queries with associated databases](/developer-docs/6.1.0/api/import-saved-queries-with-associated-databases) | `/api/v1/saved_query/import/` |
| `GET` | [Get related fields data (saved-query-related-column-name)](/developer-docs/6.1.0/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}` |
</details>
@@ -245,8 +245,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `GET` | [Get possible values for a datasource column](/developer-docs/6.1.0/api/get-possible-values-for-a-datasource-column) | `/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/` |
| `POST` | [Validate a SQL expression against a datasource](/developer-docs/6.1.0/api/validate-a-sql-expression-against-a-datasource) | `/api/v1/datasource/{datasource_type}/{datasource_id}/validate_expression/` |
| `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/` |
| `POST` | [Validate a SQL expression against a datasource](/developer-docs/api/validate-a-sql-expression-against-a-datasource) | `/api/v1/datasource/{datasource_type}/{datasource_id}/validate_expression/` |
</details>
@@ -255,8 +255,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `GET` | [Return an AdvancedDataTypeResponse](/developer-docs/6.1.0/api/return-an-advanced-data-type-response) | `/api/v1/advanced_data_type/convert` |
| `GET` | [Return a list of available advanced data types](/developer-docs/6.1.0/api/return-a-list-of-available-advanced-data-types) | `/api/v1/advanced_data_type/types` |
| `GET` | [Return an AdvancedDataTypeResponse](/developer-docs/api/return-an-advanced-data-type-response) | `/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` |
</details>
@@ -267,21 +267,21 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| `DELETE` | [Bulk delete tags](/developer-docs/6.1.0/api/bulk-delete-tags) | `/api/v1/tag/` |
| `GET` | [Get a list of tags](/developer-docs/6.1.0/api/get-a-list-of-tags) | `/api/v1/tag/` |
| `POST` | [Create a tag](/developer-docs/6.1.0/api/create-a-tag) | `/api/v1/tag/` |
| `GET` | [Get metadata information about tag API endpoints](/developer-docs/6.1.0/api/get-metadata-information-about-tag-api-endpoints) | `/api/v1/tag/_info` |
| `POST` | [Add tags to an object](/developer-docs/6.1.0/api/add-tags-to-an-object) | `/api/v1/tag/{object_type}/{object_id}/` |
| `DELETE` | [Delete a tagged object](/developer-docs/6.1.0/api/delete-a-tagged-object) | `/api/v1/tag/{object_type}/{object_id}/{tag}/` |
| `DELETE` | [Delete a tag](/developer-docs/6.1.0/api/delete-a-tag) | `/api/v1/tag/{pk}` |
| `GET` | [Get a tag detail information](/developer-docs/6.1.0/api/get-a-tag-detail-information) | `/api/v1/tag/{pk}` |
| `PUT` | [Update a tag](/developer-docs/6.1.0/api/update-a-tag) | `/api/v1/tag/{pk}` |
| `DELETE` | [Delete tag by pk favorites](/developer-docs/6.1.0/api/delete-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
| `POST` | [Create tag by pk favorites](/developer-docs/6.1.0/api/create-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
| `POST` | [Bulk create tags and tagged objects](/developer-docs/6.1.0/api/bulk-create-tags-and-tagged-objects) | `/api/v1/tag/bulk_create` |
| `GET` | [Get tag favorite status](/developer-docs/6.1.0/api/get-tag-favorite-status) | `/api/v1/tag/favorite_status/` |
| `GET` | [Get all objects associated with a tag](/developer-docs/6.1.0/api/get-all-objects-associated-with-a-tag) | `/api/v1/tag/get_objects/` |
| `GET` | [Get related fields data (tag-related-column-name)](/developer-docs/6.1.0/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}` |
</details>
@@ -290,20 +290,20 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `DELETE` | [Delete multiple annotation layers in a bulk operation](/developer-docs/6.1.0/api/delete-multiple-annotation-layers-in-a-bulk-operation) | `/api/v1/annotation_layer/` |
| `GET` | [Get a list of annotation layers (annotation-layer)](/developer-docs/6.1.0/api/get-a-list-of-annotation-layers-annotation-layer) | `/api/v1/annotation_layer/` |
| `POST` | [Create an annotation layer (annotation-layer)](/developer-docs/6.1.0/api/create-an-annotation-layer-annotation-layer) | `/api/v1/annotation_layer/` |
| `GET` | [Get metadata information about this API resource (annotation-layer--info)](/developer-docs/6.1.0/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/6.1.0/api/delete-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
| `GET` | [Get an annotation layer (annotation-layer-pk)](/developer-docs/6.1.0/api/get-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
| `PUT` | [Update an annotation layer (annotation-layer-pk)](/developer-docs/6.1.0/api/update-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
| `DELETE` | [Bulk delete annotation layers](/developer-docs/6.1.0/api/bulk-delete-annotation-layers) | `/api/v1/annotation_layer/{pk}/annotation/` |
| `GET` | [Get a list of annotation layers (annotation-layer-pk-annotation)](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/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/6.1.0/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}` |
</details>
@@ -312,14 +312,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `DELETE` | [Bulk delete CSS templates](/developer-docs/6.1.0/api/bulk-delete-css-templates) | `/api/v1/css_template/` |
| `GET` | [Get a list of CSS templates](/developer-docs/6.1.0/api/get-a-list-of-css-templates) | `/api/v1/css_template/` |
| `POST` | [Create a CSS template](/developer-docs/6.1.0/api/create-a-css-template) | `/api/v1/css_template/` |
| `GET` | [Get metadata information about this API resource (css-template--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-css-template-info) | `/api/v1/css_template/_info` |
| `DELETE` | [Delete a CSS template](/developer-docs/6.1.0/api/delete-a-css-template) | `/api/v1/css_template/{pk}` |
| `GET` | [Get a CSS template](/developer-docs/6.1.0/api/get-a-css-template) | `/api/v1/css_template/{pk}` |
| `PUT` | [Update a CSS template](/developer-docs/6.1.0/api/update-a-css-template) | `/api/v1/css_template/{pk}` |
| `GET` | [Get related fields data (css-template-related-column-name)](/developer-docs/6.1.0/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}` |
</details>
@@ -330,8 +330,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `POST` | [Create a new dashboard's permanent link](/developer-docs/6.1.0/api/create-a-new-dashboards-permanent-link) | `/api/v1/dashboard/{pk}/permalink` |
| `GET` | [Get dashboard's permanent link state](/developer-docs/6.1.0/api/get-dashboards-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` |
| `POST` | [Create a new dashboard's permanent link](/developer-docs/api/create-a-new-dashboards-permanent-link) | `/api/v1/dashboard/{pk}/permalink` |
| `GET` | [Get dashboard's permanent link state](/developer-docs/api/get-dashboards-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` |
</details>
@@ -340,8 +340,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| `POST` | [Create a new permanent link (explore-permalink)](/developer-docs/6.1.0/api/create-a-new-permanent-link-explore-permalink) | `/api/v1/explore/permalink` |
| `GET` | [Get chart's permanent link state](/developer-docs/6.1.0/api/get-charts-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-charts-permanent-link-state) | `/api/v1/explore/permalink/{key}` |
</details>
@@ -350,8 +350,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| `POST` | [Create a new permanent link (sqllab-permalink)](/developer-docs/6.1.0/api/create-a-new-permanent-link-sqllab-permalink) | `/api/v1/sqllab/permalink` |
| `GET` | [Get permanent link state for SQLLab editor.](/developer-docs/6.1.0/api/get-permanent-link-state-for-sql-lab-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-sql-lab-editor) | `/api/v1/sqllab/permalink/{key}` |
</details>
@@ -360,7 +360,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `GET` | [Get a report schedule log (embedded-dashboard-uuid)](/developer-docs/6.1.0/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}` |
</details>
@@ -369,10 +369,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `POST` | [Create a dashboard's filter state](/developer-docs/6.1.0/api/create-a-dashboards-filter-state) | `/api/v1/dashboard/{pk}/filter_state` |
| `DELETE` | [Delete a dashboard's filter state value](/developer-docs/6.1.0/api/delete-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
| `GET` | [Get a dashboard's filter state value](/developer-docs/6.1.0/api/get-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
| `PUT` | [Update a dashboard's filter state value](/developer-docs/6.1.0/api/update-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
| `POST` | [Create a dashboard's filter state](/developer-docs/api/create-a-dashboards-filter-state) | `/api/v1/dashboard/{pk}/filter_state` |
| `DELETE` | [Delete a dashboard's filter state value](/developer-docs/api/delete-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
| `GET` | [Get a dashboard's filter state value](/developer-docs/api/get-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
| `PUT` | [Update a dashboard's filter state value](/developer-docs/api/update-a-dashboards-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
</details>
@@ -381,10 +381,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------- | --------------------------------- |
| `POST` | [Create a new form_data](/developer-docs/6.1.0/api/create-a-new-form-data) | `/api/v1/explore/form_data` |
| `DELETE` | [Delete a form_data](/developer-docs/6.1.0/api/delete-a-form-data) | `/api/v1/explore/form_data/{key}` |
| `GET` | [Get a form_data](/developer-docs/6.1.0/api/get-a-form-data) | `/api/v1/explore/form_data/{key}` |
| `PUT` | [Update an existing form_data](/developer-docs/6.1.0/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}` |
</details>
@@ -395,17 +395,17 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `DELETE` | [Bulk delete report schedules](/developer-docs/6.1.0/api/bulk-delete-report-schedules) | `/api/v1/report/` |
| `GET` | [Get a list of report schedules](/developer-docs/6.1.0/api/get-a-list-of-report-schedules) | `/api/v1/report/` |
| `POST` | [Create a report schedule](/developer-docs/6.1.0/api/create-a-report-schedule) | `/api/v1/report/` |
| `GET` | [Get metadata information about this API resource (report--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-report-info) | `/api/v1/report/_info` |
| `DELETE` | [Delete a report schedule](/developer-docs/6.1.0/api/delete-a-report-schedule) | `/api/v1/report/{pk}` |
| `GET` | [Get a report schedule](/developer-docs/6.1.0/api/get-a-report-schedule) | `/api/v1/report/{pk}` |
| `PUT` | [Update a report schedule](/developer-docs/6.1.0/api/update-a-report-schedule) | `/api/v1/report/{pk}` |
| `GET` | [Get a list of report schedule logs](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/get-related-fields-data-report-related-column-name) | `/api/v1/report/related/{column_name}` |
| `GET` | [Get slack channels](/developer-docs/6.1.0/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/` |
</details>
@@ -416,17 +416,17 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `GET` | [Get security roles](/developer-docs/6.1.0/api/get-security-roles) | `/api/v1/security/roles/` |
| `POST` | [Create security roles](/developer-docs/6.1.0/api/create-security-roles) | `/api/v1/security/roles/` |
| `GET` | [Get security roles info](/developer-docs/6.1.0/api/get-security-roles-info) | `/api/v1/security/roles/_info` |
| `DELETE` | [Delete security roles by pk](/developer-docs/6.1.0/api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
| `GET` | [Get security roles by pk](/developer-docs/6.1.0/api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
| `PUT` | [Update security roles by pk](/developer-docs/6.1.0/api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
| `PUT` | [Update security roles by role_id groups](/developer-docs/6.1.0/api/update-security-roles-by-role-id-groups) | `/api/v1/security/roles/{role_id}/groups` |
| `POST` | [Create security roles by role_id permissions](/developer-docs/6.1.0/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/6.1.0/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/6.1.0/api/update-security-roles-by-role-id-users) | `/api/v1/security/roles/{role_id}/users` |
| `GET` | [List roles](/developer-docs/6.1.0/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}` |
| `PUT` | [Update security roles by role_id groups](/developer-docs/api/update-security-roles-by-role-id-groups) | `/api/v1/security/roles/{role_id}/groups` |
| `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/` |
</details>
@@ -435,12 +435,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ------------------------------------------------------------------------------ | ------------------------------ |
| `GET` | [Get security users](/developer-docs/6.1.0/api/get-security-users) | `/api/v1/security/users/` |
| `POST` | [Create security users](/developer-docs/6.1.0/api/create-security-users) | `/api/v1/security/users/` |
| `GET` | [Get security users info](/developer-docs/6.1.0/api/get-security-users-info) | `/api/v1/security/users/_info` |
| `DELETE` | [Delete security users by pk](/developer-docs/6.1.0/api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` |
| `GET` | [Get security users by pk](/developer-docs/6.1.0/api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` |
| `PUT` | [Update security users by pk](/developer-docs/6.1.0/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}` |
</details>
@@ -449,9 +449,9 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ------------------------------------------------------------------------------------ | ------------------------------------ |
| `GET` | [Get security permissions](/developer-docs/6.1.0/api/get-security-permissions) | `/api/v1/security/permissions/` |
| `GET` | [Get security permissions info](/developer-docs/6.1.0/api/get-security-permissions-info) | `/api/v1/security/permissions/_info` |
| `GET` | [Get security permissions by pk](/developer-docs/6.1.0/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}` |
</details>
@@ -460,12 +460,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------------- | ---------------------------------- |
| `GET` | [Get security resources](/developer-docs/6.1.0/api/get-security-resources) | `/api/v1/security/resources/` |
| `POST` | [Create security resources](/developer-docs/6.1.0/api/create-security-resources) | `/api/v1/security/resources/` |
| `GET` | [Get security resources info](/developer-docs/6.1.0/api/get-security-resources-info) | `/api/v1/security/resources/_info` |
| `DELETE` | [Delete security resources by pk](/developer-docs/6.1.0/api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
| `GET` | [Get security resources by pk](/developer-docs/6.1.0/api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
| `PUT` | [Update security resources by pk](/developer-docs/6.1.0/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}` |
</details>
@@ -474,12 +474,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `GET` | [Get security permissions resources](/developer-docs/6.1.0/api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
| `POST` | [Create security permissions resources](/developer-docs/6.1.0/api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
| `GET` | [Get security permissions resources info](/developer-docs/6.1.0/api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` |
| `DELETE` | [Delete security permissions resources by pk](/developer-docs/6.1.0/api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
| `GET` | [Get security permissions resources by pk](/developer-docs/6.1.0/api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
| `PUT` | [Update security permissions resources by pk](/developer-docs/6.1.0/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}` |
</details>
@@ -488,14 +488,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `DELETE` | [Bulk delete RLS rules](/developer-docs/6.1.0/api/bulk-delete-rls-rules) | `/api/v1/rowlevelsecurity/` |
| `GET` | [Get a list of RLS](/developer-docs/6.1.0/api/get-a-list-of-rls) | `/api/v1/rowlevelsecurity/` |
| `POST` | [Create a new RLS rule](/developer-docs/6.1.0/api/create-a-new-rls-rule) | `/api/v1/rowlevelsecurity/` |
| `GET` | [Get metadata information about this API resource (rowlevelsecurity--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-rowlevelsecurity-info) | `/api/v1/rowlevelsecurity/_info` |
| `DELETE` | [Delete an RLS](/developer-docs/6.1.0/api/delete-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
| `GET` | [Get an RLS](/developer-docs/6.1.0/api/get-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
| `PUT` | [Update an RLS rule](/developer-docs/6.1.0/api/update-an-rls-rule) | `/api/v1/rowlevelsecurity/{pk}` |
| `GET` | [Get related fields data (rowlevelsecurity-related-column-name)](/developer-docs/6.1.0/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}` |
</details>
@@ -506,8 +506,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------------- | ------------------------ |
| `GET` | [Export all assets](/developer-docs/6.1.0/api/export-all-assets) | `/api/v1/assets/export/` |
| `POST` | [Import multiple assets](/developer-docs/6.1.0/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/` |
</details>
@@ -516,7 +516,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `POST` | [Invalidate cache records and remove the database records](/developer-docs/6.1.0/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` |
</details>
@@ -525,10 +525,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------------------------------- | ------------------------------ |
| `GET` | [Get a list of logs](/developer-docs/6.1.0/api/get-a-list-of-logs) | `/api/v1/log/` |
| `POST` | [Create log](/developer-docs/6.1.0/api/create-log) | `/api/v1/log/` |
| `GET` | [Get a log detail information](/developer-docs/6.1.0/api/get-a-log-detail-information) | `/api/v1/log/{pk}` |
| `GET` | [Get recent activity data for a user](/developer-docs/6.1.0/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/` |
</details>
@@ -539,9 +539,9 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------- | ------------------- |
| `GET` | [Get the user object](/developer-docs/6.1.0/api/get-the-user-object) | `/api/v1/me/` |
| `PUT` | [Update the current user](/developer-docs/6.1.0/api/update-the-current-user) | `/api/v1/me/` |
| `GET` | [Get the user roles](/developer-docs/6.1.0/api/get-the-user-roles) | `/api/v1/me/roles/` |
| `GET` | [Get the user object](/developer-docs/api/get-the-user-object) | `/api/v1/me/` |
| `PUT` | [Update the current user](/developer-docs/api/update-the-current-user) | `/api/v1/me/` |
| `GET` | [Get the user roles](/developer-docs/api/get-the-user-roles) | `/api/v1/me/roles/` |
</details>
@@ -550,7 +550,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------- | ----------------------------------- |
| `GET` | [Get the user avatar](/developer-docs/6.1.0/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` |
</details>
@@ -559,7 +559,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------- | --------------- |
| `GET` | [Get menu](/developer-docs/6.1.0/api/get-menu) | `/api/v1/menu/` |
| `GET` | [Get menu](/developer-docs/api/get-menu) | `/api/v1/menu/` |
</details>
@@ -568,7 +568,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | -------------------------------------------------------------------------- | ---------------------------- |
| `GET` | [Get all available domains](/developer-docs/6.1.0/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/` |
</details>
@@ -577,7 +577,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------------------------------- | ---------------------- |
| `GET` | [Read off of the Redis events stream](/developer-docs/6.1.0/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/` |
</details>
@@ -586,7 +586,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| ------ | ---------------------------------------------------------------------------- | ------------------------- |
| `GET` | [Get api by version openapi](/developer-docs/6.1.0/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` |
</details>
@@ -597,12 +597,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------- | ------------------------------- |
| `GET` | [Get security groups](/developer-docs/6.1.0/api/get-security-groups) | `/api/v1/security/groups/` |
| `POST` | [Create security groups](/developer-docs/6.1.0/api/create-security-groups) | `/api/v1/security/groups/` |
| `GET` | [Get security groups info](/developer-docs/6.1.0/api/get-security-groups-info) | `/api/v1/security/groups/_info` |
| `DELETE` | [Delete security groups by pk](/developer-docs/6.1.0/api/delete-security-groups-by-pk) | `/api/v1/security/groups/{pk}` |
| `GET` | [Get security groups by pk](/developer-docs/6.1.0/api/get-security-groups-by-pk) | `/api/v1/security/groups/{pk}` |
| `PUT` | [Update security groups by pk](/developer-docs/6.1.0/api/update-security-groups-by-pk) | `/api/v1/security/groups/{pk}` |
| `GET` | [Get security groups](/developer-docs/api/get-security-groups) | `/api/v1/security/groups/` |
| `POST` | [Create security groups](/developer-docs/api/create-security-groups) | `/api/v1/security/groups/` |
| `GET` | [Get security groups info](/developer-docs/api/get-security-groups-info) | `/api/v1/security/groups/_info` |
| `DELETE` | [Delete security groups by pk](/developer-docs/api/delete-security-groups-by-pk) | `/api/v1/security/groups/{pk}` |
| `GET` | [Get security groups by pk](/developer-docs/api/get-security-groups-by-pk) | `/api/v1/security/groups/{pk}` |
| `PUT` | [Update security groups by pk](/developer-docs/api/update-security-groups-by-pk) | `/api/v1/security/groups/{pk}` |
</details>
@@ -611,20 +611,20 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `DELETE` | [Bulk delete themes](/developer-docs/6.1.0/api/bulk-delete-themes) | `/api/v1/theme/` |
| `GET` | [Get a list of themes](/developer-docs/6.1.0/api/get-a-list-of-themes) | `/api/v1/theme/` |
| `POST` | [Create a theme](/developer-docs/6.1.0/api/create-a-theme) | `/api/v1/theme/` |
| `GET` | [Get metadata information about this API resource (theme--info)](/developer-docs/6.1.0/api/get-metadata-information-about-this-api-resource-theme-info) | `/api/v1/theme/_info` |
| `DELETE` | [Delete a theme](/developer-docs/6.1.0/api/delete-a-theme) | `/api/v1/theme/{pk}` |
| `GET` | [Get a theme](/developer-docs/6.1.0/api/get-a-theme) | `/api/v1/theme/{pk}` |
| `PUT` | [Update a theme](/developer-docs/6.1.0/api/update-a-theme) | `/api/v1/theme/{pk}` |
| `PUT` | [Set a theme as the system dark theme](/developer-docs/6.1.0/api/set-a-theme-as-the-system-dark-theme) | `/api/v1/theme/{pk}/set_system_dark` |
| `PUT` | [Set a theme as the system default theme](/developer-docs/6.1.0/api/set-a-theme-as-the-system-default-theme) | `/api/v1/theme/{pk}/set_system_default` |
| `GET` | [Download multiple themes as YAML files](/developer-docs/6.1.0/api/download-multiple-themes-as-yaml-files) | `/api/v1/theme/export/` |
| `POST` | [Import themes from a ZIP file](/developer-docs/6.1.0/api/import-themes-from-a-zip-file) | `/api/v1/theme/import/` |
| `GET` | [Get related fields data (theme-related-column-name)](/developer-docs/6.1.0/api/get-related-fields-data-theme-related-column-name) | `/api/v1/theme/related/{column_name}` |
| `DELETE` | [Clear the system dark theme](/developer-docs/6.1.0/api/clear-the-system-dark-theme) | `/api/v1/theme/unset_system_dark` |
| `DELETE` | [Clear the system default theme](/developer-docs/6.1.0/api/clear-the-system-default-theme) | `/api/v1/theme/unset_system_default` |
| `DELETE` | [Bulk delete themes](/developer-docs/api/bulk-delete-themes) | `/api/v1/theme/` |
| `GET` | [Get a list of themes](/developer-docs/api/get-a-list-of-themes) | `/api/v1/theme/` |
| `POST` | [Create a theme](/developer-docs/api/create-a-theme) | `/api/v1/theme/` |
| `GET` | [Get metadata information about this API resource (theme--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-theme-info) | `/api/v1/theme/_info` |
| `DELETE` | [Delete a theme](/developer-docs/api/delete-a-theme) | `/api/v1/theme/{pk}` |
| `GET` | [Get a theme](/developer-docs/api/get-a-theme) | `/api/v1/theme/{pk}` |
| `PUT` | [Update a theme](/developer-docs/api/update-a-theme) | `/api/v1/theme/{pk}` |
| `PUT` | [Set a theme as the system dark theme](/developer-docs/api/set-a-theme-as-the-system-dark-theme) | `/api/v1/theme/{pk}/set_system_dark` |
| `PUT` | [Set a theme as the system default theme](/developer-docs/api/set-a-theme-as-the-system-default-theme) | `/api/v1/theme/{pk}/set_system_default` |
| `GET` | [Download multiple themes as YAML files](/developer-docs/api/download-multiple-themes-as-yaml-files) | `/api/v1/theme/export/` |
| `POST` | [Import themes from a ZIP file](/developer-docs/api/import-themes-from-a-zip-file) | `/api/v1/theme/import/` |
| `GET` | [Get related fields data (theme-related-column-name)](/developer-docs/api/get-related-fields-data-theme-related-column-name) | `/api/v1/theme/related/{column_name}` |
| `DELETE` | [Clear the system dark theme](/developer-docs/api/clear-the-system-dark-theme) | `/api/v1/theme/unset_system_dark` |
| `DELETE` | [Clear the system default theme](/developer-docs/api/clear-the-system-default-theme) | `/api/v1/theme/unset_system_default` |
</details>
@@ -633,14 +633,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
| Method | Endpoint | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `GET` | [Get security user registrations](/developer-docs/6.1.0/api/get-security-user-registrations) | `/api/v1/security/user_registrations/` |
| `POST` | [Create security user registrations](/developer-docs/6.1.0/api/create-security-user-registrations) | `/api/v1/security/user_registrations/` |
| `GET` | [Get security user registrations info](/developer-docs/6.1.0/api/get-security-user-registrations-info) | `/api/v1/security/user_registrations/_info` |
| `DELETE` | [Delete security user registrations by pk](/developer-docs/6.1.0/api/delete-security-user-registrations-by-pk) | `/api/v1/security/user_registrations/{pk}` |
| `GET` | [Get security user registrations by pk](/developer-docs/6.1.0/api/get-security-user-registrations-by-pk) | `/api/v1/security/user_registrations/{pk}` |
| `PUT` | [Update security user registrations by pk](/developer-docs/6.1.0/api/update-security-user-registrations-by-pk) | `/api/v1/security/user_registrations/{pk}` |
| `GET` | [Get distinct values from field data (security-user-registrations-distinct-column-name)](/developer-docs/6.1.0/api/get-distinct-values-from-field-data-security-user-registrations-distinct-column-name) | `/api/v1/security/user_registrations/distinct/{column_name}` |
| `GET` | [Get related fields data (security-user-registrations-related-column-name)](/developer-docs/6.1.0/api/get-related-fields-data-security-user-registrations-related-column-name) | `/api/v1/security/user_registrations/related/{column_name}` |
| `GET` | [Get security user registrations](/developer-docs/api/get-security-user-registrations) | `/api/v1/security/user_registrations/` |
| `POST` | [Create security user registrations](/developer-docs/api/create-security-user-registrations) | `/api/v1/security/user_registrations/` |
| `GET` | [Get security user registrations info](/developer-docs/api/get-security-user-registrations-info) | `/api/v1/security/user_registrations/_info` |
| `DELETE` | [Delete security user registrations by pk](/developer-docs/api/delete-security-user-registrations-by-pk) | `/api/v1/security/user_registrations/{pk}` |
| `GET` | [Get security user registrations by pk](/developer-docs/api/get-security-user-registrations-by-pk) | `/api/v1/security/user_registrations/{pk}` |
| `PUT` | [Update security user registrations by pk](/developer-docs/api/update-security-user-registrations-by-pk) | `/api/v1/security/user_registrations/{pk}` |
| `GET` | [Get distinct values from field data (security-user-registrations-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-security-user-registrations-distinct-column-name) | `/api/v1/security/user_registrations/distinct/{column_name}` |
| `GET` | [Get related fields data (security-user-registrations-related-column-name)](/developer-docs/api/get-related-fields-data-security-user-registrations-related-column-name) | `/api/v1/security/user_registrations/related/{column_name}` |
</details>
+1 -1
View File
@@ -58,7 +58,7 @@ A modern, enterprise-ready business intelligence web application.
[**Why Superset?**](#why-superset) |
[**Supported Databases**](#supported-databases) |
[**Installation and Configuration**](#installation-and-configuration) |
[**Release Notes**](https://github.com/apache/superset/releases) |
[**Release Notes**](https://github.com/apache/superset/blob/master/RELEASING/README.md#release-notes-for-recent-releases) |
[**Get Involved**](#get-involved) |
[**Contributor Guide**](#contributor-guide) |
[**Resources**](#resources) |
@@ -328,12 +328,6 @@ Conditional formatting rules highlight cells based on their values. Rules can be
Each rule has a **"Use gradient"** toggle: enabled applies a varying opacity (lighter = further from threshold), disabled applies a solid fill at full opacity regardless of value.
For numeric rules, the optional **"Min bound"** / **"Max bound"** fields let you override the auto-detected color range with fixed values instead of relying on the minimum/maximum found in the data — useful when you want consistent coloring across dashboards or data refreshes.
Set **"Bound unit"** to **"% of column"** to express the bounds as percentages of either the column maximum or **"Column sum"**, which adds the absolute values in the column so positive and negative values do not cancel into an unstable denominator. A non-positive maximum or zero sum falls back to the automatic data range. A percentage center based on **"Column sum"** must still resolve inside the color range to activate diverging colors. This option is unavailable with server pagination because the browser only receives one page of rows and cannot calculate a stable full-column denominator; existing percentage rules also use the automatic range while server pagination is enabled.
For a diverging scale, set a **"Center value"** together with **"Low color"**, **"Mid color"**, and **"High color"**. Values below and above the center interpolate toward the corresponding endpoint color. Turning off **"Use gradient"** disables this interpolation and applies the base color as a solid fill.
Each rule's color is set with a full color picker rather than a fixed dropdown of presets. Pick any custom color, or use the **Colors** preset swatches, which reference theme tokens (success, warning, error, and their background variants) so a rule's color updates automatically if the active theme changes, including switching between light and dark mode.
When a rule targets a column with an active time comparison, a **Trend colors** preset also appears, letting you color cells green for an increase and red for a decrease (or the reverse).
@@ -151,20 +151,6 @@ see some data!
You should see months in the rows and Department and Travel Class in the columns. Publish this chart
to your existing Tutorial Dashboard you created earlier.
:::note
Row and column totals/subtotals for the Pivot Table are correct even for non-additive metrics,
such as ratios (`SUM(a)/SUM(b)`), `COUNT_DISTINCT`, `AVG`, and percentiles, not just additive ones
like `SUM` or `COUNT`. Totals derive client-side, by reducing the same full-detail query results
used to build the table, only when every selected metric is additive; if any selected metric is
non-additive, Superset instead issues a database query at each total's own granularity for all
metrics, so the total reflects each metric's own definition evaluated at that level rather than an
incorrect combination of the displayed cells. Because of this, there's no separate "Aggregation
function" control for totals in the Pivot Table: a total always reflects the metric's own
definition. The Table chart's **Show summary** row is different: its **Summary aggregation**
control can override a simple metric's own aggregation (to Sum or Average) for the summary row
only — metrics built from custom SQL keep their own aggregation regardless.
:::
### Line Chart
In this section, we are going to create a line chart to understand the average price of a ticket by
@@ -70,24 +70,5 @@ alert or report is removed, and the reason is shown. Charts that belong to
dashboards are removed from those dashboards as part of the deletion; the
dashboards themselves are left in place.
Before an archived dataset is deleted permanently, Superset checks which
charts still use it and which dashboards contain those charts. The confirmation
shows the total number of affected charts and dashboards, identifies the ones
you are allowed to access, and reports the remaining objects only as restricted
counts. Restricted names, identifiers, and links are not displayed. Archived
dependents are included because they can still be recovered after the dataset
is gone.
Deleting the dataset does not delete those charts or dashboards. They remain
in place without a usable dataset and may therefore be broken. If there are no
dependents, the confirmation explicitly reports zero affected charts and
dashboards.
The dependency check fails closed. While it is loading, or if its result is
unavailable, permanent deletion is disabled; cancel or retry the check. Superset
checks again when you submit. If dependencies changed while the confirmation
was open, the refreshed impact replaces the previous result and you must type
DELETE again before proceeding.
Objects are also deleted permanently on their own once they have been in the
archive longer than the retention window, without anyone acting.
@@ -78,18 +78,6 @@ Charts are **not saved by default**. The workflow is intentionally iterative:
To skip the preview and save immediately, include "and save it" in your prompt.
:::
:::info Deployment-specific chart types
Use `get_chart_type_schema` before generating a chart to discover the types
available on your Superset instance. Some deployments expose additional
feature-gated visualizations. For example, a deployment with an AG Grid pivot
extension enabled can expose `interactive_pivot`, which supports interactive
row groups, pivot columns, totals, and period-over-period comparisons. Pair
`comparison_period` (for example, `1 year ago`) with `comparison_type`
(`values`, `difference`, `percentage`, or `ratio`). It is distinct from the
built-in `pivot_table` chart type and is not offered when the host visualization
is unavailable.
:::
### Create Dashboards
Build dashboards from a collection of charts:
+71
View File
@@ -0,0 +1,71 @@
/* eslint-env node */
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const typescriptEslintParser = require('@typescript-eslint/parser');
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
const js = require('@eslint/js');
const ts = require('typescript-eslint');
const react = require('eslint-plugin-react');
const globals = require('globals');
const { defineConfig, globalIgnores } = require('eslint/config');
module.exports = defineConfig([
{
files: ['**/*.{js,jsx,ts,tsx}'],
},
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
js.configs.recommended,
...ts.configs.recommended,
{
files: ['eslint.config.js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
{
languageOptions: {
parser: typescriptEslintParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2020,
sourceType: 'module',
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
typescript: typescriptEslintPlugin,
react,
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
]);
-139
View File
@@ -1,139 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"typescript",
"react"
],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true,
"browser": true,
"node": true
},
"ignorePatterns": [
"build/**/*",
".docusaurus/**/*",
"node_modules/**/*"
],
"settings": {
"react": {
"version": "18.3.1"
}
},
"options": {
"typeAware": true
},
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"no-array-constructor": "error",
"no-unused-expressions": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "error",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-misused-new": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error"
},
"overrides": [
{
"files": [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts"
],
"rules": {
"constructor-super": "off",
"getter-return": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unreachable": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
}
]
}
+16 -10
View File
@@ -29,7 +29,7 @@
"lint:db-metadata": "python3 ../superset/db_engine_specs/lint_metadata.py",
"lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md",
"update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme",
"lint": "oxlint --config oxlint.json",
"eslint": "eslint .",
"lint:docs-links": "node scripts/lint-docs-links.mjs",
"version:add": "node scripts/manage-versions.mjs add",
"version:remove": "node scripts/manage-versions.mjs remove",
@@ -61,12 +61,12 @@
"@storybook/addon-docs": "^10.5.10",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.16.1",
"antd": "^6.6.2",
"baseline-browser-mapping": "^2.11.20",
"caniuse-lite": "^1.0.30001810",
"antd": "^6.6.1",
"baseline-browser-mapping": "^2.11.17",
"caniuse-lite": "^1.0.30001809",
"docusaurus-plugin-openapi-docs": "^5.2.0",
"docusaurus-theme-openapi-docs": "^5.2.0",
"js-yaml": "^5.4.1",
"js-yaml": "^5.3.0",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
@@ -85,13 +85,19 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.2",
"@docusaurus/tsconfig": "^3.10.2",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"oxfmt": "^0.65.0",
"oxlint": "^1.80.0",
"oxlint-tsgolint": "^7.0.2001",
"typescript": "7.0.2",
"webpack": "^5.110.1"
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.11.0",
"oxfmt": "^0.64.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.67.0",
"webpack": "^5.109.2"
},
"browserslist": {
"production": [
-51
View File
@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 238 55" style="enable-background:new 0 0 238 55;" xml:space="preserve">
<path d="M234.9,34.8c-0.9-1.3-2.2-2.2-3.6-2.8c-0.4-0.2-1.2-0.4-2.5-0.8c-1-0.3-1.9-0.6-2.8-1.1c-0.6-0.3-1.1-0.8-1.5-1.4
c-0.3-0.6-0.5-1.3-0.5-1.9c0-1,0.4-2,1.1-2.7c0.7-0.7,1.7-1.1,2.8-1.1c1.1-0.1,2.1,0.3,2.9,1c0.8,0.8,1.2,1.8,1.2,2.9h3.6
c0-2-0.7-3.9-2.1-5.3c-1.5-1.3-3.4-2-5.4-1.9c-2,0-4,0.7-5.5,2.1c-1.5,1.3-2.3,3.2-2.3,5.2c-0.1,1.5,0.4,3.1,1.3,4.3
c0.9,1.1,2.8,2.1,5.8,3.1c1.4,0.4,2.7,1.2,3.8,2.1c0.7,0.8,1.1,1.9,1.1,3c0,1.2-0.5,2.3-1.3,3.1c-0.9,0.8-2.1,1.3-3.3,1.2
c-1.4,0-2.6-0.6-3.5-1.6c-1-1.1-1.5-2.6-1.4-4.1V38h-3.5c-0.1,2.4,0.8,4.7,2.4,6.5c1.5,1.6,3.7,2.5,6,2.4c2.2,0.1,4.4-0.7,6.1-2.2
c1.6-1.4,2.5-3.5,2.4-5.6C236.2,37.6,235.8,36.1,234.9,34.8z"/>
<path d="M117,31.4c0.3-2.4,1.5-4.7,3.3-6.4c1.8-1.6,4.1-2.4,6.5-2.4c2.4-0.1,4.8,0.7,6.7,2.2c1.9,1.8,3.1,4.1,3.6,6.6L117,31.4z
M140.9,34.1c0-4.3-1.3-7.9-3.8-10.6s-6.1-4.2-9.8-4.1c-3.7-0.1-7.4,1.3-10,3.9s-4,6.2-3.9,9.9c-0.1,3.7,1.4,7.2,4,9.8
c2.5,2.7,6.1,4.1,9.8,4.1c2.7,0.1,5.4-0.8,7.7-2.3c2.3-1.7,4.1-4,5.2-6.7h-3.8c-0.8,1.7-2.1,3.2-3.7,4.2c-1.6,1-3.5,1.6-5.4,1.5
c-2.6,0.1-5.1-0.9-7-2.6c-1.9-1.8-3-4.3-3.1-6.9h24L140.9,34.1z"/>
<path d="M175.1,19.6c-1.9,0-3.7,0.4-5.4,1.2c-1.6,0.8-3,2-4,3.5c-0.9-1.5-2.2-2.6-3.8-3.4c-1.7-0.8-3.6-1.3-5.5-1.3
c-1.6,0-3.2,0.3-4.7,0.9c-1.4,0.6-2.6,1.5-3.6,2.6v-2.9h-3.4v26.2h3.3V33.6c0-1.6,0-3.2,0.2-4.8c0.2-0.9,0.4-1.7,0.9-2.4
c0.6-1.1,1.6-2,2.8-2.6c1.3-0.6,2.7-1,4.1-0.9c2.6,0,4.5,0.7,5.7,2.2c1.2,1.4,1.9,3.7,1.9,6.7v14.7h3.3V33.6c0-1.6,0-3.2,0.3-4.8
c0.2-0.9,0.5-1.7,0.9-2.4c0.6-1.1,1.6-2,2.7-2.6c1.2-0.6,2.6-1,4-0.9c2.6,0,4.5,0.7,5.7,2.2s1.7,3.9,1.7,7.4v13.9h3.3V33.1
c0-4.7-0.8-8.2-2.5-10.3S178.7,19.6,175.1,19.6z"/>
<path d="M193.1,31.4c0.3-2.4,1.5-4.7,3.3-6.4c1.8-1.6,4.1-2.4,6.5-2.4c2.4-0.1,4.9,0.7,6.8,2.2c1.9,1.8,3.1,4.1,3.5,6.6L193.1,31.4z
M216.9,34.1c0-4.3-1.3-7.9-3.8-10.6s-6.1-4.2-9.8-4.1c-3.7-0.1-7.4,1.3-10,3.9s-4,6.2-3.9,9.9c-0.1,3.7,1.4,7.2,4,9.8
c2.5,2.7,6.1,4.2,9.8,4.1c2.7,0,5.4-0.8,7.7-2.3c2.3-1.7,4.1-4,5.2-6.7h-3.8c-0.8,1.7-2.1,3.2-3.8,4.2c-1.6,1-3.5,1.5-5.4,1.5
c-2.6,0.1-5.2-0.9-7.1-2.6c-1.9-1.8-3-4.3-3.1-6.9h24L216.9,34.1L216.9,34.1z"/>
<path d="M108.4,20.7c-1.1,0.7-2,1.5-2.7,2.6v-3.1h-3.2v26.2h3.5V30.6c0-2.4,0.5-4.1,1.4-5.2c0.9-1.1,2.4-1.7,4.6-1.9v-3.7
C110.6,19.8,109.4,20.1,108.4,20.7z"/>
<path d="M74.6,31.4c0.3-2.4,1.5-4.7,3.3-6.4c1.8-1.6,4.1-2.4,6.5-2.4c2.4-0.1,4.8,0.7,6.7,2.2c1.9,1.8,3.1,4.1,3.5,6.6L74.6,31.4z
M98.5,34.1c0-4.3-1.3-7.9-3.8-10.6s-6.1-4.2-9.8-4.1c-3.7-0.1-7.4,1.3-10,3.9s-4,6.2-3.9,9.9c-0.1,3.7,1.4,7.2,4,9.8
c2.5,2.7,6.1,4.1,9.8,4.1c2.7,0,5.4-0.8,7.7-2.3c2.3-1.7,4.1-4,5.1-6.7h-3.8c-0.8,1.7-2.1,3.2-3.7,4.2s-3.5,1.5-5.4,1.5
c-2.6,0.1-5.1-0.9-7-2.6c-1.9-1.8-3-4.3-3.1-6.9h24L98.5,34.1z"/>
<path d="M47.4,11.7c-1.7-4-4.2-8.4-8.5-10.1C34.2,0,29.1,1,25.4,4.2c-2.7,2.4-4.3,6.1-4.8,11L20.4,16c-0.1,1.3-0.3,2.6-0.3,3.8
c-0.8-3.5-0.5-5.9-0.7-9.7c-0.8,0.4-1.2,1.2-1.2,2.1c0,0.2-1-0.4-1.2-0.3c-0.8,0.4-1.6-1-2-1.5c-0.2,0.4-0.4,0.7-0.7,1.1
C14,11,13.5,11,13,10.5c-0.4,1-1.6,0.5-2.7,0.5c0.6,1,0.8,2-0.1,2.4c0.1,0.1,0.9,0.3,0.9,0.5c-0.3,0.4-1.3,0-1.8-0.1v0.8
c-1.4-0.8-3.7-2.3-2.9,0.9c-1,0.2-0.5,0-0.5,0.9c-0.4,0.1-0.4,0.1-0.4,0.6c-1.2-0.6-2.2,4.9-2.1,6.5c0.9,0.2,1,0.5,1.5,1.4
c-0.7,0.3-1.1,1-1.6,1.3L4,26.6c-0.6,0.3-1.1,0.7-1.5,1.3c0.2-0.3,0.6,0.4,0.5,0.3L2.6,28c0.1,0.4,0.3,0.8,0.4,1.2
c-0.8,0.3-0.6,0.6-1.1,1.3c0.2,0,0.5,0.1,0.7,0.1c-0.4,0-0.5,1.9-0.4,2.2c0.2-0.4,0.6-0.9,0.8-1.4l0.5,0.5l-0.7,0.6
c1.8,0.5-0.2,1.7,0.6,3.1C3.6,34.8,4,34.7,4,33.8l0.4,0.3c-0.2,0.3-1.4,2.6-0.3,2.6c0.1,0,0.1,3.8,0.3,4.8C4.6,41.3,4.8,41,5,40.6
c0,0.6,0.3,0.8,0.1,1.5c1.8-0.5,1.1,0.5,0.4,1.4c0.6-0.1,1.2-0.3,1.8-0.5c0.8-0.3,0.1,1.1,0.4,1.1c0.3,0,1.2-1.8,1.4-2.1v0.6
c0.4-0.2,1.8-2,2-2c0.2,0.3,0.3,0.7,0.2,1.1c1.6-2,3-4.2,4-6.6c0.1,0.1,0.2,0.2,0.4,0.2c-0.2,0-4.1,7.7-4.1,8.1l0.9-0.2
c-0.3,0.6-0.5,1.2-0.6,1.8c1.6-0.2,0.9,1.2,0.9,2.5c1.3-0.8,3.2-1.3,2.5,0.9c0.4-0.2,0.9-0.4,1.3-0.6c-1.1,0.4,0.5,2.6,0.7,3.2
c0.2,0.7,2.3,0.2,3,0.4c0.7-1.1,1,0.2,1.2,1.3s1.4-0.6,1.9-0.4c0.4,0.1-0.3,2.7,1.2,1.6c0.6-0.5,0.8,0.4,1.7-0.5
c0,0,2.7,0.4,2.6,0.4c0.5-2.6,2.5-0.1,2.2-2.4h0.6c-0.1-2,2,0.9,2-2c0-0.7-1.6-1.5,0.4-0.9c-0.2-0.9,0.2-1.1-0.8-1.3
c-0.1-0.2-0.1-0.4,0.1-0.5c0.9,0,2.5,1.7,3.1,0.9c0.2-0.2-0.8-2-0.9-2.5c0.5,0.1,1.1-0.1,1.6,0c-0.9-0.5-0.3-0.6-1.4-0.9
c0.9-1.7,2.6-0.1,3.4-1.4c-1.7,0.3-2.6-3.3-1.5-3.6c-0.9-0.8-1.6-1-2.3-1.9c-1.8-2.3,1.7,0.4,2.3,0.9c0-0.4,0.2-0.9,0.2-1.3l0.9,0.8
c0-0.2,0.1-0.4,0.1-0.6c0.8,0.7,1.9,1,2.9,0.9c-0.2-0.5,0-0.6-0.2-1.1c0.8,0.2,1.3,0,2.2,0.1c-0.6-1.8,1.5-1.7,2.9-2.1
c2.3-0.7-1.4-1-1.6-1.2c-0.4-0.4,0.5-0.9,0.6-0.9s-0.8-1.1-0.6-0.8c-0.3-1-2.6-0.3-0.6-1.6c-0.9-0.2-2.1-0.4-2.1-1.6
c0.7-0.1,1.5-0.2,2.2-0.4c-0.5-0.4-0.9-1.1-1.5-1.4l0.5-0.2c-2.8-0.5,0.3-2.7,1-4c-1.9,0.4-2-1.1-3.7-1.3l0.8-0.7
c-0.9,0-1.9-0.1-2.8-0.3c0.2-1,0.9-1.9,1.9-2.2c-3.1-1.5-2.4-3.8-5.6-4.7l0.7-0.7c-0.9-1.3-1.6-0.4-2.6-0.7
c-2.1-0.6-1.9,2.3-1.9-1.1c0,0.4-0.5-0.4-0.6-0.6c-0.8,0.5-3.4,2.6-3.9,2.1C26.1,12,26,12.2,25.5,13c-0.3,0.3-1.3,0.9-1.1,0.5
c-0.6,0.9-0.6,4.3-1.3,6.6c0-1.2,0.2-2.4,0.3-3.6l0.1-0.9c0.4-4.1,1.7-7.1,3.8-8.9c2.8-2.4,6.7-3.1,10.3-2c3.4,1.2,5.5,5.4,7,9.1
L57,46.5h2.7l13.2-34.7h-3.5L58.5,41.2L47.4,11.7z"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

+9176 -7410
View File
File diff suppressed because it is too large Load Diff
+6 -23
View File
@@ -1,30 +1,14 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
// "extends": "@docusaurus/tsconfig",
// First compilerOptions section comes from above commented @docusaurus/tsconfig
// We moved them here to help with TS v7 migration so whenever Docusaurus readily supports TS v7,
// re-install @docusaurus/tsconfig and remove said section.
// Commented options are overriden in the next section.
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"allowJs": true,
// "esModuleInterop": true,
// "jsx": "preserve",
"target": "ES2022",
"lib": ["ES2022", "DOM"],
// "moduleResolution": "bundler",
"module": "esnext",
"noEmit": true,
// "paths": {
// "@site/*": ["./*"]
// },
// "skipLibCheck": true,
"baseUrl": ".",
"ignoreDeprecations": "6.0",
"skipLibCheck": true,
"noImplicitAny": false,
"strict": false,
"jsx": "react-jsx",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"types": ["@docusaurus/module-type-aliases"],
@@ -39,10 +23,9 @@
// Runtime resolution uses webpack alias pointing to actual source (see src/webpack.extend.ts)
// Using /ui path matches the established pattern used throughout the Superset codebase
"@apache-superset/core/components": ["./src/types/apache-superset-core"],
"@site/*": ["./*"],
"*": ["./src/*", "./node_modules/*"]
"*": ["src/*", "node_modules/*"]
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.d.ts"],
"exclude": ["./node_modules", "../superset-frontend/**/*", "src/shims/**"]
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
"exclude": ["node_modules", "../superset-frontend/**/*", "src/shims/**"]
}
+1422 -527
View File
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -44,7 +44,7 @@ dependencies = [
# without the ``base.txt`` lock file (#40962).
"cachetools>=7.1.7, <8",
"celery>=5.6.3, <6.0.0",
"click>=8.5.0",
"click>=8.4.2",
"click-option-group",
"colorama",
"flask-cors>=6.0.5, <7.0",
@@ -83,7 +83,7 @@ dependencies = [
"marshmallow>=3.0, <5",
"marshmallow-union>=0.1.15.post1",
"msgpack>=1.2.0, <1.3",
"nh3>=0.3.7, <0.4",
"nh3>=0.3.5, <0.4",
"numpy>=1.23.5, <2.5",
"packaging",
# --------------------------
@@ -109,14 +109,11 @@ dependencies = [
"shillelagh[gsheetsapi]>=1.4.5, <2.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=4.1.2",
"simplejson>=4.1.1",
"slack_sdk>=3.43.0, <4",
"sqlalchemy>=2.0.52, <2.1",
"sqlalchemy-continuum>=1.6.0, <2.0.0",
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
# Dialect-specific gaps/bugs against this pin are worked around in
# superset/sql/dialects/ (e.g. starrocks.py); check there for anything
# that can be cleaned up when bumping
"sqlglot>=30.17.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
# newer pandas needs 0.9+
"tabulate>=0.10.0, <1.0",
@@ -138,7 +135,7 @@ athena = ["pyathena[pandas]>=3.35.4, <4"]
# superset/db_engine_specs/aurora.py's known_incompatibilities metadata.
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.2",
"pandas-gbq>=0.35.1",
# 1.17.1 is likely the final release: googleapis/python-bigquery-sqlalchemy
# was archived 2026-05-16. Both 1.17.0 and 1.17.1 support SQLAlchemy 1.4/2.0.
"sqlalchemy-bigquery>=1.17.2",
@@ -213,7 +210,7 @@ firebird = ["sqlalchemy-firebird>=2.2.0"]
firebolt = ["firebolt-sqlalchemy>=1.1.2, <2"]
gevent = ["gevent>=26.8.0"]
gsheets = ["shillelagh[gsheetsapi]>=1.4.5, <2"]
hana = ["hdbcli==2.29.27", "sqlalchemy_hana==3.0.3"]
hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
hive = [
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
+3 -3
View File
@@ -58,7 +58,7 @@ cffi==2.0.0
# pynacl
charset-normalizer==3.4.2
# via requests
click==8.5.0
click==8.4.2
# via
# apache-superset (pyproject.toml)
# celery
@@ -240,7 +240,7 @@ msgpack==1.2.1
# via apache-superset (pyproject.toml)
msgspec==0.19.0
# via flask-session
nh3==0.3.7
nh3==0.3.6
# via apache-superset (pyproject.toml)
numexpr==2.10.2
# via -r requirements/base.in
@@ -370,7 +370,7 @@ setuptools==84.0.0
# via -r requirements/base.in
shillelagh==1.4.5
# via apache-superset (pyproject.toml)
simplejson==4.1.2
simplejson==4.1.1
# via apache-superset (pyproject.toml)
six==1.17.0
# via
+4 -4
View File
@@ -131,7 +131,7 @@ charset-normalizer==3.4.2
# via
# -c requirements/base-constraint.txt
# requests
click==8.5.0
click==8.4.2
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -569,7 +569,7 @@ msgspec==0.19.0
# flask-session
mysqlclient==2.2.8
# via apache-superset
nh3==0.3.7
nh3==0.3.6
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -639,7 +639,7 @@ pandas==2.3.3
# db-dtypes
# pandas-gbq
# prophet
pandas-gbq==0.35.2
pandas-gbq==0.35.1
# via apache-superset
parameterized==0.9.0
# via apache-superset
@@ -932,7 +932,7 @@ shillelagh==1.4.5
# via
# -c requirements/base-constraint.txt
# apache-superset
simplejson==4.1.2
simplejson==4.1.1
# via
# -c requirements/base-constraint.txt
# apache-superset
+3 -3
View File
@@ -289,11 +289,11 @@ function extractArgs(args, regexes) {
* For example: `superset-frontend/foo/bar.ts` -> `foo/bar.ts`
*
* @param {string[]} args
* @param {string} packageName
* @param {string} package
* @returns {string[]}
*/
function removePackageSegment(args, packageName) {
const packageSegment = packageName.concat(sep);
function removePackageSegment(args, package) {
const packageSegment = package.concat(sep);
return args.map((arg) => {
const normalizedPath = normalize(arg);
@@ -146,8 +146,6 @@ class Operator(str, enum.Enum):
NOT_IN = "NOT IN"
LIKE = "LIKE"
NOT_LIKE = "NOT LIKE"
ILIKE = "ILIKE"
NOT_ILIKE = "NOT ILIKE"
IS_NULL = "IS NULL"
IS_NOT_NULL = "IS NOT NULL"
ADHOC = "ADHOC"
+2261 -2108
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -37,18 +37,18 @@
"jwt-decode": "^4.0.0"
},
"devDependencies": {
"@babel/cli": "^8.0.1",
"@babel/core": "^8.0.1",
"@babel/preset-env": "^8.0.1",
"@babel/preset-typescript": "^8.0.1",
"@types/node": "^26.4.0",
"babel-loader": "^10.1.1",
"jsdom": "^30.0.1",
"@babel/cli": "^7.29.7",
"@babel/core": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
"jsdom": "^26.1.0",
"tscw-config": "^1.1.2",
"typescript": "^7.0.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"webpack": "^5.110.0",
"webpack-cli": "^7.2.3"
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4"
},
"repository": {
"type": "git",
-2
View File
@@ -1,7 +1,5 @@
{
"compilerOptions": {
"rootDir": "./src",
// syntax rules
"strict": true,
+48 -61
View File
@@ -3163,15 +3163,11 @@
]
},
"node_modules/baseline-browser-mapping": {
"version": "2.11.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
"integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
"license": "Apache-2.0",
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bcrypt-pbkdf": {
@@ -3213,9 +3209,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [
{
"type": "opencollective",
@@ -3230,13 +3226,12 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
"electron-to-chromium": "^1.5.402",
"node-releases": "^2.0.53",
"update-browserslist-db": "^1.3.0"
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -3330,9 +3325,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"version": "1.0.30001769",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
"funding": [
{
"type": "opencollective",
@@ -3346,8 +3341,7 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
]
},
"node_modules/caseless": {
"version": "0.12.0",
@@ -4003,10 +3997,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.418",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz",
"integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==",
"license": "ISC"
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@@ -4115,7 +4108,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -6638,13 +6630,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.54",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
}
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="
},
"node_modules/npm-run-path": {
"version": "4.0.1",
@@ -8356,9 +8344,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
@@ -8373,7 +8361,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@@ -11131,9 +11118,9 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"baseline-browser-mapping": {
"version": "2.11.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
"integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
@@ -11169,15 +11156,15 @@
}
},
"browserslist": {
"version": "4.28.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"requires": {
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
"electron-to-chromium": "^1.5.402",
"node-releases": "^2.0.53",
"update-browserslist-db": "^1.3.0"
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
}
},
"buffer-crc32": {
@@ -11237,9 +11224,9 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"caniuse-lite": {
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="
"version": "1.0.30001769",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="
},
"caseless": {
"version": "0.12.0",
@@ -11738,9 +11725,9 @@
}
},
"electron-to-chromium": {
"version": "1.5.418",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz",
"integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA=="
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="
},
"emoji-regex": {
"version": "8.0.0",
@@ -13483,9 +13470,9 @@
}
},
"node-releases": {
"version": "2.0.54",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="
},
"npm-run-path": {
"version": "4.0.1",
@@ -14712,9 +14699,9 @@
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="
},
"update-browserslist-db": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"requires": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@@ -1,5 +1,5 @@
{
"name": "@superset-ui/eslint-plugin-i18n-strings",
"name": "eslint-plugin-i18n-strings",
"version": "1.0.0",
"description": "Warns about translation variables",
"keywords": [],
@@ -1,5 +1,5 @@
{
"name": "@superset-ui/eslint-plugin-icons",
"name": "eslint-plugin-icons",
"version": "1.0.0",
"description": "Warns about direct usage of Ant Design icons",
"keywords": [],
@@ -1,5 +1,5 @@
{
"name": "@superset-ui/eslint-plugin-theme-colors",
"name": "eslint-plugin-theme-colors",
"version": "1.0.0",
"description": "Warns about rgb(a)/hex/literal colors",
"keywords": [],
+3 -3
View File
@@ -37,9 +37,9 @@
require('tsx/cjs');
const tsParser = require('@typescript-eslint/parser');
const themeColorsPlugin = require('@superset-ui/eslint-plugin-theme-colors');
const iconsPlugin = require('@superset-ui/eslint-plugin-icons');
const i18nStringsPlugin = require('@superset-ui/eslint-plugin-i18n-strings');
const themeColorsPlugin = require('eslint-plugin-theme-colors');
const iconsPlugin = require('eslint-plugin-icons');
const i18nStringsPlugin = require('eslint-plugin-i18n-strings');
module.exports = [
// Files this config applies to. Flat config has no `--ext`; globs live here.
+9 -14
View File
@@ -18,19 +18,6 @@
*/
// timezone for unit tests
process.env.TZ = 'America/New_York';
const reporters = ['default'];
// HTML reporter is not used on CI so skipping its generation for saving time
if (!process.env.CI) {
reporters.push([
'./node_modules/jest-html-reporter',
{
pageTitle: 'Test Report',
},
]);
}
module.exports = {
// [/\\] matches both path separators so the suite also collects on
// native Windows, where jest hands the regex backslash-separated paths.
@@ -101,6 +88,14 @@ module.exports = {
__DEV__: true,
caches: true,
},
reporters: reporters,
reporters: [
'default',
[
'./node_modules/jest-html-reporter',
{
pageTitle: 'Test Report',
},
],
],
testTimeout: 20000,
};
+1143 -2005
View File
File diff suppressed because it is too large Load Diff
+24 -24
View File
@@ -79,7 +79,7 @@
"playwright:report": "playwright show-report",
"docs:screenshots": "playwright test --config=playwright/generators/playwright.config.ts docs/",
"prod": "npm run build",
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache ./coverage ./storybook-static",
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache",
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
"test-storybook": "test-storybook",
"test-storybook:ci": "concurrently --kill-others --success first --names \"SB,TEST\" --prefix-colors \"magenta,blue\" \"python3 -m http.server 6006 --directory storybook-static\" \"npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook -- --maxWorkers=2\"",
@@ -146,6 +146,7 @@
"@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map",
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
"@types/d3-selection": "^3.0.11",
"@types/d3-time-format": "^4.0.3",
"@types/react-google-recaptcha": "^2.1.9",
@@ -157,7 +158,7 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.6.2",
"antd": "^6.6.1",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^3.0.0",
@@ -192,7 +193,6 @@
"mustache": "^4.2.0",
"nanoid": "^6.0.1",
"ol": "^10.10.0",
"postcss": "^8.5.26",
"query-string": "9.5.0",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
@@ -203,7 +203,7 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^18.3.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^11.0.1",
"react-intersection-observer": "^11.0.0",
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.16.1",
"react-loadable": "^5.5.0",
@@ -256,21 +256,18 @@
"@formatjs/intl-durationformat": "^0.10.18",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.5.10",
"@storybook/addon-links": "10.5.10",
"@storybook/react-webpack5": "10.5.10",
"@storybook/test-runner": "0.24.4",
"@superset-ui/eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"@superset-ui/eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"@superset-ui/eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.16.1",
"@swc/plugin-emotion": "^15.0.0",
"@swc/plugin-transform-imports": "^13.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
@@ -280,7 +277,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.4.0",
"@types/node": "^26.2.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
@@ -292,21 +289,23 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.68.0",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.5.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.11.20",
"baseline-browser-mapping": "^2.11.17",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.5",
"eslint": "^10.9.1",
"css-loader": "^7.1.4",
"eslint": "^10.9.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.10.1",
"eslint-plugin-lodash": "^8.0.0",
@@ -315,13 +314,14 @@
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.2",
"eslint-plugin-storybook": "10.5.10",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.8",
"imports-loader": "^5.0.0",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.5.0",
"jest-environment-jsdom": "^30.4.1",
"jest-html-reporter": "^4.4.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
@@ -329,10 +329,10 @@
"lerna": "^10.0.1",
"lightningcss": "^1.33.0",
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.8.0",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxfmt": "^0.65.0",
"oxlint": "^1.80.0",
"oxfmt": "^0.64.0",
"oxlint": "^1.79.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"process": "^0.11.10",
@@ -353,10 +353,10 @@
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
"webpack": "^5.110.1",
"webpack-bundle-analyzer": "^5.3.2",
"webpack-cli": "^7.2.3",
"webpack-dev-server": "^6.0.0",
"webpack": "^5.109.2",
"webpack-bundle-analyzer": "^5.3.1",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-manifest-plugin": "^6.0.1",
"webpack-sources": "^3.5.1",
"webpack-visualizer-plugin2": "^2.0.0"
@@ -406,7 +406,7 @@
"eslint-plugin-jest-dom": {
"eslint": "$eslint"
},
"fast-uri": "^3.1.7",
"fast-uri": "^3.1.5",
"fast-xml-parser": "^5.8.0",
"http-proxy-middleware": "^2.0.10",
"jest-circus": "^30.4.0",
@@ -33,7 +33,7 @@
"dependencies": {
"chalk": "^6.0.0",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.3.0",
"yeoman-generator": "^8.2.2",
"yosay": "^3.0.0"
},
"devDependencies": {
@@ -104,7 +104,7 @@
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "*",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/lodash": "^4.17.25",
"@types/react": "*",
@@ -550,12 +550,6 @@ export interface ThemeContextType {
canDetectOSPreference: () => boolean;
createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>;
getAppliedThemeId: () => number | null;
/**
* Re-reads the persisted system default/dark themes from the server and
* re-applies them live, so changes made on the Themes admin page take effect
* without a full page reload.
*/
refreshSystemThemes: () => Promise<void>;
}
/**
@@ -39,7 +39,7 @@
"@superset-ui/core": "*",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "*",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
@@ -489,16 +489,6 @@ export const MultipleValueComparators = [
Comparator.BetweenOrRightEqual,
];
export enum BoundUnit {
Value = 'value',
Percent = 'percent',
}
export enum PercentDenominator {
Sum = 'sum',
Max = 'max',
}
export type ConditionalFormattingConfig = {
operator?: Comparator;
targetValue?: number | string;
@@ -511,14 +501,6 @@ export type ConditionalFormattingConfig = {
useGradient?: boolean;
columnFormatting?: string;
objectFormatting?: ObjectFormattingEnum;
minBound?: number;
maxBound?: number;
centerValue?: number;
lowColor?: RGBColor | string;
midColor?: RGBColor | string;
highColor?: RGBColor | string;
boundUnit?: BoundUnit;
percentDenominator?: PercentDenominator;
};
export type ColorFormatters = {
@@ -16,11 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import memoizeOne, { type MemoizedFn } from 'memoize-one';
import memoizeOne from 'memoize-one';
import { isString, isBoolean } from 'lodash-es';
import { isBlank } from '@apache-superset/core/utils';
import { addAlpha, DataRecord, rgbaToHex } from '@superset-ui/core';
import { type RGBColor } from '@superset-ui/core/components';
import tinycolor from 'tinycolor2';
import {
ColorFormatters,
@@ -29,8 +28,6 @@ import {
MultipleValueComparators,
ResolvedColorFormatterResult,
ColorSchemeEnum,
BoundUnit,
PercentDenominator,
} from '../types';
export const round = (num: number, precision = 0) =>
@@ -75,58 +72,6 @@ export const getOpacity = (
);
};
const parseColorToRgb = (color: RGBColor | string) => {
if (typeof color === 'string') {
const { r, g, b } = tinycolor(color).toRgb();
return { r, g, b };
}
return { r: color.r, g: color.g, b: color.b };
};
export const getDivergingColor = (
value: number,
cutoffValue: number,
centerValue: number,
extremeValue: number,
lowColor: RGBColor | string,
midColor: RGBColor | string,
highColor: RGBColor | string,
): string => {
const clampedValue = Math.min(Math.max(value, cutoffValue), extremeValue);
const belowCenter = clampedValue <= centerValue;
const from = parseColorToRgb(belowCenter ? lowColor : midColor);
const to = parseColorToRgb(belowCenter ? midColor : highColor);
const rangeStart = belowCenter ? cutoffValue : centerValue;
const rangeEnd = belowCenter ? centerValue : extremeValue;
const ratio =
rangeEnd === rangeStart
? 1
: (clampedValue - rangeStart) / (rangeEnd - rangeStart);
return rgbaToHex({
r: from.r + (to.r - from.r) * ratio,
g: from.g + (to.g - from.g) * ratio,
b: from.b + (to.b - from.b) * ratio,
a: 1,
});
};
const isValidDivergingConfig = (
centerValue: number | undefined,
lowColor: RGBColor | string | undefined,
midColor: RGBColor | string | undefined,
highColor: RGBColor | string | undefined,
cutoffValue: number | string,
extremeValue: number | string,
): centerValue is number =>
centerValue !== undefined &&
lowColor !== undefined &&
midColor !== undefined &&
highColor !== undefined &&
typeof cutoffValue === 'number' &&
typeof extremeValue === 'number' &&
centerValue > cutoffValue &&
centerValue < extremeValue;
const isSpecialColor = (value: unknown): value is ColorSchemeEnum =>
Object.values(ColorSchemeEnum).includes(value as ColorSchemeEnum);
@@ -138,61 +83,17 @@ export const getColorFunction = (
targetValueRight,
colorScheme,
useGradient,
minBound: rawMinBound,
maxBound: rawMaxBound,
centerValue: rawCenterValue,
lowColor,
midColor,
highColor,
boundUnit,
percentDenominator,
}: ConditionalFormattingConfig,
columnValues: number[] | string[] | (boolean | null)[],
alpha?: boolean,
) => {
const resolvePercentBound = (bound: number | undefined) => {
if (boundUnit !== BoundUnit.Percent || bound === undefined) {
return bound;
}
const numericColumnValues = (
columnValues as (number | string | boolean | null)[]
).filter((value): value is number => typeof value === 'number');
if (numericColumnValues.length === 0) {
return undefined;
}
// Sum magnitudes independently so mixed signs do not cancel into an
// unstable denominator. A non-positive column maximum cannot produce
// ordered percentage bounds, so it falls back to the automatic range.
const denominatorValue =
percentDenominator === PercentDenominator.Sum
? numericColumnValues.reduce((sum, value) => sum + Math.abs(value), 0)
: numericColumnValues.reduce(
(max, value) => (value > max ? value : max),
-Infinity,
);
if (denominatorValue <= 0) {
return undefined;
}
return (bound / 100) * denominatorValue;
};
const minBound = resolvePercentBound(rawMinBound);
const maxBound = resolvePercentBound(rawMaxBound);
const centerValue = resolvePercentBound(rawCenterValue);
let minOpacity = MIN_OPACITY_BOUNDED;
const maxOpacity = MAX_OPACITY;
let comparatorFunction: (
value: number | string | boolean | null,
allValues: number[] | string[] | (boolean | null)[],
) =>
| false
| {
cutoffValue: number | string;
extremeValue: number | string;
opacityValue?: number;
};
) => false | { cutoffValue: number | string; extremeValue: number | string };
if (operator === undefined || colorScheme === undefined) {
return () => undefined;
}
@@ -216,19 +117,8 @@ export const getColorFunction = (
if (typeof value !== 'number') {
return { cutoffValue: value!, extremeValue: value! };
}
const cutoffValue = minBound ?? Math.min(...allValues);
const extremeValue = maxBound ?? Math.max(...allValues);
const hasManualBound = minBound !== undefined || maxBound !== undefined;
if (cutoffValue > extremeValue) {
return false;
}
if (hasManualBound) {
return {
cutoffValue,
extremeValue,
opacityValue: Math.min(Math.max(value, cutoffValue), extremeValue),
};
}
const cutoffValue = Math.min(...allValues);
const extremeValue = Math.max(...allValues);
return value >= cutoffValue && value <= extremeValue
? { cutoffValue, extremeValue }
: false;
@@ -239,10 +129,7 @@ export const getColorFunction = (
typeof targetValue === 'number' && value > targetValue!
? {
cutoffValue: targetValue!,
extremeValue:
maxBound !== undefined && maxBound > targetValue
? maxBound
: Math.max(...allValues),
extremeValue: Math.max(...allValues),
}
: false;
break;
@@ -251,10 +138,7 @@ export const getColorFunction = (
typeof targetValue === 'number' && value < targetValue!
? {
cutoffValue: targetValue!,
extremeValue:
minBound !== undefined && minBound < targetValue
? minBound
: Math.min(...allValues),
extremeValue: Math.min(...allValues),
}
: false;
break;
@@ -263,10 +147,7 @@ export const getColorFunction = (
typeof targetValue === 'number' && value >= targetValue!
? {
cutoffValue: targetValue!,
extremeValue:
maxBound !== undefined && maxBound > targetValue
? maxBound
: Math.max(...allValues),
extremeValue: Math.max(...allValues),
}
: false;
break;
@@ -275,10 +156,7 @@ export const getColorFunction = (
typeof targetValue === 'number' && value <= targetValue!
? {
cutoffValue: targetValue!,
extremeValue:
minBound !== undefined && minBound < targetValue
? minBound
: Math.min(...allValues),
extremeValue: Math.min(...allValues),
}
: false;
break;
@@ -394,32 +272,7 @@ export const getColorFunction = (
}
const compareResult = comparatorFunction(value, columnValues);
if (compareResult === false) return undefined;
const { cutoffValue, extremeValue, opacityValue } = compareResult;
const resolvedValue = opacityValue ?? value;
if (
useGradient !== false &&
operator === Comparator.None &&
typeof resolvedValue === 'number' &&
isValidDivergingConfig(
centerValue,
lowColor,
midColor,
highColor,
cutoffValue,
extremeValue,
)
) {
return getDivergingColor(
resolvedValue,
cutoffValue as number,
centerValue,
extremeValue as number,
lowColor!,
midColor!,
highColor!,
);
}
const { cutoffValue, extremeValue } = compareResult;
if (typeof colorScheme === 'string') {
if (isSpecialColor(colorScheme)) {
@@ -444,13 +297,7 @@ export const getColorFunction = (
if (alpha === undefined || alpha) {
return addAlpha(
cleanHex,
getOpacity(
opacityValue ?? value,
cutoffValue,
extremeValue,
minOpacity,
maxOpacity,
),
getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
);
}
return colorScheme;
@@ -468,47 +315,22 @@ export const getColorFunction = (
if (alpha === undefined || alpha) {
return addAlpha(
baseHexColor,
getOpacity(
opacityValue ?? value,
cutoffValue,
extremeValue,
minOpacity,
maxOpacity,
),
getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
);
}
return baseHexColor;
};
};
type GetColorFormatters = (
columnConfig: ConditionalFormattingConfig[] | undefined,
data: DataRecord[],
theme?: Record<string, any>,
alpha?: boolean,
disablePercentBounds?: boolean,
) => ColorFormatters;
export const getColorFormatters: MemoizedFn<GetColorFormatters> = memoizeOne(
export const getColorFormatters = memoizeOne(
(
columnConfig: ConditionalFormattingConfig[] | undefined,
data: DataRecord[],
theme?: Record<string, any>,
alpha?: boolean,
disablePercentBounds = false,
) =>
columnConfig?.reduce(
(acc: ColorFormatters, config: ConditionalFormattingConfig) => {
const colorFunctionConfig =
disablePercentBounds && config.boundUnit === BoundUnit.Percent
? {
...config,
boundUnit: BoundUnit.Value,
minBound: undefined,
maxBound: undefined,
centerValue: undefined,
}
: config;
let resolvedColorScheme = config.colorScheme;
if (
theme &&
@@ -535,7 +357,7 @@ export const getColorFormatters: MemoizedFn<GetColorFormatters> = memoizeOne(
columnFormatting: config?.columnFormatting,
objectFormatting: config?.objectFormatting,
getColorFromValue: getColorFunction(
{ ...colorFunctionConfig, colorScheme: resolvedColorScheme },
{ ...config, colorScheme: resolvedColorScheme },
data.map(row => row[config.column!] as number),
alpha,
),
@@ -21,13 +21,10 @@ import {
Comparator,
getOpacity,
round,
getColorFormatters,
getColorFunction,
getDivergingColor,
BoundUnit,
PercentDenominator,
} from '../../src';
import {
getColorFormatters,
getReadableTextColor,
getNormalizedTextColor,
getTextColorForBackground,
@@ -87,18 +84,6 @@ test('getOpacity', () => {
expect(getOpacity(50, 75, NaN)).toEqual(1);
});
test('getDivergingColor parses string color endpoints', () => {
expect(
getDivergingColor(25, 0, 50, 100, '#ff0000', '#ffffff', '#008000'),
).toEqual('#ff8080');
});
test('getDivergingColor returns the segment endpoint outright when its range has zero width', () => {
expect(
getDivergingColor(10, 10, 10, 20, '#ff0000', '#ffffff', '#008000'),
).toEqual('#ffffff');
});
test('getColorFunction GREATER_THAN', () => {
const colorFunction = getColorFunction(
{
@@ -750,38 +735,6 @@ test('correct column config', () => {
expect(colorFormatters[2].getColorFromValue(100)).toEqual('#FF000087');
});
test('getColorFormatters falls back to automatic bounds for saved percentage rules when requested', () => {
const colorFormatters = getColorFormatters(
[
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
useGradient: true,
boundUnit: BoundUnit.Percent,
minBound: 0,
maxBound: 200,
},
{
operator: Comparator.None,
colorScheme: '#00FF00',
column: 'sum',
boundUnit: BoundUnit.Value,
},
],
mockData,
undefined,
undefined,
true,
);
expect(colorFormatters).toHaveLength(2);
expect(colorFormatters[0].column).toBe('count');
expect(colorFormatters[0].getColorFromValue(50)).toEqual('#FF000000');
expect(colorFormatters[0].getColorFromValue(100)).toEqual('#FF0000FF');
expect(colorFormatters[1].column).toBe('sum');
});
test('undefined column config', () => {
const colorFormatters = getColorFormatters(undefined, mockData);
expect(colorFormatters.length).toEqual(0);
@@ -1163,410 +1116,3 @@ test('should strip alpha channel when alpha is false and colorScheme has 9 chars
expect(colorFunction(100)).toEqual('#FF0000');
expect(colorFunction(100)).toHaveLength(7);
});
test('should discard the fixed alpha of a 9-char colorScheme before applying gradient opacity', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 50,
colorScheme: '#FF000080',
useGradient: true,
column: 'count',
},
countValues,
);
expect(colorFunction(75)).toEqual('#FF000087');
});
test('getColorFunction GREATER_THAN respects manual maxBound', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 0,
maxBound: 100,
colorScheme: '#FF0000',
column: 'count',
},
[25, 50],
);
expect(colorFunction(0)).toBeUndefined();
expect(colorFunction(50)).toEqual('#FF000087');
expect(colorFunction(100)).toEqual('#FF0000FF');
expect(colorFunction(150)).toEqual('#FF0000FF');
});
test('getColorFunction GREATER_THAN ignores a maxBound below its target', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 100,
maxBound: 90,
colorScheme: '#FF0000',
column: 'count',
},
[100, 150],
);
expect(colorFunction(125)).toEqual('#FF000087');
expect(colorFunction(150)).toEqual('#FF0000FF');
});
test('getColorFunction LESS_THAN respects manual minBound', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.LessThan,
targetValue: 100,
minBound: 0,
colorScheme: '#FF0000',
column: 'count',
},
[50, 75],
);
expect(colorFunction(100)).toBeUndefined();
expect(colorFunction(50)).toEqual('#FF000087');
expect(colorFunction(0)).toEqual('#FF0000FF');
expect(colorFunction(-50)).toEqual('#FF0000FF');
});
test('getColorFunction LESS_THAN ignores a minBound above its target', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.LessThan,
targetValue: 100,
minBound: 110,
colorScheme: '#FF0000',
column: 'count',
},
[50, 100],
);
expect(colorFunction(75)).toEqual('#FF000087');
expect(colorFunction(50)).toEqual('#FF0000FF');
});
test('getColorFunction GREATER_OR_EQUAL respects manual maxBound', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterOrEqual,
targetValue: 0,
maxBound: 100,
colorScheme: '#FF0000',
column: 'count',
},
[25, 50],
);
expect(colorFunction(-10)).toBeUndefined();
expect(colorFunction(0)).toEqual('#FF00000D');
expect(colorFunction(50)).toEqual('#FF000087');
expect(colorFunction(100)).toEqual('#FF0000FF');
});
test('getColorFunction LESS_OR_EQUAL respects manual minBound', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.LessOrEqual,
targetValue: 100,
minBound: 0,
colorScheme: '#FF0000',
column: 'count',
},
[50, 75],
);
expect(colorFunction(150)).toBeUndefined();
expect(colorFunction(100)).toEqual('#FF00000D');
expect(colorFunction(50)).toEqual('#FF000087');
expect(colorFunction(0)).toEqual('#FF0000FF');
});
test('getColorFunction NONE respects manual minBound and maxBound', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
minBound: 0,
maxBound: 200,
},
countValues,
);
expect(colorFunction(-10)).toEqual('#FF000000');
expect(colorFunction(0)).toEqual('#FF000000');
expect(colorFunction(100)).toEqual('#FF000080');
expect(colorFunction(200)).toEqual('#FF0000FF');
expect(colorFunction(250)).toEqual('#FF0000FF');
});
test('getColorFunction NONE applies a diverging low/mid/high scale when centerValue and all three colors are set', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#000000',
column: 'count',
minBound: 0,
maxBound: 100,
centerValue: 50,
lowColor: { r: 255, g: 0, b: 0, a: 1 },
midColor: { r: 255, g: 255, b: 255, a: 1 },
highColor: { r: 0, g: 128, b: 0, a: 1 },
},
[10, 90],
);
expect(colorFunction(0)).toEqual('#ff0000');
expect(colorFunction(25)).toEqual('#ff8080');
expect(colorFunction(50)).toEqual('#ffffff');
expect(colorFunction(75)).toEqual('#80c080');
expect(colorFunction(100)).toEqual('#008000');
});
test('getColorFunction NONE uses the solid base color when gradient is disabled for a complete diverging config', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#000000',
useGradient: false,
column: 'count',
minBound: 0,
maxBound: 100,
centerValue: 50,
lowColor: { r: 255, g: 0, b: 0, a: 1 },
midColor: { r: 255, g: 255, b: 255, a: 1 },
highColor: { r: 0, g: 128, b: 0, a: 1 },
},
[10, 90],
);
expect(colorFunction(25)).toEqual('#000000');
expect(colorFunction(75)).toEqual('#000000');
});
test('getColorFunction NONE ignores an incomplete diverging config and falls back to colorScheme', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
minBound: 0,
maxBound: 100,
centerValue: 50,
lowColor: { r: 255, g: 0, b: 0, a: 1 },
// midColor and highColor intentionally omitted
},
[10, 90],
);
expect(colorFunction(100)).toEqual('#FF0000FF');
});
test('getColorFunction NONE ignores a centerValue outside the min/max range', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
minBound: 0,
maxBound: 100,
centerValue: 150,
lowColor: { r: 255, g: 0, b: 0, a: 1 },
midColor: { r: 255, g: 255, b: 255, a: 1 },
highColor: { r: 0, g: 128, b: 0, a: 1 },
},
[10, 90],
);
expect(colorFunction(100)).toEqual('#FF0000FF');
});
test('getColorFunction NONE resolves percent bounds against column sum', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Sum,
minBound: 0,
maxBound: 100,
},
[50, 150],
);
// sum = 200, so minBound 0% -> 0, maxBound 100% -> 200 -- the same absolute
// range as the existing 'NONE respects manual minBound and maxBound' test
// above, just reached via percent-of-sum instead of typed directly.
expect(colorFunction(-10)).toEqual('#FF000000');
expect(colorFunction(0)).toEqual('#FF000000');
expect(colorFunction(100)).toEqual('#FF000080');
expect(colorFunction(200)).toEqual('#FF0000FF');
expect(colorFunction(250)).toEqual('#FF0000FF');
});
test('getColorFunction NONE resolves percent bounds against column max', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Max,
minBound: 0,
maxBound: 100,
},
[10, 40, 90],
);
// max = 90, so maxBound 100% -> 90.
expect(colorFunction(0)).toEqual('#FF000000');
expect(colorFunction(45)).toEqual('#FF000080');
expect(colorFunction(90)).toEqual('#FF0000FF');
});
test('getColorFunction NONE defaults percentDenominator to column max when unset', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
minBound: 0,
maxBound: 100,
},
[10, 40, 90],
);
expect(colorFunction(45)).toEqual('#FF000080');
expect(colorFunction(90)).toEqual('#FF0000FF');
});
test('getColorFunction NONE ignores percentDenominator and treats bounds as absolute when boundUnit is unset', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
percentDenominator: PercentDenominator.Sum,
minBound: 0,
maxBound: 200,
},
[50, 150],
);
// percentDenominator is present but boundUnit is not 'percent', so minBound
// and maxBound are used exactly as typed rather than resolved against the
// sum.
expect(colorFunction(100)).toEqual('#FF000080');
expect(colorFunction(200)).toEqual('#FF0000FF');
});
test('getColorFunction NONE applies a percent-resolved centerValue to the diverging scale', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#000000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Max,
minBound: 0,
maxBound: 100,
centerValue: 50,
lowColor: { r: 255, g: 0, b: 0, a: 1 },
midColor: { r: 255, g: 255, b: 255, a: 1 },
highColor: { r: 0, g: 128, b: 0, a: 1 },
},
[0, 100],
);
// max = 100, so every bound resolves to exactly the value it names. This
// verifies that boundUnit and percentDenominator also apply to centerValue.
expect(colorFunction(0)).toEqual('#ff0000');
expect(colorFunction(50)).toEqual('#ffffff');
expect(colorFunction(100)).toEqual('#008000');
});
test('getColorFunction NONE degrades percent bounds to unset when the column has no numeric values', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Sum,
minBound: 0,
maxBound: 100,
},
[],
);
// No numeric values to sum -> both bounds resolve to unset -> the None
// branch's own no-manual-bound path runs Math.min/Math.max on an empty
// array (Infinity/-Infinity), so cutoffValue > extremeValue and every
// value is rejected -- the same degrade-safe "no coloring" outcome Phase
// 1/2 use elsewhere for invalid config shapes.
expect(colorFunction(0)).toBeUndefined();
expect(colorFunction(50)).toBeUndefined();
});
test('getColorFunction NONE falls back to automatic bounds for a non-positive column max', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Max,
minBound: 0,
maxBound: 100,
},
[-100, -5],
);
// A negative denominator would reverse the configured 0%-100% bounds.
// Treating both as unset preserves an ordered, data-derived range.
expect(colorFunction(-100)).toEqual('#FF000000');
expect(colorFunction(-5)).toEqual('#FF0000FF');
});
test('getColorFunction NONE uses the sum of magnitudes for mixed-sign values', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Sum,
minBound: 0,
maxBound: 100,
},
[500, -499, 400, -400],
);
// The signed sum is 1, but the magnitude sum is 1799. Using the signed sum
// would make every positive value saturate at the upper endpoint.
expect(colorFunction(-400)).toEqual('#FF000000');
expect(colorFunction(0)).toEqual('#FF000000');
expect(colorFunction(899.5)).toEqual('#FF000080');
expect(colorFunction(1799)).toEqual('#FF0000FF');
});
test('getColorFunction NONE keeps the running column max when a later value is smaller', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.None,
colorScheme: '#FF0000',
column: 'count',
boundUnit: BoundUnit.Percent,
percentDenominator: PercentDenominator.Max,
minBound: 0,
maxBound: 100,
},
[90, 10, 40],
);
// max = 90 regardless of position in the array, so maxBound 100% -> 90.
expect(colorFunction(45)).toEqual('#FF000080');
expect(colorFunction(90)).toEqual('#FF0000FF');
});
test('getColorFunction GREATER_THAN ignores diverging fields even when fully set', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.GreaterThan,
targetValue: 0,
colorScheme: '#FF0000',
column: 'count',
centerValue: 50,
lowColor: { r: 255, g: 0, b: 0, a: 1 },
midColor: { r: 255, g: 255, b: 255, a: 1 },
highColor: { r: 0, g: 128, b: 0, a: 1 },
},
[25, 100],
);
expect(colorFunction(100)).toEqual('#FF0000FF');
});
@@ -53,7 +53,6 @@
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.7",
"@braintree/sanitize-url": "^7.1.2",
"@types/d3-format": "^3.0.4",
"@types/json-bigint": "^1.0.4",
"@visx/responsive": "^4.0.0",
"ace-builds": "^1.44.0",
@@ -97,13 +96,14 @@
},
"devDependencies": {
"@emotion/styled": "^11.14.1",
"@types/d3-format": "^3.0.4",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^4.0.9",
"@types/d3-time": "^3.0.4",
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.25",
"@types/node": "^26.4.0",
"@types/node": "^26.2.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -119,7 +119,7 @@
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "*",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -146,61 +146,3 @@ test('Calling "onConfirm" only after typing "delete" in the input', async () =>
// confirm input has been cleared
expect(screen.getByTestId('delete-modal-input')).toHaveValue('');
});
test('external disable keeps the destructive action unavailable after confirmation', async () => {
const onConfirm = jest.fn();
render(
<DeleteModal
title="Delete permanently?"
description="This cannot be undone."
onConfirm={onConfirm}
onHide={jest.fn()}
open
disablePrimaryButton
/>,
);
await userEvent.type(screen.getByTestId('delete-modal-input'), 'DELETE');
expect(screen.getByRole('button', { name: 'Delete' })).toBeDisabled();
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
expect(onConfirm).not.toHaveBeenCalled();
});
test('loading disables the destructive action and exposes busy state', async () => {
render(
<DeleteModal
title="Delete permanently?"
description="Checking dependencies"
onConfirm={jest.fn()}
onHide={jest.fn()}
open
loading
/>,
);
await userEvent.type(screen.getByTestId('delete-modal-input'), 'DELETE');
expect(screen.getByTestId('modal-confirm-button')).toBeDisabled();
expect(screen.getByTestId('antd-modal')).toHaveAttribute('aria-busy', 'true');
});
test('confirmation reset key clears and re-arms type-to-confirm', async () => {
const props = {
title: 'Delete permanently?',
description: 'This cannot be undone.',
onConfirm: jest.fn(),
onHide: jest.fn(),
open: true,
};
const { rerender } = render(
<DeleteModal {...props} confirmationResetKey="initial" />,
);
await userEvent.type(screen.getByTestId('delete-modal-input'), 'DELETE');
expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled();
rerender(<DeleteModal {...props} confirmationResetKey="impact-changed" />);
expect(screen.getByTestId('delete-modal-input')).toHaveValue('');
expect(screen.getByRole('button', { name: 'Delete' })).toBeDisabled();
});
@@ -40,9 +40,6 @@ export function DeleteModal({
title,
name,
recoverable = false,
disablePrimaryButton = false,
loading = false,
confirmationResetKey,
}: DeleteModalProps) {
// Recoverable (archive) deletes drop the "type DELETE to confirm" step;
// a permanent delete keeps it.
@@ -57,11 +54,6 @@ export function DeleteModal({
}
}, [open]);
useEffect(() => {
setConfirmation('');
setDisableChange(true);
}, [confirmationResetKey]);
// Re-arm the gate alongside clearing the text: resetting only the string
// leaves disableChange=false behind, so a user who typed DELETE, cancelled,
// and reopened would face an enabled Delete button over an empty input.
@@ -84,19 +76,14 @@ export function DeleteModal({
};
const onPressEnter = () => {
if (!disableChange && !disablePrimaryButton && !loading) {
if (!disableChange) {
confirm();
}
};
return (
<Modal
disablePrimaryButton={
disablePrimaryButton ||
loading ||
(showConfirmationInput ? disableChange : false)
}
primaryButtonLoading={loading}
disablePrimaryButton={showConfirmationInput ? disableChange : false}
onHide={hide}
onHandledPrimaryAction={confirm}
primaryButtonName={recoverable ? t('Archive') : t('Delete')}
@@ -104,7 +91,6 @@ export function DeleteModal({
show={open}
name={name}
title={title}
wrapProps={{ 'aria-busy': loading }}
centered
>
{description}
@@ -32,10 +32,4 @@ export interface DeleteModalProps {
* friction and uses a primary (non-danger) confirm button.
*/
recoverable?: boolean;
/** Disable confirmation independently of the typed-text gate. */
disablePrimaryButton?: boolean;
/** Show progress on the primary action and prevent duplicate submission. */
loading?: boolean;
/** Clear and re-arm the typed-text gate when the reviewed data changes. */
confirmationResetKey?: string | number;
}
@@ -0,0 +1,115 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { fireEvent, render, screen, userEvent } from '@superset-ui/core/spec';
import { useState } from 'react';
import { DynamicEditableTitle } from '.';
const Harness = ({ initialTitle = 'Original' }: { initialTitle?: string }) => {
const [title, setTitle] = useState(initialTitle);
return (
<DynamicEditableTitle
title={title}
placeholder="placeholder"
canEdit
label="Title"
onSave={setTitle}
/>
);
};
test('rapid typing then backspacing keeps every keystroke', async () => {
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'abc', { delay: 1 });
expect(input.value).toBe('Originalabc');
await userEvent.type(input, '{backspace}{backspace}{backspace}', {
delay: 1,
});
expect(input.value).toBe('Original');
});
test('a change event that arrives before isEditing flips is not dropped', () => {
// Reproduces the regression: the input is focused but `isEditing` is still
// false because no click has been registered yet (e.g. focus arrived via
// tab, autofocus, or programmatic focus). The pre-fix `handleChange`
// bailed out with `!isEditing`, dropping the keystroke. Because the
// input is controlled, antd's internal `useMergedState` then resyncs the
// DOM value back to the (stale) `props.value`, so the user sees their
// typed character disappear. This test fires a raw change event so it
// doesn't go through userEvent's implicit click.
const onSave = jest.fn();
render(
<DynamicEditableTitle
title="Foo"
placeholder="placeholder"
canEdit
label="Title"
onSave={onSave}
/>,
);
const input = screen.getByRole('textbox') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'FooX' } });
expect(input.value).toBe('FooX');
});
test('prop changes mid-edit do not clobber unsaved typing', async () => {
// Rerender DynamicEditableTitle directly with a changed title prop so the
// sync effect actually runs. Going through Harness would not exercise the
// bug because Harness owns its own state and only reads initialTitle once.
const onSave = jest.fn();
const props = {
placeholder: 'placeholder',
canEdit: true,
label: 'Title',
onSave,
};
const { rerender } = render(<DynamicEditableTitle {...props} title="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
await userEvent.type(input, 'X', { delay: 1 });
expect(input.value).toBe('FooX');
rerender(<DynamicEditableTitle {...props} title="Bar" />);
expect(input.value).toBe('FooX');
// Locks in commit semantics: blur after a real edit must persist the
// user's typed value, even when a competing parent-driven title arrived
// mid-edit.
fireEvent.blur(input);
expect(onSave).toHaveBeenCalledWith('FooX');
});
test('passive focus then parent-driven title change then blur does not revert', () => {
// Phantom-revert scenario: user clicks the input but does not type, the
// parent autosaves a new title from elsewhere, then the user blurs. The
// component must NOT call onSave with the stale local value, otherwise it
// would silently overwrite the parent's update.
const onSave = jest.fn();
const props = {
placeholder: 'placeholder',
canEdit: true,
label: 'Title',
onSave,
};
const { rerender } = render(<DynamicEditableTitle {...props} title="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
userEvent.click(input);
rerender(<DynamicEditableTitle {...props} title="Bar" />);
fireEvent.blur(input);
expect(onSave).not.toHaveBeenCalled();
});
@@ -16,20 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from '@superset-ui/core/spec';
import { useState } from 'react';
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { DynamicEditableTitle } from '.';
import type { DynamicEditableTitleProps } from './types';
const createProps = (
overrides: Partial<DynamicEditableTitleProps> = {},
): DynamicEditableTitleProps => ({
const createProps = (overrides: Record<string, any> = {}) => ({
title: 'Chart title',
placeholder: 'Add the name of the chart',
canEdit: true,
@@ -38,147 +28,41 @@ const createProps = (
...overrides,
});
const makeRect = (width: number): DOMRect => ({
bottom: 0,
height: 0,
left: 0,
right: width,
top: 0,
width,
x: 0,
y: 0,
toJSON: () => ({}),
});
const Harness = ({ initialTitle = 'Original' }: { initialTitle?: string }) => {
const [title, setTitle] = useState(initialTitle);
return (
<DynamicEditableTitle
title={title}
placeholder="placeholder"
canEdit
label="Title"
onSave={setTitle}
/>
);
};
test('renders chart title', () => {
const props = createProps();
render(<DynamicEditableTitle {...props} />);
expect(screen.getByText('Chart title')).toBeVisible();
});
test('renders placeholder', () => {
const props = createProps({
title: '',
describe('Chart editable title', () => {
test('renders chart title', () => {
const props = createProps();
render(<DynamicEditableTitle {...props} />);
expect(screen.getByText('Chart title')).toBeVisible();
});
render(<DynamicEditableTitle {...props} />);
expect(screen.getByText('Add the name of the chart')).toBeVisible();
});
test('click, edit and save title', async () => {
const props = createProps();
render(<DynamicEditableTitle {...props} />);
const textboxElement = screen.getByRole('textbox');
await userEvent.click(textboxElement);
await userEvent.type(textboxElement, ' edited');
expect(screen.getByText('Chart title edited')).toBeVisible();
await userEvent.type(textboxElement, '{enter}');
expect(props.onSave).toHaveBeenCalled();
});
test('renders in non-editable mode', async () => {
const props = createProps({ canEdit: false });
render(<DynamicEditableTitle {...props} />);
const titleElement = screen.getByLabelText('Chart title');
const inputElement = screen.getByRole('textbox');
expect(inputElement).toBeDisabled();
expect(titleElement).toBeVisible();
await userEvent.click(titleElement);
await userEvent.type(titleElement, ' edited{enter}');
expect(props.onSave).not.toHaveBeenCalled();
});
test('rapid typing then backspacing keeps every keystroke', async () => {
render(<Harness />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await userEvent.click(input);
await userEvent.type(input, 'abc', { delay: 1 });
expect(input.value).toBe('Originalabc');
await userEvent.type(input, '{backspace}{backspace}{backspace}', {
delay: 1,
test('renders placeholder', () => {
const props = createProps({
title: '',
});
render(<DynamicEditableTitle {...props} />);
expect(screen.getByText('Add the name of the chart')).toBeVisible();
});
expect(input.value).toBe('Original');
});
test('a change event that arrives before edit mode is committed is not dropped', () => {
const onSave = jest.fn();
render(
<DynamicEditableTitle
title="Foo"
placeholder="placeholder"
canEdit
label="Title"
onSave={onSave}
/>,
);
const input = screen.getByRole('textbox') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'FooX' } });
expect(input.value).toBe('FooX');
});
test('click, edit and save title', async () => {
const props = createProps();
render(<DynamicEditableTitle {...props} />);
const textboxElement = screen.getByRole('textbox');
await userEvent.click(textboxElement);
await userEvent.type(textboxElement, ' edited');
expect(screen.getByText('Chart title edited')).toBeVisible();
await userEvent.type(textboxElement, '{enter}');
expect(props.onSave).toHaveBeenCalled();
});
test('prop changes mid-edit do not clobber unsaved typing', async () => {
const onSave = jest.fn();
const props = {
placeholder: 'placeholder',
canEdit: true,
label: 'Title',
onSave,
};
const { rerender } = render(<DynamicEditableTitle {...props} title="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await userEvent.click(input);
await userEvent.type(input, 'X', { delay: 1 });
expect(input.value).toBe('FooX');
rerender(<DynamicEditableTitle {...props} title="Bar" />);
expect(input.value).toBe('FooX');
fireEvent.blur(input);
expect(onSave).toHaveBeenCalledWith('FooX');
});
test('passive focus then parent-driven title change then blur does not revert', async () => {
const onSave = jest.fn();
const props = {
placeholder: 'placeholder',
canEdit: true,
label: 'Title',
onSave,
};
const { rerender } = render(<DynamicEditableTitle {...props} title="Foo" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await userEvent.click(input);
rerender(<DynamicEditableTitle {...props} title="Bar" />);
fireEvent.blur(input);
expect(onSave).not.toHaveBeenCalled();
});
test('rounds fractional title measurements up when sizing the input', async () => {
const getBoundingClientRect = jest
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(function getRect(this: HTMLElement) {
return this.classList.contains('input-sizer')
? makeRect(280.31)
: makeRect(0);
});
try {
render(<DynamicEditableTitle {...createProps({ title: 'Trends' })} />);
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveStyle({ width: '281px' });
});
} finally {
getBoundingClientRect.mockRestore();
}
test('renders in non-editable mode', async () => {
const props = createProps({ canEdit: false });
render(<DynamicEditableTitle {...props} />);
const titleElement = screen.getByLabelText('Chart title');
const inputElement = screen.getByRole('textbox');
expect(inputElement).toBeDisabled();
expect(titleElement).toBeVisible();
await userEvent.click(titleElement);
await userEvent.type(titleElement, ' edited{enter}');
expect(props.onSave).not.toHaveBeenCalled();
});
});
@@ -34,9 +34,6 @@ import { Input } from '../Input';
import type { InputRef } from '../Input';
import type { DynamicEditableTitleProps } from './types';
const measureWidth = (element: HTMLElement) =>
Math.ceil(element.getBoundingClientRect().width);
const titleStyles = (theme: SupersetTheme) => css`
display: flex;
font-size: ${theme.fontSizeXL}px;
@@ -120,14 +117,14 @@ export const DynamicEditableTitle = memo(
// a trick to make the input grow when user types text
// we make an additional span component, place it somewhere out of view and
// mirror the input value, then measure the span synchronously (pre-paint)
// to resize the input element. Measuring in a useLayoutEffect forces a
// sync layout, so the input width updates in the same commit as the value
// change — preventing a flicker frame where the input is shown with new
// value but stale width.
// to resize the input element. Reading offsetWidth in a useLayoutEffect
// forces a sync layout, so the input width updates in the same commit as
// the value change — preventing a flicker frame where the input is shown
// with new value but stale width.
useLayoutEffect(() => {
if (sizerRef.current) {
sizerRef.current.textContent = currentTitle || placeholder;
setInputWidth(measureWidth(sizerRef.current));
setInputWidth(sizerRef.current.offsetWidth);
}
}, [currentTitle, placeholder]);
@@ -138,7 +135,7 @@ export const DynamicEditableTitle = memo(
let cancelled = false;
document.fonts?.ready?.then(() => {
if (!cancelled && sizerRef.current) {
setInputWidth(measureWidth(sizerRef.current));
setInputWidth(sizerRef.current.offsetWidth);
}
});
return () => {
@@ -239,7 +236,6 @@ export const DynamicEditableTitle = memo(
onClick={handleClick}
onPressEnter={handleKeyPress}
placeholder={placeholder}
style={inputWidth > 0 ? { width: inputWidth } : undefined}
css={css`
${
!canEdit &&
@@ -250,6 +246,13 @@ export const DynamicEditableTitle = memo(
}
font-size: ${theme.fontSizeXL}px;
transition: auto;
${
inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`
}
`}
disabled={!canEdit}
/>
@@ -752,97 +752,6 @@ test('displays an error message when an exception is thrown while fetching', asy
expect(screen.getByText(error)).toBeInTheDocument();
});
test('clears a previous fetch error once a later fetch succeeds', async () => {
const error = 'Fetch error';
const loadOptions = jest.fn(async (search: string) => {
if (search === 'fail') {
throw new Error(error);
}
// Report more results than are loaded so every new search hits the server.
return { data: [{ label: search, value: search }], totalCount: 100 };
});
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
await type('fail');
expect(await screen.findByText(error)).toBeInTheDocument();
await type('retry');
expect(await findSelectOption('retry')).toBeInTheDocument();
expect(screen.queryByText(error)).not.toBeInTheDocument();
});
test('clears a previous fetch error when the next page comes from cache', async () => {
const error = 'Fetch error';
const loadOptions = jest.fn(
async (search: string, page: number, pageSize: number) => {
if (search === 'fail') {
throw new Error(error);
}
return defaultProps.options(search, page, pageSize);
},
);
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
await findSelectOption(OPTIONS[0].label);
await type('fail');
expect(await screen.findByText(error)).toBeInTheDocument();
// Clearing the input re-requests the first page, which is already cached
// and therefore never reaches the network.
await userEvent.clear(getSelect());
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
expect(screen.queryByText(error)).not.toBeInTheDocument();
});
test('ignores a late failure from a search the user has moved on from', async () => {
const error = 'Fetch error';
let rejectSlow: (reason: Error) => void = () => {};
const loadOptions = jest.fn(async (search: string) => {
if (search === 'slow') {
return new Promise<never>((_, reject) => {
rejectSlow = reject;
});
}
return { data: [{ label: search, value: search }], totalCount: 100 };
});
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
await type('slow');
await waitFor(() => expect(loadOptions).toHaveBeenCalledWith('slow', 0, 10));
await type('fast');
expect(await findSelectOption('fast')).toBeInTheDocument();
rejectSlow(new Error(error));
await waitFor(() => expect(loadOptions).toHaveBeenCalledTimes(3));
expect(screen.queryByText(error)).not.toBeInTheDocument();
expect(await findSelectOption('fast')).toBeInTheDocument();
});
test('still surfaces a base-fetch failure that lands mid-search', async () => {
const error = 'Fetch error';
let rejectBase: (reason: Error) => void = () => {};
const loadOptions = jest.fn(async (search: string) => {
if (search === '') {
// Defer the base page so it can fail after the user starts searching.
return new Promise<never>((_, reject) => {
rejectBase = reject;
});
}
return { data: [{ label: search, value: search }], totalCount: 100 };
});
render(<AsyncSelect {...defaultProps} options={loadOptions} />);
await open();
await type('abc');
expect(await findSelectOption('abc')).toBeInTheDocument();
// Base fetches keep the accumulator and allValuesLoaded up to date even
// mid-search, so their failures must surface too.
rejectBase(new Error(error));
expect(await screen.findByText(error)).toBeInTheDocument();
});
test('does not fire a new request for the same search input', async () => {
const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
render(
@@ -401,10 +401,6 @@ const AsyncSelect = forwardRef(
const fetchPage = useMemo(
() => (search: string, page: number) => {
setPage(page);
// A previous fetch may have left an error on screen. Clear it before
// any early return so a page served from cache, or from an already
// complete option set, is not shown next to a stale error.
setError('');
if (allValuesLoaded) {
setIsLoading(false);
return;
@@ -490,19 +486,7 @@ const AsyncSelect = forwardRef(
setTotalCount(totalCount);
}
})
.catch((response: Response) => {
// Mirror the results guard above: a failure belonging to a search
// the user has since moved on from must not replace the outcome
// of the fetch that superseded it. Base fetches (search === '')
// are exempt exactly as their results are — they maintain the
// accumulator and allValuesLoaded, so their failures must stay
// visible even when they land mid-search. The consumer's onError
// is skipped along with the banner for superseded searches.
if (search && inputValueRef.current !== search) {
return undefined;
}
return internalOnError(response);
})
.catch(internalOnError)
.finally(() => {
inFlightFetchesRef.current = Math.max(
0,
@@ -17,7 +17,6 @@
* under the License.
*/
import {
act,
createEvent,
fireEvent,
render,
@@ -27,7 +26,6 @@ import {
within,
} from '@superset-ui/core/spec';
import { formatNumber } from '@superset-ui/core';
import { Constants } from '@superset-ui/core/components';
import { Select } from '.';
type Option = {
@@ -71,38 +69,6 @@ const NULL_OPTION = { label: '<NULL>', value: null } as unknown as {
value: number;
};
// A dedicated option set for the stableSelectAll tests, kept local so it is
// isolated from tests that mutate the shared OPTIONS array (e.g. toggling
// `disabled`). A search for "Ap" matches a strict subset (Apple, Apricot).
const STABLE_OPTIONS = [
{ label: 'Apple', value: 1 },
{ label: 'Apricot', value: 2 },
{ label: 'Banana', value: 3 },
{ label: 'Blueberry', value: 4 },
{ label: 'Cherry', value: 5 },
{ label: 'Cranberry', value: 6 },
];
// A grouped option list for the stableSelectAll tests: bulk "Select all" must
// target the five leaf options, not the two value-less group headers.
const GROUPED_STABLE_OPTIONS = [
{
label: 'Citrus',
options: [
{ label: 'Orange', value: 1 },
{ label: 'Lemon', value: 2 },
],
},
{
label: 'Berries',
options: [
{ label: 'Strawberry', value: 3 },
{ label: 'Blueberry', value: 4 },
{ label: 'Raspberry', value: 5 },
],
},
];
const defaultProps = {
allowClear: true,
ariaLabel: ARIA_LABEL,
@@ -1186,342 +1152,6 @@ test('abbreviates large numbers in bulk action buttons', async () => {
expect(await screen.findByText('Select all (1.5k)')).toBeInTheDocument();
});
// The stableSelectAll tests advance fake timers past the FAST_DEBOUNCE so the
// component's own search filter narrows `visibleOptions` (and flips
// `isSearching`) before asserting — the exact point at which the un-fixed code
// drops the badge to the search-scoped count. Asserting before that debounce
// fires (as an earlier revision did) would pass against the un-fixed code too.
test('stableSelectAll pins the "Select all" count to the full option set while searching', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
render(
<Select
{...defaultProps}
options={STABLE_OPTIONS}
mode="multiple"
stableSelectAll
/>,
);
const select = getSelect();
userEvent.click(select);
// Baseline: the full-column count is shown before any search.
expect(
await screen.findByText(selectAllButtonText(STABLE_OPTIONS.length)),
).toBeInTheDocument();
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
// The visible list narrows to the searched subset...
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
// ...but the badge must still report the full column count.
expect(
screen.getByText(selectAllButtonText(STABLE_OPTIONS.length)),
).toBeInTheDocument();
expect(screen.queryByText(selectAllButtonText(2))).not.toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('without stableSelectAll the "Select all" count narrows to the searched subset', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
render(
<Select {...defaultProps} options={STABLE_OPTIONS} mode="multiple" />,
);
const select = getSelect();
userEvent.click(select);
expect(
await screen.findByText(selectAllButtonText(STABLE_OPTIONS.length)),
).toBeInTheDocument();
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
// Generic consumers keep the search-scoped count.
expect(screen.getByText(selectAllButtonText(2))).toBeInTheDocument();
expect(
screen.queryByText(selectAllButtonText(STABLE_OPTIONS.length)),
).not.toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll selects the entire option set even while a search is active', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
const onChange = jest.fn();
render(
<Select
{...defaultProps}
options={STABLE_OPTIONS}
mode="multiple"
stableSelectAll
onChange={onChange}
/>,
);
const select = getSelect();
userEvent.click(select);
await screen.findByText(selectAllButtonText(STABLE_OPTIONS.length));
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
await userEvent.click(
screen.getByText(selectAllButtonText(STABLE_OPTIONS.length)),
);
await waitFor(() => expect(onChange).toHaveBeenCalled());
expect(onChange.mock.calls.at(-1)?.[0]).toHaveLength(STABLE_OPTIONS.length);
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll excludes disabled options from the full-set count while searching', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
// Cherry is disabled → 5 of the 6 options are selectable.
const options = STABLE_OPTIONS.map(option =>
option.label === 'Cherry' ? { ...option, disabled: true } : option,
);
render(
<Select
{...defaultProps}
options={options}
mode="multiple"
stableSelectAll
/>,
);
const select = getSelect();
userEvent.click(select);
expect(await screen.findByText(selectAllButtonText(5))).toBeInTheDocument();
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
// The count reflects the full selectable set (5), not the 2 visible.
expect(screen.getByText(selectAllButtonText(5))).toBeInTheDocument();
expect(screen.queryByText(selectAllButtonText(2))).not.toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll deduplicates already-selected values when selecting the full set', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
const onChange = jest.fn();
// Apple (1) is already selected; the "Ap" search narrows the visible list
// to a subset while "Select all" still targets the whole set.
render(
<Select
{...defaultProps}
options={STABLE_OPTIONS}
mode="multiple"
stableSelectAll
value={[{ label: 'Apple', value: 1 }]}
onChange={onChange}
/>,
);
const select = getSelect();
userEvent.click(select);
await screen.findByText(selectAllButtonText(STABLE_OPTIONS.length));
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
await userEvent.click(
screen.getByText(selectAllButtonText(STABLE_OPTIONS.length)),
);
await waitFor(() => expect(onChange).toHaveBeenCalled());
// Full set is 6; the already-selected Apple is not duplicated (a failed
// dedup would push it again and yield 7).
expect(onChange.mock.calls.at(-1)?.[0]).toHaveLength(STABLE_OPTIONS.length);
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll keeps "Clear" counting and clearing the full selection while searching', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
const onChange = jest.fn();
// Pre-select Banana (3) and Cherry (5); neither matches the "Ap" search.
render(
<Select
{...defaultProps}
options={STABLE_OPTIONS}
mode="multiple"
stableSelectAll
value={[
{ label: 'Banana', value: 3 },
{ label: 'Cherry', value: 5 },
]}
onChange={onChange}
/>,
);
const select = getSelect();
userEvent.click(select);
expect(
await screen.findByText(deselectAllButtonText(2)),
).toBeInTheDocument();
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
// "Clear" still reflects the full selection (2), not the visible subset (0).
expect(screen.getByText(deselectAllButtonText(2))).toBeInTheDocument();
expect(
screen.queryByText(deselectAllButtonText(0)),
).not.toBeInTheDocument();
// Clicking it removes the whole selection even though those values are not
// in the search results.
await userEvent.click(screen.getByText(deselectAllButtonText(2)));
await waitFor(() => expect(onChange).toHaveBeenCalled());
expect(onChange.mock.calls.at(-1)?.[0]).toHaveLength(0);
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll "Clear" count matches the action for a selected <NULL> value while searching', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
const onChange = jest.fn();
// The <NULL> option carries a falsy value, which "Select all" skips but
// "Clear" (like the un-gated path) still removes. Pre-select <NULL> and
// Banana (3); "Ap" hides both. The Clear count must equal what Clear
// removes — otherwise the label overstates the action.
render(
<Select
{...defaultProps}
options={[...STABLE_OPTIONS, NULL_OPTION]}
mode="multiple"
stableSelectAll
value={[NULL_OPTION, { label: 'Banana', value: 3 }]}
onChange={onChange}
/>,
);
const select = getSelect();
userEvent.click(select);
expect(
await screen.findByText(deselectAllButtonText(2)),
).toBeInTheDocument();
await userEvent.type(select, 'Ap');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
await waitFor(() => expect(getAllSelectOptions().length).toBe(2));
// Count still reflects the full selection (2)...
expect(screen.getByText(deselectAllButtonText(2))).toBeInTheDocument();
// ...and clicking removes all of it, including the <NULL> selection.
await userEvent.click(screen.getByText(deselectAllButtonText(2)));
await waitFor(() => expect(onChange).toHaveBeenCalled());
expect(onChange.mock.calls.at(-1)?.[0]).toHaveLength(0);
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll does not read a normal selection as the "Select all" sentinel while searching', async () => {
jest.useFakeTimers({ advanceTimers: true });
try {
// Pre-select four values, then search so exactly three eligible options are
// visible: selectValue.length (4) === selectAllEligible.length (3) + 1 — the
// coincidence that used to flip selectAllMode true. stableSelectAll adds real
// values with no phantom "Select all" slot, so the collapsed-tag count must
// not subtract one for a sentinel that does not exist.
render(
<Select
{...defaultProps}
options={STABLE_OPTIONS}
mode="multiple"
stableSelectAll
maxTagCount={2}
value={[
{ label: 'Apple', value: 1 },
{ label: 'Apricot', value: 2 },
{ label: 'Banana', value: 3 },
{ label: 'Blueberry', value: 4 },
]}
/>,
);
const select = getSelect();
userEvent.click(select);
await userEvent.type(select, 'erry');
act(() => {
jest.advanceTimersByTime(Constants.FAST_DEBOUNCE + 50);
});
// Blueberry, Cherry, Cranberry match "erry".
await waitFor(() => expect(getAllSelectOptions().length).toBe(3));
// Four selected, two shown → two hidden. The overflow badge must report
// "+ 2 ...", not the sentinel-undercounted "+ 1 ...".
expect(screen.getByText('+ 2 ...')).toBeInTheDocument();
expect(screen.queryByText('+ 1 ...')).not.toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('stableSelectAll counts and selects grouped options by their leaf values', async () => {
const onChange = jest.fn();
render(
<Select
{...defaultProps}
options={GROUPED_STABLE_OPTIONS}
mode="multiple"
stableSelectAll
onChange={onChange}
/>,
);
const select = getSelect();
userEvent.click(select);
// Five leaf options across two groups. The group headers carry no value, so
// without flattening the full set the count collapses to zero and the bulk
// actions disappear entirely.
const selectAll = await screen.findByText(selectAllButtonText(5));
expect(selectAll).toBeInTheDocument();
await userEvent.click(selectAll);
await waitFor(() => expect(onChange).toHaveBeenCalled());
const [selectedValues, selectedOptions] = onChange.mock.calls.at(-1) ?? [];
const valueOf = (item: number | { value: number }) =>
item && typeof item === 'object' ? item.value : item;
const sortNumeric = (values: number[]) => [...values].sort((a, b) => a - b);
// The five leaf values are selected — never the value-less group headers.
expect(sortNumeric(selectedValues.map(valueOf))).toEqual([1, 2, 3, 4, 5]);
// The option metadata alongside the values is also the flattened leaf set,
// not an empty array left over from filtering the group nodes.
expect(sortNumeric(selectedOptions.map(valueOf))).toEqual([1, 2, 3, 4, 5]);
});
test('dropdown takes full width of the select input for multi select', async () => {
render(
<div style={{ width: '400px' }}>
@@ -78,23 +78,6 @@ import {
import { Space } from '../Space';
import { Button } from '../Button';
// An option is eligible for a bulk "Select all" when it carries a truthy value
// and is neither disabled nor the transient "create new option" entry. Shared
// by the count, the visibility gate, and the click handler so they cannot drift.
const isBulkSelectable = (option: SelectOptionsType[number]): boolean =>
Boolean(option.value) && !option.disabled && !option.isNewOption;
// Grouped option lists nest their selectable entries under `.options`; the group
// headers themselves carry no `value`. Flatten one level so the bulk "Select
// all" machinery targets the actual leaf options rather than the headers. A flat
// list passes through unchanged.
const flattenGroupedOptions = (options: SelectOptionsType): SelectOptionsType =>
options.flatMap(option =>
'options' in option && Array.isArray(option.options)
? (option.options as SelectOptionsType)
: option,
);
/**
* This component is a customized version of the Antdesign 4.X Select component
* https://ant.design/components/select/.
@@ -115,7 +98,6 @@ const Select = forwardRef(
allowNewOptions = false,
allowNewOptionsOnPaste = false,
allowSelectAll = true,
stableSelectAll = false,
ariaLabel,
autoClearSearchValue = true,
filterOption = true,
@@ -311,27 +293,6 @@ const Select = forwardRef(
[visibleOptions],
);
// The full (search-independent) option set flattened to its leaf options, so
// the stableSelectAll bulk machinery treats grouped columns the same as flat
// ones. Gated on stableSelectAll so consumers that don't use the feature skip
// the work; a flat list is returned unchanged either way.
const flatFullSelectOptions = useMemo(
() =>
stableSelectAll
? flattenGroupedOptions(fullSelectOptions)
: EMPTY_OPTIONS,
[fullSelectOptions, stableSelectAll],
);
// The stable, full-set counterpart of enabledOptions: every bulk-selectable
// option across the entire (search-independent) option set. Used when
// stableSelectAll is on so the "Select all" action and visibility stay
// pinned to the full column while a search narrows visibleOptions.
const fullSelectAllOptions = useMemo(
() => flatFullSelectOptions.filter(isBulkSelectable),
[flatFullSelectOptions],
);
const selectAllEligible = useMemo(
() =>
visibleOptions.filter(
@@ -347,62 +308,49 @@ const Select = forwardRef(
!isSingleMode &&
allowSelectAll &&
selectOptions.length > 0 &&
// When stableSelectAll is on, gate visibility on the full eligible set
// so the bulk control does not hide/flicker while a search narrows
// visibleOptions.
(stableSelectAll
? fullSelectAllOptions.length
: enabledOptions.length) > 1,
enabledOptions.length > 1,
[
isSingleMode,
allowSelectAll,
selectOptions.length,
enabledOptions.length,
stableSelectAll,
fullSelectAllOptions.length,
],
);
const selectAllMode = useMemo(
() =>
// stableSelectAll adds real values across the full column with no legacy
// "Select all" sentinel occupying a slot in selectValue, so the
// eligible+1 phantom detection does not apply. Force it off so a search
// that narrows selectAllEligible cannot make a normal selection read as
// the sentinel (which drives the collapsed-tag off-by-one).
!stableSelectAll &&
ensureIsArray(selectValue).length === selectAllEligible.length + 1,
[selectValue, selectAllEligible, stableSelectAll],
() => ensureIsArray(selectValue).length === selectAllEligible.length + 1,
[selectValue, selectAllEligible],
);
const bulkSelectCounts = useMemo(() => {
const selectedValuesSet = new Set(
ensureIsArray(selectValue).map(getValue),
);
// When stableSelectAll is on, both counts reduce over the full
// (search-independent) loaded option set — flattened so grouped columns
// count their leaf options — so they stay stable and consistent with each
// other while searching. When it is off, countSource === visibleOptions,
// so the counts are unchanged for generic consumers.
const countSource = stableSelectAll
? flatFullSelectOptions
: visibleOptions;
const selectable = countSource.reduce((acc, option) => {
// "Select all" only adds, so the post-click count is every eligible
// option plus any already-selected option that will remain selected
// (a selected disabled/new option is not toggled off). Falsy-valued
// options (e.g. the <NULL> option) are never bulk-selectable.
const willBeSelected =
isBulkSelectable(option) ||
(Boolean(option.value) && selectedValuesSet.has(option.value));
return willBeSelected ? acc + 1 : acc;
}, 0);
const deselectable = countSource.reduce((acc, option) => {
const isSelected = selectedValuesSet.has(option.value);
return isSelected && !option.disabled ? acc + 1 : acc;
}, 0);
return { selectable, deselectable };
}, [visibleOptions, selectValue, flatFullSelectOptions, stableSelectAll]);
return visibleOptions.reduce(
(acc, option) => {
const isSelected = selectedValuesSet.has(option.value);
const isDisabled = option.disabled;
const isNew = option.isNewOption;
// Mirror handleSelectAll, which skips falsy-valued options (e.g. the
// <NULL> option whose value is null): they are not bulk-selectable,
// so counting them here makes the "Select all" badge overstate what
// gets selected.
if (
option.value &&
(!isDisabled || isSelected) &&
((isNew && isSelected) || !isNew)
) {
acc.selectable += 1;
}
if (isSelected && !isDisabled) {
acc.deselectable += 1;
}
return acc;
},
{ selectable: 0, deselectable: 0 },
);
}, [visibleOptions, selectValue]);
const handleOnSelect: SelectProps['onSelect'] = (selectedItem, option) => {
if (isSingleMode) {
@@ -646,15 +594,9 @@ const Select = forwardRef(
const handleSelectAll = useCallback(() => {
if (isSingleMode) return;
const searchScopedOptions = isSearching
const optionsToSelect = isSearching
? visibleOptions.filter(option => !option.isNewOption)
: enabledOptions;
// When stableSelectAll is on, always select the full eligible set
// regardless of any active search, so "Select all" targets the whole
// column rather than the search-filtered subset.
const optionsToSelect = stableSelectAll
? fullSelectAllOptions
: searchScopedOptions;
const currentValues = ensureIsArray(selectValue);
const currentValuesSet = new Set(currentValues.map(getValue));
@@ -677,8 +619,6 @@ const Select = forwardRef(
isSearching,
visibleOptions,
enabledOptions,
stableSelectAll,
fullSelectAllOptions,
selectValue,
fireOnChange,
]);
@@ -686,18 +626,7 @@ const Select = forwardRef(
const handleDeselectAll = useCallback(() => {
if (isSingleMode) return;
// In stableSelectAll mode "Clear" removes the whole non-disabled
// selection across the full loaded set — flattened so grouped columns
// clear their leaf options, the full-set parallel of enabledOptions — so
// it matches the `deselectable` count (which counts every selected
// non-disabled option). Otherwise it clears only the search-scoped visible
// options.
const deselectionSource = stableSelectAll
? flatFullSelectOptions.filter(option => !option.disabled)
: enabledOptions;
const deselectionValues = new Set(
deselectionSource.map(opt => opt.value),
);
const deselectionValues = new Set(enabledOptions.map(opt => opt.value));
const newValues = ensureIsArray(selectValue).filter(item => {
const itemValue = getValue(item);
@@ -706,14 +635,7 @@ const Select = forwardRef(
setSelectValue(newValues);
fireOnChange();
}, [
isSingleMode,
enabledOptions,
stableSelectAll,
flatFullSelectOptions,
selectValue,
fireOnChange,
]);
}, [isSingleMode, enabledOptions, selectValue, fireOnChange]);
const bulkSelectComponent = useMemo(
() => (
@@ -832,16 +754,8 @@ const Select = forwardRef(
if (onChangeCount !== previousChangeCount) {
const array = ensureIsArray(selectValue);
const set = new Set(array.map(getValue));
// Flatten grouped columns so the option metadata resolves to the leaf
// options rather than the value-less group headers (which would leave
// this array empty). A flat list is returned unchanged.
// Flatten grouped columns so the option metadata resolves to the leaf
// options rather than the value-less group headers (which would leave
// this array empty). A flat list is returned unchanged.
const options = mapOptions(
flattenGroupedOptions(fullSelectOptions).filter(opt =>
set.has(opt.value),
),
fullSelectOptions.filter(opt => set.has(opt.value)),
);
if (isSingleMode) {
handleOnChange(selectValue, selectValue ? options[0] : undefined);
@@ -205,21 +205,6 @@ export interface SelectProps extends BaseSelectProps {
* True by default.
* */
allowSelectAll?: boolean;
/**
* When true, the bulk "Select all" / "Clear" controls operate on the full
* loaded option set instead of the search-filtered subset, so their counts
* stay stable and the controls stay visible while searching. Clicking
* "Select all" selects every selectable option in the loaded set regardless
* of the active search, and "Clear" removes the corresponding selections;
* options with a falsy value (e.g. `0`, `''`, `false`, `<NULL>`), disabled
* options, and the transient "create new option" entry are not selectable.
* "Full set" here means the currently loaded options, which for async or
* row-limited selects may be fewer than the column's full cardinality.
* Intended for the native Value filter, whose "Select all" targets the whole
* column.
* Default false.
* */
stableSelectAll?: boolean;
/**
* It defines the options of the Select.
* The options can be static, an array of options.
@@ -19,8 +19,6 @@
export { default as NumberFormats } from './NumberFormats';
export { default as NumberFormatter, PREVIEW_VALUE } from './NumberFormatter';
export { formatSpecifier } from 'd3-format';
export type { FormatLocaleDefinition } from 'd3-format';
export { DEFAULT_D3_FORMAT } from './D3FormatConfig';
export {
@@ -47,8 +47,6 @@ export default function extractQueryFields(
metric: 'metrics',
metric_2: 'metrics',
secondary_metric: 'metrics',
left_metric: 'metrics',
right_metric: 'metrics',
x: 'metrics',
y: 'metrics',
size: 'metrics',
@@ -59,16 +59,6 @@ describe('extractQueryFields', () => {
).toEqual(['metric_1', 'metric_2', 'my_custom_metric']);
});
test('should extract butterfly chart metrics', () => {
expect(
extractQueryFields({
groupby: ['category'],
left_metric: 'left_sum',
right_metric: 'right_sum',
}).metrics,
).toEqual(['left_sum', 'right_sum']);
});
test('should extract columns', () => {
expect(extractQueryFields({ columns: 'col_1' })).toEqual({
columns: ['col_1'],
@@ -45,7 +45,7 @@
"@superset-ui/core": "*",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "*",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^18.3.0",
@@ -247,12 +247,6 @@ const config: ControlPanelConfig = {
config: {
...sharedControls.time_grain_sqla,
visibility: ({ controls }) => {
// Time grain only applies to the aggregate query, so the
// control must follow the query mode like its siblings do.
if (!isAggMode({ controls })) {
return false;
}
const dttmLookup = Object.fromEntries(
ensureIsArray(controls?.groupby?.options).map(option => [
(option.column_name || '').toLowerCase(),
@@ -779,9 +773,6 @@ const config: ControlPanelConfig = {
columnOptions,
verboseMap,
extraColorChoices,
serverPagination: Boolean(
explore?.controls?.server_pagination?.value,
),
};
},
},
@@ -779,8 +779,6 @@ const transformProps = (
),
passedData,
theme,
undefined,
serverPagination,
) ?? [];
const hasPageLength = isPositiveNumber(pageLength);
@@ -18,7 +18,6 @@ import {
CustomControlItem,
isCustomControlItem,
} from '@superset-ui/chart-controls';
import { QueryMode } from '@superset-ui/core';
import config from '../src/controlPanel';
type VisibilityFn = (
@@ -69,16 +68,6 @@ function mkProps(
} as unknown as ControlPanelsContainerProps;
}
function withControls(
props: ControlPanelsContainerProps,
controls: Record<string, unknown>,
): ControlPanelsContainerProps {
return {
...props,
controls: { ...props.controls, ...controls },
} as unknown as ControlPanelsContainerProps;
}
test('time_grain_sqla visibility should be case-insensitive', () => {
const vis = getVisibility(config, 'time_grain_sqla');
const controlState = {} as ControlState;
@@ -88,68 +77,6 @@ test('time_grain_sqla visibility should be case-insensitive', () => {
expect(vis(mkProps(['some_other_col']), controlState)).toBe(false);
});
test('time_grain_sqla is hidden in raw records mode', () => {
const vis = getVisibility(config, 'time_grain_sqla');
const controlState = {} as ControlState;
const temporalGroupby = mkProps(['ORDERDATE']);
expect(
vis(
withControls(temporalGroupby, {
query_mode: { value: QueryMode.Aggregate },
}),
controlState,
),
).toBe(true);
// `groupby` is kept when the query mode switches to raw records, both in the
// control state and in a saved chart's form data, so a temporal dimension on
// its own must not bring the control back.
expect(
vis(
withControls(temporalGroupby, { query_mode: { value: QueryMode.Raw } }),
controlState,
),
).toBe(false);
// Charts saved before the query mode control existed are inferred as raw
// records from their columns.
expect(
vis(
withControls(temporalGroupby, { all_columns: { value: ['name'] } }),
controlState,
),
).toBe(false);
});
test('time_grain_sqla is hidden in raw records mode for an adhoc dimension', () => {
const vis = getVisibility(config, 'time_grain_sqla');
const controlState = {} as ControlState;
// An adhoc column reports temporal without the lookup, so it needs the guard too.
const adhocGroupby = withControls(mkProps([]), {
groupby: {
value: [{ sqlExpression: 'ds', label: 'ds', expressionType: 'SQL' }],
options: [],
},
});
expect(
vis(
withControls(adhocGroupby, {
query_mode: { value: QueryMode.Aggregate },
}),
controlState,
),
).toBe(true);
expect(
vis(
withControls(adhocGroupby, { query_mode: { value: QueryMode.Raw } }),
controlState,
),
).toBe(false);
});
test('show_totals renders in the customize tab atop visual formatting', () => {
const visualFormatting = config.controlPanelSections.find(
section => section?.label === 'Visual formatting',
@@ -20,7 +20,6 @@ import transformProps from '../src/transformProps';
import { TableChartProps } from '../src/types';
import { GenericDataType } from '@apache-superset/core/common';
import { QueryMode } from '@superset-ui/core';
import { BoundUnit } from '@superset-ui/chart-controls';
function createMockChartProps(
overrides: Partial<TableChartProps> = {},
@@ -315,59 +314,3 @@ test('excludes Green/Red color-scheme rules from columnColorFormatters', () => {
// ...but the Green rule is excluded.
expect(formattedColumns).not.toContain('metric_a');
});
test('retains saved percentage rules with automatic bounds when server pagination is enabled', () => {
const props = createMockChartProps({
rawFormData: {
viz_type: 'table',
datasource: '1__table',
query_mode: QueryMode.Aggregate,
metrics: ['metric_a', 'metric_b'],
percent_metrics: [],
column_config: {},
table_timestamp_format: '',
granularity_sqla: 'day',
time_range: 'No filter',
server_pagination: true,
conditional_formatting: [
{
column: 'metric_a',
operator: 'None',
colorScheme: '#FF0000',
boundUnit: BoundUnit.Percent,
minBound: 0,
maxBound: 100,
},
{
column: 'metric_b',
operator: 'None',
colorScheme: '#00FF00',
boundUnit: BoundUnit.Value,
},
],
} as unknown as TableChartProps['rawFormData'],
queriesData: [
{
data: [{ metric_a: 10, metric_b: 20 }],
colnames: ['metric_a', 'metric_b'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
rowcount: 1,
applied_filters: [],
rejected_filters: [],
},
{
data: [{ rowcount: 1 }],
colnames: ['rowcount'],
coltypes: [GenericDataType.Numeric],
rowcount: 1,
applied_filters: [],
rejected_filters: [],
},
] as unknown as TableChartProps['queriesData'],
});
const result = transformProps(props);
expect(
result.columnColorFormatters.map(formatter => formatter.column),
).toEqual(['metric_a', 'metric_b']);
});
@@ -16,44 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { allEventHandlers, type Event } from '../utils/eventHandlers';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { ButterflyTransformedProps } from './types';
type ButterflyChartEvent = {
name?: string;
data?: { name?: string };
event?: Event['event'];
};
function getCategoryKey(params: ButterflyChartEvent): string {
return params.data?.name ?? params.name ?? '';
}
import { EventHandlers } from '../types';
export default function Butterfly(props: ButterflyTransformedProps) {
const {
height,
width,
echartOptions,
selectedValues,
refs,
onLegendStateChanged,
formData,
} = props;
const { click, contextmenu } = allEventHandlers(props);
const { height, width, echartOptions, refs, onLegendStateChanged, formData } =
props;
const eventHandlers: EventHandlers = {
click: (params: ButterflyChartEvent) => {
click({ name: getCategoryKey(params) });
},
contextmenu: (params: ButterflyChartEvent) => {
contextmenu({
...params,
name: getCategoryKey(params),
});
},
legendselectchanged: payload => {
onLegendStateChanged?.(payload.selected);
},
@@ -72,7 +43,6 @@ export default function Butterfly(props: ButterflyTransformedProps) {
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
selectedValues={selectedValues}
vizType={formData.vizType}
/>
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

@@ -17,15 +17,11 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import { EchartsButterflyChartProps, EchartsButterflyFormData } from './types';
import example from './images/example.png';
import exampleDark from './images/example-dark.png';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
export default class EchartsButterflyChartPlugin extends ChartPlugin<
EchartsButterflyFormData,
@@ -37,18 +33,12 @@ export default class EchartsButterflyChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./Butterfly'),
metadata: new ChartMetadata({
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
credits: ['https://echarts.apache.org'],
category: t('Comparison'),
description: t(
'A butterfly chart compares two metrics across categories using horizontal bars ' +
'that extend left and right from a central axis.',
),
exampleGallery: [{ url: example, urlDark: exampleDark }],
name: t('Butterfly Chart'),
tags: [
t('Categorical'),
@@ -56,8 +46,7 @@ export default class EchartsButterflyChartPlugin extends ChartPlugin<
t('ECharts'),
t('Multi-Variables'),
],
thumbnail,
thumbnailDark,
thumbnail: '',
}),
transformProps,
});
@@ -34,13 +34,8 @@ import { DEFAULT_FORM_DATA } from './constants';
import { defaultGrid } from '../defaults';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
import { OpacityEnum } from '../constants';
import {
getChartPadding,
getLegendProps,
getColtypesMapping,
extractGroupbyLabel,
} from '../utils/series';
import { NULL_STRING } from '../constants';
import { getChartPadding, getLegendProps } from '../utils/series';
import { resolveLegendLayout } from '../utils/legendLayout';
import { convertInteger } from '../utils/convertInteger';
@@ -49,11 +44,19 @@ type EChartsOption = ComposeOption<BarSeriesOption>;
const LABEL_LEFT = { position: 'left' as const };
const LABEL_RIGHT = { position: 'right' as const };
function formatCategory(value: unknown): string {
if (value == null) {
return NULL_STRING;
}
if (typeof value === 'string' || typeof value === 'number') {
return String(value);
}
return String(value);
}
function formatTooltip(
params: CallbackDataParams[],
formatter: NumberFormatter | CurrencyFormatter,
categoryLabels: string[],
categoryByKey: Map<string, string>,
) {
const axisParams = params.filter(
param => param.seriesName && typeof param.value === 'number',
@@ -62,13 +65,7 @@ function formatTooltip(
return '';
}
const { dataIndex, name } = axisParams[0];
const title =
(typeof dataIndex === 'number'
? categoryLabels.at(dataIndex)
: undefined) ??
(typeof name === 'string' ? categoryByKey.get(name) : undefined) ??
name;
const title = axisParams[0].name;
const rows = axisParams.map(param => [
param.seriesName!,
formatter(Math.abs(param.value as number)),
@@ -89,8 +86,6 @@ export default function transformProps(
hooks,
theme,
inContextMenu,
filterState,
emitCrossFilters,
} = chartProps;
const refs: Refs = {};
const { data = [] } = queriesData[0];
@@ -122,75 +117,32 @@ export default function transformProps(
...formData,
};
const groupbyColumn = ensureIsArray(groupby)[0];
const categoryLabel = getColumnLabel(groupbyColumn);
const leftMetricLabel = leftMetric ? getMetricLabel(leftMetric) : '';
const rightMetricLabel = rightMetric ? getMetricLabel(rightMetric) : '';
const leftSeriesName = leftLabel || leftMetricLabel;
const rightSeriesName = rightLabel || rightMetricLabel;
const coltypeMapping = getColtypesMapping(queriesData[0]);
const groupbyColumns = ensureIsArray(groupby);
const groupbyLabels = groupbyColumns.map(getColumnLabel);
const defaultFormatter = currencyFormat?.symbol
? new CurrencyFormatter({ d3Format: xAxisFormat, currency: currencyFormat })
: getNumberFormatter(xAxisFormat);
const categories = data.map(datum =>
extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
);
const categoryKeys = data.map((datum, index) => {
const label = categories.at(index) ?? '';
return `${label}__${JSON.stringify(
groupbyLabels.map(col =>
Object.hasOwn(datum, col) ? datum[col] : undefined,
),
)}`;
const categories = data.map(row => formatCategory(row[categoryLabel]));
const leftData = data.map(row => {
const value = Number(row[leftMetricLabel] ?? 0);
return {
value: -Math.abs(value),
label: LABEL_LEFT,
};
});
const rightData = data.map(row => {
const value = Number(row[rightMetricLabel] ?? 0);
return {
value: Math.abs(value),
label: LABEL_RIGHT,
};
});
const categoryByKey = new Map(
categoryKeys.flatMap((key, index) => {
const label = categories.at(index);
return label === undefined ? [] : [[key, label] as const];
}),
);
const labelMap = data.reduce<Record<string, string[]>>(
(acc, datum, index) => {
const uniqueKey = categoryKeys.at(index);
if (uniqueKey === undefined) {
return acc;
}
acc[uniqueKey] = groupbyLabels.map(col =>
Object.hasOwn(datum, col) ? (datum[col] as string) : '',
);
return acc;
},
{},
);
const selectedValues = (filterState.selectedValues || []).reduce(
(acc: Record<number, string>, value: string) => {
const index = categoryKeys.indexOf(value);
return index >= 0 ? { ...acc, [index]: value } : acc;
},
{},
);
const getOpacity = (categoryKey: string) =>
filterState.selectedValues?.length &&
!filterState.selectedValues.includes(categoryKey)
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent;
const leftData = data.map((row, i) => ({
name: categoryKeys[i],
value: -Math.abs(Number(row[leftMetricLabel] ?? 0)),
label: LABEL_LEFT,
itemStyle: { opacity: getOpacity(categoryKeys[i]) },
}));
const rightData = data.map((row, i) => ({
name: categoryKeys[i],
value: Math.abs(Number(row[rightMetricLabel] ?? 0)),
label: LABEL_RIGHT,
itemStyle: { opacity: getOpacity(categoryKeys[i]) },
}));
const labelFormatter = (params: CallbackDataParams) => {
const value = Math.abs(params.value as number);
@@ -328,8 +280,6 @@ export default function transformProps(
formatTooltip(
ensureIsArray(params) as CallbackDataParams[],
defaultFormatter,
categories,
categoryByKey,
),
},
series,
@@ -344,10 +294,5 @@ export default function transformProps(
setDataMask,
onContextMenu,
onLegendStateChanged,
groupby: groupbyColumns,
labelMap,
selectedValues,
emitCrossFilters,
coltypeMapping,
};
}
@@ -24,12 +24,7 @@ import {
QueryFormMetric,
RgbaColor,
} from '@superset-ui/core';
import {
BaseTransformedProps,
LegendFormData,
TitleFormData,
CrossFilterTransformedProps,
} from '../types';
import { BaseTransformedProps, LegendFormData, TitleFormData } from '../types';
export type EchartsButterflyFormData = QueryFormData &
LegendFormData &
@@ -54,4 +49,4 @@ export interface EchartsButterflyChartProps extends ChartProps {
}
export type ButterflyTransformedProps =
BaseTransformedProps<EchartsButterflyFormData> & CrossFilterTransformedProps;
BaseTransformedProps<EchartsButterflyFormData>;
@@ -59,7 +59,6 @@ import {
LegendOrientation,
Refs,
} from '../types';
import { BarValueLabelPosition } from '../Timeseries/types';
import { parseAxisBound } from '../utils/controls';
import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
import {
@@ -74,8 +73,6 @@ import {
getLegendProps,
getMinAndMaxFromBounds,
getOverMaxHiddenFormatter,
getTemporalAxisTickConfig,
resolveTemporalTickValues,
} from '../utils/series';
import { resolveLegendLayout } from '../utils/legendLayout';
import {
@@ -103,9 +100,7 @@ import {
import { TIMEGRAIN_TO_TIMESTAMP, TIMESERIES_CONSTANTS } from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import {
createSpacedXAxisFormatter,
getTooltipTimeFormatter,
getXAxisDomain,
getXAxisFormatter,
getYAxisFormatter,
} from '../utils/formatters';
@@ -522,7 +517,6 @@ export default function transformProps(
areaOpacity: opacity,
seriesType,
showValue,
valueLabelPosition: BarValueLabelPosition.OutsideEnd,
onlyTotal,
stack: Boolean(stack),
stackIdSuffix: '\na',
@@ -612,7 +606,6 @@ export default function transformProps(
areaOpacity: opacityB,
seriesType: seriesTypeB,
showValue: showValueB,
valueLabelPosition: BarValueLabelPosition.OutsideEnd,
onlyTotal: onlyTotalB,
stack: Boolean(stackB),
stackIdSuffix: '\nb',
@@ -666,26 +659,44 @@ export default function transformProps(
? getXAxisFormatter(xAxisTimeFormat, resolvedTimeGrain)
: String;
// hideOverlap must stay off so the forced boundary label from showMaxLabel
// is never suppressed (#39899). The formatter itself dedupes consecutive
// identical labels and thins out labels that would otherwise visually
// collide, since hideOverlap can no longer do that for us.
const showMaxLabel =
xAxisType === AxisType.Time &&
xAxisLabelRotation === 0 &&
!!resolvedTimeGrain;
const deduplicatedFormatter = showMaxLabel
? createSpacedXAxisFormatter(
xAxisFormatter,
...getXAxisDomain(
[
rebasedDataA as Record<string, unknown>[],
rebasedDataB as Record<string, unknown>[],
],
xAxisLabel,
),
Math.max(width - 2 * TIMESERIES_CONSTANTS.gridOffsetLeft, 0),
)
? (() => {
let lastLabel: string | undefined;
let lastValue: number | undefined;
const wrapper = (value: number | string) => {
// ECharts formats the labels in repeated ascending passes. Reset the
// dedup state when the sequence restarts so a forced boundary label
// (e.g. the min date) isn't blanked by the previous pass's last label
// when both format identically (e.g. a May-to-May range).
if (
typeof value === 'number' &&
lastValue !== undefined &&
value <= lastValue
) {
lastLabel = undefined;
}
if (typeof value === 'number') {
lastValue = value;
}
const label =
typeof xAxisFormatter === 'function'
? (xAxisFormatter as Function)(value)
: String(value);
if (label === lastLabel) {
return '';
}
lastLabel = label;
return label;
};
if (typeof xAxisFormatter === 'function' && 'id' in xAxisFormatter) {
(wrapper as any).id = (xAxisFormatter as any).id;
}
return wrapper;
})()
: xAxisFormatter;
const yAxisTitleMarginPx = convertInteger(yAxisTitleMargin);
@@ -753,26 +764,6 @@ export default function transformProps(
const { setDataMask = () => {}, onContextMenu } = hooks;
const alignTicks = yAxisIndex !== yAxisIndexB;
// Both queries share the axis, so a bucket contributed by either needs a tick.
const temporalTickValues = resolveTemporalTickValues(
[...rebasedDataA, ...rebasedDataB],
xAxisLabel,
xAxisType,
resolvedTimeGrain,
annotationLayers,
);
const temporalAxisTickConfig = getTemporalAxisTickConfig(
temporalTickValues,
showMaxLabel,
xAxisType,
xAxisLabelRotation,
xAxisLabelInterval,
deduplicatedFormatter,
false,
zoomable,
);
const echartOptions: EChartsCoreOption = {
useUTC: true,
grid: {
@@ -784,12 +775,22 @@ export default function transformProps(
name: xAxisTitle,
nameGap: xAxisTitleMarginPx,
nameLocation: 'middle',
...temporalAxisTickConfig,
minorTick: { show: minorTicks },
axisTick: {
...temporalAxisTickConfig.axisTick,
show: axisTicks ? 'auto' : false,
axisLabel: {
hideOverlap: showMaxLabel
? false
: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
formatter: deduplicatedFormatter,
rotate: xAxisLabelRotation,
interval: xAxisLabelInterval,
...(showMaxLabel && {
showMaxLabel: true,
alignMaxLabel: 'right',
showMinLabel: true,
alignMinLabel: 'left',
}),
},
minorTick: { show: minorTicks },
axisTick: { show: axisTicks ? 'auto' : false },
...(gridlines ? {} : { splitLine: { show: false } }),
minInterval:
xAxisType === AxisType.Time && resolvedTimeGrain && !forceMaxInterval
@@ -40,7 +40,6 @@ import {
} from '../types';
import EchartsTimeseries from './EchartsTimeseries';
import {
BarValueLabelPosition,
EchartsTimeseriesSeriesType,
OrientationType,
type EchartsTimeseriesFormData,
@@ -160,7 +159,6 @@ const defaultFormData: EchartsTimeseriesFormData & {
xAxisLabelRotation: 0,
xAxisLabelInterval: 0,
showValue: false,
valueLabelPosition: BarValueLabelPosition.Auto,
onlyTotal: false,
showExtraControls: true,
percentageThreshold: 0,
@@ -290,66 +288,6 @@ test('observes extra control height changes when ResizeObserver is available', a
expect(disconnectSpy).toHaveBeenCalled();
});
test('uses the post-control body height for compact custom-legend visibility', async () => {
mockOffsetHeight = 40;
const { queryByTestId } = render(
<EchartsTimeseries
{...defaultProps}
height={140}
echartOptions={{
grid: { bottom: 80, containLabel: true, top: 20 },
}}
formData={{ ...defaultFormData, zoomable: true }}
customLegend={
{
grid: { bottom: 80, top: 20 },
items: Array.from({ length: 20 }, (_, index) => ({
color: '#123456',
interactive: true,
name: `Series ${index}`,
selected: true,
})),
orientation: LegendOrientation.Top,
showSelectors: true,
} as never
}
/>,
);
await waitFor(() => {
expect(queryByTestId('timeseries-custom-legend')).not.toBeInTheDocument();
expect(getLatestHeight()).toBe(100);
expect(getLatestEchartProps().echartOptions.grid).toEqual({
bottom: 80,
containLabel: false,
top: 12,
});
});
});
test('keeps a no-legend grid within a very small post-control body', async () => {
mockOffsetHeight = 40;
render(
<EchartsTimeseries
{...defaultProps}
height={50}
echartOptions={{
grid: { bottom: 37, containLabel: false, top: 12 },
}}
formData={{ ...defaultFormData, zoomable: true }}
/>,
);
await waitFor(() => {
expect(getLatestHeight()).toBe(10);
expect(getLatestEchartProps().echartOptions.grid).toEqual({
bottom: 0,
containLabel: false,
top: 9,
});
});
});
test('falls back to window resize listener when ResizeObserver is unavailable', async () => {
(globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver =
undefined;
@@ -26,7 +26,6 @@ import {
getColumnLabel,
getNumberFormatter,
LegendState,
WithLegend,
ensureIsArray,
createTimeRangeFromGranularity,
} from '@superset-ui/core';
@@ -41,50 +40,17 @@ import type GlobalModel from 'echarts/types/src/model/Global';
import type ComponentModel from 'echarts/types/src/model/Component';
import { EchartsHandler, EventHandlers } from '../types';
import Echart from '../components/Echart';
import {
getViableTimeseriesEchartOptions,
resolveTimeseriesGridOffset,
} from './transformers';
import {
rebaseSeriesData,
snapToNearestX,
SeriesDataPoint,
} from './percentChange';
import {
OrientationType,
TimeseriesChartTransformedProps,
TimeseriesCustomLegend,
} from './types';
import { OrientationType, TimeseriesChartTransformedProps } from './types';
import { formatSeriesName } from '../utils/series';
import { getTemporalXAxisDrillByFilter } from '../utils/xAxisDrillByFilter';
import { ExtraControls } from '../components/ExtraControls';
import TimeseriesLegend from './TimeseriesLegend';
import { TIMESERIES_CONSTANTS } from '../constants';
const TIMER_DURATION = 300;
const MAX_CUSTOM_LEGEND_HEIGHT = 160;
const MAX_CUSTOM_LEGEND_HEIGHT_RATIO = 0.3;
const MIN_TIMESERIES_PLOT_HEIGHT = 80;
// Bound the legend after accounting for the fixed ECharts grid reservations,
// leaving enough coordinate space for the plot itself to remain usable.
export const getTimeseriesLegendMaxHeight = (
chartBodyHeight: number,
grid: TimeseriesCustomLegend['grid'],
) =>
Math.min(
MAX_CUSTOM_LEGEND_HEIGHT,
Math.floor(Math.max(chartBodyHeight, 0) * MAX_CUSTOM_LEGEND_HEIGHT_RATIO),
Math.max(
Math.floor(
chartBodyHeight -
resolveTimeseriesGridOffset(grid.top, chartBodyHeight) -
resolveTimeseriesGridOffset(grid.bottom, chartBodyHeight) -
MIN_TIMESERIES_PLOT_HEIGHT,
),
0,
),
);
const getTimestampFromTimeAxisValue = (value: string | number) => {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : undefined;
@@ -118,7 +84,6 @@ const BASELINE_HANDLE_STRIPE_WIDTH = 2;
export default function EchartsTimeseries({
formData,
height,
customLegend,
width,
echartOptions,
groupby,
@@ -810,87 +775,23 @@ export default function EchartsTimeseries({
},
};
const dispatchLegendAction = useCallback(
(action: { name?: string; seriesName?: string; type: string }) => {
echartRef.current?.getEchartInstance()?.dispatchAction(action);
},
[],
);
const chartBodyHeight = Math.max(height - extraControlHeight, 0);
const customLegendMaxHeight = customLegend
? getTimeseriesLegendMaxHeight(chartBodyHeight, customLegend.grid)
: 0;
const shouldRenderCustomLegend =
customLegend !== undefined &&
chartBodyHeight > TIMESERIES_CONSTANTS.compactChartHeight &&
customLegendMaxHeight > 0;
const chartEchartOptions = useMemo(
() =>
getViableTimeseriesEchartOptions(
echartOptions,
chartBodyHeight,
formData.zoomable,
),
[chartBodyHeight, echartOptions, formData.zoomable],
);
const renderEchart = ({
chartHeight,
chartWidth,
}: {
chartHeight: number;
chartWidth: number;
}) => (
<Echart
ref={echartRef}
refs={refs}
height={chartHeight}
width={chartWidth}
echartOptions={chartEchartOptions}
eventHandlers={eventHandlers}
queryEventHandlers={queryEventHandlers}
zrEventHandlers={zrEventHandlers}
selectedValues={selectedValues}
vizType={formData.vizType}
/>
);
return (
<>
<div ref={extraControlRef}>
<ExtraControls formData={formData} setControlValue={setControlValue} />
</div>
{customLegend && shouldRenderCustomLegend ? (
<WithLegend
height={chartBodyHeight}
position={customLegend.orientation}
width={width}
renderLegend={() => (
<TimeseriesLegend
{...customLegend}
maxHeight={customLegendMaxHeight}
onAll={() => dispatchLegendAction({ type: 'legendAllSelect' })}
onHover={name =>
dispatchLegendAction({
seriesName: name ?? undefined,
type: name === null ? 'downplay' : 'highlight',
})
}
onInverse={() =>
dispatchLegendAction({ type: 'legendInverseSelect' })
}
onToggle={name =>
dispatchLegendAction({ name, type: 'legendToggleSelect' })
}
/>
)}
renderChart={({ height: chartHeight, width: chartWidth }) =>
renderEchart({ chartHeight, chartWidth })
}
/>
) : (
renderEchart({ chartHeight: chartBodyHeight, chartWidth: width })
)}
<Echart
ref={echartRef}
refs={refs}
height={height - extraControlHeight}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
queryEventHandlers={queryEventHandlers}
zrEventHandlers={zrEventHandlers}
selectedValues={selectedValues}
vizType={formData.vizType}
/>
</>
);
}
@@ -1,196 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { memo } from 'react';
import { styled } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import type { TimeseriesCustomLegend } from './types';
const LegendViewport = styled.div<{ maxHeight: number }>`
${({ maxHeight, theme }) => `
box-sizing: border-box;
color: ${theme.colorText};
font-size: ${theme.fontSizeSM}px;
max-height: ${maxHeight}px;
overflow-x: hidden;
overflow-y: auto;
padding: ${theme.sizeUnit}px ${theme.sizeUnit * 2}px;
width: 100%;
`}
`;
const SelectorRow = styled.div`
${({ theme }) => `
align-items: center;
background: ${theme.colorBgContainer};
display: flex;
gap: ${theme.sizeUnit}px;
justify-content: flex-end;
padding-bottom: ${theme.sizeUnit}px;
position: sticky;
top: 0;
z-index: 1;
`}
`;
const SelectorButton = styled.button`
${({ theme }) => `
background: transparent;
border: 1px solid ${theme.colorBorder};
border-radius: ${theme.borderRadius}px;
color: ${theme.colorText};
cursor: pointer;
font: inherit;
padding: 0 ${theme.sizeUnit}px;
&:hover {
color: ${theme.colorPrimary};
}
`}
`;
const ItemList = styled.ul`
${({ theme }) => `
align-items: flex-start;
display: flex;
flex-wrap: wrap;
gap: ${theme.sizeUnit * 2}px;
list-style: none;
margin: 0;
padding: 0;
`}
`;
const LegendItem = styled.li`
min-width: 0;
`;
const ItemButton = styled.button<{ selected: boolean }>`
${({ selected, theme }) => `
align-items: flex-start;
appearance: none;
background: none;
border: none;
color: ${selected ? theme.colorText : theme.colorTextDisabled};
cursor: pointer;
display: inline-flex;
font: inherit;
gap: ${theme.sizeUnit}px;
max-width: 100%;
padding: 0;
text-align: left;
&:disabled {
cursor: default;
}
`}
`;
const Swatch = styled.span<{ color: string; selected: boolean }>`
${({ color, selected }) => `
background: ${selected ? color : 'transparent'};
border: 1px solid ${color};
box-sizing: border-box;
flex: 0 0 auto;
height: 12px;
margin-top: 2px;
width: 12px;
`}
`;
const Label = styled.span`
overflow-wrap: anywhere;
white-space: pre-line;
`;
const RowBreak = styled.li`
flex-basis: 100%;
height: 0;
`;
export type TimeseriesLegendProps = TimeseriesCustomLegend & {
maxHeight: number;
onAll: () => void;
onHover?: (name: string | null) => void;
onInverse: () => void;
onToggle: (name: string) => void;
};
function TimeseriesLegend({
items,
maxHeight,
onAll,
onHover,
onInverse,
onToggle,
showSelectors,
}: TimeseriesLegendProps) {
if (items.length === 0 || maxHeight <= 0) {
return null;
}
return (
<LegendViewport data-test="timeseries-custom-legend" maxHeight={maxHeight}>
{showSelectors && (
<SelectorRow>
<SelectorButton type="button" onClick={onAll}>
{t('All')}
</SelectorButton>
<SelectorButton type="button" onClick={onInverse}>
{t('Inverse')}
</SelectorButton>
</SelectorRow>
)}
<ItemList>
{items.map((item, index) =>
item.name === '' || item.name === '\n' ? (
<RowBreak
// ECharts treats these exact values as row-break sentinels.
key={`row-break-${item.name}-${index}`}
/>
) : (
<LegendItem key={item.name}>
<ItemButton
aria-pressed={item.selected}
disabled={!item.interactive}
selected={item.selected}
type="button"
onClick={() => {
if (item.interactive) {
onToggle(item.name);
}
}}
onMouseEnter={() => onHover?.(item.name)}
onMouseLeave={() => onHover?.(null)}
>
<Swatch
aria-hidden
color={item.color}
selected={item.selected}
/>
<Label>{item.name}</Label>
</ItemButton>
</LegendItem>
),
)}
</ItemList>
</LegendViewport>
);
}
export default memo(TimeseriesLegend);
@@ -23,7 +23,6 @@ import {
import { t } from '@apache-superset/core/translation';
import { LegendOrientation, LegendType } from '../types';
import {
BarValueLabelPosition,
OrientationType,
EchartsTimeseriesSeriesType,
EchartsTimeseriesFormData,
@@ -89,10 +88,6 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
xAxisLabelInterval: defaultXAxis.xAxisLabelInterval,
groupby: [],
showValue: false,
// Legacy charts saved before this field existed have no valueLabelPosition
// in form_data and must keep their pre-existing Outside End placement;
// Auto is opt-in via the Value label position control, not the default.
valueLabelPosition: BarValueLabelPosition.OutsideEnd,
labelPosition: 'auto',
onlyTotal: false,
percentageThreshold: 0,
@@ -41,14 +41,12 @@ import {
isIntervalAnnotationLayer,
isPhysicalColumn,
isTimeseriesAnnotationLayer,
LegendState,
resolveAutoCurrency,
TimeseriesChartDataResponseResult,
TimeseriesDataRecord,
NumberFormats,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { isThemeDark } from '@apache-superset/core/theme';
import {
extractExtraMetrics,
getOriginalSeries,
@@ -65,11 +63,8 @@ import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
BarValueLabelPosition,
OrientationType,
TimeseriesCustomLegend,
TimeseriesChartTransformedProps,
TimeseriesLegendItem,
} from './types';
import { DEFAULT_FORM_DATA } from './constants';
import {
@@ -93,8 +88,6 @@ import {
getHorizontalLegendAvailableWidth,
getLegendProps,
getMinAndMaxFromBounds,
getTemporalAxisTickConfig,
resolveTemporalTickValues,
} from '../utils/series';
import { resolveLegendLayout } from '../utils/legendLayout';
import {
@@ -115,7 +108,6 @@ import { defaultGrid, defaultYAxis } from '../defaults';
import {
getBaselineSeriesForStream,
getPadding,
getViableTimeseriesEchartOptions,
transformEventAnnotation,
transformFormulaAnnotation,
transformIntervalAnnotation,
@@ -130,88 +122,14 @@ import {
} from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import {
createDedupXAxisFormatter,
createSpacedXAxisFormatter,
getPercentFormatter,
getTooltipTimeFormatter,
getXAxisDomain,
getXAxisFormatter,
getYAxisFormatter,
} from '../utils/formatters';
import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser';
import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions';
type LegendSeriesVisual = {
itemStyle?: { color?: unknown };
lineStyle?: { color?: unknown };
name?: string | number;
};
function getLegendSeriesColor(
series: SeriesOption | undefined,
fallbackColor: string,
): string {
const visual = series as LegendSeriesVisual | undefined;
const color = visual?.itemStyle?.color ?? visual?.lineStyle?.color;
return typeof color === 'string' ? color : fallbackColor;
}
function buildTimeseriesCustomLegend({
fallbackColor,
grid,
interactive,
legendNames,
legendState,
orientation,
series,
}: {
fallbackColor: string;
grid: TimeseriesCustomLegend['grid'];
interactive: boolean;
legendNames: string[];
legendState?: LegendState;
orientation: LegendOrientation.Top | LegendOrientation.Bottom;
series: SeriesOption[];
}): TimeseriesCustomLegend {
const firstSeriesByName = new Map<string, SeriesOption>();
series.forEach(seriesOption => {
const { name } = seriesOption as LegendSeriesVisual;
if (name !== undefined && !firstSeriesByName.has(String(name))) {
firstSeriesByName.set(String(name), seriesOption);
}
});
const seen = new Set<string>();
const items = legendNames.flatMap<TimeseriesLegendItem>(name => {
if (seen.has(name)) {
return [];
}
seen.add(name);
const rowBreak = name === '' || name === '\n';
const matchingSeries = firstSeriesByName.get(name);
if (!rowBreak && !matchingSeries) {
return [];
}
return [
{
color: getLegendSeriesColor(matchingSeries, fallbackColor),
interactive: interactive && !rowBreak,
name,
selected: legendState?.[name] !== false,
},
];
});
return {
grid,
items,
orientation,
showSelectors: interactive,
};
}
const visibleDashPatterns: ([number, number] | 'dashed' | 'dotted')[] = [
'dashed',
'dotted',
@@ -375,7 +293,6 @@ export default function transformProps(
seriesType,
showLegend,
showValue,
valueLabelPosition,
size,
labelPosition,
colorByPrimaryAxis,
@@ -414,8 +331,6 @@ export default function transformProps(
zoomable,
stackDimension,
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
const resolvedValueLabelPosition =
valueLabelPosition ?? BarValueLabelPosition.OutsideEnd;
const refs: Refs = {};
const groupBy = ensureIsArray(groupby);
@@ -826,7 +741,6 @@ export default function transformProps(
labelMap?.[seriesName]?.[0],
) ?? defaultFormatter),
showValue,
valueLabelPosition: resolvedValueLabelPosition,
onlyTotal,
totalStackedValues: sortedTotalValues,
showValueIndexes,
@@ -1213,30 +1127,9 @@ export default function transformProps(
name,
icon: 'roundRect',
}));
const isSmallChart = height < TIMESERIES_CONSTANTS.compactChartHeight;
const usesCompactLayout = height <= TIMESERIES_CONSTANTS.compactChartHeight;
const isLegendVisible = showLegend && !usesCompactLayout;
const usesPrimaryAxisLegend = colorByPrimaryAxis && groupBy.length === 0;
const resolvedLegendData = usesPrimaryAxisLegend
? colorByPrimaryAxisLegendData
: sortedLegendData;
const resolvedLegendNames = (
usesPrimaryAxisLegend ? legendData : sortedLegendData
).map(String);
const usesCustomLegend =
isLegendVisible &&
legendType === LegendType.Plain &&
(legendOrientation === LegendOrientation.Top ||
legendOrientation === LegendOrientation.Bottom);
const nativeLegendVisible = isLegendVisible && !usesCustomLegend;
// Use the exact final ordering ECharts receives. Forecast components share
// a legend name, and ECharts takes the first matching series as its visual.
const renderedSeries = dedupSeries(
reorderForecastSeries([...series]) as SeriesOption[],
);
const getLegendLayout = (candidateLegendMargin?: string | number | null) => {
const padding = getPadding(
nativeLegendVisible,
showLegend,
legendOrientation,
addYAxisLabelOffset,
zoomable,
@@ -1261,18 +1154,20 @@ export default function transformProps(
: undefined,
chartHeight: height,
chartWidth: width,
legendItems: resolvedLegendData,
legendItems:
colorByPrimaryAxis && groupBy.length === 0
? colorByPrimaryAxisLegendData
: sortedLegendData,
legendMargin: candidateLegendMargin,
orientation: legendOrientation,
show: nativeLegendVisible,
showSelectors: !usesPrimaryAxisLegend,
show: showLegend,
showSelectors: !(colorByPrimaryAxis && groupBy.length === 0),
theme,
type: legendType,
});
};
const initialLegendLayout = getLegendLayout(legendMargin);
const legendLayout =
nativeLegendVisible &&
isHorizontal &&
legendOrientation === LegendOrientation.Bottom &&
initialLegendLayout.effectiveLegendType === LegendType.Plain
@@ -1286,7 +1181,7 @@ export default function transformProps(
? legendMargin
: legendLayout.effectiveLegendMargin;
const padding = getPadding(
nativeLegendVisible,
showLegend,
legendOrientation,
addYAxisLabelOffset,
zoomable,
@@ -1301,7 +1196,7 @@ export default function transformProps(
// Reduce grid padding for small charts to maximize the drawing area.
// Keep enough top padding so the max label doesn't clip against the cell border.
// Preserve bottom padding when zoomable, since getPadding() reserves space for the dataZoom slider.
if (usesCompactLayout) {
if (height < TIMESERIES_CONSTANTS.compactChartHeight) {
padding.top = Math.min(padding.top, 12);
if (!zoomable) {
padding.bottom = Math.min(padding.bottom, 5);
@@ -1310,50 +1205,48 @@ export default function transformProps(
// When showMaxLabel is true, ECharts may render a label at the axis
// boundary that formats identically to the last data-point tick (e.g.
// "2005" appears twice with Year grain), and hideOverlap must stay off so
// that forced boundary label is never suppressed (#39899). Wrap the
// formatter to suppress consecutive duplicate labels and to thin out
// labels that would otherwise visually collide, since hideOverlap can no
// longer do that for us. The spacing estimate assumes the axis runs along
// the bottom of the chart (pixel width, character width); a horizontal
// orientation chart puts the time axis on the side instead, so it falls
// back to dedup-only there.
// "2005" appears twice with Year grain). Wrap the formatter to suppress
// consecutive duplicate labels.
const showMaxLabel =
xAxisType === AxisType.Time &&
xAxisLabelRotation === 0 &&
!!resolvedTimeGrain;
const deduplicatedFormatter = showMaxLabel
? isHorizontal
? createDedupXAxisFormatter(xAxisFormatter)
: createSpacedXAxisFormatter(
xAxisFormatter,
...getXAxisDomain(
[rebasedData as Record<string, unknown>[]],
xAxisLabel,
),
Math.max(width - 2 * TIMESERIES_CONSTANTS.gridOffsetLeft, 0),
)
? (() => {
let lastLabel: string | undefined;
let lastValue: number | undefined;
const wrapper = (value: number | string) => {
// ECharts formats the labels in repeated ascending passes. Reset the
// dedup state when the sequence restarts so a forced boundary label
// (e.g. the min date) isn't blanked by the previous pass's last label
// when both format identically (e.g. a May-to-May range).
if (
typeof value === 'number' &&
lastValue !== undefined &&
value <= lastValue
) {
lastLabel = undefined;
}
if (typeof value === 'number') {
lastValue = value;
}
const label =
typeof xAxisFormatter === 'function'
? (xAxisFormatter as Function)(value)
: String(value);
if (label === lastLabel) {
return '';
}
lastLabel = label;
return label;
};
if (typeof xAxisFormatter === 'function' && 'id' in xAxisFormatter) {
(wrapper as any).id = (xAxisFormatter as any).id;
}
return wrapper;
})()
: xAxisFormatter;
const temporalTickValues = resolveTemporalTickValues(
rebasedData,
xAxisLabel,
xAxisType,
resolvedTimeGrain,
annotationLayers,
);
const temporalAxisTickConfig = getTemporalAxisTickConfig(
temporalTickValues,
showMaxLabel,
xAxisType,
xAxisLabelRotation,
xAxisLabelInterval,
deduplicatedFormatter,
isHorizontal,
zoomable,
);
let xAxis: any = {
type: xAxisType,
name: xAxisTitle,
@@ -1363,12 +1256,38 @@ export default function transformProps(
groupBy.length === 0 && {
triggerEvent: true,
}),
...temporalAxisTickConfig,
minorTick: { show: minorTicks },
axisTick: {
...temporalAxisTickConfig.axisTick,
show: axisTicks ? 'auto' : false,
axisLabel: {
// When rotation is applied on time axes, hideOverlap can
// aggressively hide the last label. Rotated labels already
// have less overlap, so disabling hideOverlap is safe.
// At 0° rotation, also disable hideOverlap when showMaxLabel
// is active so the forced boundary label is never suppressed
// by ECharts' overlap detection (#39899).
hideOverlap: showMaxLabel
? false
: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
formatter: deduplicatedFormatter,
rotate: xAxisLabelRotation,
interval: xAxisLabelInterval,
// Force the boundary labels on non-rotated time axes so the first
// and last dates stay visible: hideOverlap can hide the last label,
// and a min date that falls between "nice" ticks otherwise renders
// no beginning label. Skipped when rotated to avoid phantom labels
// at the axis boundary.
...(showMaxLabel && {
showMaxLabel: true,
showMinLabel: true,
}),
// The alignments assume the axis runs along the bottom; a horizontal
// chart puts this axis on the side, where they misplace the labels.
...(showMaxLabel &&
!isHorizontal && {
alignMaxLabel: 'right',
alignMinLabel: 'left',
}),
},
minorTick: { show: minorTicks },
axisTick: { show: axisTicks ? 'auto' : false },
...(gridlines ? {} : { splitLine: { show: false } }),
minInterval:
xAxisType === AxisType.Time && resolvedTimeGrain && !forceMaxInterval
@@ -1395,6 +1314,7 @@ export default function transformProps(
// >= 100px: full axis with proportional tick count
// 60-99px: show only min/max boundary labels (splitNumber=1), hide lines/ticks
// < 60px: hide all axis decorations, show line only
const isSmallChart = height < TIMESERIES_CONSTANTS.compactChartHeight;
const isMicroChart = height < TIMESERIES_CONSTANTS.microChartHeight;
const yAxisSplitNumber = isMicroChart
? undefined
@@ -1464,16 +1384,9 @@ export default function transformProps(
const echartOptions: EChartsCoreOption = {
useUTC: true,
...(seriesType === EchartsTimeseriesSeriesType.Bar &&
resolvedValueLabelPosition === BarValueLabelPosition.Auto
? { darkMode: isThemeDark(theme) }
: {}),
grid: {
...defaultGrid,
...padding,
// Compact charts prioritize a viable coordinate system over keeping
// axis labels inside an already constrained grid rectangle.
containLabel: !usesCompactLayout,
},
xAxis,
yAxis,
@@ -1600,23 +1513,27 @@ export default function transformProps(
...getLegendProps(
effectiveLegendType,
legendOrientation,
nativeLegendVisible,
// Hide legend on compact charts — not enough vertical space
isSmallChart ? false : showLegend,
theme,
zoomable,
legendState,
padding,
),
scrollDataIndex: legendIndex || 0,
data: resolvedLegendData,
data:
colorByPrimaryAxis && groupBy.length === 0
? colorByPrimaryAxisLegendData
: sortedLegendData,
// Disable legend selection and buttons when colorByPrimaryAxis is enabled
...(usesPrimaryAxisLegend
...(colorByPrimaryAxis && groupBy.length === 0
? {
selectedMode: false, // Disable clicking legend items
selector: false, // Hide All/Invert buttons
}
: {}),
},
series: renderedSeries,
series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]),
toolbox: {
show: zoomable,
top: TIMESERIES_CONSTANTS.toolboxTop,
@@ -1673,57 +1590,9 @@ export default function transformProps(
const mergedEchartOptions = customEchartOptions
? mergeCustomEChartOptions(echartOptions, customEchartOptions)
: echartOptions;
const viableEchartOptions = getViableTimeseriesEchartOptions(
mergedEchartOptions,
height,
zoomable,
);
const mergedSeries = viableEchartOptions.series;
const finalSeries = Array.isArray(mergedSeries)
? (mergedSeries as SeriesOption[])
: mergedSeries && typeof mergedSeries === 'object'
? [mergedSeries as SeriesOption]
: renderedSeries;
const mergedGrid = Array.isArray(viableEchartOptions.grid)
? viableEchartOptions.grid[0]
: viableEchartOptions.grid;
const finalGrid =
mergedGrid && typeof mergedGrid === 'object' ? mergedGrid : padding;
const customLegend = usesCustomLegend
? buildTimeseriesCustomLegend({
fallbackColor: theme.colorTextSecondary,
grid: {
bottom:
typeof finalGrid.bottom === 'number' ||
typeof finalGrid.bottom === 'string'
? finalGrid.bottom
: padding.bottom,
top:
typeof finalGrid.top === 'number' ||
typeof finalGrid.top === 'string'
? finalGrid.top
: padding.top,
},
interactive: !usesPrimaryAxisLegend,
legendNames: resolvedLegendNames,
legendState,
orientation: legendOrientation,
series: finalSeries,
})
: undefined;
const finalEchartOptions = usesCustomLegend
? {
...viableEchartOptions,
legend: {
...(viableEchartOptions.legend as Record<string, unknown>),
show: false,
},
}
: viableEchartOptions;
return {
customLegend,
echartOptions: finalEchartOptions,
echartOptions: mergedEchartOptions,
emitCrossFilters,
formData,
groupby: groupBy,
@@ -35,9 +35,6 @@ import type {
CallbackDataParams,
DefaultStatesMixin,
ItemStyleOption,
LabelLayoutOption,
LabelLayoutOptionCallback,
LabelLayoutOptionCallbackParams,
LineStyleOption,
OptionName,
SeriesLabelOption,
@@ -52,7 +49,6 @@ import type {
import type { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { extractForecastSeriesContext } from '../utils/forecast';
import {
BarValueLabelPosition,
EchartsTimeseriesSeriesType,
ForecastSeriesEnum,
LabelPositionEnum,
@@ -74,107 +70,6 @@ import {
TIMESERIES_CONSTANTS,
} from '../constants';
const AUTO_LABEL_FIT_RATIO = 0.8;
const BAR_LABEL_DISTANCE = 5;
// Neither an inside nor an outside placement gives a stacked segment's value
// label legible, non-overlapping room once the segment's own extent drops
// below roughly the label text's height, since the closest available
// placement then collides with a neighboring segment's label regardless of
// which side it's drawn on. Highcharts and D3 apply the same kind of floor.
// The label font size is theme.fontSizeSM (~12px, see series.ts), so 16px
// covers the glyph height plus a couple of pixels of breathing room.
const MIN_LABEL_SEGMENT_SIZE_PX = 16;
// The labelLayout callback only applies align/verticalAlign/width/height/
// fontSize from its return value (LABEL_OPTION_TO_STYLE_KEYS in ECharts'
// LabelManager) — there is no hide/ignore field, so a zero font size is the
// supported way to suppress an individual label from this callback.
const HIDDEN_LABEL_LAYOUT: LabelLayoutOption = { fontSize: 0 };
type BarLabelPosition =
| 'bottom'
| 'inside'
| 'insideBottom'
| 'insideLeft'
| 'insideRight'
| 'insideTop'
| 'left'
| 'right'
| 'top';
type NegativeBarLabelPosition = BarLabelPosition | 'outside';
/** Resolve the fixed ECharts label position for a bar value. */
function getBarLabelPosition(
position: BarValueLabelPosition,
isHorizontal: boolean,
isNegative = false,
): BarLabelPosition {
if (position === BarValueLabelPosition.OutsideEnd) {
if (isHorizontal) return isNegative ? 'left' : 'right';
return isNegative ? 'bottom' : 'top';
}
if (position === BarValueLabelPosition.InsideCenter) return 'inside';
const isEnd = position !== BarValueLabelPosition.InsideBase;
const usePositiveEnd = isEnd !== isNegative;
if (isHorizontal) return usePositiveEnd ? 'insideRight' : 'insideLeft';
return usePositiveEnd ? 'insideTop' : 'insideBottom';
}
/** Place a horizontal bar label just beyond its value end. */
function getHorizontalOutsideLayout(
params: LabelLayoutOptionCallbackParams,
isNegative: boolean,
): LabelLayoutOption {
return {
x: isNegative
? params.rect.x - BAR_LABEL_DISTANCE
: params.rect.x + params.rect.width + BAR_LABEL_DISTANCE,
y: params.rect.y + params.rect.height / 2,
align: isNegative ? 'right' : 'left',
verticalAlign: 'middle',
};
}
/** Place a vertical bar label just beyond its value end. */
function getVerticalOutsideLayout(
params: LabelLayoutOptionCallbackParams,
isNegative: boolean,
): LabelLayoutOption {
return {
x: params.rect.x + params.rect.width / 2,
y: isNegative
? params.rect.y + params.rect.height + BAR_LABEL_DISTANCE
: params.rect.y - BAR_LABEL_DISTANCE,
align: 'center',
verticalAlign: isNegative ? 'top' : 'bottom',
};
}
/** Keep fitting labels inside, move oversized labels outside the bar, and
* suppress labels for segments too small to legibly fit one either way. */
export function getAutoBarLabelLayout(
params: LabelLayoutOptionCallbackParams,
isHorizontal: boolean,
isNegative = false,
): LabelLayoutOption {
const segmentSize = isHorizontal
? Math.abs(params.rect.width)
: Math.abs(params.rect.height);
if (segmentSize < MIN_LABEL_SEGMENT_SIZE_PX) {
return HIDDEN_LABEL_LAYOUT;
}
const fitsWidth =
params.labelRect.width <=
Math.abs(params.rect.width) * AUTO_LABEL_FIT_RATIO;
const fitsHeight =
params.labelRect.height <=
Math.abs(params.rect.height) * AUTO_LABEL_FIT_RATIO;
if (fitsWidth && fitsHeight) return {};
return isHorizontal
? getHorizontalOutsideLayout(params, isNegative)
: getVerticalOutsideLayout(params, isNegative);
}
function parseTimeShiftToMs(timeShift?: string | null): number {
if (!timeShift) return 0;
@@ -273,69 +168,35 @@ export const getBaselineSeriesForStream = (
};
};
/** Identify object-form ECharts data items. */
function isDataItemObject(
dataItem: unknown,
): dataItem is Record<string, unknown> {
return (
typeof dataItem === 'object' &&
dataItem !== null &&
!Array.isArray(dataItem)
);
}
/** Return whether an ECharts bar datum is negative on its value axis. */
function isNegativeBarDataItem(
dataItem: unknown,
isHorizontal: boolean,
): boolean {
const value = isDataItemObject(dataItem) ? dataItem.value : dataItem;
const axisValue = Array.isArray(value)
? value[isHorizontal ? 0 : 1]
: undefined;
return typeof axisValue === 'number' && axisValue < 0;
}
/** Create a fit-aware layout callback bound to one bar series. */
function createAutoBarLabelLayout(
data: unknown,
isHorizontal: boolean,
): LabelLayoutOptionCallback {
return params => {
const dataItem =
Array.isArray(data) && params.dataIndex !== undefined
? data[params.dataIndex]
: undefined;
return getAutoBarLabelLayout(
params,
isHorizontal,
isNegativeBarDataItem(dataItem, isHorizontal),
);
};
}
/** Apply the value-end label position to a negative bar datum. */
function transformNegativeLabel(
dataItem: unknown,
isHorizontal: boolean,
negativePosition: NegativeBarLabelPosition,
): unknown {
if (!isNegativeBarDataItem(dataItem, isHorizontal)) return dataItem;
const value = isDataItemObject(dataItem) ? dataItem.value : dataItem;
const item = isDataItemObject(dataItem) ? dataItem : { value };
const label = isDataItemObject(item.label) ? item.label : {};
return { ...item, label: { ...label, position: negativePosition } };
}
/** Adjust label positions for negative values in a bar series. */
export function transformNegativeLabelsPosition(
series: SeriesOption,
isHorizontal: boolean,
negativePosition: NegativeBarLabelPosition = 'outside',
labelPosition?: string,
): TimeseriesDataRecord[] {
return (series.data as unknown[]).map(dataItem =>
transformNegativeLabel(dataItem, isHorizontal, negativePosition),
) as TimeseriesDataRecord[];
/*
* Adjusts label position for negative values in bar series
* @param series - Array of series options
* @param isHorizontal - Whether chart is horizontal
* @returns data with adjusted label positions for negative values
*/
const transformValue = (value: any) => {
const [xValue, yValue] = Array.isArray(value) ? value : [null, null];
const axisValue = isHorizontal ? xValue : yValue;
return axisValue < 0
? {
value,
label: {
position:
labelPosition && labelPosition !== 'auto'
? labelPosition
: 'outside',
},
}
: value;
};
return (series.data as TimeseriesDataRecord[]).map(transformValue);
}
export function applyColorByPrimaryAxis(
@@ -381,7 +242,6 @@ export function transformSeries(
stackIdSuffix?: string;
yAxisIndex?: number;
showValue?: boolean;
valueLabelPosition?: BarValueLabelPosition;
onlyTotal?: boolean;
legendState?: LegendState;
formatter?: ValueFormatter;
@@ -418,7 +278,6 @@ export function transformSeries(
stackIdSuffix,
yAxisIndex = 0,
showValue,
valueLabelPosition = BarValueLabelPosition.Auto,
onlyTotal,
formatter,
legendState,
@@ -540,40 +399,29 @@ export function transformSeries(
symbol = opts.lineSymbol || (isDarkMode ? 'circle' : 'emptyCircle');
}
let transformedData = data;
if (Array.isArray(data) && colorByPrimaryAxis) {
transformedData = applyColorByPrimaryAxis(
series,
colorScale,
sliceId,
opacity,
isHorizontal,
);
}
if (Array.isArray(transformedData) && plotType === 'bar') {
// An explicit labelPosition (set before valueLabelPosition existed, or
// still relevant to a saved chart) takes precedence for negative values;
// otherwise fall back to the fit-aware valueLabelPosition-derived spot.
const negativeLabelPosition: NegativeBarLabelPosition =
labelPosition && labelPosition !== 'auto'
? (labelPosition as NegativeBarLabelPosition)
: getBarLabelPosition(valueLabelPosition, isHorizontal, true);
transformedData = transformNegativeLabelsPosition(
{ ...series, data: transformedData },
isHorizontal,
negativeLabelPosition,
);
}
const isAutoBarLabel =
plotType === 'bar' && valueLabelPosition === BarValueLabelPosition.Auto;
const isInsideBarLabel =
plotType === 'bar' &&
valueLabelPosition !== BarValueLabelPosition.OutsideEnd;
return {
...series,
...(Array.isArray(data) ? { data: transformedData } : null),
...(Array.isArray(data)
? colorByPrimaryAxis
? {
data: applyColorByPrimaryAxis(
series,
colorScale,
sliceId,
opacity,
isHorizontal,
),
}
: seriesType === 'bar' && !stack
? {
data: transformNegativeLabelsPosition(
series,
isHorizontal,
labelPosition,
),
}
: null
: null),
connectNulls,
queryIndex,
yAxisIndex,
@@ -606,31 +454,15 @@ export function transformSeries(
showSymbol,
symbol,
symbolSize: symbolSizeFn ?? markerSize,
...(isAutoBarLabel
? {
labelLayout: createAutoBarLabelLayout(transformedData, isHorizontal),
}
: {}),
label: {
show: !!showValue,
// An explicit labelPosition (the generic control still used by
// MixedTimeseries' bar series, and by standalone bar charts saved
// before valueLabelPosition existed) wins outright. Otherwise bar
// charts fall back to the fit-aware valueLabelPosition control, and
// every other "Show value" chart type falls back to an
// orientation-aware default.
position:
labelPosition && labelPosition !== 'auto'
? (labelPosition as LabelPositionEnum)
: plotType === 'bar'
? getBarLabelPosition(valueLabelPosition, isHorizontal)
: isHorizontal
? LabelPositionEnum.Right
: LabelPositionEnum.Top,
// ECharts derives contrast from the bar fill for inside positions.
// Auto x/y overflow clears the position, selecting its outside fill.
...(isInsideBarLabel ? {} : { color: theme?.colorText }),
position: (labelPosition === 'auto' || !labelPosition
? isHorizontal
? LabelPositionEnum.Right
: LabelPositionEnum.Top
: labelPosition) as LabelPositionEnum,
...(plotType === 'bar' ? { overflow: 'truncate' } : {}),
color: theme?.colorText,
textBorderWidth: 0,
formatter: (params: any) => {
// don't show confidence band value labels, as they're already visible on the tooltip
@@ -1002,76 +834,3 @@ export function getPadding(
isHorizontal,
);
}
const MIN_ECHARTS_GRID_HEIGHT = 1;
export function resolveTimeseriesGridOffset(
offset: unknown,
chartHeight: number,
) {
if (typeof offset === 'number') {
return Number.isFinite(offset) ? Math.max(offset, 0) : 0;
}
if (typeof offset !== 'string') {
return 0;
}
const percentage = offset.match(/^\s*(-?\d+(?:\.\d+)?)%\s*$/);
const pixels = percentage
? (Number(percentage[1]) / 100) * chartHeight
: Number(offset);
return Number.isFinite(pixels) ? Math.max(pixels, 0) : 0;
}
export function getViableTimeseriesEchartOptions<Options extends object>(
options: Options,
chartHeight: number,
zoomable: boolean,
): Options {
const optionWithGrid = options as Options & { grid?: unknown };
const gridOption = Array.isArray(optionWithGrid.grid)
? optionWithGrid.grid[0]
: optionWithGrid.grid;
if (!gridOption || typeof gridOption !== 'object') {
return options;
}
const grid = gridOption as Record<string, unknown>;
const rawTop = resolveTimeseriesGridOffset(grid.top, chartHeight);
const rawBottom = resolveTimeseriesGridOffset(grid.bottom, chartHeight);
const isCompact = chartHeight <= TIMESERIES_CONSTANTS.compactChartHeight;
const requestedTop = isCompact ? Math.min(rawTop, 12) : rawTop;
const requestedBottom =
isCompact && !zoomable ? Math.min(rawBottom, 5) : rawBottom;
// Cap both reservations so even a tiny canvas retains a coordinate region.
const reservationBudget = Math.max(chartHeight - MIN_ECHARTS_GRID_HEIGHT, 0);
const top = Math.min(requestedTop, reservationBudget);
const bottom = Math.min(
requestedBottom,
Math.max(reservationBudget - top, 0),
);
const mustDisableContainLabel =
isCompact || requestedTop + requestedBottom > reservationBudget;
if (
top === rawTop &&
bottom === rawBottom &&
(!mustDisableContainLabel || grid.containLabel === false)
) {
return options;
}
const viableGrid = {
...grid,
bottom,
...(mustDisableContainLabel ? { containLabel: false } : {}),
top,
};
return {
...options,
grid: Array.isArray(optionWithGrid.grid)
? [viableGrid, ...optionWithGrid.grid.slice(1)]
: viableGrid,
} as Options;
}

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